如何控制 Android 控件的样式
大家都知道,通过 Android 控件的样式是可以自定义的,像是 TextView 中的 TextSize (文字大小),TextColor (文字颜色)等等,那么怎么样自定义这些样式呢?对于这个问题你可能早就有了答案,但是你的答案真的是一个好的方式吗?下面我将介绍几种自定义 Android 控件样式的方式,希望对读者有所启发。
小白模式 —— 直接在布局文件中指定/直接在代码中设置
相信大多数人和我一样,在刚刚学习 Android 的时候就已经了解掌握了这种方式,直接在 xml 中设置控件样式,像这样:
<TextView android:layout_width="match_parent" android:textColor="#ffffff00" android:textSize="24dp" android:layout_height="wrap_content"/>
或者,在 Java 代码中定义控件的样式:
TextView textView = (TextView) findViewById(R.id.text1); textView.setTextColor(0xffffff00); textView.setTextSize(40);
当然,你可能会把颜色和 dimen 值写到 “values” 文件夹写对应的文件中,这也是推荐去做的。
这种方式非常直观,想要定义哪个控件的样式,直接写就行了。但是难免显得眼界有点狭窄,如果当多个控件拥有相同的样式,或者之后细微的差别,那你就需要做很多的重复工作。
进阶模式 —— 使用 style
使用 style 之所以比直接定义在布局文件/直接在代码中设置高级一点,就是因为它在一定程度上改善了眼界狭窄的问题。通过使用 style ,你可以使多个控件很方便的使用相同或者相似的样式。
具体操作步骤是:
在 “res/values/styles.xml” 中定义你想要的 style:
<style name="MyTextStyle"> <item name="android:layout_width">100dp</item> <item name="android:layout_height">30dp</item> <item name="android:textColor">#ffffff00</item> <item name="android:textSize">24sp</item> <!-- 当然可能还有很多属性 --> </style>
在你希望使用这个 style 的控件所在的布局文件中设置 style:
<TextView style="@style/MyTextStyle" android:text="hello"/>
如果你想要给某个控件所设置的样式和 style 所定义的有一点区别,你可以通过两种方式进行实现:
直接在布局文件中加入有区别的属性,它会覆盖 style 中定义的属性。这种方式比较适合只有少数一两个控件与其他不同的情况:
<TextView style="@style/MyTextStyle" android:textColor="#ff000000" android:text="hello"/>
写一个 style 继承自之前的 style,在新的 style 中重写你想要改变的属性,把新的 style 赋给控件。这种方式比较适合一类控件与另一类控件有很少差别的情况:
<style name="MyTextStyle2" parent="MyTextStyle"> <item name="android:textColor">#ff000000</item> </style>
<TextView style="@style/MyTextStyle2" android:text="hello"/>
使用 style 之后,你会发现你的布局文件代码数量一下子减少了一般以上,看上去漂亮多了,你也可以更加方便的看出整个布局文件的结构,可以放更多的注意力到如何布局每一个控件而不是每一个控件中的样式。
高级模式 —— 使用 theme
虽然使用 style 的方式已经比直接写在布局文件中好很多了,但是我们不得不在定义每一个控件的时候给它加上一个 style,能不能连这个属性都省了呢?答案是,至少对于一部分是可以的。用到的方法便是,使用自定义 theme。
简单来说,theme 就是一系列 style 的集合,它会使你所使用的控件都默认使用它里面定义的样式;而你的 app 中总会有几个异类和其他控件背景颜色不一样,或者是文字颜色不一样,所以我之前说“至少对于一部分是可以的”,而这部分异类就是需要你去自定义 style 了,但是值得庆幸的是,你并不需要显式继承 theme 中引用的 style,只需要定义“不同”的部分就行了,其他部分还是会使用 theme 中的。
具体操作步骤是:
在 “res/values/styles.xml” 中定义一些基本的 style:
<style name="MyTextStyle"> <item name="android:textColor">#ffffff00</item> <item name="android:textSize">24sp</item> <!-- 当然可能还有很多属性 --> </style> <style name="MyButtonStyle"> <item name="android:textColor">#ffff00ff</item> <item name="android:textSize">20sp</item> <item name="android:background">#ff000000</item> </style>
在 “res/values/themes.xml” 中定义自己的 theme
<style name="MyAppTheme" parent="@android:style/Theme.DeviceDefault"> <item name="android:textViewStyle">@style/MyTextStyle</item> <item name="android:buttonStyle">@style/MyButtonStyle</item> </style>
需要注意两点:1. 自定义的 theme 最好继承系统的一个内置 theme,除非你自信把所有的属性都定义的向内置 theme 一样全面。 2. theme 的本质也是一个 style,只是它是一系列 style 的集合(有点像 Animation 和 AnimationSet 的关系),所以它的标签也是 style。
将自定义的 theme 设置到 AndroidManifest 中:
<application android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/MyAppTheme" <!-- 可能还有更多属性 --> > <!-- 省略中间的节点 --> </application>
当然,如果你希望只将 theme 应用到某个 Activity 的话,你可以给这个 Activity 单独设置 theme。
真正的干货
也许上面的内容你早就知道了,也许你觉得看起来很简单。的确是这样。但是难点在于,我怎么知道 theme 中有哪些属性可以定义呢?这个问题也困扰了我很久,不过最近我解决了这个问题,下面会和大家分享下。
如何在 theme 中自定义某个控件的默认样式
相信你在自定义 Android 控件的时候已经发现了一个点,那就是 View 的构造方法中有三个参数的构造方法(5.0之后甚至有4个的),其中第三个参数是 int 型的,叫做 defStyle。从名字上看,它和一个控件的默认样式应该是有关的;实际上也是这样,Android 就是通过这个参数给控件设置默认 style 的。很多人都已经知道,当在布局文件中定义控件的时候,是调用控件的两个参数的构造方法,那么三个参数的构造方法是在什么时候被调用又是如何赋给控件默认 style 的呢?
我们可以到控件的源码中去寻找答案,以 TextView 为例,我们可以看到它的构造方法如下:(Android 4.4.4版本的)
public TextView(Context context) { this(context, null); } public TextView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.textViewStyle); } public TextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // ...... }
代码中有两点值得注意:
TextView 的一个参数构造方法的实现就是调用两个参数的构造方法,而两个参数的构造方法的实现就是调用三个参数的构造方法。所以说,不论开发者调用哪个构造方法,最终都会调到三个参数的构造方法。
TextView 两个参数的构造方法在调用三个参数的构造方法时,将第三个参数指定为 com.android.internal.R.attr.textViewStyle。这正是 theme 中指定默认 style 起作用的关键,它意味着,你可以在 theme 中通过 <item name="android:textViewStyle">@style/MyTextViewStyle</item> 来指定 TextView 的默认 style。在其他控件的代码中你可以看到类似的代码。现在你知道如何为每个控件定义默认的 style 了。
自定义控件中的样式控制
到目前位置,我们都是在说如何控制 Android 提供的控件的样式,那么如果是我们自定义的控件呢?
下面我们通过一个例子来进行讲解,这个列子要实现的目标是这样的:
实现一个类,可以并排显示两个图片
可以在布局文件中通过属性标签设置两个图片
可以在布局文件中通过 style 来设置默认图片(第2个目标完成之后,这个基本没有问题)
可以在 theme 中定义默认的 style
我知道你可能会觉得这个控件真傻,用 LinearLayout 不就能实现这个布局了吗。的确是这样,但是这并不重要,只是为了让你了解如何进行自定义控件的样式控制,同时我也的确打算直接继承 LinearLayout 来实现这个类。
目标1:实现一个类,可以并排显示两张图片
实现类 CustomView:
public class CustomView extends LinearLayout { // 横向并排的两个 imageview private ImageView mLeftImage; private ImageView mRightImage; public CustomView(Context context) { this(context, null); } public CustomView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setupView(context, attrs, defStyleAttr); } private void setupView(Context context, AttributeSet attrs, int defStyleAttr) { // 设置为横向排列 setOrientation(HORIZONTAL); // 初始化 imageview mLeftImage = new ImageView(context); mRightImage = new ImageView(context); // 两个图片都拉伸匹配 xy mLeftImage.setScaleType(ImageView.ScaleType.FIT_XY); mRightImage.setScaleType(ImageView.ScaleType.FIT_XY); // 两个图片各占一半 LinearLayout.LayoutParams params = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); params.weight = 1; // 添加两个imageview addView(mLeftImage, params); addView(mRightImage, params); } /** * 设置左边图片 * @param drawable */ public void setLeftImage(Drawable drawable) { mLeftImage.setImageDrawable(drawable); } /** * 设置右边图片 * @param drawable */ public void setRightImage(Drawable drawable) { mRightImage.setImageDrawable(drawable); } }
在布局文件中定义:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.zhaoshe.democustomimageview.CustomView android:id="@+id/custom" android:layout_width="match_parent" android:layout_height="200dp"/> </FrameLayout>
在 MainActivity 中设置两个图片:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CustomView view = (CustomView) findViewById(R.id.custom); // demo1 和 demo2 是我事先放在 drawable 文件夹下的两张图片 view.setLeftImage(getResources().getDrawable(R.drawable.demo1)); view.setRightImage(getResources().getDrawable(R.drawable.demo2)); }
OK,现在把程序运行起来,你就可以看到两个并排的图片了,非常的简单,相信很多人早就会自定义比这复杂很多的控件了。
目标2:可以在布局文件中通过属性标签设置两个图片
想要在布局文件中使用属性标签来设置图片,首先要知道用哪个属性标签。查了一下之后你会发现,Android 根本就没有给你提供这样的标签(这很正常,毕竟是我们自己定义的控件)。既然 Android 没有为我们定义,那我们就自己定义吧:
在 “res/values/attrs.xml” 中定义属性便签
你可以这样:
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="left_img" format="reference"/> <attr name="right_img" format="reference"/> </resources>
或者你可以把一组属性标签放在一个 declare-styleable 下,像这样:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CustomView"> <attr name="left_img" format="reference"/> <attr name="right_img" format="reference"/> </declare-styleable> </resources>
format 代表这个属性标签可以接受的赋值的类型,format 的值可以是 reference、color 等等,你也可以设置多种类型,比如 <attr name="text" format="reference|string" 定义了一个名字叫做 text 的可以接受字符串和引用赋值的属性标签。
建议使用第二种方式,使用这种方式对于在布局文件中通过标签设置图片是必要的。
在布局文件中通过标签设置图片
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.zhaoshe.democustomimageview.CustomView android:id="@+id/custom" custom:left_img="@drawable/demo1" custom:right_img="@drawable/demo2" android:layout_width="match_parent" android:layout_height="200dp"/> </FrameLayout>
我们平时在写布局文件的时候,每个属性标签前面都有一个前缀 android:,这个其实是在布局文件的根节点中由 xmlns:android="http://schemas.android.com/apk/res/android" 这一句定义的,“xmlns” 的意思是 xml 命名空间(xml namespace),这个有点类似宏定义。所以你可以任性的把它改成 xmlns:apple="http://schemas.android.com/apk/res/android",然后把所有默认属性标签的前缀改为 apple: 也是没有问题的。
那么命名空间的值到底代表什么呢?在 Android 中的命名空间 “http://schemas.android.com/apk/res/” 是固定的,而后面的部分代表着 包名 ,如果你的属性标签用了某个命名空间作为前缀的话,解释器会去相应的包下找这个属性标签的定义;例如默认的属性标签都是使用 android: 作为前缀,因为它们都是在 “android” 包(Android API 的包)下定义的。
那么我们自己定义的属性标签要用什么呢?使用 android: 的话肯定是不行的,系统识别不了。那就自定义一个命名空间吧。就像上面说的,名字可以自己定,我们就定为 “custom”,命名空间的前面是一样的,后面加包名(我的工程的包名是“com.zhaoshe.democustomimageview”),那它的定义应该是这样:xmlns:custom="http://schemas.android.com/apk/res/com.zhaoshe.democustomimageview",这样写在 eclipse 工程中是没有问题的;但是如果你实用的是 android studio,它就会提示你 In Gradle projectes, always use http://schemas.android.com/apk/res-auto for custom attributs,OK,既然它坚持,那就听它的吧。
于是,我们的布局文件就成了上面那样子。
在代码中处理布局文件中的属性标签。
上面的代码写完以后,你尝试运行程序,发现OK,完美,功能实现了。别傻了,把之前写在 onCreate 中的两个 set 语句注释掉再试试。
结果显而易见,没有任何图片展示,好,现在就保持那两行被注释的状态,让我们进行接下来的操作。在 CustomView 类的定义中对属性标签进行处理。(只修改了 setupView 方法)
private void setupView(Context context, AttributeSet attrs, int defStyleAttr) { // 设置为横向排列 setOrientation(HORIZONTAL); // 初始化 imageview mLeftImage = new ImageView(context); mRightImage = new ImageView(context); // 两个图片都拉伸匹配 xy mLeftImage.setScaleType(ImageView.ScaleType.FIT_XY); mRightImage.setScaleType(ImageView.ScaleType.FIT_XY); // 两个图片各占一半 LinearLayout.LayoutParams params = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); params.weight = 1; // 添加两个imageview addView(mLeftImage, params); addView(mRightImage, params); // 处理自定义属性标签 final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomView, defStyleAttr, 0); if (array != null) { Drawable leftDrawable = array.getDrawable(R.styleable.CustomView_left_img); if (leftDrawable != null) { mLeftImage.setImageDrawable(leftDrawable); } Drawable rightDrawable = array.getDrawable(R.styleable.CustomView_right_img); if (rightDrawable != null) { mRightImage.setImageDrawable(rightDrawable); } array.recycle(); } }
我们通过 Context.obtainStyledAttributes 方法可以得到一个 TypedArray 对象。Context.obtainStyledAttributes 有四个参数,第一个参数是 AttributeSet 类型,对应两个参数构造方法中的第二个参数;第二个参数是一个 int[] 类型,这个对应着你在 “attrs.xml” 中定义的 declare-styleable ,这个值通过 R.styleable.CustomView 引用的话是拿到一个数组(这也是为什么之前我建议你定义自己的属性标签的时候把它们定义在一个 declare-styleable 之中);第三个和第四个参数是一个 int 型,至于它们的含义,在后面我们会提到。实际上,Context.obtainStyledAttributes 有一个只有前两个参数的重载方法,我们在这里本可以使用这个重载方法,但是为了和后面一致,我们在这里使用了四个参数的方法。
我们得到 TypedArray 对象之后,可以通过它拿到你在布局文件中设置的属性标签的值。如果对应的标签没有被设置,则有可能拿到 null。需要注意的是,TypedArray 对象在使用之后必须调用 recycle() 方法。
运行一下,这次你在布局文件中通过自定义的属性标签设置的图片起作用了。
目标3:可以在布局文件中通过 style 来设置默认图片(第2个目标完成之后,这个基本没有问题)
现在,让我们先把布局文件中的两句设置图片的语句注释掉。
我们已经可以在布局文件中通过自定义标签设置属性了,那么如何通过 style 来定义这些属性呢,对比 Android 内置的属性标签,你可能认为应该这样做:
定义 style :
<style name="CustomViewStyle"> <item name="custom:left_img">@drawable/demo1</item> <item name="custom:right_img">@drawable/demo2</item> </style>
在布局文件中设置 style
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.zhaoshe.democustomimageview.CustomView android:id="@+id/custom" style="@style/CustomViewStyle" android:layout_width="match_parent" android:layout_height="200dp"/> <!--custom:left_img="@drawable/demo1"--> <!--custom:right_img="@drawable/demo2"--> </FrameLayout>
已经很接近正确答案了,但是当你试图运行程序的时候,发现编译不通过,爆出错误: Error:(5, 22) No resource found that matches the given name: attr 'custom:left_img'. 它说找不到匹配的资源,这是因为对于在 android framework 中定义的属性标签,你使用 android 作为命名空间前缀的话没有任何问题,但是当你使用自定义的属性标签的话,解释器不会知道 custom 命名空间到底是什么,那怎么办呢?其实解决办法是,根本不加命名空间前缀。这样解释器会在当前的工程中寻找匹配资源,正好我们的自定义属性标签是在这个工程中定义的,于是便找到了。所以我们只需要把 style 的定义写成这样:
<style name="CustomViewStyle"> <item name="left_img">@drawable/demo1</item> <item name="right_img">@drawable/demo2</item> </style>
运行起来,OK,没有任何问题。
目标4:可以在 theme 中定义默认的 style
我们先把布局文件中设置 style 的那句代码注释掉。
也许你在 如何在 theme 中自定义某个控件的默认样式 这里已经意识到,当我们自定义控件的时候想要实现可以在主题中设置默认 style 的功能需要需要用到这个;你想对了。我们的确需要自己写一个类似 com.android.internal.R.attr.textViewStyle 的资源。从名字中可以看出,这个资源是一个 attr 类型的,所有我们要做的就包括定义一个 attr 类型的资源。具体步骤如下:
在 “res/values/attrs.xml” 中定义一个 attr 类型的资源,这次不需要定义在一个 declare-styleable 内部:
<attr name="customViewStyle" format="reference"/>
在 “res/values/themes.xml” 中定义自己的 theme,并把 CustomView 的默认 style 设置进去。
<style name="AppTheme" parent="@android:style/Theme.DeviceDefault.Light"> <item name="customViewStyle">@style/CustomViewStyle</item> </style>
在 “AndroidManifest.xml” 中的 Application 或者 Activity 节点中设置 theme,不同的位置代表不同的作用范围。
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <!-- 其中的 Activity 节点这里没有列出 --> </application>
最后一步,在 CustomView 的构造方法中设置默认 style 的属性名,也就是第三个参数:
public CustomView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.customViewStyle); }
刚才讲到 Context.obtainStyledAttributes 方法的第三个参数和第四个参数略去没有讲,其实第三个参数现在看来正是 R.attr.customViewStyle 这个代表默认 style 的属性名;而第四个参数呢,正是默认 style,不过我们通常是通过在 theme 中定义默认 style,所以这里一般是 0。
ok,现在完成了。 运行起来,效果完美实现。希望各位看官有所收获,同时如果发现我有哪里写错的地方,希望大家帮忙指点。
最后,这里是下载 Demo 代码的链接,最近比较缺积分,所以定的高了点,如果你没有那么多积分的话,我也将代码上传了 github, 这里是链接。
<p>版权声明:本文为博主原创文章,未经博主允许不得转载。</p>