开发者社区> 问答> 正文

[@炯轩][¥20]如何实现Android夜间模式

如何实现Android夜间模式?

展开
收起
Nebula 2018-12-15 22:36:54 2048 0
3 条回答
写回答
取消 提交回答
  • 自定义android的常用控件,譬如TextView,ImageView等等,增加夜间模式的自定义属性,以及监听日夜间模式的切换。这样,批量替换xml文件,实现无缝对接上层的逻辑。

    2019-07-17 23:22:03
    赞同 展开评论 打赏
  • 如果是全新开始开发的APP的话,这个倒很好解决
    如果是在 已有的app中开发夜间模式,可以参考https://www.jianshu.com/p/0cd03c878def

    2019-07-17 23:22:03
    赞同 展开评论 打赏
  • XML 配置
    首先,先写一套UI界面出来,上方左边是两个 TextView,右边是两个 CheckBox,下方是一个 RecyclerView ,实现很简单,这里我不贴代码了。

    接着,在 styles 文件中添加两个 Theme,一个是日间主题,一个是夜间主题。它们的属性都是一样的,唯一区别在于颜色效果不同。

    <!--白天主题-->
    <style name="DayTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="clockBackground">@android:color/white</item>
        <item name="clockTextColor">@android:color/black</item>
    </style>
    
    <!--夜间主题-->
    <style name="NightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/color3F3F3F</item>
        <item name="colorPrimaryDark">@color/color3A3A3A</item>
        <item name="colorAccent">@color/color868686</item>
        <item name="clockBackground">@color/color3F3F3F</item>
        <item name="clockTextColor">@color/color8A9599</item>
    </style>
    

    需要注意的是,上面的 clockTextColor 和 clockBackground 是我自定义的 color 类型属性
    <?xml version="1.0" encoding="utf-8"?>

    <attr name="clockBackground" format="color" />
    <attr name="clockTextColor" format="color" />

    然后再到所有需要实现夜间模式功能的 xml 布局文件中,加入类似下面设置,比如我在 RecyclerView 的 Item 布局文件中做了如下设置

    稍稍解释下其作用,如 TextView 里的 android:textColor="?attr/clockTextColor" 是让其字体颜色跟随所设置的 Theme。到这里,xml 需要做的配置全部完成,接下来是 Java 代码实现了。
    Java 代码实现
    大家可以先看下面的实现代码,看不懂的童鞋可以边结合我代码下方实现思路解说。

    package com.clock.study.activity;

    import ...

    /**

    • 夜间模式实现方案
      *
    • @author Clock
    • @since 2016-08-11
      */

    public class DayNightActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {

    private final static String TAG = DayNightActivity.class.getSimpleName();
    /**用于将主题设置保存到SharePreferences的工具类**/
    private DayNightHelper mDayNightHelper;
    
    private RecyclerView mRecyclerView;
    
    private LinearLayout mHeaderLayout;
    private List<RelativeLayout> mLayoutList;
    private List<TextView> mTextViewList;
    private List<CheckBox> mCheckBoxList;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        initData();
        initTheme();
        setContentView(R.layout.activity_day_night);
        initView();
    }
    
    private void initView() {
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        mRecyclerView.setLayoutManager(layoutManager);
        mRecyclerView.setAdapter(new SimpleAuthorAdapter());
    
        mHeaderLayout = (LinearLayout) findViewById(R.id.header_layout);
    
        mLayoutList = new ArrayList<>();
        mLayoutList.add((RelativeLayout) findViewById(R.id.jianshu_layout));
        mLayoutList.add((RelativeLayout) findViewById(R.id.zhihu_layout));
    
        mTextViewList = new ArrayList<>();
        mTextViewList.add((TextView) findViewById(R.id.tv_jianshu));
        mTextViewList.add((TextView) findViewById(R.id.tv_zhihu));
    
        mCheckBoxList = new ArrayList<>();
        CheckBox ckbJianshu = (CheckBox) findViewById(R.id.ckb_jianshu);
        ckbJianshu.setOnCheckedChangeListener(this);
        mCheckBoxList.add(ckbJianshu);
        CheckBox ckbZhihu = (CheckBox) findViewById(R.id.ckb_zhihu);
        ckbZhihu.setOnCheckedChangeListener(this);
        mCheckBoxList.add(ckbZhihu);
    
    }
    
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        int viewId = buttonView.getId();
        if (viewId == R.id.ckb_jianshu) {
            changeThemeByJianShu();
    
        } else if (viewId == R.id.ckb_zhihu) {
            changeThemeByZhiHu();
    
        }
    }
    
    private void initData() {
        mDayNightHelper = new DayNightHelper(this);
    }
    
    private void initTheme() {
        if (mDayNightHelper.isDay()) {
            setTheme(R.style.DayTheme);
        } else {
            setTheme(R.style.NightTheme);
        }
    }
    
    /**
     * 切换主题设置
     */
    private void toggleThemeSetting() {
        if (mDayNightHelper.isDay()) {
            mDayNightHelper.setMode(DayNight.NIGHT);
            setTheme(R.style.NightTheme);
        } else {
            mDayNightHelper.setMode(DayNight.DAY);
            setTheme(R.style.DayTheme);
        }
    }
    
    /**
     * 使用简书的实现套路来切换夜间主题
     */
    private void changeThemeByJianShu() {
        toggleThemeSetting();
        refreshUI();
    }
    
    /**
     * 使用知乎的实现套路来切换夜间主题
     */
    private void changeThemeByZhiHu() {
        showAnimation();
        toggleThemeSetting();
        refreshUI();
    }
    
    /**
     * 刷新UI界面
     */
    private void refreshUI() {
        TypedValue background = new TypedValue();//背景色
        TypedValue textColor = new TypedValue();//字体颜色
        Resources.Theme theme = getTheme();
        theme.resolveAttribute(R.attr.clockBackground, background, true);
        theme.resolveAttribute(R.attr.clockTextColor, textColor, true);
    
        mHeaderLayout.setBackgroundResource(background.resourceId);
        for (RelativeLayout layout : mLayoutList) {
            layout.setBackgroundResource(background.resourceId);
        }
        for (CheckBox checkBox : mCheckBoxList) {
            checkBox.setBackgroundResource(background.resourceId);
        }
        for (TextView textView : mTextViewList) {
            textView.setBackgroundResource(background.resourceId);
        }
    
        Resources resources = getResources();
        for (TextView textView : mTextViewList) {
            textView.setTextColor(resources.getColor(textColor.resourceId));
        }
    
        int childCount = mRecyclerView.getChildCount();
        for (int childIndex = 0; childIndex < childCount; childIndex++) {
            ViewGroup childView = (ViewGroup) mRecyclerView.getChildAt(childIndex);
            childView.setBackgroundResource(background.resourceId);
            View infoLayout = childView.findViewById(R.id.info_layout);
            infoLayout.setBackgroundResource(background.resourceId);
            TextView nickName = (TextView) childView.findViewById(R.id.tv_nickname);
            nickName.setBackgroundResource(background.resourceId);
            nickName.setTextColor(resources.getColor(textColor.resourceId));
            TextView motto = (TextView) childView.findViewById(R.id.tv_motto);
            motto.setBackgroundResource(background.resourceId);
            motto.setTextColor(resources.getColor(textColor.resourceId));
        }
    
        //让 RecyclerView 缓存在 Pool 中的 Item 失效
        //那么,如果是ListView,要怎么做呢?这里的思路是通过反射拿到 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 (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    
        refreshStatusBar();
    }
    
    /**
     * 刷新 StatusBar
     */
    private void refreshStatusBar() {
        if (Build.VERSION.SDK_INT >= 21) {
            TypedValue typedValue = new TypedValue();
            Resources.Theme theme = getTheme();
            theme.resolveAttribute(R.attr.colorPrimary, typedValue, true);
            getWindow().setStatusBarColor(getResources().getColor(typedValue.resourceId));
        }
    }
    
    /**
     * 展示一个切换动画
     */
    private void showAnimation() {
        final View decorView = getWindow().getDecorView();
        Bitmap cacheBitmap = getCacheBitmapFromView(decorView);
        if (decorView instanceof ViewGroup && cacheBitmap != null) {
            final View view = new View(this);
            view.setBackgroundDrawable(new BitmapDrawable(getResources(), cacheBitmap));
            ViewGroup.LayoutParams layoutParam = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT);
            ((ViewGroup) decorView).addView(view, layoutParam);
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f);
            objectAnimator.setDuration(300);
            objectAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    ((ViewGroup) decorView).removeView(view);
                }
            });
            objectAnimator.start();
        }
    }
    
    /**
     * 获取一个 View 的缓存视图
     *
     * @param view
     * @return
     */
    private Bitmap getCacheBitmapFromView(View view) {
        final boolean drawingCacheEnabled = true;
        view.setDrawingCacheEnabled(drawingCacheEnabled);
        view.buildDrawingCache(drawingCacheEnabled);
        final Bitmap drawingCache = view.getDrawingCache();
        Bitmap bitmap;
        if (drawingCache != null) {
            bitmap = Bitmap.createBitmap(drawingCache);
            view.setDrawingCacheEnabled(false);
        } else {
            bitmap = null;
        }
        return bitmap;
    }

    }

    实现思路和代码解说:

    DayNightHelper 类是用于保存夜间模式设置到 SharePreferences 的工具类,在 initData 函数中被初始化,其他的 View 和 Layout 都是界面布局,在 initView 函数中被初始化;
    在 Activity 的 onCreate 函数调用 setContentView 之前,需要先去 setTheme,因为当 View 创建成功后 ,再去 setTheme 是无法对 View 的 UI 效果产生影响的;
    onCheckedChanged 用于监听日间模式和夜间模式的切换操作;

    refreshUI 是本实现的关键函数,起着切换效果的作用,通过 TypedValue 和 Theme.resolveAttribute 在代码中获取 Theme 中设置的颜色,来重新设置控件的背景色或者字体颜色等等。需要特别注意的是 RecyclerView 和 ListView 这种比较特殊的控件处理方式,代码注释中已经说明,大家可以看代码中注释;
    refreshStatusBar 用于刷新顶部通知栏位置的颜色;

    showAnimation 和 getCacheBitmapFromView 同样是本实现的关键函数,getCacheBitmapFromView 用于将 View 中的内容转换成 Bitmap(类似于截屏操作那样),showAnimation 是用于展示一个渐隐效果的属性动画,这个属性作用在哪个对象上呢?是一个 View ,一个在代码中动态填充到 DecorView 中的 View(不知道 DecorView 的童鞋得回去看看 Android Window 相关的知识)。知乎之所以在夜间模式切换过程中会有渐隐效果,是因为在切换前进行了截屏,同时将截屏拿到的 Bitmap 设置到动态填充到 DecorView 中的 View 上,并对这个 View 执行一个渐隐的属性动画,所以使得我们能够看到一个漂亮的渐隐过渡的动画效果。而且在动画结束的时候再把这个动态添加的 View 给 remove 了,避免了 Bitmap 造成内存飙升问题。

    作者:D_clock爱吃葱花
    链接:https://www.jianshu.com/p/3b55e84742e5

    2019-07-17 23:22:03
    赞同 展开评论 打赏
问答分类:
问答标签:
问答地址:
问答排行榜
最热
最新

相关电子书

更多
58同城Android客户端Walle框架演进与实践之路 立即下载
Android组件化实现 立即下载
蚂蚁聚宝Android秒级编译——Freeline 立即下载