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

主目录见: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知识应用,同时包含了许多的小技巧,都是自定义控件或者其他地方能用到的,是一个解决方案。希望大家有所收获,谢谢坚持看完,说明大哥你是闲人中的战斗机!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,418评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,351评论 0 17
  • OK体魄六六六积极那就考虑考虑 计划 hello
    f8e4ed5b616b阅读 195评论 0 0
  • 刚刚下载简书谢谢让我燃起希望习画念头的美女。 很多文艺情怀沉海,总是不能坚持的半吊子惰性,让我多有惭愧。珍惜和简书...
    waterfront阅读 148评论 2 2