第二十二章 样式与主题
一、颜色资源
颜色资源就是 res/values/colors.xml 文件,里面可以配置各种颜色的 item。
使用颜色资源,可以方便地在一处定义各种颜色值,然后在整个应用里引用。
二、样式
样式是能够应用于视图组件的一套属性。res/values/styles.xml 是样式文件。
<style name="BeatBoxButton">
<item name="android:background">@color/dark_blue</item>
</style>
样式可以为很多组件共用,更新修改属性时,只修改公共样式定义就行了。定义好样式,把它添加给各个组件。
1、样式继承
样式支持继承。一个样式能继承并覆盖其他样式的属性:
<style name="BeatBoxButton.Strong">
<item name="android:textStyle">bold</item>
</style>
这个新样式继承了BeatBoxButton样式的属性。
除了通过命名表示样式继承关系,也可以采用指定父样式的方式:
<style name="StrongBeatBoxButton" parent="@style/BeatBoxButton">
<item name="android:textStyle">bold</item>
</style>
三、主题
样式很有用。在styles.xml公共文件中,可以为所有组件定义一套样式属性共用。可惜,定义公共样式属性虽方便,实际应用却很麻烦:需要逐个为所有组件添加它们要用到的样式。要是开发一个复杂应用,涉及很多布局、无数按钮,仅仅添加样式就累死人了。
该是主题闪亮登场的时候了!主题可看作样式的进化加强版。同样是定义一套公共主题属性,样式属性需要逐个添加,而主题属性则会自动应用于整个应用。主题属性能引用颜色这样的外部资源,也能引用其他样式。使用主题,不用找到每个按钮,告诉它们要用哪个主题。一句话就搞定:“所有按钮都使用这个样式。”
1、修改默认主题
theme属性指向的主题叫AppTheme。它也定义在styles.xml文件中。可见,主题实际就是一种样式,可以修改该样式里的属性,自定义主题样式。
四、添加主题颜色
现在,基于AppTheme主题模板,我们来定制它的属性:
虽然这三个主题属性看上去和前面的样式属性差不多,但它们的应用范围不一样。样式属性仅适用于单个组件;主题属性则适用所有使用同一主题的组件。例如,工具栏会以主题的 colorPrimary 属性设置自己的背景色。
colorPrimary 属性主要用于工具栏。由于应用名称是显示在工具栏上的,colorPrimary也可以称为应用品牌色。
colorPrimaryDark 用于屏幕顶部的状态栏。从名字可以看出,它是深色版colorPrimary。注意,只有Lollipop以后的系统支持状态栏主题色。对于之前的系统,无论指定什么主题色,状态栏都是不变的黑底色。
colorAccent 主题色应该和colorPrimary形成反差效果,主要用于给EditText这样的组件着色。按钮组件不支持着色。
五、覆盖主题属性
第一个任务是修改主题以更换BeatBox应用的背景色。当然,你可以打开res/layout/fragment_ beat_box.xml文件,手工设置RecyclerView视图的android:background属性。如果还有其他fragment和activity要改,都照此处理。这简直是浪费:浪费你的时间,浪费应用资源。
主题已经设置了背景色,在此基础上再设置其他颜色,就是自己给自己找事。而且,在应用里到处复制使用背景属性设置代码也不利于后期维护。
1、主题探秘
要解决上述问题,应设法覆盖主题背景色属性。为了找出可覆盖属性的名字,先来看看这个属性在其Theme.AppCompat父主题里是怎么设置的。你需要找出主题继承的源头。主题继承树有多深,谁也不知道,只能一层层向上找,一直找到目标为止。
打开styles.xml文件,按住Command键(Windows系统是Ctrl键)点击Theme.AppCompat,来看看继承有多深。(如果无法直接在Android Studio里追溯主题属性,或是想在工具之外查找,可以在your-SDK-directory/platforms/android-24/data/res/values目录找到主题源码。)
<style name="Theme.AppCompat" parent="Base.Theme.AppCompat" /> 可 知 Theme.AppCompat主题属性继承自 Base.Theme.AppCompat。有趣的是, Theme. AppCompat本身没有覆盖任何属性,仅仅指向了其父主题。
按住Command键再点击Base.Theme.AppCompat,Android Studio会提示,这个主题有资源修饰符,有多个版本可选。选择values/values.xml版本。(BeatBox支持19及以上API级别,所以这里选择了无修饰版本。如果选择v21版本,很可能还会看到API 21级里添加的新特性。)
Base.Theme.AppCompat这个主题没任何自己的定义,也就是说没覆盖任何属性。继续定位到它的父主题:Base.V7.Theme. AppCompat。
距离目标越来越近了。Base.V7.Theme.AppCompat有许多属性,但还是没找到改变背景色的属性。继续定位到Platform.AppCompat。这个主题也有多个版本,选择values/values.xml版本。
终于,在这里看到了Platform.AppCompat的android:Theme父主题。注意,这里引用的不是Theme,而是android:Theme。前面的android命名空间不能丢。
AppCompat库可以看作BeatBox应用的一部分。编译项目时,工具会引入AppCompat库和它的一堆Java和XML文件。这些文件已包含在应用里,如同你自己编写的文件。如果想引用AppCompat库里的资源,像Theme.AppCompat这样,直接引用就可以了。
有些主题包含在Android操作系统里,如Theme,引用时必须加上指向归属地的命名空间。在引用Theme主题时,AppCompat库使用了android:Theme这样的形式,这是因为Theme来自于Android操作系统。
总算找到了。在这里,终于可以看到所有可以覆盖的主题属性。当然,还可以继续定位到Theme主题,不过没这个必要。我们想要的属性已经找到了。查看代码,可以看到windowBackground这个属性。顾名思义,这就是用于主题背景色的属性。
回到styles.xml文件中,覆盖windowBackground这个属性:
<style name="AppTheme" parent="Theme.AppCompat">
<item name="colorPrimary">@color/red</item>
<item name="colorPrimaryDark">@color/dark_red</item>
<item name="colorAccent">@color/gray</item>
<item name="android:windowBackground">@color/soothing_blue</item>
</style>
注意,windowBackground这个属性来自Android操作系统,所以别忘了使用android命名空间。
总结一下,刚才我们定位查看了以下主题:
- Theme.AppCompat
- Base.Theme.AppCompat
- Base.V7.Theme.AppCompat
- Platform.AppCompat
刚才我们自下而上逐层定位,直到找到AppCompat根主题。将来,越来越熟练之后,你很可能会跳过中间步骤而直达目标。不过,建议还是按部就班,以此看清楚究竟哪个是根主题。
最后再提个醒,主题继承关系和层次可能有变(发布新系统),但上面介绍的方法不会变。想要知道该覆盖哪个属性,就沿着继承树找吧!
六、修改按钮属性
可以在主题中定义一个用于所有按钮的样式。
再次逐级定位查找主题。这次,我们找到Base.V7.Theme.AppCompat里的buttonStyle属性。这个属性指定应用中普通按钮的样式:
<item name="buttonStyle">@style/Widget.AppCompat.Button</item>
这个buttonStyle属性没有设置值,而是指向了一个样式资源。定位并查看Widget.AppCompat.Button样式。Widget. AppCompat.Button样式没有定义任何属性,继续定位找其指向的父样式。你会发现有两个版本可选,选values/values.xml版本:
BeatBox应用的所有按钮都使用了这些属性。
在BeatBox应用里复用Android自身主题:
<style name="BeatBoxButton" parent=" Widget.AppCompat.Button">
<item name="android:background">@color/dark_blue</item>
</style>
继承Widget.AppCompat.Button样式,就是首先让所有按钮都继承常规按钮的属性。然后根据需要,有选择性地修改一些属性。
BeatBoxButton样式已重新定义完毕,可以使用了。经过前面主题深挖,我们知道要覆盖buttonStyle属性。下面覆盖buttonStyle属性,让它指向BeatBoxButton样式:
七、深入学习:样式继承拾遗
在进行主题探秘时,你可能已经注意到了,样式继承的表示法时有切换。AppCompat主题都是使用主题名表示继承,直到碰到Platform. AppCompat这个主题:
这里,继承是直接使用parent属性来表示的。为什么呢?
要以主题名的形式指定父主题,有继承关系的两个主题都应处于同一个包中。因此,对于Android操作系统内部主题间的继承,就可以直接使用主题名继承表示法。同理,AppCompat库内部也是这样。然而,一旦AppCompat库要跨库继承,就一定要明确使用parent属性。
在开发自己的应用时,应遵守同样的规则。如果是继承自己内部的主题,使用主题名指定父主题即可;如果是继承Android操作系统中的样式或主题,记得使用parent属性。
八、深入学习:引用主题属性
在主题中定义好属性后,可以在XML或代码中直接使用它们。
在XML中引用具体值(如颜色值)时,我们使用@符号。@color/gray指向某个特定资源。在XML中引用主题属性时,使用?符号:
<Button xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/list_item_sound_button"
android:layout_width="match_parent"
android:layout_height="120dp"
android:background="?attr/colorAccent"
tools:text="Sound name"/>
上述XML中 ? 符号的意思是使用 styles.xml 文件中 colorAccent 属性指向的资源。
也可以在代码中使用主题属性,但是比较啰嗦:
Resources.Theme theme = getActivity().getTheme();
int[] attrsToFetch = { R.attr.colorAccent };
TypedArray a = theme.obtainStyledAttributes(R.style.AppTheme, attrsToFetch);
int accentColor = a.getInt(0, 0);
a.recycle();
先取得Theme对象,然后要求它找到定义在AppTheme(即R.style.AppTheme)中的R.attr.colorAccent属性。结果得到一个持有数据的TypedArray对象。接着,向TypedArray对象索要int值以取出颜色。颜色值取出之后就可以使用了,比如,用来更改按钮背景色。
第二十三章 XML drawable
在Android世界里,凡是要在屏幕上绘制的东西都可以叫作drawable,比如抽象图形、Drawable类的子类代码、位图图像等。还有更多的drawable:state list drawable、shape drawable 和 layer list drawable。这三个drawable都定义在XML文件中,可以归为一类,统称为XML drawable。
一、统一按钮样式
不论屏幕大小,recycler视图总是显示三列按钮。如果还有多余的空间,它会拉伸列格以适配屏幕。不过,BeatBox应用的按钮不应拉伸,所以把它们封装在frame布局里。这样,frame布局会被拉伸,而按钮不会:
二、shape drawable
使用ShapeDrawable,可以把按钮变成圆。XML drawable和屏幕像素密度无关,所以无需考虑创建特定像素密度目录,直接把它放入默认的drawable文件夹就可以了。
打开项目工具窗口,在res/drawable目录下创建一个名为button_beat_box_normal.xml的文件:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/dark_blue"/>
</shape>
该XML文件定义了一个背景为深蓝色的圆形。也可使用shape drawable定制其他各种图形,如长方形、线条以及梯形等。
在styles.xml中,使用新建的button_beat_box_normal作为按钮背景:
此时,按钮背景已变成了圆形。不过,按钮点击之后样子没有任何变化。按钮按下去时,如果能切换显示状态,用户体验应该会更好。
三、state list drawable
为解决这个问题,首先定义一个用于按钮按下状态的shape drawable。在res/drawable目录下再创建一个名为button_beat_box_pressed.xml的文件,除了背景颜色是红色外,这个shape drawable和前面的正常版本是一样的:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/red"/>
</shape>
接下来,要在按钮按下时使用这个新建的shape drawable。这需要用到state list drawable。根据按钮的状态,state list drawable可以切换指向不同的drawable。按钮没有按下的时候指向button_beat_box_normal,按下的时候就指向button_beat_box_pressed。
在drawable目录中,定义一个state list drawable(res/drawable/button_beat_box.xml):
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/button_beat_box_pressed"
android:state_pressed="true"/>
<item android:drawable="@drawable/button_beat_box_normal" />
</selector>
现在,在styles.xml中修改按钮样式,改用button_beat_box作为按钮背景:
按钮没有按下的时候使用button_beat_box_normal作背景,按下时就使用button_beat_ box_pressed作背景。
除了按下状态,state list drawable还支持禁用、聚焦以及激活等状态。若想详细了解,请访问网页:developer.android.com/guide/topics/resources/drawable-resource.html#StateList。
四、layer list drawable
layer list drawable能让两个XML drawable合二为一。借助这个工具,可以为按下状态的按钮添加一个深色的圆环:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid
android:color="@color/red"/>
</shape>
</item>
<item>
<shape android:shape="oval">
<stroke
android:width="4dp"
android:color="@color/dark_red"/>
</shape>
</item>
</layer-list>
现在,layer list drawable中指定了两个drawable。第一个是和以前一样的红圈。第二个则会绘制在第一个圈上,它定义了一个4dp粗的深红圈。这会产生一个暗红的圈。
这两个drawable可以组成一个layer list drawable。多个当然也可以,会获得一些更复杂的效果。
五、深入学习:为什么要用 XML drawable
应用总需要切换按钮状态,所以state list drawable是Android开发不可或缺的工具。那shape drawable和layer list drawable呢?应该用吗?
XML drawable用起来方便灵活,不仅用法多样,还易于更新维护。搭配使用shape drawable和layer list drawable可以做出复杂的背景图,连图像编辑器都省了。更改BeatBox应用的配色更是简单,直接修改XML drawable中的颜色就行了。
另外,XML drawable独立于屏幕像素密度,可在不带屏幕密度资源修饰符的drawable目录中直接定义。如果是普通图像,就需要准备多个版本,以适配不同屏幕像素密度的设备;而XML drawable只要定义一次,就能在任何设备的屏幕上表现出色。
六、深入学习:使用 mipmap 图像
资源修饰符和drawable用起来都很方便。应用要用到图像,就针对不同的设备尺寸准备不同尺寸的图片,再分别放入drawable-mdpi和drawable-hdpi这样的文件夹。然后,按名字引用它们。剩下的就交给Android了,它会根据当前设备的屏幕密度调用相应的图片。
但是,有个问题不得不提。发布应用到Google应用商店时,APK文件包含了项目drawable目录里的所有图片。这里面有些图片甚至从来不会用到。这是个负担。
为解决这个问题,有人想到针对设备定制APK,比如mdpi APK一个,hdpi APK一个,等等。(有关APK分包的详细信息,可参阅工具文档网页:tools.android.com/tech-docs/new-build-system/user-guide/apk-splits。)
但问题解决得不够彻底。假如想保留各个屏幕像素密度的启动图标呢?
Android启动器是个常驻主屏幕的应用。按下设备的主屏幕键,会回到启动器应用界面。有些新版启动器会显示大尺寸应用图标。想让大图标清晰好看,启动器就需要使用更高分辨率的图标。对于hdpi设备,要显示大图标,启动器就会使用xhdpi图标。找不到的话,就只能使用低分辨率的图标。可想而知,放大拉伸后的图标肯定很糟。
Android的另一解决办法是使用mipmap目录。目前,Android Studio中的新项目已经可以使用mipmap资源了,根据屏幕分辨率将各类型图片放入对应文件夹下。
七、深入学习:使用 9-patch 图像
有时候(也可能经常),按钮背景图必须用到普通图片。那么,如果按钮需要以不同尺寸显示,背景图该如何变化呢?
如果按钮的宽度大于背景图的宽度,图片会被拉伸。拉伸的图片会有很好的效果吗?朝一个方向拉伸背景图很可能会让图片失去原样,所以得想个办法控制图片拉伸方式,不然背景图会很丑~~
使用9-patch图像能解决这个问题。9-patch图像是一种特别处理过的文件,能让Android知道图像的哪些部分可以拉伸,哪些部分不可以。只要处理得当,就能确保背景图的边角与原始图像保持一致。
为什么要叫作9-patch呢?9-patch图像分成3×3的网格,即由9部分或9 patch组成的网格。网格角落部分不会被缩放,边缘部分的4个patch只按一个维度缩放,而中间部分则按两个维度缩放:
9-patch图像和普通PNG图像十分相似,只有两处不同:9-patch图像文件名以.9.png结尾,图像边缘具有1像素宽度的边框。这个边框用以指定9-patch图像的中间位置。边框像素绘制为黑线,以表明中间位置,边缘部分则用透明色表示。
任意图形编辑器都可用来创建9-patch图像,但Android SDK自带的draw9patch工具用起来更方便。
在项目工具窗口中,右键单击ic_button_beat_box_ default.png,选择Refactor → Rename...菜单项将其改名为ic_button_beat_box_default.9.png。(如果Android Studio提示有同名资源,直接点Continue按钮继续。)再用相同的步骤得到另一个文件:ic_button_beat_box_pressed.9.png。
然后,双击默认图片在Android Studio内置的9-patch工具中打开。(如果Android Studio没能顺利打开9-patch编辑器,请先关闭图片文件,并在项目工具窗口中展开drawable目录,再尝试重新打开它。)
在9-patch工具中,首先,为让图片更醒目,勾选上Show patches选项。然后,把图像顶部和左边框填充为黑色,以标记图像的可伸缩区域。
图片的顶部黑线指定了水平方向的可拉伸区域。左边的黑线标记在竖直方向哪些像素可以拉伸。
顶部以及左边框标记了图像的可拉伸区域,那么底部以及右边框又该如何处理呢?它们定义了9-patch图像的可选内容区。内容区是绘制内容(通常是文字)的地方。如果不标记内容区,那么默认与可拉伸区域保持一致。
使用内容区让按钮上的文字居中。现在继续编辑ic_button_beat_box_default.9.png,在图片上添加上右边和底部两条线。同时勾选上Show content选项。这个选项会让预览器高亮显示图片的文字显示区。
下面为做好之后的状态:
上下左右四边的黑线,可以根据个人需要去拉长或缩短,选择区域并不一定非要按上图来。