Android 开发 关于避免切换夜间模式闪屏的几种方式

4,097 阅读5分钟

Android开发

关于避免切换主题时闪屏的几种方式


在activity中调用setTheme来切换夜间模式的方法可能大家有看过相关的文章了,但是调用setTheme设置的主题后界面并没有变化,这时需要调用activity的recreate方法另设置的主题生效,但是试过的朋友们都知道,activity调用recreate方法以后会有一瞬间的闪屏

今天写这篇文章的主题主要是关于如何避免setTheme切换主题后调用recreate的闪屏
关于如何通过改变theme更换主题的文章如果您还没有看过的话可以看一下这篇文章或者自行搜索一下

recreate
recreate


效果实现

1.属性动画 实现

使用属性动画配合ArgbEvaluator这个类来对所有需要变换颜色的View设置一个渐变动画

属性动画
属性动画

这个方法的缺点有一下几个:

  • 只要是需要有颜色变化的View要设置id并通过findViewById获取其对象,增加代码量,大家都知道设置id写findViewById有多蛋疼吧(笔者最讨厌设置控件的id了)
  • 每一个设置的attr的color就需要写一个属性动画,代码量又增多了
  • RecyclerView或ListView还有某些特殊控件如何进行颜色改变(如MD风格的Button RadioButton Switch 等不能通过设置background改变颜色的控件)

接下来来看一下代码中如何实现吧

通过以下方法获取主题中设置的attrs对应颜色

/**
 * @param theme 需要获取attrs颜色的theme
 * @param id 需要获取的attrs颜色id
 * @return color
 */
public static int getColorFromTheme(Resources.Theme theme, @AttrRes int id) 
    {
        TypedValue typedValue = new TypedValue();
        theme.resolveAttribute(id, typedValue, true);
        return typedValue.data;
    }

我们要做的是获取到当前颜色和更换后的主题颜色
activity中使用getTheme获取到Theme对象

int startColorPrimary = ThemeUtil.getColorFromTheme(getTheme()R.attr.colorPrimary);
setTheme(R.style.NightTheme);
int endColorPrimary = ThemeUtil.getColorFromTheme(getTheme(),R.attr.colorPrimary);

接下来就是设置一个属性动画配合ArgbEvaluator实现颜色的渐变

ValueAnimator animator = ValueAnimator
                .ofObject(new ArgbEvaluator(), 
                startColorPrimary, endColorPrimary)
                .setDuration(300);

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int color = (int) animation.getAnimatedValue();
                //设置背景色的控件(Toolbar,背景layout等)
                mView.setBackgroundColor(color);
                //TextView字体颜色
                mTextView.setTextColor(color);
                //ImageView设置Tint
                mImageView.setColorFilter(color);
                //设置状态栏或导航栏颜色(API>=21)
                getWindow().setStatusBarColor(color);
                getWindow().setNavigationBarColor(color);
            }
        });

animator.start();

对所有需要变换颜色的控件进行操作就实现了切换效果

下面是关于RecyclerView和MD风格的Button RadioButton Switch更变颜色的方法
(其中RadioButton Switch的实现并不完美 如果有知道的欢迎补充)

通过获取RecyclerView当前在屏幕上显示的Item改变其颜色

int childCount = mRecyclerView.getChildCount();
for (int childIndex = 0; childIndex < childCount; childIndex++) {
                ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex);
                //这里的childView是RecyclerView每一个item的最外层view
                //可以通过id获取每一个item里的控件
                View mView = childView.findViewById(R.id.item_view);
                //这里设置属性动画改变view的颜色
                ............
            }

让 RecyclerView 缓存在 Pool 中的 Item 失效
这里的思路是通过反射拿到 AbsListView 类中的 RecycleBin 对象,然后同样再用反射去调用 clear 方法
此方法选取自知乎和简书的夜间模式实现套路

Class<RecyclerView> recyclerViewClass = RecyclerView.class;
            try {
                Field declaredField = recyclerViewClass.getDeclaredField("mRecycler");
                declaredField.setAccessible(true);
                Method declaredMethod = Class.forName(RecyclerView.Recycler.class.getName()).getDeclaredMethod("clear", (Class<?>[]) new Class[0]);
                declaredMethod.setAccessible(true);
                declaredMethod.invoke(declaredField.get(mRecyclerView), new Object[0]);
                RecyclerView.RecycledViewPool recycledViewPool = mRecyclerView.getRecycledViewPool();
                recycledViewPool.clear();
            } catch (Exception e) {
                e.printStackTrace();
            }

Button RadioButton Switch Progressbar通过设置Tint改变其颜色

//Switch(不完美,会改变未选择时thumb的颜色 默认为灰白色)
mSwitch.setThumbTintList(ColorStateList.valueOf(color));
//RadioButton(不完美,会改变未选中时圆圈的颜色 默认为灰色)
CompoundButtonCompat.setButtonTintList(mRadioButton, ColorStateList.valueOf(color));
//Button Progressbar
ViewCompat.setBackgroundTintList(mBotton, ColorStateList.valueOf(color));

通过属性动画实现的缺点比较明显,布局越复杂编写的难度以及代码量就越多,最终实现的效果也不是非常完美,难点在于特殊的控件如何改变颜色,这里有兴趣的读者可以自行研究一下,下面介绍一种更简单的方式


2.startActivity 实现

此方法原理和调用recreate是相似的,通过创建一个相同的activity并加上动画可以避免闪屏

startActivity
startActivity

这个方法的难点在于:

  • 如何还原上一个activity的状态让用户感觉不到控件的变化

下面上代码

创建一个新的相同activity并设置渐入渐出动画然后结束当前activity

startActivity(new Intent(this, MainActivity.class));
overridePendingTransition(R.anim.start_anim, R.anim.out_anim);
finish();

这里通过启动activity创建的intent来传递以前旧界面的数据
比如EditText的输入内容RecyclerView的数据以及滑动距离

这里列出保存RecyclerView滑动距离 具体需要保存的数据需要根据界面的内容来编写

//获取RecyclerView的滑动距离
//调用getScrollY获取到的数据为0,也可以通过监听滑动事件保存滑动距离
private int getScrollYDistance() {
        LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        int position = layoutManager.findFirstVisibleItemPosition();
        View firstVisibleChildView = layoutManager.findViewByPosition(position);
        int itemHeight = firstVisibleChildView.getHeight();
        return (position) * itemHeight - firstVisibleChildView.getTop();
    }

//通过intent传递数据到新activity
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("scrollY",getcrollYDistance());

//在activity的onCreate方法中还原数据
//RecyclerView的setScrollBy方法只有在view测量完毕后调用才能生效
mRecyclerView.post(new Runnable() {
            @Override
            public void run() {
              mRecyclerView.scrollBy(0,getIntent().getIntExtra("scrollY", 0));
            }
        });

使用startActivity这种方式实现的效果是不是比较简单,代码量相对也较小
难点就在于网络请求的list数据如何进行传递、保存,intent里面传递过多数据容易引起崩溃
因为是创建的新activity,最终实现的效果也比较完美


总结

以上就是我总结的如何避免切换主题时闪屏的方法,这是我第一次写技术总结,肯定有不少问题,希望需要的朋友们能够从种学习到新的知识
最后附上Demo链接github.com/Misutesu/Ni…
(此Demo选择切换模式的RadioButton的选中状态好像有点问题,我暂时没有找到原因,如果有发现了的朋友欢迎留言告诉我)

感谢您的阅读