日/夜模式切换

日/夜模式切换作为一个App的基本功能经常会被使用到,接下来就举出一些常用的日/夜模式切换的方法

使用UIMode的方法
这种方式操作起来比较简单,就是将不同模式下的资源分开存放,然后调用方法切换资源即可

  • 资源存放的路径

  • 切换资源的方法
if (isNight) {
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
recreate();

其中AppCompatDelegate.MODE_NIGHT_YES代表切换到夜间模式,AppCompatDelegate.MODE_NIGHT_NO代表切换到日间模式
如果是在新的Activity中切换日/夜模式则需要用RxBus(关于RxBus的用法可以参照我上篇博客)通知在后台的Activity调用recreate()重启Activity

  • 最终效果

  • 存在问题

由于需要recreate(),会重绘Activity导致屏幕闪烁,重新加载Avtivity时需要注意Activity内元素的保存

使用Theme
通过切换不同的主题来实现切换日/夜模式的效果

  • 在attrs.xml中设置主题中需要替换资源
<resources>
    <attr name="bg" format="color">attr>
    <attr name="button_bg" format="color">attr>
    <attr name="button_tv" format="color">attr>
resources>
  • 设置日/夜模式主题
 

  • 在layout文件中使用arrts中的资源属性
 android:backgroundTint="?attr/button_bg"
  • 在Activity中切换主题
 if(isNight){
    setTheme(R.style.Night);
}else{
    setTheme(R.style.Day);
}

设置主题需要放在setContentView()之前,所以每次切换完日/夜模式后都需要重新加载Activity

  • 存在问题

与UIMode方法相同由于需要recreate(),会重绘Activity导致屏幕闪烁,并且在有比较多的属性需要修改时会导致style比较复杂

为了决解改变日/夜模式后屏幕闪烁的问题,我看了不少博客,终于找到了一个比较符合要求的项目,能够实现uiMode方法的不重建Activity切换日/夜模式。
项目地址:https://github.com/geminiwen/SkinSprite

效果:

具体思路是在Activity创建View的过程中注入自己的代码。
接下来分析一下这个lib的具体代码

  • 首先是SkinnableActivity,继承自AppCompatActivity,在这个Activity中对View的创建进行拦截,我们主要关注三个方法

1)

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    LayoutInflater layoutInflater = LayoutInflater.from(this);
    LayoutInflaterCompat.setFactory(layoutInflater, this);
    super.onCreate(savedInstanceState);
}

注入自己的LayoutInflatorFactory,使inflate在这个LayoutInflaterFactory中执行

2)

@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    if (mSkinnableViewInflater == null) {
        mSkinnableViewInflater = new SkinnableViewInflater();
    }
    final boolean isPre21 = Build.VERSION.SDK_INT < 21;
    final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
    return mSkinnableViewInflater.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 */
    );
}

这是具体需要创建View的方法,可以看到他将具体的创建逻辑放到了SkinnableViewInflater中,这个类之后在做分析

3)

public void setDayNightMode(@AppCompatDelegate.NightMode int nightMode) {
    final boolean isPost21 = Build.VERSION.SDK_INT >= 21;

    getDelegate().setLocalNightMode(nightMode);

    if (isPost21) {
        applyDayNightForStatusBar();
        applyDayNightForActionBar();
    }

    View decorView = getWindow().getDecorView();
    applyDayNightForView(decorView);

}

这是一个我们之后需要切换日夜模式需要调用的方法,其中主要调用逻辑有

    getDelegate().setLocalNightMode(nightMode);

对系统日/夜模式的资源进行切换

if (isPost21) {
    applyDayNightForStatusBar();
    applyDayNightForActionBar();
}

如果api等级大于等于21(即5.0及以上版本)则更换状态栏和标题栏资源

applyDayNightForView(decorView);

对于内容中的日/夜资源进行切换,这个方法我们可以看下他的具体实现

private void applyDayNightForView(View view) {
    if (view instanceof Skinnable) {
        Skinnable skinnable = (Skinnable) view;
        if (skinnable.isSkinnable()) {
            skinnable.applyDayNight();
        }
    }
    if (view instanceof ViewGroup) {
        ViewGroup parent = (ViewGroup)view;
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            applyDayNightForView(parent.getChildAt(i));
        }
    }
}

可以看到这是一个递归的方法,功能是遍历了view下所有的子view,对实现了Skinnable接口并且isSkinnable()返回true的view调用applyDayNight()。可以猜想到这些View就是自定义的View,这个applyDayNight()就是刷新View中资源的方法。

  • 接下来可以看下上面的SkinnableViewInflater这个类
    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 = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new SkinnableTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new SkinnableButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
            case "LinearLayout":
                view = new SkinnableLinearLayout(context, attrs);
                break;
            case "FrameLayout":
                view = new SkinnableFrameLayout(context, attrs);
                break;
            case "RelativeLayout":
                view = new SkinnableRelativeLayout(context, attrs);
                break;
            case "android.support.v7.widget.Toolbar":
                view = new SkinnableToolbar(context, attrs);
                break;
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check it's android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

这个是SkinnableViewInflater中最主要的方法,根据name创建出不同的View,即自定义的View,这里并没有把所有的view都做出来,但是都大同小异,如果不够用还可以自己添加

  • 最后只剩下View的没有看了,由于View比较多而且其中的内容都相似,我们拿SkinnableLinearLayout作为案例进行研究
    public SkinnableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mAttrsHelper = new AttrsHelper();
        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.SkinnableView,
                defStyleAttr, 0);
        mAttrsHelper.storeAttributeResource(a, R.styleable.SkinnableView);
        a.recycle();
    }

    @Override
    public void applyDayNight() {
        Context context = getContext();
        int key;

        key = R.styleable.SkinnableView[R.styleable.SkinnableView_android_background];
        int backgroundResource = mAttrsHelper.getAttributeResource(key);
        if (backgroundResource > 0) {
            Drawable background = ContextCompat.getDrawable(context, backgroundResource);
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                setBackgroundDrawable(background);
            } else {
                setBackground(background);
            }
        }
    }

SkinnableLinearLayout中的思路也比较简单,在构造方法中向mAttrsHelper添加如属性,在需要刷新是再从mAttrsHelper中取出。对于不同的View也只是属性的内容不同而已

至此就是这个项目的大致源码,然后只需要将Activity继承SkinnableActivity,将uiMode中的
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
改为
setDayNightMode(AppCompatDelegate.MODE_NIGHT_YES);
并且不需要调用recreate()方法。需要注意的是还需要在该Activity中添加上android:configChanges=”uiMode”。

结语:这是我第一次写关于阅读源码的博客,尽管选了一个比较简单的lib但还是表达得比较凌乱。我之后还是会多多尝试写这方面的博客,努力提高自己的水平。

参考博客:
android 实现【夜晚模式】的另外一种思路
Android通过改变主题实现夜间模式
Android实现日夜间模式的深入理解

你可能感兴趣的:(Android扩展)