Android 适配深色模式

您所在的位置:网站首页 android设置背景颜色代码 Android 适配深色模式

Android 适配深色模式

2024-07-16 17:18| 来源: 网络整理| 查看: 265

Android 深色模式适配

Android 10 开始支持配置深色模式,如果系统是深色主题,但是打开APP又是浅色主题就会显得格格不入。下面介绍几种适配深色模式的方法。

一、forceDarkAllowed

样式中设置 android:forceDarkAllowed 属性,深色主题下系统会自动进行适配。

新建 values-v29 目录,因为 android:forceDarkAllowed 属性 Android 10开始才有。

设置 android:forceDarkAllowed 属性为true 在这里插入图片描述

适配效果

布局文件:

浅色主题: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TYdMNe7a-1639920853307)(E:\ws\技术分享\APP换肤实现\换肤2.png)]

深色主题: 在这里插入图片描述

从布局文件中可以看到,并没有设置任何背景色,但深色主题下,APP自动进行了适配。

这种适配方式十分简单,但是不够美观,无法自定义控件颜色样式,全凭系统控制,并不推荐这种自动化方式实现深色模式。

二、设置深色主题

官方推荐另外一种方法,即分别创建浅色和深色的主题样式。

新建 values-night 目录,存放深色主题的样式 在这里插入图片描述

适配效果

浅色主题: 在这里插入图片描述

深色主题: 在这里插入图片描述

与 forceDarkAllowed 最大的区别在于,深色主题可以手动设置颜色样式。

一些常用的方法:

判断深色主题 public static boolean isDarkTheme(Context context) { int flag = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; return flag == Configuration.UI_MODE_NIGHT_YES; } 代码中切换深色主题 if (isDarkTheme(MainActivity.this)) { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); } 禁止界面适配深色主题

Activity 的 configChanges 属性当中配置 uiMode 避免Activity 重新创建,从而阻止界面适配深色主题。

这时候虽然界面不会重新创建,但是会触发 onConfigurationChanged 方法回调,可以根据回调做一些处理。

@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); int mSysThemeConfig = newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (mSysThemeConfig) { // 亮色主题 case Configuration.UI_MODE_NIGHT_NO: break; // 深色主题 case Configuration.UI_MODE_NIGHT_YES: break; default: break; } } 三、使用 Android-skin-support

上述两种方式只支持Android 10 系统,且系统切换深色主题界面会重新创建,并不是太灵活。如果想适配Android 10 以下的系统可以使用 Android-skin-support 框架实现。

Android-skin-support 是一个换肤框架,通过加载不同的皮肤包从而实现换肤,深色模式只需要创建对应的深色主题皮肤包,然后替换当前的默认样式就可以实现适配。

它的实现流程大致如下:

控制View的创建,将所有View替换为对应的SkinxxxViewSkinxxxView中会根据布局中的属性ID值,找到皮肤包中对应的资源进行替换动态换肤,即通知所有的SkinXXXView进行更新 3.1 Android View 创建流程

在使用 Android-skin-support 之前,可以先了解下 Android View 创建流程,有利于我们之后使用该库。

从 setContentView 方法开始,它作用就是设置界面布局资源。

//Activity public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }

调用的Window中的 setContentView 方法。

//PhoneWindow @Override public void setContentView(int layoutResID) { ...... if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext()); transitionTo(newScene); } else { mLayoutInflater.inflate(layoutResID, mContentParent); } ...... }

调用 LayoutInflater 的 inflate 方法

//LayoutInflater public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); if (DEBUG) { Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" (" + Integer.toHexString(resource) + ")"); } View view = tryInflatePrecompiled(resource, res, root, attachToRoot); if (view != null) { return view; } XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } }

可以看到根据布局资源创建一个Xml 解析器进行解析

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { ...... // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ...... // Inflate all children under temp against its context. rInflateChildren(parser, temp, attrs, true); ...... } }

先通过createViewFromTag方法创建rootView,然后再使用rInflateChildren解析子布局,最终都是通过createView创建View

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) { ...... View view = tryCreateView(parent, name, context, attrs); ...... } public final View tryCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) { ...... View view; if (mFactory2 != null) { view = mFactory2.onCreateView(parent, name, context, attrs); } else if (mFactory != null) { view = mFactory.onCreateView(name, context, attrs); } else { view = null; } ...... }

可以看到如果mFactory2不为空则通过mFactory2来创建View,而mFactory2又是哪里进行初始化的呢?

通过查看代码发现,mFactory2初始化代码如下:

//AppCompatActivity @Override protected void onCreate(@Nullable Bundle savedInstanceState) { final AppCompatDelegate delegate = getDelegate(); delegate.installViewFactory(); delegate.onCreate(savedInstanceState); super.onCreate(savedInstanceState); } //AppCompatDelegateImpl @Override public void installViewFactory() { LayoutInflater layoutInflater = LayoutInflater.from(mContext); if (layoutInflater.getFactory() == null) { LayoutInflaterCompat.setFactory2(layoutInflater, this); } else { if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) { Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed" + " so we can not install AppCompat's"); } } } public static void setFactory2( @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) { inflater.setFactory2(factory); ...... } public void setFactory2(Factory2 factory) { //mFactory2 不可以重复设置,否则会直接抛出异常 if (mFactorySet) { throw new IllegalStateException("A factory has already been set on this LayoutInflater"); } if (factory == null) { throw new NullPointerException("Given factory can not be null"); } mFactorySet = true; if (mFactory == null) { mFactory = mFactory2 = factory; } else { mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2); } }

上述代码可以发现,mFactory2其实就是AppCompatDelegateImpl,现在我们再看下AppCompatDelegateImpl的 onCreateView方法

@Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { ...... return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, /* 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 */ ); } final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { ...... View view = null; // We need to 'inject' our tint aware Views in place of the standard framework versions switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; case "ImageView": view = createImageView(context, attrs); verifyNotNull(view, name); break; case "Button": view = createButton(context, attrs); verifyNotNull(view, name); break; case "EditText": view = createEditText(context, attrs); verifyNotNull(view, name); break; ...... default: // The fallback that allows extending class to take over view inflation // for other tags. Note that we don't check that the result is not-null. // That allows the custom inflater path to fall back on the default one // later in this method. view = createView(context, name, attrs); } ...... return view; } @NonNull protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); }

上述代码可以看到,创建View其实是通过控件的名称,然后再new对应的控件,通过createTextView可以发现创建的也不是TextView,而是AppCompatTextView,所以只要能够修改mFactory2,就可以控制所有View的创建。

3.2 使用方法 集成 Android-skin-support implementation 'skin.support:skin-support:4.0.5' // skin-support implementation 'skin.support:skin-support-appcompat:4.0.5' // skin-support 基础控件支持 implementation 'skin.support:skin-support-design:4.0.5' // skin-support-design material design 控件支持[可选] implementation 'skin.support:skin-support-cardview:4.0.5' // skin-support-cardview CardView 控件支持[可选] implementation 'skin.support:skin-support-constraint-layout:4.0.5' // skin-support-constraint-layout ConstraintLayout 控件支持[可选] 初始化 //初始化换肤框架 SkinCompatManager.withoutActivity(this) //添加各类控件的拦截器 .addInflater(new SkinAppCompatViewInflater()) .addInflater(new SkinConstraintViewInflater()) .addInflater(new SkinCardViewInflater()) .addInflater(new SkinMaterialViewInflater()); //Activity中重写下面方法,可以放到BaseActivity中 @NonNull @Override public AppCompatDelegate getDelegate() { return SkinAppCompatDelegateImpl.get(this, this); }

withoutActivity 方法

public static SkinCompatManager withoutActivity(Application application) { init(application); SkinActivityLifecycle.init(application); return sInstance; }

重点看 SkinActivityLifecycle.init 方法,主要做了两件事情:

注册Activity生命周期回调,替换系统的mFactory2,控制View的创建 public static SkinActivityLifecycle init(Application application) { if (sInstance == null) { synchronized (SkinActivityLifecycle.class) { if (sInstance == null) { sInstance = new SkinActivityLifecycle(application); } } } return sInstance; } private SkinActivityLifecycle(Application application) { //注册Activity生命周期回调 application.registerActivityLifecycleCallbacks(this); //替换系统的mFactory2,控制View的创建 installLayoutFactory(application); SkinCompatManager.getInstance().addObserver(getObserver(application)); } private void installLayoutFactory(Context context) { try { LayoutInflater layoutInflater = LayoutInflater.from(context); LayoutInflaterCompat.setFactory2(layoutInflater, getSkinDelegate(context)); } catch (Throwable e) { Slog.i("SkinActivity", "A factory has already been set on this LayoutInflater"); } }

getSkinDelegate(context) 方法,初始化并返回 SkinCompatDelegate 对象

private SkinCompatDelegate getSkinDelegate(Context context) { if (mSkinDelegateMap == null) { mSkinDelegateMap = new WeakHashMap(); } SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context); if (mSkinDelegate == null) { mSkinDelegate = SkinCompatDelegate.create(context); mSkinDelegateMap.put(context, mSkinDelegate); } return mSkinDelegate; }

SkinCompatDelegate 实现了LayoutInflater.Factory2 接口,重写 onCreateView 方法,从而控制所有View的创建

@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { //创建View View view = createView(parent, name, context, attrs); if (view == null) { return null; } //保存到列表中 if (view instanceof SkinCompatSupportable) { mSkinHelpers.add(new WeakReference((SkinCompatSupportable) view)); } return view; } public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (mSkinCompatViewInflater == null) { mSkinCompatViewInflater = new SkinCompatViewInflater(); } ...... return mSkinCompatViewInflater.createView(parent, name, context, attrs); } public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { View view = createViewFromHackInflater(context, name, attrs); if (view == null) { //通过初始化的各个拦截器,根据控件名称创建对应的控件 view = createViewFromInflater(context, name, attrs); } if (view == null) { //如果没有找到,就类似系统的实现方法 view = createViewFromTag(context, name, attrs); } if (view != null) { // If we have created a view, check it's android:onClick checkOnClickListener(view, attrs); } return view; }

这里的实现有些类似OkHttp的拦截器,View的创建会经过多个 “ViewInflater”,这边选一个拦截器 SkinConstraintViewInflater 看下内部是怎么实现的。

public class SkinConstraintViewInflater implements SkinLayoutInflater { @Override public View createView(Context context, final String name, AttributeSet attrs) { View view = null; switch (name) { case "androidx.constraintlayout.widget.ConstraintLayout": view = new SkinCompatConstraintLayout(context, attrs); break; default: break; } return view; } }

代码很简单,就是通过控件名称创建自己的 SkinCompatConstraintLayout 换肤控件,

public class SkinCompatConstraintLayout extends ConstraintLayout implements SkinCompatSupportable { private final SkinCompatBackgroundHelper mBackgroundTintHelper; ...... public SkinCompatConstraintLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mBackgroundTintHelper = new SkinCompatBackgroundHelper(this); mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr); } @Override public void setBackgroundResource(int resId) { super.setBackgroundResource(resId); if (mBackgroundTintHelper != null) { mBackgroundTintHelper.onSetBackgroundResource(resId); } } //收到换肤事件, @Override public void applySkin() { if (mBackgroundTintHelper != null) { mBackgroundTintHelper.applySkin(); } } }

SkinCompatConstraintLayout 实现 SkinCompatSupportable接口,触发换肤时会调用applySkin方法替换控件的背景。

加载皮肤包 //加载皮肤包 SkinCompatManager.getInstance().loadSkin(DARK_SKIN_NAME, new SkinLoaderListener() { @Override public void onStart() { } @Override public void onSuccess() { } @Override public void onFailed(String errMsg) { } }, SKIN_LOADER_STRATEGY_ASSETS); /** * 加载皮肤包. * * @param skinName 皮肤包名称. * @param listener 皮肤包加载监听. * @param strategy 皮肤包加载策略.SKIN_LOADER_STRATEGY_ASSETS从assets目录加载皮肤包 */ loadSkin(String skinName, SkinLoaderListener listener, int strategy)

重点看下loadSkin方法

public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) { SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy); if (loaderStrategy == null) { return null; } return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName); }

使用AsynTask进行异步操作,加载皮肤包

private class SkinLoadTask extends AsyncTask { private final SkinLoaderListener mListener; private final SkinLoaderStrategy mStrategy; SkinLoadTask(@Nullable SkinLoaderListener listener, @NonNull SkinLoaderStrategy strategy) { mListener = listener; mStrategy = strategy; } @Override protected void onPreExecute() { if (mListener != null) { mListener.onStart(); } } @Override protected String doInBackground(String... params) { synchronized (mLock) { while (mLoading) { try { mLock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mLoading = true; } try { if (params.length == 1) { String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]); if (TextUtils.isEmpty(skinName)) { SkinCompatResources.getInstance().reset(mStrategy); return ""; } return params[0]; } } catch (Exception e) { e.printStackTrace(); } SkinCompatResources.getInstance().reset(); return null; } @Override protected void onPostExecute(String skinName) { synchronized (mLock) { // skinName 为""时,恢复默认皮肤 if (skinName != null) { SkinPreference.getInstance().setSkinName(skinName).setSkinStrategy(mStrategy.getType()).commitEditor(); notifyUpdateSkin(); if (mListener != null) { mListener.onSuccess(); } } else { SkinPreference.getInstance().setSkinName("").setSkinStrategy(SKIN_LOADER_STRATEGY_NONE).commitEditor(); if (mListener != null) { mListener.onFailed("皮肤资源获取失败"); } } mLoading = false; mLock.notifyAll(); } } }

doInBackground 通过皮肤包名称获取皮肤包资源,notifyUpdateSkin 通知所有 SkinCompatSupportable 对象更新皮肤

创建皮肤包 新建一个Application Module创建对应皮肤的颜色等资源资源名称和原工程一样,颜色值修改成对应皮肤包色值打apk包,改为 .skin 文件,放在原工程的 assets目录下 在这里插入图片描述 在这里插入图片描述

实现效果:

浅色模式 在这里插入图片描述

浅色模式色值 在这里插入图片描述

点击深色模式按钮切换深色模式 在这里插入图片描述

皮肤包的色值 在这里插入图片描述

其他使用 恢复默认样式 SkinCompatManager.getInstance().restoreDefaultTheme(); setBackgroundColor、setBackground、setTextColor等方法失效问题

因为框架是根据资源ID,如R.color.black,找到皮肤包中对应名称的资源,所以如果代码中直接设置颜色或者图片,是不支持换肤的。可以改为setBackgroundResource

适配Dialog

SkinXXXView 创建时会获取控件设置的颜色(textColor)、背景(background)等属性的ID,然后再根据ID加载皮肤包中对应的资源。

但是系统的Dialog的布局控件并没有设置背景属性 background等,所以默认情况下Dialog是不支持换肤的。

Skin-Support的解决方法就是替换系统默认的Dialog布局,然后再设置对应的颜色等属性,最终实现换肤效果。

在styles.xml中做如下的声明:

@style/AlertDialog.SkinCompat 自定义控件换肤

Skin-Support 只提供系统组件的换肤,自定义控件需要实现 SkinCompatSupportable 接口或者直接继承 SkinXXXView,然后获取布局中属性的ID和重写 applySkin 方法。

public class SkinSwitchButton extends SwitchButton implements SkinCompatSupportable { private static final String TAG = "SkinSwitchButton"; private int kswThumbDrawable; private int kswBackDrawable; public SkinSwitchButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); //获取SwitchButton的属性ID TypedArray ta = attrs == null ? null : context.obtainStyledAttributes(attrs, R.styleable.SwitchButton); if (ta != null) { kswThumbDrawable = ta.getResourceId(R.styleable.SwitchButton_kswThumbDrawable, INVALID_ID); kswBackDrawable = ta.getResourceId(R.styleable.SwitchButton_kswBackDrawable, INVALID_ID); ta.recycle(); } applySkin(); } public SkinSwitchButton(Context context, AttributeSet attrs) { this(context, attrs, 0); } public SkinSwitchButton(Context context) { this(context, null, 0); } @Override public void applySkin() { //通过ID获取皮肤包对应的资源 kswThumbDrawable = SkinCompatHelper.checkResourceId(kswThumbDrawable); if (kswThumbDrawable != INVALID_ID) { setThumbDrawable(SkinCompatResources.getDrawable(getContext(), kswThumbDrawable)); } kswBackDrawable = SkinCompatHelper.checkResourceId(kswBackDrawable); if (kswBackDrawable != INVALID_ID) { setBackDrawable(SkinCompatResources.getDrawable(getContext(), kswBackDrawable)); } } } 动态修改颜色

Skin-Support 也支持代码中动态修改颜色,且优先级最高

//动态修改颜色 SkinCompatUserThemeManager.get().addColorState(colorId, newColor); //动态修改图片 SkinCompatUserThemeManager.get().addDrawablePath(int drawableRes, String drawablePath); //颜色修改完后需要调用该方法才可以生效 SkinCompatUserThemeManager.get().apply(); //清除自定义的颜色和图片 SkinCompatUserThemeManager.get().clearColors(); SkinCompatUserThemeManager.get().clearDrawables();

参考:

https://www.jianshu.com/p/2c3833b8a1d2?utm_campaign=hugo

https://blog.csdn.net/c10WTiybQ1Ye3/article/details/119223672



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3