Android开发之路高级UI安卓UI

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

2019-04-26  本文已影响70人  成虫_62d0

背景

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

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

原理

    public interface SkinCompatSupportable {
        void applySkin();
    }

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

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


注意  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原生控件的时候返回我们想要的支持“换肤”动作的对应控件。

代码如下

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);
    }
上一篇 下一篇

猜你喜欢

热点阅读