十分钟带你看懂Android-skin-support换肤框架,完美实现夜间模式

元英朗
2023-12-01

如题,这篇文章我讲的非常通俗易懂,是为了方便小白能够大致了解这个框架的粗略实现过程,大神们请绕道哦~

  最近在做夜间模式,引用了ximsfei大神的Android-skin-support换肤框架来实现。该框架的整体的实现思想是:自定义ActivityLifecycle来记录Activities的生命周期,在每一个Activity执行完onResume()后,将Activity和LazySkinObserver接口绑定,在用户点击换肤操作时,通过接口回调来通知每一个Activity的换肤。
  这篇文章以“应用内换肤”方式为例来讲解Android-skin-support换肤框架的实现。

转载请注明:https://www.jianshu.com/p/78e75dba9e55

使用方法:

  在Application中初始化SkinCompatManager

SkinCompatManager.withoutActivity(this)
                // 自定义加载策略,指定SDCard路径
                .addStrategy(new CustomSDCardLoader())
                // material design
                .addInflater(new SkinMaterialViewInflater())
                // ConstraintLayout
                .addInflater(new SkinConstraintViewInflater())
                // CardView v7
                .addInflater(new SkinCardViewInflater())
                // 关闭状态栏换肤
                .setSkinStatusBarColorEnable(false)
                // 关闭windowBackground换肤
                .setSkinWindowBackgroundEnable(false)
                .loadSkin();

  用户点击换肤操作后,我们只需调用以下方法,一句话即可实现换肤:

SkinCompatManager.getInstance().loadSkin("night", null, SkinCompatManager.SKIN_LOADER_STRATEGY_BUILD_IN);

换肤流程:

1、开启SkinLoadTask:

  当调用了loadSkin()方法后,SkinCompatManager会开启一个SkinLoadTask,该方法的doInBackground()会获取皮肤包名字,然后在onPostExecute()方法中,将皮肤名写入SharePreference,同时调用notifyUpdateSkin()开始换肤。具体可以看SkinCompatManager中如下几段核心代码:

    private class SkinLoadTask extends AsyncTask<String, Void, String> {
       ······
        @Override
        protected String doInBackground(String... params) {
          ······
          if (params.length == 1) {
              String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);
              return params[0];
          }
          ······
        }

        @Override
        protected void onPostExecute(String skinName) {
           ······
          if (skinName != null) {
              SkinPreference.getInstance()
                .setSkinName(skinName)
                .setSkinStrategy(mStrategy.getType())
                .commitEditor();
              notifyUpdateSkin();
              ······
          } else {
              ······
          }
     }
  }
2、通知Activity开始换肤:

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

for (int i = arrLocal.length-1; i>=0; i--) {
     arrLocal[i].updateSkin(this, arg);
}

  那这里的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();     
    }
 }

  这样,通过接口回调,就可以通知每一个已经存在了的Activity去刷新UI啦。
  PS:这里我们还需要注意下onActivityCreated()回调:

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
    if (isContextSkinEnable(activity)) {
        installLayoutFactory(activity);
        updateWindowBackground(activity);
        if (activity instanceof SkinCompatSupportable) {
            ((SkinCompatSupportable) activity).applySkin();
        }
    }
}

  这里有一个很重要的方法:installLayoutFactory():

private void installLayoutFactory(Context context) {
    ······
    LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
    ······
}

  我们只看这行比较重要的代码,这里边有个getSkinDelegate()方法:

private SkinCompatDelegate getSkinDelegate(Context context) {
    ······        
    SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
    if (mSkinDelegate == null) {
        mSkinDelegate = SkinCompatDelegate.create(context);
        mSkinDelegateMap.put(context, mSkinDelegate);
    }
    return mSkinDelegate;
}

  这里将一个SkinCompatDelegate 对象和当前Activity对象绑定,放进了mSkinDelegateMap,这个mSkinDelegateMap在后续的换肤中需要用到,请记住。

3、通知View开始刷新UI:

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

 void updateSkinForce() {
    ······
    if (mContext instanceof Activity && isContextSkinEnable(mContext)) {
        updateWindowBackground((Activity) mContext);
    }
    getSkinDelegate(mContext).applySkin();
    if (mContext instanceof SkinCompatSupportable) {
         ((SkinCompatSupportable) mContext).applySkin();
    }
    ······
 }

  updateWindowBackground()对于换肤并不是那么重要,这里我们就不讨论了。主要是看这里最终调用了一个非常重要的方法:applySkin()。applySkin()方法十分重要,我们以后在写自定义View支持换肤时,都要和它打交道。getSkinDelegate(mContext).applySkin();是通知View去刷新UI,((SkinCompatSupportable) mContext).applySkin();则是通知实现了SkinCompatSupportable的Activity(如果你的Activity实现了SkinCompatSupportable接口,则可以在回调中做一些自己想要的操作)。
  getSkinDelegate()方法则是从mSkinDelegateMap中读取当前Activity的SkinDelegate对象(这个mSkinDelegateMap就是我们在第2点中提到的):

 private SkinCompatDelegate getSkinDelegate(Context context) {
        ······
        SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);
        if (mSkinDelegate == null) {
            mSkinDelegate = SkinCompatDelegate.create(context);
            mSkinDelegateMap.put(context, mSkinDelegate);
        }
        return mSkinDelegate;
    }
4、开始换肤:

  这里我们需要讲一下另外一个重要的类:SkinCompatDelegate。
SkinCompatDelegate这个类非常重要,它将我们写在XML中的View,偷偷转换成了支持换肤的自定义View。我们可以把这个类当成View的一个代理类,它继承自V4包的LayoutInflaterFactory接口,拥有View的生命周期管理:

   @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;
   }

  在onCreateView()中,createView()方法是将普通View替换为了支持换肤的自定义View, 同时,mSkinHelpers.add()方法将View记录起来。
  最后,我们调用SkinCompatDelegate的applySkin()方法,去通知自定义View刷新:

public void applySkin() {
        if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {
            for (WeakReference ref : mSkinHelpers) {
                if (ref != null && ref.get() != null) {
                    // 这里就是调用了自定义View的applySkin()
                    ((SkinCompatSupportable) ref.get()).applySkin();
                }
            }
        }
    }

  至此,整个换肤过程完毕。

Android标准View和自定义换肤View之间的映射

  我们之前说道,SkinCompatDelegate的onCreateView()中,将我们写在XML中的View转换成了支持换肤的自定义View,这个过程是怎么实现的呢?
  类的替换通过SkinLayoutInflater接口来实现:
我们可以自定义一个LayoutInflater实现自SkinLayoutInflater,我们以库中的SkinAppCompatViewInflater来分析。

@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;
        ······
    }
    return view;
}

   我们看到,这个类将Android标准View和自定义的支持换肤的View之间进行了映射,偷偷将普通的View替换为了支持换肤的View。
  映射完成后,回到我们之前说过的SkinCompatDelegate类,它的createView()方法,最终调用的就是 mSkinCompatViewInflater.createView()方法。

自定义View:

  关于自定义支持换肤的View,很简单,只需要让你的自定义View实现SkinCompatSupportable接口,然后在applySkin()中实现换肤代码即可。这里就不多说了,具体的参照库中的Widget进行实现即可。
  库中提供了SkinCompatBackgroundHelper、SkinCompatTextHelper等辅助类,这些类提供了loadFromAttributes()方法,可以获取对应的夜间模式颜色,方便你更快速地进行换肤实现。
  我们还可以自定义一个SkinLayoutInflater,将XML中的View名称和自定义的View进行对应。比如RecyclerView在库中并未被自定义,那我们可以自定义一个SkinCompatRecyclerView,然后在CustomSkinLayoutInflater中写上他们的对应关系:

 case "RecyclerView":
        view = new SkinCompatRecyclerView(context, attrs);
        break;

  这个设计的好处是,即使你在XML只忘记了引用SkinCompatRecyclerView,也不会影响RecyclerView的换肤。

 类似资料: