Android高级进阶(腾讯,绿网天下,哔哩哔哩,虎扑工程师联合出品)Android开发经验谈Android开发

View的绘制(6)-换肤框架实现解析(二)

2017-09-13  本文已影响185人  ZJ_Rocky

主目录见:Android高级进阶知识(这是总目录索引)
 终于迎来我们的换肤框架最终章了,前面我们也学了support v7的源码了,那么今天我们这里就轻松很多,废话不多说,直接开始讲解。想要源码的直接[点击下载],这老哥稳!!!大家向他学习。

敬礼

一.目标

看过前面几篇的问题都知道,文章前面会有目标说明,今天不例外。
1.复习《Support v7库》的知识点。
2.说下framework怎么加载资源。
3.同时说明下Application的registerActivityLifecycleCallbacks方法。

二.源码分析

1.基础使用

在Application的onCreate中初始化:

 SkinCompatManager.withoutActivity(this)                         // 基础控件换肤初始化
            .addStrategy(new CustomSDCardLoader())                  // 自定义加载策略,指定SDCard路径[可选]
            .addHookInflater(new SkinHookAutoLayoutViewInflater())  // hongyangAndroid/AndroidAutoLayout[可选]
            .addInflater(new SkinMaterialViewInflater())            // material design 控件换肤初始化[可选]
            .addInflater(new SkinConstraintViewInflater())          // ConstraintLayout 控件换肤初始化[可选]
            .addInflater(new SkinCardViewInflater())                // CardView v7 控件换肤初始化[可选]
            .addInflater(new SkinCircleImageViewInflater())         // hdodenhof/CircleImageView[可选]
            .addInflater(new SkinFlycoTabLayoutInflater())          // H07000223/FlycoTabLayout[可选]
            .setSkinStatusBarColorEnable(false)                     // 关闭状态栏换肤,默认打开[可选]
            .setSkinWindowBackgroundEnable(false)                   // 关闭windowBackground换肤,默认打开[可选]
            .loadSkin();

加载插件皮肤库:

// 指定皮肤插件
SkinCompatManager.getInstance().loadSkin("new.skin"[, SkinLoaderListener], int strategy);

// 恢复应用默认皮肤
SkinCompatManager.getInstance().restoreDefaultTheme();

本来以前库是要继承SkinCompatActivity的,但是现在用了registerActivityLifecycleCallbacks监听了Activity的生命周期,这样可以燥起来,就不用继承了,等会会说明。

2.SkinCompatManager withoutActivity

这个地方遵循看源码的一贯步骤,我们来看下withoutActivity方法到底是干了啥?方法如下:

  public static SkinCompatManager withoutActivity(Application application) {
        init(application);
        SkinActivityLifecycle.init(application);
        return sInstance;
    }

这个方法总共就两个方法,我们顺序来看init方法干了啥:

   public static SkinCompatManager init(Context context) {
        if (sInstance == null) {
            synchronized (SkinCompatManager.class) {
                if (sInstance == null) {
                    sInstance = new SkinCompatManager(context);
                }
            }
        }
        return sInstance;
    }

这个方法其实就是创建一个SkinCompatManager这个单例对象,那我们就看这个构造函数里面做了些啥:

   private SkinCompatManager(Context context) {
        mAppContext = context.getApplicationContext();
        SkinPreference.init(mAppContext);
        SkinCompatResources.init(mAppContext);
        initLoaderStrategy();
    }

这个方法主要是做一些初始化工作,第一个类SkinPreference其实就是对SharePreference的封装,SkinCompatResources其实就是存放包名,皮肤名,加载策略(就是从什么途径加载)等等,同时有一些加载资源的方法。最后initLoaderStrategy()方法是加载默认的皮肤包加载策略。
 看完第一个方法,我们现在看第二个方法SkinActivityLifecycle.init(application),这个方法非常关键,我们先看看init做了啥?

   public static SkinActivityLifecycle init(Application application) {
        if (sInstance == null) {
            synchronized (SkinActivityLifecycle.class) {
                if (sInstance == null) {
                    sInstance = new SkinActivityLifecycle(application);
                }
            }
        }
        return sInstance;
    }

首先也是初始化一个SkinActivityLifecycle单例类,我们也直接跟进构造函数里面:

 private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }

这个地方就是一句话,但是非常关键,记得在LeakCanary源码里面也有用到这个方法。这个方法是拦截Acitivity的生命周期方法来进行统一处理,这样的话我们不用让我们的Acitivity继承SkinCompatActivity(所以现在这个类被标记为废弃),就可以在每个Acitivity创建的时候做点动作了,非常管用。具体的Activity的生命周期里面做了哪些动作,我们下面会重点讲解。

3.SkinCompatManager loadSkin

这个方法是用来加载皮肤包的,我们这里先看loadSkin做了些什么东西。

   public AsyncTask loadSkin() {
        String skin = SkinPreference.getInstance().getSkinName();
        int strategy = SkinPreference.getInstance().getSkinStrategy();
        if (TextUtils.isEmpty(skin) || strategy == SKIN_LOADER_STRATEGY_NONE) {
            return null;
        }
        return loadSkin(skin, null, strategy);
    }

我们看到这个地方,首先获取本地是否有皮肤包名称和皮肤包策略的记录,如果有则取出来进行加载。

 public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {
        return new SkinLoadTask(listener, mStrategyMap.get(strategy)).execute(skinName);
    }

这个地方用到了SkinLoadTask来进行加载,SkinLoadTask是一个AsyncTask对象,所以我们先看onPreExecute()方法:

 protected void onPreExecute() {
            if (mListener != null) {
                mListener.onStart();
            }
        }

这个方法没有做啥,就是调用接口的onStart方法,但是我们前面创建来的Listener是null,所以这个地方没有调用。接下来我们看到doInBackground()方法:

        @Override
        protected String doInBackground(String... params) {
            synchronized (mLock) {
                while (mLoading) {
                    try {
                        mLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                mLoading = true;
            }
            try {
                if (params.length == 1) {
                    if (TextUtils.isEmpty(params[0])) {
                        SkinCompatResources.getInstance().reset();
                        return params[0];
                    }
                    if (!TextUtils.isEmpty(
                            mStrategy.loadSkinInBackground(mAppContext, params[0]))) {
                        return params[0];
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            SkinCompatResources.getInstance().reset();
            return null;
        }

这个方法前面加了一个同步代码块进行同步操作,然后我们看到调用了SkinCompatResources.getInstance().reset();将这个对象之前存进去的皮肤包名称,策略等信息重置。接着调用mStrategy.loadSkinInBackground(mAppContext, params[0])方法,这个地方mStrategy就是加载策略,内置了三个策略分别为:SkinAssetsLoader,SkinBuildInLoader,SkinSDCardLoader,第一个就是从Assets中加载皮肤包,第二个就是从本应用中加载皮肤包,第三个是从SD卡中加载皮肤包。这个地方我们用从SD卡中加载皮肤包为例子,因为这个场景用到还比较多。

4.SkinSDCardLoader loadSkinInBackground

这个类是个抽象类,为什么设置为抽象类,是因为SD卡的目录作者希望留给用户自己设置。我们直接进入到这个类的loadSkinInBackground方法:

 public String loadSkinInBackground(Context context, String skinName) {
        String skinPkgPath = getSkinPath(context, skinName);
        if (SkinFileUtils.isFileExists(skinPkgPath)) {
            String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);
            Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);
            if (resources != null && !TextUtils.isEmpty(pkgName)) {
                SkinCompatResources.getInstance().setupSkin(
                        resources,
                        pkgName,
                        skinName,
                        this);
                return skinName;
            }
        }
        return null;
    }

我们看到第一句就是获取皮肤包的路径,这个方法由用户自己指定,我们可以集成这个类进行重写。然后程序判断目录是否存在,如果存在则获取皮肤包的包名:

 public String getSkinPackageName(String skinPkgPath) {
        PackageManager mPm = mAppContext.getPackageManager();
        PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
        return info.packageName;
    }

然后获取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;
    }

这个地方有个知识点:就是系统是怎么加载资源的。要详细可以看Android应用程序资源管理器(Asset Manager)的创建过程分析 Android应用程序资源的查找过程分析 。这个地方我们简单归纳就是会调用AssetManager里面的final方法addAssetPath(),所以我们传进皮肤包的路径,反射调用这个方法进行添加即可。这样我们的外部皮肤包就被加进去了。

5.SkinLoadTask onPostExecute

看完onPreExecute()方法和doInBackground()方法我们就来看onPostExecute方法了:

        protected void onPostExecute(String skinName) {
            SkinLog.e("skinName = " + skinName);
            synchronized (mLock) {
                // skinName 为""时,恢复默认皮肤
                if (skinName != null) {
                    SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor();
                    notifyUpdateSkin();
                    if (mListener != null) mListener.onSuccess();
                } else {
                    SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor();
                    if (mListener != null) mListener.onFailed("皮肤资源获取失败");
                }
                mLoading = false;
                mLock.notifyAll();
            }
        }

这里首先是将我们的策略类型保存起来,然后调用notifyUpdateSkin(),这个方法是做什么呢?因为这边加载完皮肤会通知Acitivity里面的视图控件跟着皮肤进行变化,这里用到了观察者设计模式,在每个Activity onResume的时候会将Activity添加为观察者,所以这个地方notifyUpdateSkin就是调用到

   observer = new SkinObserver() {
                @Override
                public void updateSkin(SkinObservable observable, Object o) {
                    updateStatusBarColor(activity);
                    updateWindowBackground(activity);
                    getSkinDelegate((AppCompatActivity) activity).applySkin();
                }
            };

刷新状态栏,刷新背景,然后通知每个控件进行重新设置皮肤。跟之前套路不一样,我们将要进入主要知识讲解了。为了蹭iphone发布会热点,决定轻松一刻:


卖肾

6.SkinActivityLifecycle onActivityCreated

我们知道之前有一句话:

 private SkinActivityLifecycle(Application application) {
        application.registerActivityLifecycleCallbacks(this);
    }

这句话已经将SkinActivityLifecycle 设置为生命周期拦截器了,这样我们就可以拦截到Acitivity的每个生命周期,我们先来看我们Activity创建的生命周期即onActivityCreated:

@Override
    public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        if (activity instanceof AppCompatActivity) {
            LayoutInflater layoutInflater = activity.getLayoutInflater();
            try {
                Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
                field.setAccessible(true);
                field.setBoolean(layoutInflater, false);
                LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),
                        getSkinDelegate((AppCompatActivity) activity));
            } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {
                e.printStackTrace();
            }
            updateStatusBarColor(activity);
            updateWindowBackground(activity);
        }
    }

看到这个方法我们应该有种似曾相识的感觉,没错!这里其实跟support V7的源码是一样的。重点强调:LayoutInflaterCompat.setFactory(activity.getLayoutInflater(),getSkinDelegate((AppCompatActivity) activity));这个地方就是自定义Factory拦截View的创建过程。那我们还是那个套路看下getSkinDelegate()这个方法做了啥:

    private SkinCompatDelegate getSkinDelegate(AppCompatActivity activity) {
        if (mSkinDelegateMap == null) {
            mSkinDelegateMap = new WeakHashMap<>();
        }

        SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(activity);
        if (mSkinDelegate == null) {
            mSkinDelegate = SkinCompatDelegate.create(activity);
        }
        mSkinDelegateMap.put(activity, mSkinDelegate);
        return mSkinDelegate;
    }

这个地方还有缓存,作者不错,首先看HashMap(这里要用WeakHashMap主要是因为持有了Activity的实例,为了防止内存泄漏所以用了弱引用的HashMap)里面有没有SkinCompatDelegate对象,没有则创建。SkinCompatDelegate是个LayoutInflaterFactory即Factory对象,所以我们xml里面View创建的时候会调用Factory里面的onCreateView()方法。

7.SkinCompatDelegate onCreateView

因为这个方法拦截了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内,到时调用notifyUpdateSkin的时候会调用刷新
            mSkinHelpers.add(new WeakReference<SkinCompatSupportable>((SkinCompatSupportable) view));
        }

        return view;
    }

我们知道我们调用createView方法,那么创建view的任务就是这个方法了:

   public View createView(View parent, final String name, @NonNull Context context,
                           @NonNull AttributeSet attrs) {
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mSkinCompatViewInflater == null) {
            mSkinCompatViewInflater = new SkinCompatViewInflater();
        }

        // We only want the View to inherit its context if we're running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);

        return mSkinCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

前面初始化了mSkinCompatViewInflater对象然后调用它的createView方法,我们继续跟进去:

public 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(context);
        }

        View view = createViewFromHackInflater(context, name, attrs);

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        if (view == null) {
            view = createViewFromFV(context, name, attrs);
        }

        if (view == null) {
            view = createViewFromV7(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;
    }

前面跟我们support v7源码是一样的,我们直接看到createViewFromHackInflater方法,这个方法我们看到尝试创建了view对象:

   private View createViewFromHackInflater(Context context, String name, AttributeSet attrs) {
        View view = null;
        for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getHookInflaters()) {
            view = inflater.createView(context, name, attrs);
            if (view == null) {
                continue;
            } else {
                break;
            }
        }
        return view;
    }

我们看到代码应该很熟悉呀,这个方法其实就是让用户可以设置SkinLayoutInflater,然后来得到优先拦截创建view的能力。我们假装用户没有设置,那么返回的view就会为null,那么就会进入下一个createViewFromFV()方法:

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

我们可以看到这里拦截好多控件的创建过程。为了不使我们的文章冗长,这边就挑一个控件来说明,这里我们挑讲解support v7时候同样的控件TextView控件来讲解。

8.SkinCompatTextView

这是个自定义的TextView(如果自己要往这个库加入什么新的控件,套路也是一样的),又因为到时通知更新要更新控件,所以每个控件必须实现SkinCompatSupportable接口:

public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {
}

我们接下来看到构造函数:

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

这里主要有两个类SkinCompatBackgroundHelper和SkinCompatTextHelper,很明显第一个是用来设置背景的,第二个类是设置Text相关外形的。我们先来看SkinCompatBackgroundHelper的loadFromAttributes方法:

 public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
        TypedArray a = mView.getContext().obtainStyledAttributes(attrs, R.styleable.SkinBackgroundHelper, defStyleAttr, 0);
        try {
            if (a.hasValue(R.styleable.SkinBackgroundHelper_android_background)) {
                mBackgroundResId = a.getResourceId(
                        R.styleable.SkinBackgroundHelper_android_background, INVALID_ID);
            }
        } finally {
            a.recycle();
        }
        applySkin();
    }

这里清晰明了,对老司机来说这都不是事,获取自定义属性backgroud,但是这里有个小技巧我们来看下SkinBackgroundHelper_android_background对应的属性是啥:

  <declare-styleable name="SkinBackgroundHelper">
        <attr name="android:background" />
    </declare-styleable>

看到没有!!!看到没有!!!其实他对应的就是系统的background,为什么这么做呢?就是为了我们在写background的时候不需要麻烦再去自定义,只要写上background即可,是不是处处有干货,这干货很干。
接下来我们看下SkinCompatTextHelper的loadFromAttributes:

 public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
        final Context context = mView.getContext();

        // First read the TextAppearance style id
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SkinCompatTextHelper, defStyleAttr, 0);
        final int ap = a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);
        SkinLog.d(TAG, "ap = " + ap);

        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {
            mDrawableLeftResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {
            mDrawableTopResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {
            mDrawableRightResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, INVALID_ID);
        }
        if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {
            mDrawableBottomResId = a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, INVALID_ID);
        }
        a.recycle();

        if (ap != INVALID_ID) {
            a = context.obtainStyledAttributes(ap, R.styleable.SkinTextAppearance);
            if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
                mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
                SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
            }
            if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
                mTextColorHintResId = a.getResourceId(
                        R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
                SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
            }
            a.recycle();
        }

        // Now read the style's values
        a = context.obtainStyledAttributes(attrs, R.styleable.SkinTextAppearance, defStyleAttr, 0);
        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {
            mTextColorResId = a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);
            SkinLog.d(TAG, "mTextColorResId = " + mTextColorResId);
        }
        if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {
            mTextColorHintResId = a.getResourceId(
                    R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);
            SkinLog.d(TAG, "mTextColorHintResId = " + mTextColorHintResId);
        }
        a.recycle();
        applySkin();
    }

看着这么多代码不要被吓到,其实非常简单,我们看下style:

    <declare-styleable name="SkinCompatTextHelper">
        <attr name="android:drawableLeft" />
        <attr name="android:drawableTop" />
        <attr name="android:drawableRight" />
        <attr name="android:drawableBottom" />
        <attr name="android:drawableStart" />
        <attr name="android:drawableEnd" />
        <attr name="android:textAppearance" />
    </declare-styleable>

其实就是获取这些属性值。我去。。。。上面代码白贴了。最后会调用applySkin()方法,就是去设置这些属性:

  public void applySkin() {
        applyTextColorResource();
        applyTextColorHintResource();
        applyCompoundDrawablesResource();
    }

从方法名就可以很清晰地看出来。就不过多纠结了,这个方法applySkin是SkinCompatSupportable 接口里面的,在观察者模式提示更新的时候也会调用到这个方法,所以我们自定义的换肤控件都必须实现这个接口,这是个约定。
到这里我们的讲解就完成,这篇真的是干货和技能MAX,妈妈再也不用担心我求干货了。

干货君
总结:这个换肤框架是比较综合的一个support v7知识应用,同时包含了许多的小技巧,都是自定义控件或者其他地方能用到的,是一个解决方案。希望大家有所收获,谢谢坚持看完,说明大哥你是闲人中的战斗机!!!
上一篇 下一篇

猜你喜欢

热点阅读