Android 夜间模式原理

前言

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

配置

配置起来很无脑

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

Android 夜间模式原理_第1张图片

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

image.png

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

Android 夜间模式原理_第2张图片
theme中也只是改了颜色,可以忽略

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

Android 夜间模式原理_第3张图片
日间模式

Android 夜间模式原理_第4张图片
夜间模式

最后,代码切换夜间模式。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)}.
     *
     * 

This is the primary method to control the DayNight functionality, since it allows * the delegates to avoid unnecessary recreations when possible.

* *

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.

* *

Defaults to {@link #MODE_NIGHT_FOLLOW_SYSTEM}.

* * @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 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文件里,那改起来还能接受,如果书写随意,直接在布局或者代码里写色值,那就有的哭了。所以,一个良好的代码规范,还是非常重要的。

你可能感兴趣的:(Android 夜间模式原理)