2.1.1 手写动态换肤框架及高可扩展性换肤应用

我们先分析一下源码:

1. 思考xml布局文件是如何parse解析成控件加载到根布局的

用到的类有:

分析点有2条:

    protected void onCreate(Bundle savedInstanceState) {
        //分析1: super.onCreate(savedInstanceState)最终会走到AppCompatActivity.onCreate方法,
        //分析截止是在AppDelegateImpl.installViewFactory
        super.onCreate(savedInstanceState);
        //分析2:分析xml布局是如何经过parse解析添加到content布局中
        // 最终会调用到PhoneWindow.setContentView方法
        setContentView(R.layout.activity_main);
    }

 

MainActivity:

/**
 * Api28源码分析
 */
public class MainActivity extends SkinActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //分析1: super.onCreate(savedInstanceState)最终会走到AppCompatActivity.onCreate方法,
        //分析截止是在AppDelegateImp.installViewFactory
        super.onCreate(savedInstanceState);
        //分析2:分析xml布局是如何经过parse解析添加到content布局中
        // 最终会调用到PhoneWindow.setContentView方法
        //分析截止是在LayoutInflate.createViewFromTag,并最终回调到 mFactory2.onCreateView-->AppDelegateImpl.onCreateView中
        setContentView(R.layout.activity_main);
    }
}

super.onCreate(savedInstanceState);方法会走到AppCompatActivity方法中:

AppCompatActivity:

public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        final AppCompatDelegate delegate = getDelegate();
        //注意这句话,最终会调用到AppCompatDelegateImpl.installViewFactory()中去
        //作用就是设置Factory2,当调用Factory2.onCreateView方法的时候,会走到AppCompatDelegateImpl的onCreateView里
        delegate.installViewFactory();
        delegate.onCreate(savedInstanceState);
        super.onCreate(savedInstanceState);
    }
}

 delegate.installViewFactory();会走到AppCompatDelegateImpl的installViewFactory方法中

AppCompatDelegateImpl:


/**
 * @author Eason
 * @createtime 2020/3/19
 * @desc 注意实现了LayoutInflater.Factory2接口
 */
class AppCompatDelegateImpl extends AppCompatDelegate implements MenuBuilder.Callback, LayoutInflater.Factory2 {

    /**
     * 是在LayoutInflater这个类中的inflate方法中
     * 会被view = mFactory2.onCreateView(parent, name, context, attrs);调用到这个方法里面来
     */
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        return createView(parent, name, context, attrs);
    }

    public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        //下面的if语句就是想方设法创建出来mAppCompatViewInflater
        if (mAppCompatViewInflater == null) {
            TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
            String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
            if ((viewInflaterClassName == null) || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                mAppCompatViewInflater = new AppCompatViewInflater();
            } else {
                try {
                    Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
                    mAppCompatViewInflater =
                            (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                    .newInstance();
                } catch (Throwable t) {
                    Log.i(TAG, "Failed to instantiate custom view inflater "
                            + viewInflaterClassName + ". Falling back to default.", t);
                    mAppCompatViewInflater = new AppCompatViewInflater();
                }
            }
        }

        //最终会调用到AppCompatViewInflater.createView方法中去
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* 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 */
        );
    }

    /**
     * 这个方法是在 delegate.installViewFactory()中调用到
     * 作用:设置setFactory2,把自己作为 LayoutInflaterCompat.setFactory2的第二个参数传进去
     * Factory2是LayoutInflater的内部类,里面只有一个方法public View onCreateView()
     * 隐藏当调用到mFactory2.onCreateView的时候实际就会调到自己这个类AppCompatDelegateImpl的onCreateView方法
     */
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            LayoutInflaterCompat.setFactory2(layoutInflater, this);
        } else {
            if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

}

以上就是super.onCreate(savedInstanceState);的分析,其作用就是设置Factory2

 

接下来分析setContentView(R.layout.activity_main):

会走到PhoneWindow的setContent方法中

PhoneWindow:

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    /**
     * MainActivity.setContentView-->Activity.setContentView-->PhoneWindow.setContentView
     *
     * @param layoutResID 传入的布局id,此处可以理解为R.layout.activity_main
     */
    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            /**
             *  这个方法作用:
             *  1.生成DecorView对象(DecorView是继承FrameLayout)
             *  2.初始化mContentParent对象,就是R.layout.screen_simple布局中@android:id/content的FragmeLayout
             */
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            /**
             *这个方法作用:
             * 将传进来的布局加载到mContentParent容器中[在讲换肤的时候会详细讲这个方法]
             */
            //会走到LayoutInflate这个类中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

    /**
     * 里面有两个重要的方法:
     * 1. generateDecor:如果mDecor为空,则生成DecorView
     * 2. generateLayout:
     */
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //生成DecorView,DecorView是继承FrameLayout
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            //
            mContentParent = generateLayout(mDecor);
        }
    }

    /**
     * 生成DecorView 对象
     */
    protected DecorView generateDecor(int featureId) {
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

    /**
     *找相应的加载布局,加载到DecorView中,然后返回布局中FragmeLayout对象
     * @param decor
     * @return 布局中id为ID_ANDROID_CONTENT的容器
     */
    protected ViewGroup generateLayout(DecorView decor) {
        TypedArray a = getWindowStyle();
        //省略...
        //解析属性值
        if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
            requestFeature(FEATURE_NO_TITLE);
        } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
            requestFeature(FEATURE_ACTION_BAR);
        }

        if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) {
            requestFeature(FEATURE_ACTION_BAR_OVERLAY);
        }

        if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) {
            requestFeature(FEATURE_ACTION_MODE_OVERLAY);
        }

        // 根据features和属性的不同,来给layoutResource赋值,实际就是找相应的加载布局
        int layoutResource;
        int features = getLocalFeatures();
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
            setCloseOnSwipeEnabled(true);
        } else if {
            //省略...
        }else{
            layoutResource = R.layout.screen_simple;
        }

        mDecor.startChanging();
        //将找到的布局layoutResource加入到DecorView中去
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
        //找到layoutResource布局中id为ID_ANDROID_CONTENT的控件,然后返回
        ViewGroup contentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT);
        //省略...
        return contentParent;
    }
}

mLayoutInflater.inflate(layoutResID, mContentParent);会走到LayoutInflate这个类中

LayoutInflate:

public abstract class LayoutInflater {

    /**
     * 是被PhoneWindow.setContentView方法调用到
     *
     * @param resource 实际要加载的布局,如R.layout.activity_main
     * @param root 容器布局id为content的那个
     * @return 返回布局
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            //注意这句话
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            View result = root;
            //...
            if (TAG_MERGE.equals(name)) {
                //如果含有merge标签
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //root就是R.layout.screen_simple布局中@android:id/content的FragmeLayout
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                rInflateChildren(parser, temp, attrs, true);
                if (root != null && attachToRoot) {
                    root.addView(temp, params);
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
                return result;
            }
        }
    }

    private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }

    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
        // Apply a theme wrapper, if allowed and one is specified.
        if (!ignoreThemeAttr) {
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            if (themeResId != 0) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);
        }

        View view;

        //由于在MainActivity的super.onCreate(savedInstanceState);方法中设置了Factory2,并将AppCompatDelegateImpl设置为第二个参数
        if (mFactory2 != null) {
            //如果自己设置了mFactory2,则会走到mFactory2.onCreateView方法里面,通过debug发现mFactory2就是AppCompatDelegateImpl
            //而实际就是调用AppCompatDelegateImpl方法中去
            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 {
                //判断是否是自定义View
                if (-1 == name.indexOf('.')) {
                    //最终也是会走到createView
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;

    }

    protected View onCreateView(View parent, String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return onCreateView(name, attrs);
    }

    protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

    /**
     * 真正的通过反射来创建View
     */
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        //...

        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        mConstructorArgs[0] = lastContext;
        return view;
    }

    public interface Factory2 extends Factory {
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }
}

以上就是针对源码的分析。

那么回到主题如何根据理解的源码进行系统内换肤呢?

方案:将布局中的常规控件进行包装成可换肤的控件,如TextView-->SkinTextView,其中SkinTextView实现了changeSkin接口。

当用户点击切换的时候,修改系统配置为夜间模式或日间模式,然后执行代码:获取根布局DecorView,遍历控件取得实现changeSkin接口的布局然后调用改变布局的方法。

先看效果:

日间模式:

夜间模式:

第二个界面:

 

注意,改变第一个界面第二个界面也会被改变。

接下来实现:

新建lib_skin包:

ISkinable:

/**
 * @author Eason
 * @createtime 2020/3/19
 * @desc 皮肤切换的接口,所有需要修改日/夜间模式的都要实现这个接口
 */
public interface ISkinable {

    /**
     * 切换模式
     */
    void changeSkin();
}

MyAppCompatViewInflater:

/**
 * @author Eason
 * @createtime 2020/3/19
 * @desc 实现自定义的AppCompatViewInflater,主要功能就是对各种View进行匹配封装成自己的SkinXxView
 */
public class MyAppCompatViewInflater extends AppCompatViewInflater {
    private final String TAG = this.getClass().getSimpleName();
    private Context mContext;

    public MyAppCompatViewInflater(Context context) {
        this.mContext = context;
    }

    public View generateView(String name, AttributeSet attrs) {
        Log.d(TAG, "generateView-->" + name);
        View view;
        switch (name) {
            case "TextView":
                view = new SkinTextView(mContext, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = new SkinButton(mContext, attrs);
                verifyNotNull(view, name);
                break;
            case "androidx.constraintlayout.widget.ConstraintLayout":
                view = new SkinConstraintLayout(mContext, attrs);
                verifyNotNull(view, name);
                break;
            //ImageView、LinearLayout、RelativeLayout、自定义View...
            default:
                view = super.createView(mContext, name, attrs);
        }
        return view;
    }

    private void verifyNotNull(View view, String name) {
        if (view == null) {
            throw new IllegalStateException(this.getClass().getName()
                    + " asked to inflate view for <" + name + ">, but returned null");
        }
    }
}

SkinConstraintLayout:

public class SkinConstraintLayout extends ConstraintLayout implements ISkinable {
    private final String TAG = this.getClass().getSimpleName();
    private int[] mAttrs = R.styleable.SkinConstraintLayout;
    private SparseIntArray mResourceMap = new SparseIntArray();

    public SkinConstraintLayout(Context context) {
        this(context, null);
    }

    public SkinConstraintLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SkinConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, mAttrs, defStyleAttr, 0);
        for (int i = 0; i < a.length(); i++) {
            mResourceMap.put(mAttrs[i], a.getResourceId(i, -1));
        }
        a.recycle();
    }

    @Override
    public void changeSkin() {
        Log.d(TAG, "changeSkin--->" + TAG);
        //设置background
        int backgroundKey = mAttrs[R.styleable.SkinConstraintLayout_android_background];
        int backgroundId = mResourceMap.get(backgroundKey);
        if (backgroundId > 0) {
            setBackground(ContextCompat.getDrawable(getContext(), backgroundId));
        }
    }
}

SkinTextView:

/**
 * 继承TextView兼容包,9.0源码中也是如此
 * 参考:AppCompatViewInflater.java
 * 86行 + 138行 + 206行
 */
public class SkinTextView extends AppCompatTextView implements ISkinable {
    private final String TAG = this.getClass().getSimpleName();

    private int[] mAttrs = R.styleable.SkinTextView;
    private SparseIntArray mResourceMap = new SparseIntArray();

    public SkinTextView(Context context) {
        this(context, null);
    }

    public SkinTextView(Context context, AttributeSet attrs) {
        this(context, attrs, android.R.attr.textViewStyle);
    }

    public SkinTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 根据自定义属性,匹配控件属性的类型集合,如:background + textColor
        TypedArray a = context.obtainStyledAttributes(attrs, mAttrs, defStyleAttr, 0);
        //R.styleable.SkinTextView 为在attrs定义的属性值数组
        for (int i = 0; i < mAttrs.length; i++) {
            mResourceMap.put(mAttrs[i], a.getResourceId(i, -1));
        }
        a.recycle();

    }

    @Override
    public void changeSkin() {
        Log.d(TAG, "changeSkin--->" + TAG);
        //设置background
        // 根据自定义属性,获取styleable中的background属性
        int backgroundKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_background];
        int backgroundId = mResourceMap.get(backgroundKey);
        if (backgroundId > 0) {
            setBackgroundDrawable(ContextCompat.getDrawable(getContext(), backgroundId));
        }
        //设置textColor
        int textColorKey = R.styleable.SkinTextView[R.styleable.SkinTextView_android_textColor];
        int textColorId = mResourceMap.get(textColorKey);
        if (textColorId > 0) {
            setTextColor(ContextCompat.getColorStateList(getContext(), textColorId));
        }
    }
}

Const:

public class Const {
    public static final String SP_NAME = "sp_lib_skin";
    public static final String KEY_NIGHT_MODE = "isNight";
}

SkinActivity:

/**
 * 如果要实现换肤功能,所有Activity要继承SkinActivity,并复写isOpenSkin方法
 * 因为默认是关闭的
 */
public class SkinActivity extends AppCompatActivity {
    private final String TAG = this.getClass().getSimpleName();

    private MyAppCompatViewInflater mAppCompatViewInflater;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (isOpenSkin()) {
            boolean isNight = getSharedPreferences(Const.SP_NAME, MODE_PRIVATE).getBoolean(Const.KEY_NIGHT_MODE, false);
            getDelegate().setLocalNightMode(isNight ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
            LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), this);
        }
        super.onCreate(savedInstanceState);
    }

    /**
     * 是否开启换肤功能
     *
     * @return true:开启换肤功能,子类可以复用
     */
    protected boolean isOpenSkin() {
        return false;
    }

    /**
     * 此方法会被多次调用,界面只有一个 ImageView 和 Button时候的日志打印如下
     * onCreateView-->LinearLayout
     * onCreateView-->LinearLayout
     * onCreateView-->ViewStub
     * onCreateView-->ViewStub
     * onCreateView-->FrameLayout
     * onCreateView-->FrameLayout
     * onCreateView-->androidx.appcompat.widget.ActionBarOverlayLayout
     * onCreateView-->androidx.appcompat.widget.ActionBarOverlayLayout
     * onCreateView-->androidx.appcompat.widget.ContentFrameLayout
     * onCreateView-->androidx.appcompat.widget.ContentFrameLayout
     * onCreateView-->androidx.appcompat.widget.ActionBarContainer
     * onCreateView-->androidx.appcompat.widget.ActionBarContainer
     * onCreateView-->androidx.appcompat.widget.Toolbar
     * onCreateView-->androidx.appcompat.widget.Toolbar
     * onCreateView-->androidx.appcompat.widget.ActionBarContextView
     * onCreateView-->androidx.appcompat.widget.ActionBarContextView
     * onCreateView-->androidx.constraintlayout.widget.ConstraintLayout
     * onCreateView-->androidx.constraintlayout.widget.ConstraintLayout
     * onCreateView-->ImageView
     * onCreateView-->ImageView
     * onCreateView-->Button
     * onCreateView-->Button
     */
    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        if (isOpenSkin()) {
            //防止重复创建
            if (mAppCompatViewInflater == null) {
                mAppCompatViewInflater = new MyAppCompatViewInflater(context);

            }
            return mAppCompatViewInflater.generateView(name, attrs);
        } else {
            return super.onCreateView(name, context, attrs);
        }
    }

    /**
     * 设置显示模式(夜间/日间)
     *
     * @param isNight true:夜间模式
     */
    protected void setNightMode(boolean isNight) {
        if (isOpenSkin()) {
            //int uiMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
            getSharedPreferences(Const.SP_NAME, MODE_PRIVATE).edit().putBoolean(Const.KEY_NIGHT_MODE, isNight).apply();
            getDelegate().setLocalNightMode(isNight ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
            View decorView = getWindow().getDecorView();
            setNightMode(decorView);
        }
    }

    /**
     * 给具体View设置夜间模式
     *
     * @param view 具体控件
     */
    private void setNightMode(View view) {
        if (view instanceof ISkinable) {
            //如果实现了ISkinable接口,则调用接口changeSkin方法改变皮肤
            ((ISkinable) view).changeSkin();
        }
        if (view instanceof ViewGroup) {
            //继续遍历子节点
            int childCount = ((ViewGroup) view).getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = ((ViewGroup) view).getChildAt(i);
                setNightMode(child);
            }
        }
    }
}

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- SkinXx的统一控件属性 -->
    <declare-styleable name="SkinCommonAttr">
        <attr name="android:background" />
        <attr name="android:textColor" />
    </declare-styleable>

    <!-- SkinConstraintLayout控件属性 -->
    <declare-styleable name="SkinConstraintLayout">
        <attr name="android:background" />
    </declare-styleable>

    <!-- TextView控件属性 -->
    <declare-styleable name="SkinTextView">
        <attr name="android:background" />
        <attr name="android:textColor" />
        <attr name="android:textSize" />
    </declare-styleable>
    <!-- SkinButton控件属性 -->
    <declare-styleable name="SkinButton">
        <attr name="android:background" />
        <attr name="android:textColor" />
    </declare-styleable>


</resources>

以上就是lib_skin的编写。

 

接下来我们看看该如何在app内使用:

1. 首先在app的build.gradle引入lib_skin依赖

2. MainAtivity继承SkinActivity并开启isOpenSkin标志

调用方法 

setNightMode(true);开启夜间模式

setNightMode(false);关闭夜间模式

public class MainActivity extends SkinActivity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
        textView.setText("当前模式" + getDelegate().getLocalNightMode());
    }

    public void jump(View view) {
        startActivity(new Intent(this, SecondActivity.class));
    }

    public void nightMode(View view) {
        setNightMode(true);
        textView.setText("当前模式" + getDelegate().getLocalNightMode());
    }

    public void dayMode(View view) {
        setNightMode(false);
        textView.setText("当前模式" + getDelegate().getLocalNightMode());
    }

    @Override
    protected boolean isOpenSkin() {
        return true;
    }
}

为了实现这个效果需要用两套color.xml文件

values-colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>

    <color name="color_group_bg">#FF6600</color>
    <color name="color_txt">#f00</color>
    <color name="color_bg">#0f0</color>
</resources>

values-night-colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#1D8F43</color>
    <color name="colorPrimaryDark">#4DC67A</color>
    <color name="colorAccent">#262E2D</color>

    <color name="color_group_bg">#252424</color>
    <color name="color_txt">#000000</color>
    <color name="color_bg">#FFFFFF</color>
</resources>

运行发现功能实现了:

但是在切换的时候会出现卡顿一下的效果。

为了解决这个问题,需要在manifest中Activity配置android:configChanges="uiMode":

这样就可以了。

但是加上这个句话的时候,toolbar和状态栏已经导航栏都没有切换成功,模拟器6.0.1版本。

如下图:

接下来我们引入几个工具类针对性处理他们:

StatusBarUtils :

public class StatusBarUtils {

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void forStatusBar(Activity activity) {
        TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[]{
                android.R.attr.statusBarColor
        });
        int color = a.getColor(0, 0);
        activity.getWindow().setStatusBarColor(color);
        a.recycle();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public static void forStatusBar(Activity activity, int skinColor) {
        activity.getWindow().setStatusBarColor(skinColor);
    }
}

NavigationUtils:

public class NavigationUtils {

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void forNavigation(Activity activity) {
        TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[] {
                android.R.attr.statusBarColor
        });
        int color = a.getColor(0, 0);
        activity.getWindow().setNavigationBarColor(color);
        a.recycle();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void forNavigation(Activity activity, int skinColor) {
        activity.getWindow().setNavigationBarColor(skinColor);
    }
}

ActionBarUtils:

public class ActionBarUtils {

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void forActionBar(AppCompatActivity activity) {
        TypedArray a = activity.getTheme().obtainStyledAttributes(0, new int[]{
                android.R.attr.colorPrimary
        });
        int color = a.getColor(0, 0);
        a.recycle();
        ActionBar actionBar = activity.getSupportActionBar();
        if (actionBar != null) {
            actionBar.setBackgroundDrawable(new ColorDrawable(color));
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static void forActionBar(AppCompatActivity activity, int skinColor) {
        ActionBar actionBar = activity.getSupportActionBar();
        if (actionBar != null) {
            actionBar.setBackgroundDrawable(new ColorDrawable(skinColor));
        }
    }
}

拓展:实际上是用到了Android系统自带的模式切换,但同样也要两套color:

 

END.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值