美文网首页Android开发之路高级UI安卓UI
(Android-skin-support)换肤框架原理研究并运

(Android-skin-support)换肤框架原理研究并运

作者: 成虫_62d0 | 来源:发表于2019-04-26 15:41 被阅读70次

    背景

    很多app需要进行换肤,到网络上找到了一个库--android-skin-support。很快就实现了需求,根据文档集成很方便,代码侵入性也低。

    本来也没有去深究它的实现原理,前几天有个迭代的需求如下:
    美股市场是“绿涨红跌”,而A股市场是“红涨绿跌”,产品要求用户可以自由选择。这个需求跟以前的换肤很类似,所以就仔细研究学习了"android-skin-support"库的实现原理,并根据其原理实现“红涨绿跌”.
    

    原理

    • 使用观察者模式
      框架抽象了一个SkinCompatSupportable接口:
        public interface SkinCompatSupportable {
            void applySkin();
        }
    

    所有需要换肤的控件都实现该接口,在用户执行“换肤”操作时候,通知所有实现该接口的订阅者执行换肤操作applySkin()

    按照上面的原理,那所有的控件都需要实现接口SkinCompatSupportable,那原生的控件怎么办呢?框架层面把所有常用的原生控件都重写了一遍,在包skin.support.widget下。框架层在解析布局文件的时候会把原生的控件替换成实现了接口SkinCompatSupportable的对应控件。框架层是怎么做的呢?请继续查看下文。

    • 注册activity生命周期监听器
      查看类skin.support.app.SkinActivityLifecycle,

      private SkinActivityLifecycle(Application application) {
          application.registerActivityLifecycleCallbacks(this);
          installLayoutFactory(application);
          SkinCompatManager.getInstance()
             .addObserver(getObserver(application));
      }
      
      private void installLayoutFactory(Context context) {
          LayoutInflater layoutInflater = LayoutInflater.from(context);
          try {
              Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
              field.setAccessible(true);
              field.setBoolean(layoutInflater, false);
              LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));
          } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
              e.printStackTrace();
          }
      }
      
      @Override
      public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
          if (isContextSkinEnable(activity)) {
              installLayoutFactory(activity);
              updateStatusBarColor(activity);
              updateWindowBackground(activity);
              if (activity instanceof SkinCompatSupportable) {
                  ((SkinCompatSupportable) activity).applySkin();
              }
      
              if (activity instanceof SkinCompatChangeGreenRed) {
                  boolean isSupport = ((SkinCompatChangeGreenRed)activity).isSupportChange();
                  ((SkinCompatChangeGreenRed)activity).applyChangeColor(isSupport ? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
              }
          }
      }
      
    
    注意  installLayoutFactory 方法,在每个activity的#onActivityCreated中把自己的LayoutInflaterFactory类(SkinCompatDelegate)设置进去,而把原生控件替换成库中的控件就在这个类中实现的,并且把所有实现SkinCompatSupportable接口的观察者都收集起来。这样代码的侵入性就变得很低,使得几行代码就可以实现换肤操作。
    
    或许有人疑惑为什么这样做就可以实现xml解析拦截? 我们再来看看 ```AppCompatActivity``` 的代码实现。
    
    ```java
    
     protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        if (delegate.applyDayNight() && mThemeId != 0) {
            // If DayNight has been applied, we need to re-apply the theme for
            // the changes to take effect. On API 23+, we should bypass
            // setTheme(), which will no-op if the theme ID is identical to the
            // current theme ID.
            if (Build.VERSION.SDK_INT >= 23) {
                onApplyThemeResource(getTheme(), mThemeId, false);
            } else {
                setTheme(mThemeId);
            }
        }
        super.onCreate(savedInstanceState);
        }
    

    我们看到有一个AppCompatDelegate,它是Activity的委托,AppCompatActivity将大部分生命周期都委托给了AppCompatDelegate,这点可从上面的源码中可以看出.
    继续看源码我们发现,解析xml布局的解析起也是在AppCompatDelegate对象中设置的。

    AppCompatDelegateImplV9.java

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

    LayoutInflaterCompat.setFactory2(layoutInflater, this);最终是调用的LayoutInflater的setFactory2()方法,看看实现

    /**
    * Like {@link #setFactory}, but allows you to set a {@link Factory2}
    * interface.
    */
    public void setFactory2(Factory2 factory) {
        if (mFactorySet) {
            throw new IllegalStateException("A factory has already been set on this LayoutInflater");
        }
        if (factory == null) {
            throw new NullPointerException("Given factory can not be null");
        }
        mFactorySet = true;
        if (mFactory == null) {
            mFactory = mFactory2 = factory;
        } else {
            mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
        }
    }
    

    这里有个小细节,Factory2只能被设置一次,设置完成后mFactorySet属性就为true,下一次设置时被直接抛异常.
    那么Factory2有什么用呢?看看其实现
    它是一个接口,只有一个方法,看起来是用来创建View的.

    综上所述,我们就可以根据其机制实现xml解析并拦截,让解析xml原生控件的时候返回我们想要的支持“换肤”动作的对应控件。

    • 拦截xml控件解析事件

    代码如下

    SkinCompatViewInflater.java

    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;
                case "Spinner":
                    view = new SkinCompatSpinner(context, attrs);
                    break;
                case "ImageButton":
                    view = new SkinCompatImageButton(context, attrs);
                    break;
                case "CheckBox":
                    view = new SkinCompatCheckBox(context, attrs);
                    break;
                case "RadioButton":
                    view = new SkinCompatRadioButton(context, attrs);
                    break;
                case "RadioGroup":
                    view = new SkinCompatRadioGroup(context, attrs);
                    break;
                case "CheckedTextView":
                    view = new SkinCompatCheckedTextView(context, attrs);
                    break;
                case "AutoCompleteTextView":
                    view = new SkinCompatAutoCompleteTextView(context, attrs);
                    break;
                case "MultiAutoCompleteTextView":
                    view = new SkinCompatMultiAutoCompleteTextView(context, attrs);
                    break;
                case "RatingBar":
                    view = new SkinCompatRatingBar(context, attrs);
                    break;
                case "SeekBar":
                    view = new SkinCompatSeekBar(context, attrs);
                    break;
                case "ProgressBar":
                    view = new SkinCompatProgressBar(context, attrs);
                    break;
                case "ScrollView":
                    view = new SkinCompatScrollView(context, attrs);
                    break;
            }
            return view;
        }
    

    这里还是有点迷惑, 那么我们再来看看android创建view的过程

    平时我们最常使用的Activity中的setContentView()设置布局ID,看看Activity中的实现,

    public void setContentView(@LayoutRes int layoutResID) {
            getWindow().setContentView(layoutResID);
            initWindowDecorActionBar();
        }
    
    

    调用的是Window中的setContentView(),而Window只有一个实现类,就是PhoneWindow.看看setContentView()实现

    @Override
        public void setContentView(int layoutResID) {
            ...
            if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
                final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                        getContext());
                transitionTo(newScene);
            } else {
                mLayoutInflater.inflate(layoutResID, mContentParent);
            }
            ...
        }
    

    看到了今天的主角mLayoutInflater,mLayoutInflater是在PhoneWindow的构造方法中初始化的.用mLayoutInflater去加载这个布局(layoutResID).点进去看看实现,来看看createViewFromTag()的实现

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
                boolean ignoreThemeAttr) {
            ...
            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;
        }
    

    可以看到如果mFactory2不为空的话,那么就会调用mFactory2去创建View(mFactory2.onCreateView(parent, name, context, attrs)) . 这句结论很重要.前面的答案已揭晓.如果设置了mFactory2就会用mFactory2去创建View.而mFactory2在上面已经被我们替换了。****

    • 加载皮肤

    我们前面已经了解了换肤的原理,现在就根据上述原理进行换肤。 该框架提供了几种加载皮肤的策略,前后缀价值,apk加载等等。

    SkinCompatManager.getInstance().loadSkin

    总结

    简单总结一下原理(本文精髓)

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

    借鉴应用

    现在根据上述原理低侵入性实现“红涨绿跌”,

    • 接口抽象

    定义一个接口,让支持“红涨绿跌”切换的控件都实现该接口
    SkinCompatChangeGreenRed

    • 所有观察者都集合起来
    • 执行“红涨绿跌”操作的时候通知所有观察者。
    public void notifyChangeColor(boolean isSupport){
            SkinObserver[] arrLocal;
    
            synchronized (this) {
                arrLocal = observers.toArray(new SkinObserver[observers.size()]);
            }
    
            for (int i = arrLocal.length-1; i>=0; i--)
                arrLocal[i].updateChangeColor(this, isSupport? SkinCompatChangeGreenRed.STATE_CHANGE : SkinCompatChangeGreenRed.STATE_DEFAULT);
        }
    
    • 切换颜色的动作 每个控制自己处理。

    相关文章

      网友评论

        本文标题:(Android-skin-support)换肤框架原理研究并运

        本文链接:https://www.haomeiwen.com/subject/bxcqnqtx.html