Android主题切换(Theme)实现日夜间功能

前言

随着一款APP应用功能的不断完善,用户群体的不断增多,APP的更新也就不仅仅局限于功能需求,如何做好良好的用户体验,让用户传播良好的体验口碑,显得尤为重要,而用户体验一块日夜间模式俨然成为了标配。其实,日夜间功能就是换肤的一种,关于换肤功能的实现,也是众说纷纭,总的来讲分为两类:主题换肤(Theme)和插件换肤(APK换肤)。

插件换肤 插件换肤的实现原理就是主APK根据当前环境需求,解析指定目录下对应的插件APK,获得其中同名的资源文件并动态替换到主APK的应用程序中。插件APK并不需要安装,只需要放置在指定目录下即可。

  • 优点: 能够实现各种主题样式的加载,比较灵活,需要增添新的主题只要新建一个插件APK,并配置好相关的资源,放置到指定的文件目录下就行,很方便。
  • 缺点: 需要对控件进行适配修改,实现换肤功能,对于自定义控件,也需要在适配上花点时间。而且放置在文件夹中的插件APK也可能会因为被误删或是损坏而造成资源获取不到,导致换肤失败。

主题换肤 主题换肤的实现原理就是在主apk配置多套主题,每套主题对同一个属性使用相应的资源。

  • 优点: 相比插件换肤来说更容易上手,理解起来也会更容易。
  • 缺点: 增添新的主题样式必须要发布新版本。全部资源文件都放在APK中,APK会显得十分臃肿,特别是图片资源,因此个人推荐纯色线条的图标,并通过着色来实现不同主题下换肤的可能。

因为今天的主题是日夜间模式,考虑到并不会涉及主题样式增添的可能,所以权衡之下还是选择使用主题换肤来实现日夜间模式,老套路,效果预览(文末将附上高清地址入口)

准备相关的属性样式及主题:

自定义attr属性:

主题换肤和插件换肤原理其实一样,就是控制不同模式下加载对应的资源文件,只是实现的方式不同而已。以往我们在写xml布局文件的时候,默认的属性赋值都是绝对的,即Android:background="#FFFFFF"android:background="@color/white"。 
而一旦属性被这样赋值,默认的资源加载就被限制,倘若有需求需要视图在加载时能够根据当前环境配置特定的资源,那就只能在Java程序代码中动态修改,繁琐程度可想而知。那么是否一个办法能够使xml属性的赋值能够动态的根据当前主题样式的改变而去加载默认的资源呢 ? 
有,那就是今天的腕儿:自定义属性。在我看来自定义属性在主题换肤中充当着占位符的角色,它会告诉系统这是一个相对的引用,真正的资源引用是当前上下文环境所对应的主题样式属性列表中,对这个自定义属性的赋值。

1.在res-value目录下新建attr属性的资源文件,例如:custom_theme_attrs.xml。 
2.在custom_theme_attrs.xml文件中新建自定义属性。

格式:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="自定义属性名称" format="资源引用格式(color、dimen、reference...)" />
</resources>

示例:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 控制app背景色 format:颜色值、资源引用 -->
    <attr name="custom_attr_app_bg" format="color|reference" />
    <!-- 控制app标题栏背景色 format:颜色值、资源引用 -->
    <attr name="custom_attr_app_title_layout_bg" format="color|reference" />
    <!-- 用户头像显示占位Drawable format:颜色值、资源引用 -->
    <attr name="custom_attr_user_photo_place_holder" format="color|reference" />
    <!-- 用户昵称字体颜色 format:颜色值、资源引用 -->
    <attr name="custom_attr_nickname_text_color" format="color|reference" />
    <!-- 用户备注字体颜色 format:颜色值、资源引用 -->
    <attr name="custom_attr_remark_text_color" format="color|reference" />
    <!-- 用户头像显示的透明度 format:尺寸值、资源引用 -->
    <attr name="custom_attr_user_photo_alpha" format="dimension|reference" />
</resources>

写过自定义View的朋友一定不会陌生,不就是自定义属性嘛。区别就是这些属性值没有包裹在styleable中,至于为啥我就不班门弄斧,有需要的朋友可以了解简书作者楚云之南写的《深入理解Android 自定义attr Style styleable以及其应用》,感觉写的不错,感谢分享 !!

自定义theme主题:

Style想必并不陌生,在需要写很多类似的代码块时,我们通常会提取其中共有部分,配置在Style中,直接在xml中的style属性中引用即可,非常方便。这里所说的主题其实也是Style样式中的一种,只是它不仅仅局限于控件样式属性的赋值,常常还涉及到window窗口相关,就是样式属性的一个集合。既然是通过切换主题来切换应用UI样式,所以在定义Style主题样式的时候,需要准备多套主题样式。

1.在res-value目录下新建style属性的资源文件,例如:custom_theme_styles.xml。 
2.在custom_theme_styles.xml文件中新建自定义主题,并对特定的系统、自定义属性进行赋值操作。

格式:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="自定义主题样式的名称" parent="继承的主题,可以是自定义主题样式也可以是系统主题样式">
        <item name="属性名称">赋值的对应资源</item>
    </style>
</resources>

示例:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MarioTheme" parent="Theme.AppCompat.Light.DarkActionBar" >
        <!-- 隐藏Activity窗口的ActionBar -->
        <item name="windowActionBar">false</item>
        <!-- 隐藏Activity窗口的Title标题栏 -->
        <item name="windowNoTitle">true</item>
    </style>

    <style name="MarioTheme.Day" >
        <!-- 日间模式下 "custom_attr_app_bg" 的赋值为#FFFFFF -->
        <item name="custom_attr_app_bg">#FFFFFF</item>
        ...
    </style>

    <style name="MarioTheme.Night" >
        <!-- 夜间模式下 "custom_attr_app_bg" 的赋值为#1F1F1F -->
        <item name="custom_attr_app_bg">#1F1F1F</item>
        ...
    </style>
</resources>

如上述示例所示,首先是新建一个继承自系统Theme.AppCompat.Light.DarkActionBar样式的自定义主题MarioTheme算是一个主题的Base基础主题,在这个基础主题中,可以对一些通用的属性进行赋值,比如一些全局性的窗口样式,当然这些赋值上去的属性也是可以被后来继承的子类主题覆盖。 
然后又新建了两个继承自这个基础主题的MarioTheme.DayMarioTheme.Night分别作为日间和夜间的主题,而且分别在两个主题中对自定义属性custom_attr_app_bg进行了赋值。

其实通过上述两个步骤:[自定义属性 –> 自定义主题,并在主题中对自定义属性进行相应的赋值],主题换肤的准备工作可以说是已经完成。但是为了项目的可维护性更高,尚且有不少可以优化的地方,如上#1F1F1F颜色值直接出现在style中。这是我非常反对的一种操作方式,在使用主题换肤的应用中,随着应用功能的强大,自定义属性的数量一定会越来越多,而且我觉得自定义属性定义的越精细越好,所以一定会有一个庞大数量的属性列表需要去维护。其中也有可能大部分是可以被重复使用的,何不将它们整理到统一的文件中,倘若到时候需求变化,资源引用需要修改,也不至于全局搜索挨个去改,何必给自己增加这么多没有必要的工作量呢 ! 所以我还要讲讲自定义属性 。

自定义resource资源:

同类型的资源新建在对应的目录下,尺寸资源定义在values-dimens目录下,颜色资源定义再values-colors目录下,drawable资源定义在values目录下对应的drawable目录下… 并且每一种资源都应该根据不同主题样式配置多套。

自定义color:

1.在res-value目录下新建color属性的资源文件,例如:custom_theme_colors.xml。 
2.在custom_theme_colors.xml文件中新建自定义color颜色。

格式:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="自定义color名称_day">对应的日间颜色值</color>
    <color name="自定义color名称_night">对应的夜间颜色值</color>
</resources>

定义app背景色为例:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 日间模式下app背景色 -->
    <color name="custom_color_app_bg_day">#FFFFFF</color>
    <!-- 日间模式下app标题栏背景色 -->
    <color name="custom_color_app_title_layout_bg_day">#FF2F3A4C</color>
    <!-- 夜间模式下app背景色 -->
    <color name="custom_color_app_bg_night">#1F1F1F</color>
    <!-- 夜间模式下app标题栏背景色 -->
    <color name="custom_color_app_title_layout_bg_night">#FF1D1D1D</color>
    ...
</resources>
自定义drawable:

1.在res目录下新建drawable文件夹。 
1.在res-drawable目录下新建drawable资源文件。

定义圆形图片的占位drawable,示例:

custom_drawable_user_photo_place_holder_day.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/custom_color_user_photo_place_holder_bg_day" />
    <corners android:radius="32dp" />
</shape>

custom_drawable_user_photo_place_holder_night.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/custom_color_user_photo_place_holder_bg_night" />
    <corners android:radius="32dp" />
</shape>

自定义drawable中使用到的颜色值推荐也统一整理到custom_theme_colors.xml文件中。

<!-- 用户头像占位drawable背景颜色 -->
<color name="custom_color_user_photo_place_holder_bg_day">#29303B</color>
<color name="custom_color_user_photo_place_holder_bg_night">#171717</color>
自定义colorStateList:

同SelectorDrawable一样,color也可以设置Selector选择器。 
1.value目录下新建color.xml文件。 
2.在res-color.xml目录下新建color资源文件。

示例:

custom_selector_text_day.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/custom_color_text_pressed_day" android:state_pressed="true" />
    <item android:color="@color/custom_color_text_day" />
</selector>

custom_selector_text_night.xml

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/custom_color_text_pressed_night" android:state_pressed="true" />
    <item android:color="@color/custom_color_text_night" />
</selector>

同理,自定义colorStateList中使用到的颜色值推荐也统一整理到custom_theme_colors.xml文件中。

在不同的主题样式下为自定义属性赋值:

示例:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    // 日间相关属性集
    <style name="MarioTheme.Day" >
        <item name="custom_attr_app_bg">@color/custom_color_app_bg_day</item>
        <item name="custom_attr_app_title_layout_bg">@color/custom_color_app_title_layout_bg_day</item>
        <item name="custom_attr_user_photo_place_holder">@drawable/custom_drawable_user_photo_place_holder_day</item>
    </style>
    // 夜间相关属性集
    <style name="MarioTheme.Night" >
        <item name="custom_attr_app_bg">@color/custom_color_app_bg_night</item>
        <item name="custom_attr_app_title_layout_bg">@color/custom_color_app_title_layout_bg_night</item>
        <item name="custom_attr_user_photo_place_holder">@drawable/custom_drawable_user_photo_place_holder_night</item>
    </style>
</resources>

到这里就完成了相关的准备工作。因为在日夜间模式切换中基本不太会涉及字符串、尺寸的资源样式的修改,实现的方式是一样的,因此不做过多的赘述,有需要的朋友可以自定义去尝试。

在XML布局文件中使用自定义属性:

只要前期准备工作做好了使用起来其实是非常简单的,就是在属性赋值的时候不再使用绝对的资源引用,而是引用已经完成赋值的自定义的属性:

android:需要修改的属性="?attr/自定义属性名称"

这样的话只要设置自定义属性的View控件的Context上下文环境设置了对应的Theme主题样式,且对我们的自定义样式进行了相应的赋值,则样式的使用就会奏效,切记,项目中使用到的属性一定要在使用的主题样式下赋值,否则应用运行的时候会报错。当然为了更好的开发体验,我们可以在预览模式下设置对应的主题预览我们设置的样式效果是否起效,效果怎么样。 ?

Demo部分布局代码展示,示例:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://caibaoyule.cnschemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:background="?attr/custom_attr_app_bg"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:orientation="vertical">

    <View
        android:background="?attr/custom_attr_app_title_layout_bg"
        android:id="@id/custom_id_title_status_bar"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

    <RelativeLayout
        android:background="?attr/custom_attr_app_title_layout_bg"
        android:id="@id/custom_id_title_layout"
        android:layout_width="match_parent"
        android:layout_height="136dp"
        android:paddingBottom="16dp"
        android:paddingRight="12dp"
        android:paddingLeft="12dp"
        android:paddingTop="8dp" >

        <ImageView
            android:padding="3dp"
            android:layout_width="72dp"
            android:layout_height="72dp"
            android:id="@+id/theme_user_phjiadayule.cnoto"
            android:layout_alignParentLeft="true"
            android:layout_alignParentBottom="true"
            android:alpha="?attr/custom_attr_user_photo_alpha"
            tools:src="?attr/custom_attr_user_photo_place_holder" />

        <LinearLayout
            android:layout_toRightOf="@+id/theme_user_photo"
            android:layout_alignTop="@+id/theme_user_photo"
            android:layout_width="wrap_content"
            android:gravity="center_vertical"
            android:layout_marginLeft="12dp"
            android:orientation="vertical"
            android:layout_height="72dp" >

            <TextView
                android:textColor="?attr/custom_attr_nickname_text_color"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:id="@+id/theme_nickname"
                android:text="@string/nickname"
                android:textSize="19dp" />

            <TextView
                android:textColor="?attr/custom_attr_remark_text_color"
                android:text="@string/remark"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:layout_marginTop="3dp"
                android:id="@+id/theme_remark"
                android:textSize="12wanjhyl.cndp" />
        </LinearLayout>
    </RelativeLayout>
</LinearLayout>

预览中的两个主题MarioTheme.Day.PreviewMarioTheme.Night.Preview分别继承之MarioTheme.DayMarioTheme.Night,并没有在项目中使用起来,主要用来控制状态栏的颜色,个人用于编辑器状态栏沉浸效果的一个预览效果。

到这一步,在Activity中使用setTheme()就能加载对应主题的视图啦 !! 有没有很赞 ? 
需要注意的一点是 setTheme()方法必须要在系统调用setContentView()方法前调用,个人推荐统一写到基类BaseActivity的onCreate()方法中。而我们需要做的就是在本地SharePreference中配置一个tag控制BaseActivity设置不同的主题就行啦 ! 
也许看到这里你已经跃跃欲试,或者你已经一步一步照着写到这里,但是应用跑起来却发现,在Activity中点击按钮调用setTheme()方法,Activity并不会发生变化,或者返回上一个Activity也是没有变化。并不是setTheme()方法没有奏效,setTheme()方法确实起到应有的效果了(可以调用getTheme()方法查看,当前主题确实已经改变)。那又是什么原因呢? 那是因为这些视图都是已经加载完成,设置主题并不会触发系统去刷新UI,因此需要我们手动去触发。

而更改主题后的UI刷新我推荐两种:

  • 重新创建Activity 关于重新创建Activity,只需要调用Activity的recreate()方法就行,普通不复杂的UI,用这个方法基本可以满足,其中主要涉onSaveInstanceState()应用状态的保存,而使用这种方法重新创建Activity也是Google官方比较推崇的,有兴趣可以了解一下
  • 手动加载当前主题下的应用资源 这是我这里需要重点讲一下的。由于UI的复杂性和特殊性,并不是所有应用的Activity都可以通过onSaveInstanceState(kkucai.cn)来保存当前的应用状态的,因此了解如何从当前主题获取需要的属性资源显得尤为重要。

获得当前主题自定义属性指定的资源:

其实获取这个资源也很简单,也就两步:

  • Step-01 获取TypedValue

    TypedValue typedValue = new TypedValue(jiiyuan.cn);
    Resources.Theme theme = getTheme();
    try {
        theme.resolveAttribute(R.attr.自定义属性, typedValue, true);
    } catch (Exception e) {
        e.printStackTrace();
    }
    

首先定义一个TypedValue用于承载Resource资源属性,然后获取当前上下文对应的Theme主题,再是通过resolveAttribute()方法获取当前主题下给定属性ID对应的资源信息并赋值给定义好的typedValue。因为可能存在给定属性对应的资源信息获取不到而抛出的异常,所以建议try&catch一下,捕获可能存在的异常情况

  • Step-02 根据获取的TypedValue所包含的资源信息获取对应的资源

    Resources resources = getResources();
        try {
            int color = ResourcesCompat.getColor(resources, typedValue.resourceId, null); // 获取颜色值
            Drawable drawable = ResourcesCompat.getDrawable(resources, typedValue.resourceId, null); // 获取Drawable对象
            String string = resources.getString(typedValue.resourceId); // 获取字符串
        } catch (Exception e) {
            e.printStackTrace();
        }
    

TypedValue最重要的一个属性就是resourceId,只要确定获取的typedValue不为null。我们就可以通过typedValue.resourceId获取资源的id,就好比知道了一个颜色资源的ID是R.color.black,让你去获取颜色值,知道一个Drawable资源的ID是R.drawable.ic_luncher,让你去获取Drawable对象,想想就简单(捂脸.jpg)。需要注意的是在获取对应资源的时候为避免资源获取失败抛出的异常,各种获取资源的方法还是建议用try&catch包裹一下。关于资源获取,文末给出的Demo中有一个MarioResourceHelper的辅助类,该类对资源获取一块进行了一个小封装,用起来会更加方便。

而接下来需要做的就是对特定的资源进行替换就好了。

补充一点:

关于前文提到主题换肤缺点时,其中一点就是所有资源文件都需要放置在主APK文件中打包发布,也许不同的主题就会有多套图片资源,在Android有限内存的条件下,这是一种非常糟糕的情况。 
而在前文我也提及,应对这种现象,我们开发能做的就是使用drawable着色的方式,尽量用一套图片资源实现多种主题。切记! 着色的图片要求纯色且背景透明的PNG,因为着色并不能区分色彩,而是对所有非透明区域统一着色上指定的颜色。着色细节不做赘述,线上《Drawable着色的后向兼容》一文阐述的比较详细了吧,感谢作者分享 !!而我们要做的就是将需要的着色上去的颜色值定义在不同的主题下,不同主题获取对应的颜色值,并对特定的drawable进行着色即可。 而MarioResourceHelper辅助类也会对drawable的着色方法做相应的封装。

? Github项目源码地址 ?

时间: 2024-10-24 16:01:40

Android主题切换(Theme)实现日夜间功能的相关文章

Android主题切换—夜间/白天模式探究

现在市面上众多阅读类App都提供了两种主题:白天or夜间. 上述两幅图片,正是两款App的夜间模式效果,所以,依据这个功能,来看看切换主题到底是怎么实现的(当然现在github有好多PluginTheme开源插件,很多时候可以使用这些插件,不过我并不想讲怎么用那些插件,正所谓会用轮子还不如会造轮子). 关于更换主题和换肤 这里提到是做换主题功能,当然与之类似的就是换肤,换肤现在比较流行的是采用插件化动态加载技术来实现的,这样可以起到热插拔作用,需要皮肤时候用户自主的在网上下载便是了,不用皮肤时便

Android主题切换方案总结

所谓的主题切换,就是能够根据不同的设定,呈现不同风格的界面给用户,也就是所谓的换肤. 1.将主题包(图片与配置)存到SD卡上(可通过下载或手动放入指定目录),在代码里强制从本地文件创建图片与配置文字大小.颜色等信息. 2.Android平台独有的主题设置功能,在values文件夹中定义若干种style,在Activity的onCreate中使用setTheme方法设置主题. 3.将主题包做成APK的形式,使用远程Context的方式访问主题包中的资源. 4.类似小米的深度主题,修改framewo

Android 实现切换主题皮肤功能(类似于众多app中的 夜间模式,主题包等)

首先来个最简单的一键切换主题功能,就做个白天和晚上的主题好了. 先看我们的styles文件: 1 <resources> 2 3 <!-- Base application theme. --> 4 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> 5 <!-- Customize your theme here. --> 6 &l

Android 源码系列之&lt;四&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(上)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51252401 现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能.体验过它们的主题切换后你会发现大部分效果是更换相关背景图片.背景颜色.字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下

Android App切换主题的实现原理剖析

现在越来越多的APP都加入了主题切换功能或者是日间模式和夜间模式功能切换等,这些功能不仅增加了用户体验也增强了用户好感,众所周知QQ和网易新闻的APP做的用户体验都非常好,它们也都有日间模式和夜间模式的主题切换功能.体验过它们的主题切换后你会发现大部分效果是更换相关背景图片.背景颜色.字体颜色等来完成的,网上这篇文章对主题切换讲解的比较不错,今天我们从源码的角度来学习一下主题切换功能,如果你对这块非常熟悉了,请跳过本文(*^__^*) - 在开始讲解主题切换之前我们先看一下LayoutInfla

Android 主题动态切换框架:Prism

Prism(棱镜) 是一个全新的 Android 动态主题切换框架,虽然是头一次发布,但它所具备的基础功能已经足够强大了!本文介绍了 Prism 的各种用法,希望对你会有所帮助,你也可以对它进行扩展,来满足开发需求. 先说一下 Prism 的诞生背景.其实我没打算一上来就写个框架出来,当时在给 Styling Android 博客写一些使用 ViewPager 来实现 UI 动态着色的系列文章,文中用到的代码被我重构成适合讲解用的组件,然后我发现这些代码可以整理成一个简洁的 API,于是乎便有了

Android 源码系列之&lt;五&gt;从源码的角度深入理解LayoutInflater.Factory之主题切换(中)

转载请注明出处:http://blog.csdn.net/llew2011/article/details/51287391 在上篇文章中我们主要讲解了LayoutInflater渲染xml布局文件的流程,文中讲到如果在渲染之前为LayoutInflater设置了Factory,那么在渲染每一个View视图时都会调用Factory的onCreateView()方法,因此可以拿onCreateView()方法做切入口实现主题切换功能.如果你不清楚LayoutInflater的渲染流程,请点击这里.

Android 样式和主题(style &amp; theme)

Android 样式 android中的样式和CSS样式作用相似,都是用于为界面元素定义显示风格,它是一个包含一个或者多个view控件属性的集合.如:需要定义字体的颜色和大小. 在CSS中是这样定义的: <style> .wu{COLOR:#0000CC;font-size:18px;} </style> 可以像这样使用上面的css样式:<div class="wu">wuyudong‘blog</div> 在Android中可以这样定义

android 主题和样式-style和Theme的区别和使用

项目中经常使用style和Theme,但却从来没有考虑过它们的区别,只会copy来copy去的,有时候还有些迷茫,为了彻底告别迷茫,现把这两者的区别和使用总结出来,供自己和大伙参考 一.作用域 Theme是针对窗体级别的,改变窗体样式. Style是针对窗体元素级别的,改变指定控件或者Layout的样式 二.使用方式 Theme 1. 在res\values\ 下创建themes.xml或者styles.xml文件 2. 添加<resouces>节点(根节点) 3. 添加自定义的style 4