>

手把手讲解,Android一键换肤功能实现

- 编辑:澳门新葡亰平台游戏 -

手把手讲解,Android一键换肤功能实现

市情上对数的App都提供换肤功效,这里临时不讲白天和夜晚方式

*手把手讲明类别文章,是自个儿写给各位看官,也是写给我要好的。**小说大概过分详细,可是那是为着帮忙到尽量多的人,终究专业5,6年,不可能老吸血,也到了回馈开源的时候.这几个类别的稿子:1、用简单明了的讲授格局,解说一门技艺的实用价值2、详细书写源码的追踪,源码截图,绘制类的构造图,尽量详细地表明原理的探寻进度3、提供Github 的 可运维的德姆o工程,不过作者所提供代码,越来越多是提供思路,引玉之砖,请酌情cv4、集结整理原理查究进程中的一些坑,恐怕demo的周转进程中的注意事项5、用gif图,最直观地呈现demo启动作效果果*

若果以为细节太细,直接跳过看结论就能够。本身本领有限,假诺开掘描述不当之处,接待留言批评指正。

#import "BlockManager.m" 类中的达成代码

下图是新浪云音乐的换肤功用

以此动态图中,首先观望的是Activity1,点击换肤,可一直改动分界面上的background,图片的src,还有textViewtextColor,跳转Activity2之后的textView水彩,在本人换肤在此之前,和换肤之后,是例外的。换肤的历程本人并未运维别的的Activity,分界面也尚未闪烁。小编在Activity1个中换肤,直接影响了Activity2textView字体颜色。

需求:

换肤其实正是替换财富(文字、颜色、图片等)

在Apk包中设有两种财富用于换肤时候切换。

自由度低,apk文件大 通常用来未有其它供给的白昼/夜晚形式app

透过运营时动态加载皮肤包

澳门新葡亰平台游戏 1今日头条云下载的能源包

 if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; }
  1. 自定义一个分子方法
    - (void)mutiBlockWith:(CallBlock)call;
    我们定义了三个暗含block的积极分子方法,大家往往调用那几个点子,
    其内部贯彻如下:

二、换肤流程

澳门新葡亰平台游戏 2流程澳门新葡亰平台游戏 3采集

怎么才具获得具有的view那才是主要!那么我们只好从setContentView()入手

那么难点来了setContentView()到底干了怎么着

查看源码开掘setContentView()通过LayoutInflater将xml调换来View加载到window中

澳门新葡亰平台游戏 4

源码三连

inflate干了怎么着?

澳门新葡亰平台游戏 5

LayoutInflate 的宗旨是createViewFromTag()

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

boolean ignoreThemeAttr) {

...............

View view;

if (mFactory2 !=null) {//大旨View的创建工厂 是二个接口

view =mFactory2.onCreateView(parent, name, context, attrs);

}else if (mFactory !=null) {

view =mFactory.onCreateView(name, context, attrs);

}else {

view =null;

}

if (view ==null &&mPrivateFactory !=null) {

view =mPrivateFactory.onCreateView(parent, name, context, attrs);

}

if (view ==null) {

final Object lastContext =mConstructorArgs[0];

mConstructorArgs[0] = context;

try {

if (-1 == name.indexOf {//判别是或不是是自定义View

view = onCreateView(parent, name, attrs);

}else {

view = createView(name,null, attrs);

}

}finally {

mConstructorArgs[0] = lastContext;

}

}

return view;

}

通过解析下边代码能够看来Factory2假设不为空那么就调用Factory2的章程创设View

否者就使用onCreateView()方法创立View

澳门新葡亰平台游戏 6

那便是说只要我们给定一个Factory那么大家就可以监视全数的view

View设置能源文件的流程如图所示

澳门新葡亰平台游戏 7

当我们获得当前View的能源名称时就能够先去插件中的财富文件里找。

那就是换肤的原理

澳门新葡亰平台游戏 8加载外界Apk能源文件

得到财富文件相信剩下的门阀就知晓怎么玩了

代码已经上传给 github,迎接大家一道商量商量

android一键换肤作用

动用方式

澳门新葡亰平台游戏 9初始化澳门新葡亰平台游戏 10使用

皮肤包正是一个唯有财富文件的Apk

世家能够看一下本人任何小说哦

依赖APT的android路由框架

依照APT的android路由框架--APT手艺

  • 动画
  • 背景图片
  • 字体
  • 字体颜色
  • 字体大小
  • 音频
  • 视频

  • 什么是hook如题,小编是用hook达成一键换肤。那么怎么着是hook?hook,钩子. 安卓中的hook本领,其实是二个抽象概念:对系统源码的代码逻辑进行"勒迫",插入本人的逻辑,然后放行。注意:hook也许频仍利用java反射机制···

"一键换肤"中的hook思路

  1. *"恫吓"系统成立View的进程,我们风雨同舟来创建View**系统本来自个儿存在成立View的逻辑,大家要通晓那部分代码,以便为自身所用.*
  2. *搜集大家须要换肤的View(用自定义view属性来标志一个view是或不是帮衬一键换肤),保存到变量中**绑架了 系统创制view的逻辑之后,我们要把协理换肤的那一个view保存起来*
  3. *加载外界财富包,调用接口举办换肤**外界财富包,是.apk后缀的三个文书,是经过gradle包裹产生的。里面包罗须求换肤的财富文件,不过必需确认保证,要换的能源文件,和原工程里面包车型地铁文本名完全相同.*
  • *Activity 的 setContentView(XC90.layout.XXX) 到底在做什么?**抚今追昔大家写app的习惯,创建Activity,写xxx.xml,在Activity里面setContentView(R.layout.xxx). 大家写的是xml,最终表现出来的是二个一个的分界面上的UI控件,那么setContentView终究做了如何事,使得XML里面包车型大巴开始和结果,产生了UI控件呢?*

假使不先来点干货,推测有些人就看不下去了,各位观者请看下图:

澳门新葡亰平台游戏 11image.png

2016-03-07 22:23:34.299 BlockTest[57023:2003321] 111
2016-03-07 22:23:34.302 BlockTest[57023:2003321] 22
2016-03-07 22:23:34.304 BlockTest[57023:2003321] 33

澳门新葡亰平台游戏 12经典澳门新葡亰平台游戏 13

  • View parent 父组件
  • String name xml标签名
  • Context context 上下文
  • AttributeSet attrs view属性
  • boolean ignoreThemeAttr 是或不是忽略theme属性

咱俩看看的是将质量放到下边了,大家再度运维,调整台无任何输出

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { if (name.equals { name = attrs.getAttributeValue(null, "class"); } // Apply a theme wrapper, if allowed and one is specified. if (!ignoreThemeAttr) { final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId; if (themeResId != 0) { context = new ContextThemeWrapper(context, themeResId); } ta.recycle(); } if (name.equals) { // Let's party like it's 1995! return new BlinkLayout(context, attrs); } try { View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); } if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf { view = onCreateView(parent, name, attrs); } else { view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } } return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } catch (Exception e) { final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name, e); ie.setStackTrace(EMPTY_STACK_TRACE); throw ie; } }
- (void)setUserName:(NSString *)userName{
  for (CallBlock block in self.blockArray) {
      if (block) {
          block(userName); 
                 }
           }  
     }

那正是说继续追踪,一向到:AppCompatViewInflater

#import "BlockManager.h"

@implementation BlockManager
- (void)mutiBlockWith:(CallBlock)call{
    self.block = [call copy];
    [self.blockArray addObject:self.block];  
}
- (void)setUserName:(NSString *)userName{
    for (CallBlock block in self.blockArray) {
        if (block) {
            block(userName);      
        }
    }   
}
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.blockArray = [NSMutableArray array];     
    }
    return self;
}
@end

那么能还是不能够要贯彻一个全app内的一键换肤,一劳永逸~~~

此地我们透过数组将大家调用的每个block都投入到数组中了,想必下面的早就精通了

实际,可能有人要问了,你怎么领悟那边是走的哪一个if分支呢?方法:新制造贰个Project,跟踪MainActivity onCreate里面setContentView()一道找到这段代码debug:你会开掘:

澳门新葡亰平台游戏 14image.png答案很醒目了,系统在默许景况下就能够走Factory2的onCreateView(),应该有人好奇:这些mFactory2对象是哪来的?是怎么时候set进去的答案如下:澳门新葡亰平台游戏 15image.png就算留心Debug,就能发觉 《标记标记,因为后面有一段代码会跳回到这里,这里非常重要...》澳门新葡亰平台游戏 16image.png澳门新葡亰平台游戏 17image.png立即,getDelegate()获得的对象,和 LayoutInflater里面mFactory2其实是同二个目的

  1. 那正是说大家的block函数什么日期调用呢

难点:为啥会收不到block内部的出口呢?

import android.content.Context;import android.content.res.TypedArray;import android.support.v7.app.AppCompatDelegate;import android.text.TextUtils;import android.util.AttributeSet;import android.view.LayoutInflater;import android.view.View;import android.widget.TextView;import com.enjoy02.skindemo.R;import com.enjoy02.skindemo.view.ZeroView;import java.lang.reflect.Constructor;import java.util.ArrayList;import java.util.HashMap;import java.util.List;public class SkinFactory implements LayoutInflater.Factory2 { private AppCompatDelegate mDelegate;//预定义一个委托类,它负责按照系统的原有逻辑来创建view private List<SkinView> listCacheSkinView = new ArrayList<>();//我自定义的list,缓存所有可以换肤的View对象 /** * 给外部提供一个set方法 * * @param mDelegate */ public void setDelegate(AppCompatDelegate mDelegate) { this.mDelegate = mDelegate; } /** * Factory2 是继承Factory的,所以,我们这次是主要重写Factory的onCreateView逻辑,就不必理会Factory的重写方法了 * * @param name * @param context * @param attrs * @return */ @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; } /** * @param parent * @param name * @param context * @param attrs * @return */ @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { // TODO: 关键点1:执行系统代码里的创建View的过程,我们只是想加入自己的思想,并不是要全盘接管 View view = mDelegate.createView(parent, name, context, attrs);//系统创建出来的时候有可能为空,你问为啥?请全文搜索 “标记标记,因为” 你会找到你要的答案 if (view == null) {//万一系统创建出来是空,那么我们来补救 try { if (-1 == name.indexOf {//不包含. 说明不带包名,那么我们帮他加上包名 view = createViewByPrefix(context, name, prefixs, attrs); } else {//包含. 说明 是权限定名的view name, view = createViewByPrefix(context, name, null, attrs); } } catch (Exception e) { e.printStackTrace(); } } //TODO: 关键点2 收集需要换肤的View collectSkinView(context, attrs, view); return view; } /** * TODO: 收集需要换肤的控件 * 收集的方式是:通过自定义属性isSupport,从创建出来的很多View中,找到支持换肤的那些,保存到map中 */ private void collectSkinView(Context context, AttributeSet attrs, View view) { // 获取我们自己定义的属性 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Skinable); boolean isSupport = a.getBoolean(R.styleable.Skinable_isSupport, false); if (isSupport) {//找到支持换肤的view final int Len = attrs.getAttributeCount(); HashMap<String, String> attrMap = new HashMap<>(); for (int i = 0; i < Len; i++) {//遍历所有属性 String attrName = attrs.getAttributeName; String attrValue = attrs.getAttributeValue; attrMap.put(attrName, attrValue);//全部存起来 } SkinView skinView = new SkinView(); skinView.view = view; skinView.attrsMap = attrMap; listCacheSkinView.add;//将可换肤的view,放到listCacheSkinView中 } } /** * 公开给外界的换肤入口 */ public void changeSkin() { for (SkinView skinView : listCacheSkinView) { skinView.changeSkin(); } } static class SkinView { View view; HashMap<String, String> attrsMap; /** * 真正的换肤操作 */ public void changeSkin() { if (!TextUtils.isEmpty(attrsMap.get("background"))) {//属性名,例如,这个background,text,textColor.... int bgId = Integer.parseInt(attrsMap.get("background").substring;//属性值,R.id.XXX ,int类型, // 这个值,在app的一次运行中,不会发生变化 String attrType = view.getResources().getResourceTypeName; // 属性类别:比如 drawable ,color if (TextUtils.equals(attrType, "drawable")) {//区分drawable和color view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable;//加载外部资源管理器,拿到外部资源的drawable } else if (TextUtils.equals(attrType, "color")) { view.setBackgroundColor(SkinEngine.getInstance().getColor; } } if (view instanceof TextView) { if (!TextUtils.isEmpty(attrsMap.get("textColor"))) { int textColorId = Integer.parseInt(attrsMap.get("textColor").substring; ( view).setTextColor(SkinEngine.getInstance().getColor(textColorId)); } } //那么如果是自定义组件呢 if (view instanceof ZeroView) { //那么这样一个对象,要换肤,就要写针对性的方法了,每一个控件需要用什么样的方式去换,尤其是那种,自定义的属性,怎么去set, // 这就对开发人员要求比较高了,而且这个换肤接口还要暴露给 自定义View的开发人员,他们去定义 // .... } } } /** * 所谓hook,要懂源码,懂了之后再劫持系统逻辑,加入自己的逻辑。 * 那么,既然懂了,系统的有些代码,直接拿过来用,也无可厚非。 */ //*******************************下面一大片,都是从源码里面抄过来的,并不是我自主设计****************************** // 你问我抄的哪里的?到 AppCompatViewInflater类源码里面去搜索:view = createViewFromTag(context, name, attrs); static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};// final Object[] mConstructorArgs = new Object[2];//View的构造函数的2个"实"参对象 private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,将View的反射构造函数都存起来 static final String[] prefixs = new String[]{//安卓里面控件的包名,就这么3种,这个变量是为了下面代码里,反射创建类的class而预备的 "android.widget.", "android.view.", "android.webkit." }; /** * 反射创建View * * @param context * @param name * @param prefixs * @param attrs * @return */ private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs) { Constructor<? extends View> constructor = sConstructorMap.get; Class<? extends View> clazz = null; if (constructor == null) { try { if (prefixs != null && prefixs.length > 0) { for (String prefix : prefixs) { clazz = context.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class);//控件 if (clazz != null) break; } } else { if (clazz == null) { clazz = context.getClassLoader().loadClass.asSubclass(View.class); } } if (clazz == null) { return null; } constructor = clazz.getConstructor(mConstructorSignature);//拿到 构造方法, } catch (Exception e) { e.printStackTrace(); return null; } constructor.setAccessible;// sConstructorMap.put(name, constructor);//然后缓存起来,下次再用,就直接从内存中去取 } Object[] args = mConstructorArgs; args[1] = attrs; try { //通过反射创建View对象 final View view = constructor.newInstance;//执行构造函数,拿到View对象 return view; } catch (Exception e) { e.printStackTrace(); } return null; } //**********************************************************************************************}

比方我们修改一下代码:

如此那般一大段好像有个别令人无所用心。其实真正供给关爱的,正是反光的代码,最终的 newInstance().OK,Activity上那个精彩纷呈的View的发源,就提及此处, 要是有看不懂的,招待留言商量. !

  • app中能源文件大管家 Resources / AssetManager 是怎么专门的学问的

*从大家的顶峰指标出发:大家要做的是“换肤”,借使我们得到了要换肤的View,能够对她们开展setXXX属性来改换UI,那么属性值从哪个地方来?**分界面成分多姿多彩,不过这个View,都以用财富文件来开展 "装扮"出来的,能源文件差十分的少能够分成:图片,文字,颜色,声音视频,字体等。假诺大家决定了能源文件,那么是否有力量对分界面成分进行set某某属性来拓宽“再装扮”呢? 当然,那是平价的。因为,我们平日得到多少个TextView,就会对它进行setTextColor,这种操作,在view还存世的时候,都足以开展操作,况兼这种操作,并不会促成Activity的重启。这几个能源文件,有多个联结的大管家。可能有的人说是PRADO.java文件,它其中执会调查总计局筹了富有的能源文件int值.没有错,但是那些福特Explorer文件是何许产生效果与利益的吧? 答案:Resources.*

本来这里应该写上源码追踪记录的,然而出于 源码不能够追踪,原因一时半刻还没找到,此前追查setContentView(R.layout.xxxx)的时候还是能debug,以后以致十三分了,很奇异!

澳门新葡亰平台游戏 18image.png答案找到了:因为自个儿利用的是 真机,平时手提式有线电话机商家都会对原生系统举行修改,然后将系统写到到真机里面。而,大家debug,用的是原生SDK。 用实例来说,作者本地是SDK 27的源码,真机也是27的类别,可是真机的运营起来的系统的代码,是被厂商修改了的,和自己本地的终将有所出入,所以,有个别代码报红,就很健康了,不恐怕debug也很正规。

前几日这一个操作大家就提及那边,大家下一次说一下多图上传的主题素材:
怎么自定义线程,完成我们的多图上传思路

 if (view == null && originalContext != context) { // If the original context does not equal our themed context, then we need to manually // inflate it using the name so that android:theme takes effect. view = createViewFromTag(context, name, attrs); }
  • 尤为重要类的调用方式
  1. 我们日益来深入分析:
    typedef void (^CallBlock)(NSString * string); 自定义block
    typedef -> 自定义内容
    void -> block块再次来到值
    (^CallBclok) -> block 名字,也正是大家的章程名,其实真正的调用约等于基于isa 指针调用的 。
    ^ -> 也正是大家的三个block方法标示
    (NSString * string) : 相当于形参,这里大家就写了三个参数,能够写五个参数。

  2. 我们将block定义了三个属性
    @property (nonatomic,copy) CallBlock block; block通过品质访谈。
    平常在ARC机制中,block大家用的平日是copy

  3. 我们在BlockManager 中定义个数组
    @property (nonatomic,strong) NSMutableArray * blockArray;
    干什么定义数组,这里有如何效劳?
    我们清楚copy 的效果与利益复制二个新的指标步向新的内部存款和储蓄器地址,引用计数会追加,大家copy其实正是代码块,大家给各样代码块一个独立的正经空间,我们用数组存款和储蓄每三个block块,以便我们可以在调用的时候任何调用到。

那么就进去下贰个环节:LayoutInflater又做了哪些?

#import <Foundation/Foundation.h>
typedef  void  (^CallBlock)(NSString * string);


@interface BlockManager : NSObject
@property (nonatomic,strong) NSString * userName;
@property (nonatomic,copy) CallBlock  block;
@property (nonatomic,strong) NSMutableArray * blockArray;

- (void)mutiBlockWith:(CallBlock)call;

@end

澳门新葡亰平台游戏 19image.png

  • 种类工程组织:

    澳门新葡亰平台游戏 20image.png

  • *关键类 SkinFactory**SkinFactory类, 承继LayoutInflater.Factory2 ,它的实例,会担当创制View,收集补助换肤的view*

block: 我们誉为闭包函数、佚名函数,代码块。
block,是自身相比较喜欢用的贰个回调函数,因为他现已丰硕庞大

这里的七个章程onCreateView(parent, name, attrs)createView(name, null, attrs);都最后索引到:

关键类 SkinEngine

作者们假如通过叁个性能的setter 方法开展调用block,上面大家看下使用:

import android.content.Context;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.res.AssetManager;import android.content.res.Resources;import android.graphics.drawable.Drawable;import android.support.v4.content.ContextCompat;import android.util.Log;import java.io.File;import java.lang.reflect.Method;public class SkinEngine { //单例 private final static SkinEngine instance = new SkinEngine(); public static SkinEngine getInstance() { return instance; } private SkinEngine() { } public void init(Context context) { mContext = context.getApplicationContext(); //使用application的目的是,如果万一传进来的是Activity对象 //那么它被静态对象instance所持有,这个Activity就无法释放了 } private Resources mOutResource;// TODO: 资源管理器 private Context mContext;//上下文 private String mOutPkgName;// TODO: 外部资源包的packageName /** * TODO: 加载外部资源包 */ public void load(final String path) {//path 是外部传入的apk文件名 File file = new File; if (!file.exists { return; } //取得PackageManager引用 PackageManager mPm = mContext.getPackageManager(); //“检索在包归档文件中定义的应用程序包的总体信息”,说人话,外界传入了一个apk的文件路径,这个方法,拿到这个apk的包信息,这个包信息包含什么? PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES); mOutPkgName = mInfo.packageName;//先把包名存起来 AssetManager assetManager;//资源管理器 try { //TODO: 关键技术点3 通过反射获取AssetManager 用来加载外面的资源包 assetManager = AssetManager.class.newInstance();//反射创建AssetManager对象,为何要反射?使用反射,是因为他这个类内部的addAssetPath方法是hide状态 //addAssetPath方法可以加载外部的资源包 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);//为什么要反射执行这个方法?因为它是hide的,不直接对外开放,只能反射调用 addAssetPath.invoke(assetManager, path);//反射执行方法 mOutResource = new Resources(assetManager,//参数1,资源管理器 mContext.getResources().getDisplayMetrics(),//这个好像是屏幕参数 mContext.getResources().getConfiguration;//资源配置 //最终创建出一个 "外部资源包"mOutResource ,它的存在,就是要让我们的app有能力加载外部的资源文件 } catch (Exception e) { e.printStackTrace(); } } /** * 提供外部资源包里面的颜色 * @param resId * @return */ public int getColor(int resId) { if (mOutResource == null) { return resId; } String resName = mOutResource.getResourceEntryName; int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName); if (outResId == 0) { return resId; } return mOutResource.getColor; } /** * 提供外部资源包里的图片资源 * @param resId * @return */ public Drawable getDrawable(int resId) {//获取图片 if (mOutResource == null) { return ContextCompat.getDrawable(mContext, resId); } String resName = mOutResource.getResourceEntryName; int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName); if (outResId == 0) { return ContextCompat.getDrawable(mContext, resId); } return mOutResource.getDrawable; } //..... 这里还可以提供外部资源包里的String,font等等等,只不过要手动写代码来实现getXX方法}
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    BlockManager * block = [[BlockManager alloc]init];

    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"111");
    }];
    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"22");
    }];
    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"33");
    }];
    block.userName = @"lala";

}

那边使用了汪洋的switch case来扩充系统控件的创设,比如:TextView

留意的前后相继猿会开采你的调用block是在设置属性的setter 方法内部调用的,代码实行时遵照编写翻译顺序试行的,所以立刻的block还向来不被拷贝内部存款和储蓄器中,所以此时并从未调用block块,相信那几个主题材料都知晓自身的block为啥不施行了吧。

  • LayoutInflater其一类是怎么把layout.xml<TextView> 变成TextView对象的?咱俩知道,大家传入的是int,是xxx.xml以此布局文件,在GL450文件里面包车型地铁相应int值。LayoutInflater得到了那些int随后,又干了怎么着事啊?

联手目录进去:会意识这么些办法:

澳门新葡亰平台游戏 21image.png澳门新葡亰平台游戏 22image.png

发觉四个第一办法:CreateViewFromTag,tag是指的怎么?其实正是 xml里面包车型客车标签头:<TextView ....> 里的TextView.追踪进去:

注:小说一经不是之处,还望指正,以便更加好的驾驭。
在此间不常创造了贰个微小的争辨群QQ:375940898
也足以联手抵触专门的学问中的难题

hook本领是安卓高档等级次序的能力,学起来并不轻松,demo里面包车型地铁讲授作者自感觉写的很理解了,假使还应该有不懂的,接待留言商量。读源码也并非那样轻便的事,可是照旧那句话,太简单的东西,不值钱,有高难度才有高回报。为了百万年收入,fighting!

切磋:那些是还是不是和代办很类似?
[self respondsToSelector:@selector(<#selector#>)] 代理平日都会选取响应函数,其实和后面部分原理同样,都以找到相应的函数指针,我们调用的是函数指针。
如果大家调用了相应的指针,那么相应的秘诀如故block一定会调用,有个别同学只怕会境遇block收不到回调,其实正是对应的isa 指针并从未接收调用,大家不论怎么学习,要掌握原理,那么化解难题的笔触就非常快。

出品大佬又提要求啦,要求app内部的图样要兑现白天黑夜形式的切换,以满意区别光线下都能确定保障丰盛的图纸清晰度. 如何做?也许解决的不二等秘书籍相当多,你能够给图表view扩大两个toggle方法,参数Stringday/night,然后切换之后postInvalidate 刷新重绘.OK,可行,不过这种办法切换白天黑夜,只是单个View中有效,那么一旦曾几何时产品又要另贰个View换肤,难道笔者要一个三个去写toggle么?未免太low了.

    BlockManager * block = [[BlockManager alloc]init];
     block.userName = @"lala";
    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"111");
    }];
    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"22");
    }];
    [block mutiBlockWith:^(NSString *string) {
        NSLog(@"33");
    }];

大家先本人定义多个model类:我们先来谙习一下完完全全代码
#import "BlockManager.h" 头文件定义部分

澳门新葡亰平台游戏 23换肤.gif

  1. 举例我们一键换肤功能,
  2. 一键切换字体的时候,
  3. 这一类都属于实践三个动作,发出通报一致。

以此艺术有4个参数,意义分别是:

实则选拔很轻易,只是告诉一下,block其实能够左近咱们打招呼一致在大局工程中调用。调节台打字与印刷结果是

 if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; }

既然如此交给了意义,那么必然要给出德姆o,不然太没诚意,嘿嘿嘿github地址奉上:

- (void)mutiBlockWith:(CallBlock)call{
   self.block = [call copy];
   [self.blockArray addObject:self.block];  
}

况兼在此间,开掘一段主要代码:

都以new 出来叁个兼有格外性格的TextView,再次回到出去。可是,使用过switch 的人都驾驭,这种case格局的分段,不只怕包涵全体的门类怎么做呢?这里switch之后,view长久以来只怕是null.所以,switch之后,Google大佬加了叁个if,但是很好奇,这段代码并没有步入if,因为 originalContext != context并不满意....具体原因小编也没查出来,ゞ

先看getDelegate:这里再次来到了三个AppCompatDelegate对象,跟踪到AppCompatDelegate内部,阅读源码,能够吸取两个定论:AppCompatDelegate 是 替Activity生成View对象的委托类,它提供了一体系setContentView方法,在Activity中参与UI控件。那它的AppCompatDelegatesetContentView方法又做了怎么样?

@NonNull protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); }
protected void changeSkin(String path) { if (ifAllowChangeSkin) { File skinFile = new File(Environment.getExternalStorageDirectory; SkinEngine.getInstance().load(skinFile.getAbsolutePath;//加载外部资源包 mSkinFactory.changeSkin();//执行换肤操作 mCurrentSkin = path; } }

这段代码的上边,假若view是空,补救措施如下:

下面的换肤动态图,作者换了ImageView,换了background,换了TextView的字体颜色,那么毕竟哪些东西得以换?

答案其实就一句话: 小编们项目代码里面 res目录下的具有东西,差不离都可以被替换。(为啥说大致?因为有个别犄角旮旯的东西笔者从来不经常间四个一个去试验....囧)

public class MyApp extends Application { @Override public void onCreate() { super.onCreate(); //初始化换肤引擎 SkinEngine.getInstance().init; }}

学到老活到老,路漫漫其修远兮。与众君共勉 !

具体来讲便是之类这个

public class BaseActivity extends AppCompatActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { // TODO: 关键点1:hook系统创建view的过程 if (ifAllowChangeSkin) { mSkinFactory = new SkinFactory(); mSkinFactory.setDelegate(getDelegate; LayoutInflater layoutInflater = LayoutInflater.from; layoutInflater.setFactory2(mSkinFactory);//劫持系统源码逻辑 } super.onCreate(savedInstanceState); }
final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy // by using the parent's context if (inheritContext && parent != null) { context = parent.getContext(); } if (readAndroidTheme || readAppTheme) { // We then apply the theme on the context, if specified context = themifyContext(context, attrs, readAndroidTheme, readAppTheme); } if (wrapContext) { context = TintContextWrapper.wrap; } View view = null; // We need to 'inject' our tint aware Views in place of the standard framework versions switch  { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; case "ImageView": view = createImageView(context, attrs); verifyNotNull(view, name); break; case "Button": view = createButton(context, attrs); verifyNotNull(view, name); break; case "EditText": view = createEditText(context, attrs); verifyNotNull(view, name); break; case "Spinner": view = createSpinner(context, attrs); verifyNotNull(view, name); break; case "ImageButton": view = createImageButton(context, attrs); verifyNotNull(view, name); break; case "CheckBox": view = createCheckBox(context, attrs); verifyNotNull(view, name); break; case "RadioButton": view = createRadioButton(context, attrs); verifyNotNull(view, name); break; case "CheckedTextView": view = createCheckedTextView(context, attrs); verifyNotNull(view, name); break; case "AutoCompleteTextView": view = createAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "MultiAutoCompleteTextView": view = createMultiAutoCompleteTextView(context, attrs); verifyNotNull(view, name); break; case "RatingBar": view = createRatingBar(context, attrs); verifyNotNull(view, name); break; case "SeekBar": view = createSeekBar(context, attrs); verifyNotNull(view, name); break; default: // The fallback that allows extending class to take over view inflation // for other tags. Note that we don't check that the result is not-null. // That allows the custom inflater path to fall back on the default one // later in this method. view = createView(context, name, attrs); } if (view == null && originalContext != context) { // If the original context does not equal our themed context, then we need to manually // inflate it using the name so that android:theme takes effect. view = createViewFromTag(context, name, attrs); } if (view != null) { // If we have created a view, check its android:onClick checkOnClickListener(view, attrs); } return view; }
  • Activity 的 setContentView(福睿斯.layout.XXX) 到底在做哪些?
  • LayoutInflater这些类是怎么把 layout.xml 的 <TextView> 形成TextView对象的?
  • app中财富文件大管家 Resources / AssetManager 是怎么专门的职业的

  • 关键类 SkinEngine SkinFactory

  • 最首要类的调用形式,联系此前的android源码,解释hook起效果的法规
  • 作用显示
  • 注意事项

所谓"一键",正是通过"一个"接口的调用,就能够兑现全app范围内的有着财富文件的替换.满含文本,颜色,图片等.

局部换肤完毕方式的对照

  • 澳门新葡亰平台游戏,方案1:自定义View中,要换肤,那似乎引言中所述,toggle方法,invalidate重绘。缺欠:换肤范围仅限于这几个View.
  • 方案2:给静态变量赋值,然后重启Activity. 假使三个Activity内用静态变量定义了三种色系,那么确实是足以经过关闭Activity,再起步的形式,完毕貌似换肤的成效(其实是重复开动了Activity)缺欠:太low,并且很浪费能源

大概还大概有别的方案吗,View重绘,重启Activity,都能兑现,但是照旧不是最高尚的方案,那么,有未有一种方案,能够落到实处全app内的换肤效果,又不会像重启 Activity 那样浪费能源呢?请看下图:

  • 作用体现澳门新葡亰平台游戏 24换肤.gif

  • *注意事项**1. 皮肤包skin_plugin module,里面,只提供应和要求要换肤的财富就可以,不要求换肤的能源,还大概有src目录下的源码(只是删掉java源码文件,不要删目录结构啊....),不要放在此处,无端增大皮肤包的容积.2. 皮肤包 skin_plugin module的gradle sdk版本最棒和app module的保险完全一致,不然不可能确认保障不会出现奇葩难点.3. 用皮肤包skin_plugin module 打包生成的apk文件,常规来说,是放在手提式有线电话机内存里面,然后由app module内的代码去加载。至于是手提式有线电话机内部存款和储蓄器里面包车型客车哪个位置,那就差别了. 我是利用的mumu模拟器,小编放在了最外层的根目录上边,然后读取那一个地方的代码是:File skinFile = new File(Environment.getExternalStorageDirectory(), "skin.apk");澳门新葡亰平台游戏 25image.png4. 上航海用体育场地中,打了四个皮肤包,要在乎:打五个皮肤包运维demo,打此前,一定要记得替换drawable图片能源为同名文件,以及澳门新葡亰平台游戏 26image.png不然切换未有效果.*

 if (view == null) { final Object lastContext = mConstructorArgs[0]; mConstructorArgs[0] = context; try { if (-1 == name.indexOf {//包含.说明这不是权限定名的类名 view = onCreateView(parent, name, attrs); } else {//权限定名走这里 view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; } }

OK,这里暴流露了多个方法,getDelegate()setContentView()

源码索引:setContentView(R.layout.activity_main); ---》getDelegate().setContentView(layoutResID);

而是,这里的补救措施未有实施,那当然有地点有其余的补救措施:回到在此之前的LayoutInflater的上边这段代码:

既是,那自个儿就平昔写结论了,一张图表达全数:

public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException { Constructor<? extends View> constructor = sConstructorMap.get; if (constructor != null && !verifyClassLoader(constructor)) { constructor = null; sConstructorMap.remove; } Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass; if  { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible; sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get; if (allowedState == null) { // New class -- remember whether it is allowed clazz = mContext.getClassLoader().loadClass( prefix != null ? (prefix + name) : name).asSubclass(View.class); boolean allowed = clazz != null && mFilter.onLoadClass; mFilterMap.put(name, allowed); if  { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object lastContext = mConstructorArgs[0]; if (mConstructorArgs[0] == null) { // Fill in the context if not already within inflation. mConstructorArgs[0] = mContext; } Object[] args = mConstructorArgs; args[1] = attrs; final View view = constructor.newInstance; // 真正需要关注的关键代码,就是这一行,执行了构造函数,返回了一个View对象 if (view instanceof ViewStub) { // Use the same context when inflating ViewStub later. final ViewStub viewStub =  view; viewStub.setLayoutInflater(cloneInContext args[0])); } mConstructorArgs[0] = lastContext; return view; } catch (NoSuchMethodException e) { ····· } }

谢谢享学课堂的无需付费视频课程 供给摄像的兄弟能够给自身留言冲突

插曲:关于怎么着阅读源码?在本身的上一篇小说中详尽表达了。然而漏了一个细节:那正是,当你在源码中看出三个接口或者抽象类,你想领会接口的实现类在哪?很轻松...假让你从未改变androidStudio的火速键设置的话,Ctrl+T能够帮你间接定位 接口和抽象类的实现类.

用地点的点子,找到setContentView的实际经过

澳门新葡亰平台游戏 27源码.png

本文由java编程发布,转载请注明来源:手把手讲解,Android一键换肤功能实现