如何控制Android控件的样式

来源:互联网 发布:数据库 migration 编辑:程序博客网 时间:2024/06/04 19:10
大家都知道,通过Android空间的样式是可以自定义的,像是TextView中的TextSize(文字大小),TextColor(文字颜色)等等,那么怎么样自定义这些样式呢?对于这个问题你可能早就有了答案,但是你的答案真的是一个好的方式吗?下面我将介绍几种自定义Android控件样式的方式,希望对读者有所启发。
小白模式——直接在布局文件中指定/直接在代码中设置

相信大多数人和我一样在刚刚学习Android的时候就已经了解掌握了这种方式,直接在xml中设置控件样式,像这样:

<Textview     android:layout_width="match_parent"    android:layout_height="warp_content"    android:textColor="#ffffff00"    android:textSize="24dp"/>

或者,在Java代码中定义控件的样式

    TextView textView = (TextView) finadViewById(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 android:style="@style/MyTextStyle"/>

如果你想要给某个控件所设置的样式和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>

使用style之后,你会发现你的布局文件代码数量一下减少了一半以上,看上去漂亮多了,你也可以更加方便的看出整个布局文件的结构,可以放更多的注意力到如何布局每一个控件而不是每一个控件中的样式。

高级模式——使用theme

虽然使用style的方式已经比直接写在布局文件中好很多了,但是我们不得不在定义每一个控件的时候给它加上一个style,能不能连这个属性都省了呢?答案是,至少对于一部分是可以的。用到的方法便是,使用自定义theme。

简单来说,theme就是一系列style的集合,它会使你所使用的控件都默认使用它里面定义的样式;而你的app中总会有几个异类和其它控件背景颜色不一样,或者是文字颜色不一样,所以我之前说“至少对于一部分是可以的”,而这部分异类就是需要你去定义style了,但是值得庆幸的是,你并不需要显示继承theme中引用的theme,只需要定义“不同”的部分就行了,其他部分还是会使用theme中的。

具体操作步骤是:

在“res/values/styles.xml”中定义一些基本的style:

    <style name="MyTextStyle">        <item name="android:textColor">#ffffff00</item>        <item name="android:textSize">24sp</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>

需要注意两点:

  • 自定义的theme最好继承系统的一个内置theme,除非你自信把所有的属性都定义的像内置theme一样全面。
  • theme的本质也是一个style,只是它是一系列style的集合(有点像Animation和animationSet的关系),所以它的标签也是style。

将自定义的theme设置到AndroidManifest中:

    <application        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        <!--  可能还有更多属性  -->        <!--  省略中间的节点  -->        android:theme="@style/AppTheme">    </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, @Nullable AttributeSet attrs) {        this(context, attrs, com.android.internal.R.attr.textViewStyle);    }    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr, 0);        //......    }

代码中有两点值得注意:

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 {    private ImageView mLeftView;    private ImageView mRightView;    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) {        this(context, attrs, defStyleAttr, 0);    }    public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        super(context, attrs, defStyleAttr, defStyleRes);        setView(context, attrs, defStyleAttr, defStyleRes);    }    private void setView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        //设为横向排列        setOrientation(HORIZONTAL);        //初始化ImageView        mLeftView = new ImageView(context);        mRightView = new ImageView(context);        //两个图片都匹配拉伸xy        mLeftView.setScaleType(ImageView.ScaleType.FIT_XY);        mRightView.setScaleType(ImageView.ScaleType.FIT_XY);        //两个图片各占一半        LinearLayout.LayoutParams params = new LayoutParams(0, LayoutParams.MATCH_PARENT);        params.weight = 1;        //添加两个child        addView(mLeftView, params);        addView(mRightView, params);    }    public void setLeftImage(Drawable drawable){        mLeftView.setImageDrawable(drawable);    }    public void setRightImage(Drawable drawable){        mRightView.setImageDrawable(drawable);    }}

在布局文件中定义

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.android.pretty.widget.CustomView        android:layout_width="match_parent"        android:layout_height="200dp" /></FrameLayout>

在Activity中设置两个图片

public class Main2Activity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main2);        CustomView customView = (CustomView)findViewById(R.id.customView);        customView.setLeftImage(getResources().getDrawable(R.mipmap.ic_launcher));        customView.setRightImage(getResources().getDrawable(R.mipmap.ic_launcher));    }}

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" />    <!--第二种-->    <declare-styleable name="CustomView">        <attr name="left_drawable" format="reference" />        <attr name="right_drawable" format="reference" />    </declare-styleable></resources>

format 代表这个属性标签可以接受的赋值的类型,format的值可以是reference、color等等,你也可以设置多种类型,比如

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    xmlns:app="http://schemas.android.com/apk/res-auto">    <com.android.pretty.widget.CustomView        android:id="@+id/customView"        android:layout_width="match_parent"        android:layout_height="wrap_content"        app:left_drawable="@mipmap/ic_launcher"        app:right_img="@mipmap/ic_launcher"/></FrameLayout>

我们平时在写布局文件的时候,每个属性标签前面都有一个前缀android:,这个其实是在布局文件的根节点中由xmlns:android="http://schemas.android.com/apk/res/android"这一句定义的,“xmlns”的意思是xml命名空间(xml name sapce),这个有点类似宏定义,所以你可以任性的把它改成xmlns:apple="http://schemas.android.com/apk/res/android",然后把所有默认属性标签的前缀改为apple:也是没有问题的。

那么命名空间的值到底代表什么呢?在Android中的命名空间"http://schemas.android.com/apk/res/android"是固定的,而后面的部分代表着 包名,如果你的属性标签用了某个命名空间作为前缀的话,解释器会去相应的包下找这个属性标签的定义;例如默认的属性标签都是使用android:作为前缀,因为他们都是在“android”包(Android API的包)下定义的。

那么我们自己定义的属性标签要用什么呢?使用android:的话肯定是不行的,系统识别不了。那就自定义一个命名空间吧。就像上面说的,名字可以自己定,我们就定为“app”,命名空间的前面是一样,后面加包名(我的工程的包名是“com.android.pretty”),那它的定义应该是这样:xmlns:app="http://schemas.android.com/apk/res/com.android.pretty",这样写在eclipse工程中是没有问题的;但是如果你使用的是android studio,它就会提示你 In Gradle projectes, always use http://schemas.android.com/apk/res-auto for custom attributs,OK,既然它坚持,那就听它的吧。

于是,我们的布局文件就成了上面的样子。

在代码中处理布局文件中的属性标签。

上面的代码写完以后,你尝试运行程序,发现OK,完美,功能实现了,别傻了,把之前写在onCreate中的两个set语句注释掉再试试。

结果显而易见,没有任何图片展示,好,现在就保持那两行被注释的状态,让我们进行接下来的操作。在CustomoView类的定义中对属性标签进行处理。(只修改了setupView方法)

    private void setView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {        //设为横向排列        setOrientation(HORIZONTAL);        //初始化ImageView        mLeftView = new ImageView(context);        mRightView = new ImageView(context);        //两个图片都匹配拉伸xy        mLeftView.setScaleType(ImageView.ScaleType.FIT_XY);        mRightView.setScaleType(ImageView.ScaleType.FIT_XY);        //两个图片各占一半        LinearLayout.LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);        params.weight = 1;        //添加两个child        addView(mLeftView, params);        addView(mRightView, params);        //处理自定义标签        final TypedArray a = context.obtainStyledAttributes(                attrs, R.styleable.CustomView, defStyleAttr, defStyleRes);        Drawable left = a.getDrawable(R.styleable.CustomView_left_drawable);        if (left != null) {            setLeftImage(left);        }        Drawable right = a.getDrawable(R.styleable.CustomView_right_drawable);        if (right != null) {            setRightImage(right);        }        a.recycle();    }

我们通过Context.obtainStyledAttributes方法可以得到一个TypeArray对像,Context.obtainStyledAttributes有四个参数:

  • 第一个参数是AttributeSet类型,对应两个参数构造方法中的第二个参数;
  • 第二个参数是一个 int[] 类型,这个对应着你在“attrs.xml”中定义的declare-styleable,这个值通过R.styleable.CustomView引用拿到一个数组(这也是为什么之前我建议定义自己属性标签的时候把它们定义在一个declare-styleable之中)
  • 第三个和第四个参数是一个int型,至于它们的含义,在后面会提到。

实际上,Context.obtainStyledAttributes有一个只有前两个参数的重载方法,我们在这里本可以使用这个重载方法,但是为了和后面一致,我们在这里使用了四个参数的方法。

我们得到TypedArray对象之后,可以通过它拿到你在布局文件中设置的属性标签的值。如果对应的标签没有被设置,则有可能拿到null。需要注意的是,TypeArray对象在使用之后必须调用recycle()方法。

运行一下,这次你在布局文件中通过自定义的属性标签设置的图片起作用了。

目标3:可以在布局文件中通过style来设置默认图片(第2目标完成之后,这个基本没有问题)

现在,让我们先把布局文件中的两句设置图片的语句注释掉。

我们已经可以在布局文件中通过自定义标签设置属性了,那么如何通过style来定义这些属性呢,对比Android内置的属性标签,你可能认为应该这样:

定义style:

    <style name="CustomViewStyle">        <item name="custom:left_drawable">@mipmap/ic_launcher</item>        <item name="custom:right_drawable">@mipmap/ic_launcher</item>    </style>

在布局文件中设置style

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <com.android.pretty.widget.CustomView        android:id="@+id/customView"        style="@style/CustomViewStyle"        android:layout_width="match_parent"        android:layout_height="wrap_content" />    <!--app:left_drawable="@mipmap/ic_launcher"-->    <!--app:right_drawable="@mipmap/ic_launcher"/>--></FrameLayout>

已经很接近正确答案了,但是当你试图运行程序的时候,发现编译不通过,爆出结果:

Error:(18, 21) No resource found that matches the given name: attr 'custom:left_drawable'.Error:(19, 21) No resource found that matches the given name: attr 'custom:right_drawable'.

它说找不到匹配的资源,这是因为对于在android framework 中定义的属性标签,你使用android作为命名空间前缀的话没有任何问题,但是当你使用自定义的属性标签的话,解释器不会知道custom命名空间到底是什么,那怎么办呢?其实解决办法是,根本不加命名空间前缀。这样解释器会在当前的工程中寻找匹配资源,正好我们的自定义属性标签是在这个工程中定义的,于是便找到了。所以我们只需要把style的定义写成这样:

    <style name="CustomViewStyle">        <item name="left_drawable">@mipmap/ic_launcher</item>        <item name="right_drawable">@mipmap/ic_launcher</item>    </style>

运行起来,OK,没有任何问题。

目标4:可以在theme中定义默认的style

我们先把布局文件中设置style的那句代码注释掉。

也许你在 如何在theme中自定义某个控件的默认样式 这里已经意识到,当我们自定义控件的时候想要实现可以在主题中设置默认style的功能需要用到这个;你想对了,我们的确需要写一个类似com.android.internal.R.attr.textViewStyle 的资源。从名字可以看出,这个资源是一个attr类型的,所以我们要做的就是包括定义一个attr类型的资源。具体步骤如下:

在“res/values/themes.xml” 中定义一个 attr 类型资源,这次不需要定义在一个declare-styleable 内部:

    <attr name="customViewStyle" format="reference" />

在“res/values/themes.xml” 中定义自己的theme,并把CustomView的默认style设置进去设置进去。

    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">        <!-- Customize your theme here. -->        <item name="colorPrimary">@color/colorPrimary</item>        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>        <item name="colorAccent">@color/colorAccent</item>        <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:supportsRtl="true"        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,现在完成了,运行起来,效果完美实现。希望各位有所收获。

1 0
原创粉丝点击