Android 夜间模式原理

前言

Android 夜间模式早在API 23的时候就可以使用了,不过那时候还有些限制,仅对新入栈的Activity生效,已在栈中的Activity不生效。但现在大家的App一般都是API28,甚至29,所以这个限制已经没有了。设置了夜间模式,对已入栈的Activity也生效,相当完美。

配置

配置起来很无脑

首先基类Activity必须继承AppCompatActivity,基于现在国内应用商店sdk最低要求28,绝大部分应用都升级到AndroidX,所以基本上来说,基类都是AppCompatActivity。如果之前用的FragmentActivity,那现在就要换成AppCompatActivity

其次,修改默认主题。Theme.AppCompat.DayNight.xx之前用Light的,只要中间这部分改成DayNight就可以了。这块可以直接修改,没啥隐患。

image.png

然后,建立对应的夜间资源文件,与values对应的是values-night,图片也一样,在对应目录后面加上-night即可。

theme中也只是改了颜色,可以忽略

再然后,同步更新values,一般夜间模式也只是颜色发生变化,所以主要修改的就是colors.xml文件。将日间模式下的资源copy一份到夜间模式下的colors.xml下,然后对应修改夜间模式的values值就可以了,注意,colors的key要保持一致。

日间模式

夜间模式

最后,代码切换夜间模式。AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

原理

细扒起来代码配置只有一句:AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)

深入了解下:

    /**
     * Sets the default night mode. This is the default value used for all components, but can
     * be overridden locally via {@link #setLocalNightMode(int)}.
     *
     * <p>This is the primary method to control the DayNight functionality, since it allows
     * the delegates to avoid unnecessary recreations when possible.</p>
     *
     * <p>If this method is called after any host components with attached
     * {@link AppCompatDelegate}s have been 'created', a {@code uiMode} configuration change
     * will occur in each. This may result in those components being recreated, depending
     * on their manifest configuration.</p>
     *
     * <p>Defaults to {@link #MODE_NIGHT_FOLLOW_SYSTEM}.</p>
     *
     * @see #setLocalNightMode(int)
     * @see #getDefaultNightMode()
     */
    public static void setDefaultNightMode(@NightMode int mode) {
        if (DEBUG) {
            Log.d(TAG, String.format("setDefaultNightMode. New:%d, Current:%d",
                    mode, sDefaultNightMode));
        }
        switch (mode) {
            case MODE_NIGHT_NO:
            case MODE_NIGHT_YES:
            case MODE_NIGHT_FOLLOW_SYSTEM:
            case MODE_NIGHT_AUTO_TIME:
            case MODE_NIGHT_AUTO_BATTERY:
                if (sDefaultNightMode != mode) {
                    sDefaultNightMode = mode;
                    applyDayNightToActiveDelegates();
                }
                break;
            default:
                Log.d(TAG, "setDefaultNightMode() called with an unknown mode");
                break;
        }
    }

代码上面有一句注释:
This is the primary method to control the DayNight functionality, since it allows the delegates to avoid unnecessary recreations when possible.
翻译过来大概的意思是:如果此方法调用在已经创建完成的页面,那么这些页面可能会被重建。

简单的理解为,已经入栈的Activity,系统会自动完成重建过程。无需我们再操作。这就非常爽了,我只需要配置,其他的交给系统来完成。那到底是怎么实现的呢?
继续扒源码:

    private static void applyDayNightToActiveDelegates() {
        synchronized (sActivityDelegatesLock) {
            for (WeakReference<AppCompatDelegate> activeDelegate : sActivityDelegates) {
                final AppCompatDelegate delegate = activeDelegate.get();
                if (delegate != null) {
                    if (DEBUG) {
                        Log.d(TAG, "applyDayNightToActiveDelegates. Applying to " + delegate);
                    }
                    delegate.applyDayNight();
                }
            }
        }
    }

这很好理解,通过一个for循环,对循环内的Activity应用新的模式。 有遍历就必然有新增和删除,很容易就能找出对应的代码:

    static void addActiveDelegate(@NonNull AppCompatDelegate delegate) {
        synchronized (sActivityDelegatesLock) {
            // Remove any existing records pointing to the delegate.
            // There should not be any, but we'll make sure
            removeDelegateFromActives(delegate);
            // Add a new record to the set
            sActivityDelegates.add(new WeakReference<>(delegate));
        }
    }

    static void removeActivityDelegate(@NonNull AppCompatDelegate delegate) {
        synchronized (sActivityDelegatesLock) {
            // Remove any WeakRef records pointing to the delegate in the set
            removeDelegateFromActives(delegate);
        }
    }

调用的地方盲猜也是在 onCreateonDestory,继续追代码:

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // attachBaseContext will only be called from an Activity, so make sure we switch this for
        // Dialogs, etc
        mBaseContextAttached = true;

        // Our implicit call to applyDayNight() should not recreate until after the Activity is
        // created
        applyDayNight(false);

        // We lazily fetch the Window for Activities, to allow DayNight to apply in
        // attachBaseContext
        ensureWindow();

        if (mHost instanceof Activity) {
            String parentActivityName = null;
            try {
                parentActivityName = NavUtils.getParentActivityName((Activity) mHost);
            } catch (IllegalArgumentException iae) {
                // Ignore in this case
            }
            if (parentActivityName != null) {
                // Peek at the Action Bar and update it if it already exists
                ActionBar ab = peekSupportActionBar();
                if (ab == null) {
                    mEnableDefaultActionBarUp = true;
                } else {
                    ab.setDefaultDisplayHomeAsUpEnabled(true);
                }
            }

            // Only activity-hosted delegates should apply night mode changes.
            addActiveDelegate(this);
        }

        mCreated = true;
    }

首先 onCreate会先配置下默认的日夜间模式,然后会把当前Activity添加到弱引用中

@Override
    public void onDestroy() {
        if (mHost instanceof Activity) {
            removeActivityDelegate(this);
        }

        if (mInvalidatePanelMenuPosted) {
            mWindow.getDecorView().removeCallbacks(mInvalidatePanelMenuRunnable);
        }

        mStarted = false;
        mIsDestroyed = true;

        if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED
                && mHost instanceof Activity
                && ((Activity) mHost).isChangingConfigurations()) {
            // If we have a local night mode set, save it
            sLocalNightModes.put(mHost.getClass().getName(), mLocalNightMode);
        } else {
            sLocalNightModes.remove(mHost.getClass().getName());
        }

        if (mActionBar != null) {
            mActionBar.onDestroy();
        }

        // Make sure we clean up any receivers setup for AUTO mode
        cleanupAutoManagers();
    }

onDestory中也会移除已添加的Activity,跟我们盲猜的一样。
然后继续看下如何应用配置的:

    @SuppressWarnings("deprecation")
    private boolean applyDayNight(final boolean allowRecreation) {
        if (mIsDestroyed) {
            if (DEBUG) {
                Log.d(TAG, "applyDayNight. Skipping because host is destroyed");
            }
            // If we're destroyed, ignore the call
            return false;
        }

        @NightMode final int nightMode = calculateNightMode();
        @ApplyableNightMode final int modeToApply = mapNightMode(mContext, nightMode);
        final boolean applied = updateForNightMode(modeToApply, allowRecreation);

        if (nightMode == MODE_NIGHT_AUTO_TIME) {
            getAutoTimeNightModeManager(mContext).setup();
        } else if (mAutoTimeNightModeManager != null) {
            // Make sure we clean up the existing manager
            mAutoTimeNightModeManager.cleanup();
        }
        if (nightMode == MODE_NIGHT_AUTO_BATTERY) {
            getAutoBatteryNightModeManager(mContext).setup();
        } else if (mAutoBatteryNightModeManager != null) {
            // Make sure we clean up the existing manager
            mAutoBatteryNightModeManager.cleanup();
        }

        return applied;
    }

其他的不用管,有一个比较显眼的方法:updateForNightMode
继续看:

    private boolean updateForNightMode(@ApplyableNightMode final int mode,
            final boolean allowRecreation) {
          ... //省略
        if (currentNightMode != newNightMode
                && allowRecreation
                && !activityHandlingUiMode
                && mBaseContextAttached
                && (sCanReturnDifferentContext || mCreated)
                && mHost instanceof Activity
                && !((Activity) mHost).isChild()) {
            // If we're an attached, standalone Activity, we can recreate() to apply using the
            // attachBaseContext() + createConfigurationContext() code path.
            // Else, we need to use updateConfiguration() before we're 'created' (below)
            if (DEBUG) {
                Log.d(TAG, "updateForNightMode. Recreating Activity: " + mHost);
            }
             // 重启代码
            ActivityCompat.recreate((Activity) mHost);
            handled = true;
        }

        if (!handled && currentNightMode != newNightMode) {
            // Else we need to use the updateConfiguration path
            if (DEBUG) {
                Log.d(TAG, "updateForNightMode. Updating resources config on host: " + mHost);
            }
            //更新模式资源  
            updateResourcesConfigurationForNightMode(newNightMode, activityHandlingUiMode, null);
            handled = true;
        }

        if (DEBUG && !handled) {
            Log.d(TAG, "updateForNightMode. Skipping. Night mode: " + mode + " for host:" + mHost);
        }

        // Notify the activity of the night mode. We only notify if we handled the change,
        // or the Activity is set to handle uiMode changes
        // 模式更换监听
        if (handled && mHost instanceof AppCompatActivity) {
            ((AppCompatActivity) mHost).onNightModeChanged(mode);
        }

        return handled;
    }

具体关键代码,已经在上面加了注释。已入栈的Activity,会走 recreate重新完成模式替换,然后更新资源,以及回调模式改版状态。

至此,夜间模式分析完毕。

结语

配置起来就这些,很简单,对于一个新app来说,完全没有任何工作量可言。但对于一个已经成型的商业app来说,新增夜间模式的工作量是巨大的。平常代码书写的规范与否,间接的影响夜间模式的工作量。如果书写规范,所有的color都写在了xml文件里,那改起来还能接受,如果书写随意,直接在布局或者代码里写色值,那就有的哭了。所以,一个良好的代码规范,还是非常重要的。

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

推荐阅读更多精彩内容