Loading...
墨滴

掀乱书页的风

2021/10/13  阅读:38  主题:兰青

Android换肤调研

背景

Android换肤技术已经是很久之前就已经被成熟使用的技术了,公司业务上需要用到换肤.为了不重复造轮子,并且快速实现需求,并且求稳,发现主要有两个框架比较流行,Android-Skin-Loader和Android-skin-support

Android-Skin-Loader

GitHub - fengjundev/Android-Skin-Loader: 一个通过动态加载本地皮肤包进行换肤的皮肤框架 在这里插入图片描述 可以看到好几年都没人维护了,出了问题也不好解决(这里没有丝毫贬低该框架的意思)

这里大概说下原理,通过LayoutInflater.setFactory的方式, 在回调的onCreateView中解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等. 然后保存到map中, 对每一个View做for循环去遍历所有的attr, 想要对更多的属性进行换肤, 需要Activity实现接口, 将需要换肤的View, 以及相应的属性收集到一起

Android-skin-support

[外链图片转存失败(img-N53580LB-1569138768035)(:storage/b880fd6d-37c9-45b8-894a-22181a6c4adf/aab99155.png)]
[外链图片转存失败(img-N53580LB-1569138768035)(:storage/b880fd6d-37c9-45b8-894a-22181a6c4adf/aab99155.png)]

Github上一个star数比较多的换肤框架-Android-skin-support(一款用心去做的Android 换肤框架, 极低的学习成本, 极好的用户体验. 一行代码就可以实现换肤, 你值得拥有!!!). 简单了解之后,可以快速上手,并且侵入性很低,源码地址: https://github.com/ximsfei/Android-skin-support

介绍

SkinCompatManager.withoutActivity(this).loadSkin(); 就这么简单, 你的APK已经拥有了强大的换肤功能, 当然现在是拥有了换肤功能, 别忘了制作皮肤包.

功能

支持布局中用到的资源换肤。 支持代码中设置的资源换肤。 默认支持大部分基础控件,Material Design换肤。 支持动态设置主题颜色值,支持选择sdcard上的图片作为drawable换肤资源。 支持多种加载策略(应用内/插件式/自定义sdcard路径/zip等资源等)。 资源加载优先级: 动态设置资源-加载策略中的资源-插件式换肤/应用内换肤-应用资源。 支持定制化,选择需要的模块加载。 支持矢量图(vector/svg)换肤。 skin-support 4.0.0以上支持AndroidX,4.0.0以下支持support库 更详细的信息可以直接参考官方说明,很详细

那么它是如何实现换肤的呢,下面先来点预备知识

AppCompatActivity实现

吐槽一下,Google为了让开发者升级androidx,support28版本很多库都不提供源码,大家可能也发现了,好了回到正题

public class AppCompatActivity extends FragmentActivity {
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       final AppCompatDelegate delegate = getDelegate();
       delegate.installViewFactory();
       delegate.onCreate(savedInstanceState);
       ...
   }

   @Override
   public MenuInflater getMenuInflater() {
       return getDelegate().getMenuInflater();
   }

   @Override
   public void setContentView(@LayoutRes int layoutResID) {
       getDelegate().setContentView(layoutResID);
   }

   @Override
   public void setContentView(View view) {
       getDelegate().setContentView(view);
   }
   ....
}

AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate

类图 源码中主要使用了AppCompateDelegate的子类AppCompatDelegateImpl

class AppCompatDelegateImpl extends AppCompatDelegate implements Callback, Factory2

 public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i("AppCompatDelegate", "The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's");
        }

    }

AppCompatDelegateImpl中, 在LayoutInflaterFactory的接口方法onCreateView 中将View的创建交给了AppCompatViewInflater

public View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (this.mAppCompatViewInflater == null) {
            TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
            String viewInflaterClassName = a.getString(styleable.AppCompatTheme_viewInflaterClass);
            if (viewInflaterClassName != null && !AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                try {
                    Class viewInflaterClass = Class.forName(viewInflaterClassName);
                    this.mAppCompatViewInflater = (AppCompatViewInflater)viewInflaterClass.getDeclaredConstructor().newInstance();
                } catch (Throwable var8) {
                    Log.i("AppCompatDelegate", "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", var8);
                    this.mAppCompatViewInflater = new AppCompatViewInflater();
                }
            } else {
                this.mAppCompatViewInflater = new AppCompatViewInflater();
            }
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = attrs instanceof XmlPullParser ? ((XmlPullParser)attrs).getDepth() > 1 : this.shouldInheritContext((ViewParent)parent);
        }
       //可以直接看这里
        return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
    }

再来看一下AppCompatViewInflater中createView的实现

public final View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    ......
    View view = null;
    switch (name) {
        case "TextView":
            view = new AppCompatTextView(context, attrs);
            break;
        case "ImageView":
            view = new AppCompatImageView(context, attrs);
            break;
        case "Button":
            view = new AppCompatButton(context, attrs);
            break;
        case "EditText":
            view = new AppCompatEditText(context, attrs);
            break;
        case "Spinner":
            view = new AppCompatSpinner(context, attrs);
            break;
        case "ImageButton":
            view = new AppCompatImageButton(context, attrs);
            break;
        case "CheckBox":
            view = new AppCompatCheckBox(context, attrs);
            break;
        ......
    }
    ......
    return view;
}

我们看下其中一个类AppCompatTextView的实现

public class AppCompatTextView extends TextView implements TintableBackgroundView {
	//这2个是关键类
    private final AppCompatBackgroundHelper mBackgroundTintHelper;
    private final AppCompatTextHelper mTextHelper;
    
    public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(TintContextWrapper.wrap(context), attrs, defStyleAttr);

        mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);

        mTextHelper = AppCompatTextHelper.create(this);
        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
        mTextHelper.applyCompoundDrawablesTints();
    }

    @Override
    public void setBackgroundResource(@DrawableRes int resId) {
        super.setBackgroundResource(resId);
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.onSetBackgroundResource(resId);
        }
    }
    ......
}

源码中可以通过拦截View创建过程, 替换一些基础的组件, 然后对一些特殊的属性(eg: background, textColor) 做处理, 换肤框架中的核心思想就是这个,这个框架的一系列操作都是围绕这个展开的

拦截Delegate

如果项目中使用的Activity继承自AppCompatActivity,需要重载getDelegate()方法 这里有个坑,不使用AppCompatActivity的话就要使用之前框架封装的一个BaseActivity,刚开始用的时候完全不知道有这个基类的,以为所有Activity都支持,后来看了源码才搞明白

@NonNull
@Override
public AppCompatDelegate getDelegate() {
    return SkinAppCompatDelegateImpl.get(this, this);
}

创建控件的过程

首先我们从库的初始化处着手,这里将Application传入,又添加了一个SkinAppCompatViewInflater

SkinCompatManager.withoutActivity(application)
                .addInflater(new SkinAppCompatViewInflater());

SkinAppCompatViewInflater其实就是用来创建View的.我们来看看withoutActivity(application)做了什么.

//SkinCompatManager.java
/**
     * 初始化换肤框架,监听Activity生命周期. 通过该方法初始化,应用中Activity无需继承{@link skin.support.app.SkinCompatActivity}.
     */
public static SkinCompatManager withoutActivity(Application application) {
    init(application);
    SkinActivityLifecycle.init(application);
    return sInstance;
}

//SkinActivityLifecycle.java
public static SkinActivityLifecycle init(Application application) {
    if (sInstance == null) {
        synchronized (SkinActivityLifecycle.class) {
            if (sInstance == null) {
                sInstance = new SkinActivityLifecycle(application);
            }
        }
    }
    return sInstance;
}
private SkinActivityLifecycle(Application application) {
    //就是这里,注册了ActivityLifecycleCallbacks,可以监听所有Activity的生命周期
    application.registerActivityLifecycleCallbacks(this);
    //很重要,稍后看
    installLayoutFactory(application);
    SkinCompatManager.getInstance().addObserver(getObserver(application));
}

可以看到,初始化的时候在SkinActivityLifecycle中其实就注册了ActivityLifecycleCallbacks,这样就可以监听app所有Activity的生命周期. 来看看SkinActivityLifecycle中监听到Activity的onCreate()方法时干了什么

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    //判断是否需要换肤  这个可以外部初始化时控制
    if (isContextSkinEnable(activity)) {
        //在Activity创建的时候,直接将Factory设置成三方库里面的
        installLayoutFactory(activity);

        //更新状态栏颜色
        updateStatusBarColor(activity);
        //更新window背景颜色
        updateWindowBackground(activity);
        if (activity instanceof SkinCompatSupportable) {
            ((SkinCompatSupportable) activity).applySkin();
        }
    }
}

/**
    * 设置Factory(创建View的工厂)
    */
 private void installLayoutFactory(Context context) {
        try {
            LayoutInflater layoutInflater = LayoutInflater.from(context);
            //看这里getSkinDelegate
            LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
        } catch (Exception e) {
            Slog.i("SkinActivity", "A factory has already been set on this LayoutInflater");
        }
    }

在我们的Activity创建的时候,首先判断一下是否需要换肤,需要换肤才去搞.

下面我们来看看setFactory()的第二个参数创建过程,第二个参数其实是一个创建View的工厂.

//SkinActivityLifecycle.java
private SkinCompatDelegate getSkinDelegate(Context context) {
    if (mSkinDelegateMap == null) {
        mSkinDelegateMap = new WeakHashMap<>();
    }
	//获得SkinCompatDelegate
    SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
    if (mSkinDelegate == null) {
        mSkinDelegate = SkinCompatDelegate.create(context);
        mSkinDelegateMap.put(context, mSkinDelegate);
    }
    return mSkinDelegate;
}

//SkinCompatDelegate.java
public class SkinCompatDelegate implements LayoutInflaterFactory {
    private final Context mContext;
    //主角  在这里 在这里!!!
    private SkinCompatViewInflater mSkinCompatViewInflater;
    private List<WeakReference<SkinCompatSupportable>> mSkinHelpers = new ArrayList<>();

    private SkinCompatDelegate(Context context) {
        mContext = context;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        View view = createView(parent, name, context, attrs);

        if (view == null) {
            return null;
        }
        if (view instanceof SkinCompatSupportable) {
            mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));
        }

        return view;
    }

    public View createView(View parent, final String name, @NonNull Context context,
                           @NonNull AttributeSet attrs) {
        if (mSkinCompatViewInflater == null) {
            mSkinCompatViewInflater = new SkinCompatViewInflater();
        }

        List<SkinWrapper> wrapperList = SkinCompatManager.getInstance().getWrappers();
        for (SkinWrapper wrapper : wrapperList) {
            Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);
            if (wrappedContext != null) {
                context = wrappedContext;
            }
        }
        //看这里
        return mSkinCompatViewInflater.createView(parent, name, context, attrs);
    }

    public static SkinCompatDelegate create(Context context) {
        return new SkinCompatDelegate(context);
    }

    public void applySkin() {
        if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
            for (WeakReference ref : mSkinHelpers) {
                if (ref != null && ref.get() != null) {
                    ((SkinCompatSupportable) ref.get()).applySkin();
                }
            }
        }
    }
}

可以看到SkinCompatDelegate是一个SkinCompatViewInflater的委托. 当系统需要创建View的时候,就会回调SkinCompatDelegate的@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,因为前面设置了LayoutInflater的Factory为SkinCompatDelegate. 然后SkinCompatDelegate将创建View的工作交给SkinCompatViewInflater去处理

来看看SkinCompatViewInflater是如何创建View的

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    View view = createViewFromHackInflater(context, name, attrs);

    if (view == null) {
        view = createViewFromInflater(context, name, attrs);
    }

    if (view == null) {
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check it's android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}
private View createViewFromInflater(Context context, String name, AttributeSet attrs) {
    View view = null;
    //这里的SkinLayoutInflater就是我们之前在初始化时设置的SkinAppCompatViewInflater
    //当然,SkinLayoutInflater可以有多个
    for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {
        view = inflater.createView(context, name, attrs);
        if (view == null) {
            continue;
        } else {
            break;
        }
    }
    return view;
}

public View createViewFromTag(Context context, String name, AttributeSet attrs) {
    if ("view".equals(name)) {
        name = attrs.getAttributeValue(null, "class");
    }
    try {
        mConstructorArgs[0] = context;
        mConstructorArgs[1] = attrs;

        //自定义控件
        if (-1 == name.indexOf('.')) {
            for (int i = 0; i < sClassPrefixList.length; i++) {
             final View view = createView(context, name, sClassPrefixList[i]);
                if (view != null) {
                    return view;
                }
            }
            return null;
        } else {
            return createView(context, name, null);
        }
    } catch (Exception e) {
        // We do not want to catch these, lets return null and let the actual LayoutInflater
        // try
        return null;
    } finally {
        // Don't retain references on context.
        mConstructorArgs[0] = null;
        mConstructorArgs[1] = null;
    }

createViewFromInflater()方法中利用了我们在初始化库时设置的SkinLayoutInflater去创建view. 为什么要在SkinCompatViewInflater还要细化,还需要交由更细的SkinLayoutInflater来处理呢?应该是因为方便扩展,库中给出了几个SkinLayoutInflater,有SkinAppCompatViewInflater(基础控件构建器)、SkinMaterialViewInflater(material design控件构造器)、SkinConstraintViewInflater(ConstraintLayout构建器)、SkinCardViewInflater(CardView v7构建器)。 由于初始化时我们设置的是SkinAppCompatViewInflater,其他的构建器都是类似的原理.我们来看下SkinAppCompatViewInflater

//SkinAppCompatViewInflater.java
@Override
public View createView(Context context, String name, AttributeSet attrs) {
    View view = createViewFromFV(context, name, attrs);

    if (view == null) {
        view = createViewFromV7(context, name, attrs);
    }
    return view;
}

private View createViewFromFV(Context context, String name, AttributeSet attrs) {
    View view = null;
    if (name.contains(".")) {
        return null;
    }
    switch (name) {
       //重点来了,替换成可以换肤的控件
        case "View":
            view = new SkinCompatView(context, attrs);
            break;
        case "LinearLayout":
            view = new SkinCompatLinearLayout(context, attrs);
            break;
        case "RelativeLayout":
            view = new SkinCompatRelativeLayout(context, attrs);
            break;
        case "FrameLayout":
            view = new SkinCompatFrameLayout(context, attrs);
            break;
        case "TextView":
            view = new SkinCompatTextView(context, attrs);
            break;
        case "ImageView":
            view = new SkinCompatImageView(context, attrs);
            break;
        case "Button":
            view = new SkinCompatButton(context, attrs);
            break;
        case "EditText":
            view = new SkinCompatEditText(context, attrs);
            break;
        ......
        default:
            break;
    }
    return view;
}

private View createViewFromV7(Context context, String name, AttributeSet attrs) {
    View view = null;
    switch (name) {
        case "android.support.v7.widget.Toolbar":
        	//替换成可以换肤的控件
            view = new SkinCompatToolbar(context, attrs);
            break;
        default:
            break;
    }
    return view;
}

我们在这里将View的创建拦截,然后创建自己的控件。既然是我们自己创建的控件,想干啥就很容易了

我们看一下SkinCompatTextView的源码

//SkinCompatTextView.java
public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
    private SkinCompatTextHelper mTextHelper;
    private SkinCompatBackgroundHelper mBackgroundTintHelper;

    public SkinCompatTextView(Context context) {
        this(context, null);
    }

    public SkinCompatTextView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);
        mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
        mTextHelper = SkinCompatTextHelper.create(this);
        mTextHelper.loadFromAttributes(attrs, defStyleAttr);
    }
    ......

    @Override
    public void applySkin() {
        if (mBackgroundTintHelper != null) {
            mBackgroundTintHelper.applySkin();
        }
        if (mTextHelper != null) {
            mTextHelper.applySkin();
        }
    }

}

实现了SkinCompatSupportable接口,那么在换肤操作时会回调applySkin方法,在这个方法里 background相关的属性会交给SkinCompatBackgroundHelper去处理,textColor相关的操作交给SkinCompatTextHelper去处理。

从皮肤包加载皮肤

上面讲到的是初始化的一个过程,下面讲一下点击换肤操作,它是如何执行的 其实皮肤包就是一个apk,只不过里面没有任何代码,只有一些需要换肤的资源或者颜色什么的.而且这些资源的名称必须和当前app中的资源名称是一致的,才能替换.

使用方式

SkinCompatManager.getInstance().loadSkin("night.skin", null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);

直接从loadSkin()方法看起

/**
* 加载皮肤包.
* @param skinName 皮肤包名称.
* @param listener 皮肤包加载监听.
* @param strategy 皮肤包加载策略.
*/
public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
    //加载策略  分为好几种:从SD卡中加载皮肤,从assets文件中加载皮肤等等
    SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);
    if (loaderStrategy == null) {
        return null;
    }
    return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);
}

可以看到SkinLoadTask是一个AsyncTask,然后在后台去解析这个皮肤包.既然是AsyncTask,那么我们来看doInBackground()方法

我们来看看SkinLoadTask的doInBackground()

//SkinLoadTask.java
@Override
protected String doInBackground(String... params) {
    ......
    try {
        if (params.length == 1) {
            //根据加载策略去后台加载皮肤
            String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
            if (TextUtils.isEmpty(skinName)) {
                SkinCompatResources.getInstance().reset(mStrategy);
            }
            return params[0];
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    SkinCompatResources.getInstance().reset();
    return null;
}

//加载策略 随便挑一个吧 SkinSDCardLoader.java  从SD卡加载皮肤
@Override
public String loadSkinInBackground(Context context, String skinName) {
    if (TextUtils.isEmpty(skinName)) {
        return skinName;
    }
    //获取皮肤路径
    String skinPkgPath = getSkinPath(context, skinName);
    if (SkinFileUtils.isFileExists(skinPkgPath)) {
        //获取皮肤包包名.
        String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
        //获取皮肤包的Resources
        Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
        if (resources != null && !TextUtils.isEmpty(pkgName)) {
        //Resources,包名,皮肤名,加载策略全部存下来
            SkinCompatResources.getInstance().setupSkin(
                    resources,
                    pkgName,
                    skinName,
                    this);
            return skinName;
        }
    }
    return null;
}

//SkinCompatManager.java
//获取皮肤包包名.
public String getSkinPackageName(String skinPkgPath) {
    PackageManager mPm = mAppContext.getPackageManager();
    PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
    return info.packageName;
}
//获取皮肤包资源{@link Resources}.
@Nullable
public Resources getSkinResources(String skinPkgPath) {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, skinPkgPath);

        Resources superRes = mAppContext.getResources();
        return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

大概就是去子线程获取皮肤包的包名和Resources(后面我们需要获取皮肤包中的颜色或者资源时需要通过这个进行获取). SkinCompatResources.getInstance().setupSkin()方法中就是将这些从皮肤包中加载的Resources,包名,皮肤名,加载策略全部存下来.有了这些东西,待会儿就能取皮肤包里面的资源了.

通知Activity换肤

在dobackground方法结束之后

 @Override
        protected void onPostExecute(String skinName) {
           ······
          if (skinName != null) {
              SkinPreference.getInstance()
                .setSkinName(skinName)
                .setSkinStrategy(mStrategy.getType())
                .commitEditor();
                //通知换肤,观察者模式
              notifyUpdateSkin();
              ······
          } else {
              ······
          }

SkinObservable的notifyUpdateSkin()方法,通过循环记录的observers,一个一个地通知Activity开始换肤

那这里的observers从何而来呢?我们找下在哪里调用了addObserver()这个方法就可以了。我们可以看到,在SkinActivityLifecycle的onActivityResumed()方法中,为每一个Activity都添加了独立的SkinObservable

@Override
public void onActivityResumed(Activity activity) {
    mCurActivityRef = new WeakReference<>(activity);
    if (isContextSkinEnable(activity)) {
        LazySkinObserver observer = getObserver(activity);
        //这里
        SkinCompatManager.getInstance().addObserver(observer);
        observer.updateSkinIfNeeded();     
    }
 }

通知View刷新UI

刷新UI最终会走到LazySkinObserver的updateSkinForce()方法

 void updateSkinForce() {
    ······
    if (mContext instanceof Activity && isContextSkinEnable(mContext)) {
        updateWindowBackground((Activity) mContext);
    }
    //通知View去刷新UI
    getSkinDelegate(mContext).applySkin();
    //通知实现了SkinCompatSupportable的Activity更新UI
    if (mContext instanceof SkinCompatSupportable) {
         ((SkinCompatSupportable) mContext).applySkin();
    }
    ······
 }

库中定义的控件都是实现了SkinCompatSupportable接口的,方便控制换肤。比如SkinCompatTextView的applySkin()方法中调用了BackgroundTintHelper和TextHelper的applySkin()方法,就是说换肤时会去动态的更换背景或文字颜色什么的。我们来看看mBackgroundTintHelper.applySkin()的实现

//SkinCompatBackgroundHelper.java
@Override
public void applySkin() {
    //该控件是否有背景  检测
    mBackgroundResId = checkResourceId(mBackgroundResId);
    if (mBackgroundResId == INVALID_ID) {
        return;
    }
    Drawable drawable = SkinCompatVectorResources.getDrawableCompat(mView.getContext(), mBackgroundResId);
    if (drawable != null) {
        int paddingLeft = mView.getPaddingLeft();
        int paddingTop = mView.getPaddingTop();
        int paddingRight = mView.getPaddingRight();
        int paddingBottom = mView.getPaddingBottom();
        ViewCompat.setBackground(mView, drawable);
        mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
    }
}

就是获取drawable然后给view设置背景.关键在于这里的获取drawable是怎么实现的.来看看具体实现

//SkinCompatVectorResources.java
private Drawable getSkinDrawableCompat(Context context, int resId) {
    //当前是非默认皮肤
    if (!SkinCompatResources.getInstance().isDefaultSkin()) {
        try {
            return SkinCompatDrawableManager.get().getDrawable(context, resId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    ......
    return AppCompatResources.getDrawable(context, resId);
}

//SkinCompatDrawableManager.java
public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {
    return getDrawable(context, resId, false);
}

Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,
                        boolean failIfNotKnown) {
    //检查Drawable是否能被正确解码
    checkVectorDrawableSetup(context);

    Drawable drawable = loadDrawableFromDelegates(context, resId);
    if (drawable == null) {
        drawable = createDrawableIfNeeded(context, resId);
    }
    if (drawable == null) {
        //这里是关键
        drawable = SkinCompatResources.getDrawable(context, resId);
    }

    if (drawable != null) {
        // Tint it if needed
        drawable = tintDrawable(context, resId, failIfNotKnown, drawable);
    }
    if (drawable != null) {
        // See if we need to 'fix' the drawable
        SkinCompatDrawableUtils.fixDrawable(drawable);
    }
    return drawable;
}

最后是调用的SkinCompatDrawableManager去获取drawable,在这里我们去创建drawable时就使用SkinCompatResources去获取. 还记得SkinCompatResources么?就是上面我们获取了皮肤包的信息后,将信息全部保存到了这个类里面.

//SkinCompatResources.java
//皮肤的Resources可以通过它来获取皮肤里面的资源
private Resources mResources;
//皮肤包名
private String mSkinPkgName = "";
//皮肤名
private String mSkinName = "";
//加载策略
private SkinCompatManager.SkinLoaderStrategy mStrategy;
//是默认皮肤?
private boolean isDefaultSkin = true;

public static Drawable getDrawable(Context context, int resId) {
    return getInstance().getSkinDrawable(context, resId);
}
/**
* 通过id获取皮肤中的drawable资源
* @param context Context
* @param resId   资源id
*/
private Drawable getSkinDrawable(Context context, int resId) {
    //是否有皮肤颜色缓存
    if (!SkinCompatUserThemeManager.get().isColorEmpty()) {
        ColorStateList colorStateList = SkinCompatUserThemeManager.get().getColorStateList(resId);
        if (colorStateList != null) {
            return new ColorDrawable(colorStateList.getDefaultColor());
        }
    }
    //是否有皮肤drawable缓存
    if (!SkinCompatUserThemeManager.get().isDrawableEmpty()) {
        Drawable drawable = SkinCompatUserThemeManager.get().getDrawable(resId);
        if (drawable != null) {
            return drawable;
        }
    }
    //加载策略非空  可以通过加载策略去加载drawable,开发者可自定义
    if (mStrategy != null) {
        Drawable drawable = mStrategy.getDrawable(context, mSkinName, resId);
        if (drawable != null) {
            return drawable;
        }
    }
    //非默认皮肤 去皮肤中加载资源
    if (!isDefaultSkin) {
        //皮肤资源id   这是我们的目标
        int targetResId = getTargetResId(context, resId);
        if (targetResId != 0) {
            //根据id通过皮肤的Resources去获取drawable
            return mResources.getDrawable(targetResId);
        }
    }
    return context.getResources().getDrawable(resId);
}

策略就是有缓存资源(之前在皮肤包中取过这个resId的资源)则取缓存资源,没有缓存则根据resId通过皮肤的Resources去获取drawable. 到此,已经获取到皮肤包中的drawable,也就是实现了动态的加载皮肤包中的图片,shape等等的资源,加载皮肤中的颜色的过程也是类似的,这里就不多介绍了整个换肤流程大概就是这样.

原理总结

  1. 监听APP所有Activity的生命周期(registerActivityLifecycleCallbacks())
  2. 在每个Activity的onCreate()方法调用时setFactory(),设置创建View的工厂.将创建View的琐事交给SkinCompatViewInflater去处理.
  3. 库中自己重写了系统的控件(比如View对应于库中的SkinCompatView),实现换肤接口(接口里面只有一个applySkin()方法),表示该控件是支持换肤的.并且将这些控件在创建之后收集起来,方便随时换肤.
  4. 在库中自己写的控件里面去解析出一些特殊的属性(比如:background, textColor),并将其保存起来
  5. 在切换皮肤的时候,遍历一次之前缓存的View,调用其实现的接口方法applySkin(),在applySkin()中从皮肤资源(可以是从网络或者本地获取皮肤包)中获取资源.获取资源后设置其控件的background或textColor等,就可实现换肤.

优势(引用作者原文)

这样的做法与网上其他框架相比优势在哪里, 为什么重复造轮子

  1. 在增加框架开发成本的基础上降低了框架使用的成本, 一次开发, 所有Android 开发者都受用;
  2. 换肤框架对业务代码的侵入性比较小, 不需要实现接口重写方法, 不需要其他额外的代码, 接入方便
  3. 深入源码, 和源码实现方式类似, 兼容性更好.

特别感谢:https://blog.csdn.net/ximsfei/article/details/54604310 https://github.com/ximsfei/Android-skin-support https://juejin.im/post/5b5b127d5188251acd0f3885 https://blog.csdn.net/yumi0629/article/details/80392906

掀乱书页的风

2021/10/13  阅读:38  主题:兰青

作者介绍

掀乱书页的风