背景:
在这个看脸的时代,好的UI效果能让人更容易记住你。有不少人说,不懂自定义控件的Android 工程师,不是合格的Android工程师。本文旨在说明自定义控件的总体步骤,以便对自定义控件有个大致的了解。
使用情景:
当遇到下面几点要求的时候,就需要自定义控件了:
1.特定的显示风格 :比如我们想要实现一个效果但系统并没有提供给我们这样一个控件,那我们就可以自己设计显示风格
2.处理特有的用户交互 :比如Touch Remove这样做一些其它的操作,于是为了实现对应的交互,我们选择自定义控件
3.优化布局 :这个可以体现在我们的 ListView 中,大家都知道 ListView 是通过调用 getView() 方法来渲染item,那么当用户快速滑动我们的ListView的时候,getView() 方法就会被频繁地调用。这时如果我们的item的布局比较复杂的话,那么渲染的速度可能就会变得缓慢,用户在快滑的时候就会感觉到卡顿。所以我们可以通过自定义控件去实现复杂的item布局,极高地提升渲染的效率。
4.封装 : 就是为了封装和复用
自定义控件的流程:
1.自定义属性的声明与获取
2.测量 onMeasure
3.[布局onLayout(ViewGroup)]
4.绘制onDraw
5.onTouchEvent
6.[onInterceptTouchEvent(ViewGroup)]
7.状态的恢复和保存
带了[ ]的,第3个因为是自定义ViewGroup时需要对子View的位置进行设置,而第6个则是对子View的Touch事件,如ScrollView,需要监听子View上是不是触发move事件,如果用户有上下移动的手势,ScrollView就会将其拦截,然后调用自己的onIntercepTouchEvent方法,实现上下滑动的效果。所以这俩是针对于ViewGroup的,如果你要自定义的是View控件,这两步就可以不做。
状态的恢复和保存,拿进度条来举例,当用户正在下载的时候,如果下载时间比较久的话,用户就可能不会在页面等待,也许会切到别的界面去玩会儿游戏,聊会儿天,做别的事情,一会儿之后再回到我们的app。但是因为内存的原因,我们的在后台可能会被系统杀死,但用户第二次进入我们的app时,Activity可能已经被重建了,于是我们的View也会被销毁和重建,如果下载到75%,这个时候我们的进度条即使被重建也应该显示为75%,否则用户就会疑惑下载的内容去了哪里,所以就需要就涉及到状态的恢复和保存。
这些步骤只是总结,在实际开发中,可以根据自己的需要选择步骤去处理,这些步骤并不是一定都是必须的。
自定义属性
在自定义属性之前,我们首先应该先了解Android中是如何定义一个属性的,就拿我们最熟悉的LinearLayout来举例:
我们在xml文件中引用了LinearLayout之后,就要设置它的layout_width和layout_height属性,我们可以在SDK目录下的路径platforms\android-25\data\res\values 找到attrs.xml,Android控件中的属性都在里面。
<declare-styleable name="LinearLayout_Layout">
<attr name="layout_width" />
<attr name="layout_height" />
<attr name="layout_weight" format="float" />
<attr name="layout_gravity" />
</declare-styleable>
这就是Android定义的LinearLayout的几个属性,我们应该对它们很熟悉。谷歌自定义属性的方法十分清晰,我们要定义一个attrs.xml的文件用来配置我们需要的属性。
其实大部分的标题栏可以分为三部分,即返回按钮,标题和更多按钮
我们可以知道是把标题栏TopBar分成了三部分,左边的Button,中间的文字,还有右边的Button,看到这个可以知道我们要自定义的属性分别是左边右边Button的文字内容,文字尺寸和背景色,中间文字的内容,尺寸,字体颜色共九个属性。
首先要在我们res/values下创建一个attrs.xml,我们自定义属性要用到的就是declare-styleable标签,我们通过declare-styleable标签来告诉系统这是我们自定义的属性。然后我们在declare-styleable下通过attr这个标签为我们的自定义属性制定名字,类型等。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Topbar">
<attr name="title" format="string"/>
<attr name="titleTextSize" format="dimension"/>
<attr name="titleTextColor" format="color"/>
<attr name="leftText" format="string"/>
<attr name="leftTextColor" format="color"/>
<attr name="leftBackground" format="reference|color"/>
<attr name="rightText" format="string"/>
<attr name="rightTextColor" format="color"/>
<attr name="rightBackground" format="reference|color"/>
</declare-styleable>
</resources>
declare-styleable包围所有属性,它的name为该属性集的名字,主要用途是标识该属性集。这个对于我们引用里面的属性是必要的。
attr标签有几个需要我们注意的地方:
format是我们最常用的属性,format是格式的意思,所以这个属性是申明我们自定义的属性是什么类型,使用的类型如下:
reference:参考指定Theme中资源ID。也就是从资源文件中获取值。
//属性定义
<declare-styleable name = "Topbar">
<attr name = "background" format = "reference" />
</declare-styleable>
//属性使用
<ImageView
android:layout_width = "42dp"
android:layout_height = "42dp"
lht:background = "@drawable/..." />
color:颜色值
boolean:布尔值
dimension:尺寸值。这里如果是dp那就会做像素转换
float:浮点值
integer:整型值
string:字符串
fraction:百分数
reference|color:颜色的资源文件
reference|boolean:布尔值的资源文件
enum:枚举值,一般是用作子标签。
flag:是自己定义的,类似于 android:gravity=”top”,就是里面对应了自己的属性值。一般是用作子标签。
enum:
//属性定义
<declare-styleable name="My">
<attr name="language">
<enum name="English" value="1"/>
</attr>
</declare-styleable>
//属性使用
<Button lht:language="English"/>
flag: 与 enum不一样的就是flag还能做位或运算。
//属性定义
<declare-styleable name="My">
<attr name="math">
<flag name="one" value="1" />
<flag name="two" value = "0x30" />
</attr>
</declare-styleable>
//属性使用
<activity lht:math="one|two"/>
像上面那样就把我们的自定义属性设计好了,我们就可以在控件中调用它们了。
自定义属性获取
属性已经定义好了,因为我们并不是要在已有的控件中添加属性,所以我们不是在xml直接使用,而是先写一个继承系统提供的控件的类,这里我们的TopBar就用RelativeLayout来实现。
public class Topbar extends RelativeLayout {
public Topbar(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
如果没有自定义属性,选择的构造方法只要有上下文参数就可以了,但我们这里有,所以就使用这一个构造方法。
既然要获取我们在xml中的属性,自然要先在Java中声明对应的变量,如下:
public class Topbar extends RelativeLayout {
private Button leftButton, rightButton;
private TextView tvTitle;
private int leftTextColor;
private String leftText;
private Drawable leftBackground;
private int rightTextColor;
private String rightText;
private Drawable rightBackground;
private float titleTextSize;
private int titleTextColor;
private String title;
public Topbar(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
setBackgroundColor(0xFFF59563);
}
private void init(Context context, AttributeSet attrs) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Topbar);
leftText = ta.getString(R.styleable.Topbar_leftText);
leftTextColor = ta.getColor(R.styleable.Topbar_leftTextColor, 0);
leftBackground = ta.getDrawable(R.styleable.Topbar_leftBackground);
rightText = ta.getString(R.styleable.Topbar_rightText);
rightTextColor = ta.getColor(R.styleable.Topbar_rightTextColor, 0);
rightBackground = ta.getDrawable(R.styleable.Topbar_rightBackground);
title = ta.getString(R.styleable.Topbar_title);
titleTextColor = ta.getColor(R.styleable.Topbar_titleTextColor, 0);
titleTextSize = ta.getDimension(R.styleable.Topbar_titleTextSize, 0);
ta.recycle();
leftButton = new Button(context);
rightButton = new Button(context);
tvTitle = new TextView(context);
leftButton.setText(leftText);
leftButton.setTextColor(leftTextColor);
leftButton.setBackground(leftBackground);
rightButton.setText(rightText);
rightButton.setTextColor(rightTextColor);
rightButton.setBackground(rightBackground);
tvTitle.setText(title);
tvTitle.setTextSize(titleTextSize);
tvTitle.setTextColor(titleTextColor);
tvTitle.setGravity(Gravity.CENTER);
}
}
控件和变量的声明我相信大家都能明白的,因为我们的TopBar由两个Button和一个TextView组成,所以这里做的工作就是把我们的自定义属性和控件组合。
要把我们刚才自定义的属性和变量关联,Android给我们提供了很好的API,我们可以通过TypedArray来存储我们在xml中获取到的那些自定义属性,方式就是通过上下文调用obtainStyledAttributes(AttributeSet set, @StyleableRes int[] attrs)方法,set是我们构造方法里的attrs,int数组就是我们用declare-styleable定义的属性集的ID,通过R.styleable.NAME获取。
把属性集拿到后就是从这个TypedArray中取对应的属性了,方法像键值对一样用get方法取之前定义的format类型的值。拿getColor(@StyleableRes int index, @ColorInt int defValue)方法来说,index就是对应属性的ID,这里的写法是属性集名加下划线加属性名,defValue顾名思义就是默认值。还有要注意的一点是我们在用完TypedArray这个变量后,要用recycle()将它回收,既是为了避免浪费资源,也是为了防止因为缓存出现一些错误。
把属性与我们的变量关联后,就是为我们的控件赋值了,这比较简单,相信大家都对setText()、setTextColor()这样的方法十分熟悉了,只要把变量添加进去即可,顺便把TextView设置为居中。
以上,就完成了我们自定义属性的获取。
子View布局参数
接下来就为我们的Button和TextView设置LayoutParams,把它们添加到布局中来:
private LayoutParams leftParams, rightParams, titleParams;
private void layout() {
leftParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
leftParams.addRule(ALIGN_PARENT_LEFT);
this.addView(leftButton, leftParams);
rightParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
rightParams.addRule(ALIGN_PARENT_RIGHT);
this.addView(rightButton, rightParams);
titleParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
titleParams.addRule(CENTER_IN_PARENT, TRUE);
this.addView(tvTitle, titleParams);
}
只是用了LayoutParams里的几个很简单的方法,相信大家都能看懂。这样就把我们的控件完成了,最后就把这个TopBar添加到布局文件中:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lht="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_top_bar"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.ht.animator.TopBarActivity">
<com.ht.custom.Topbar
android:id="@+id/topbar"
android:layout_width="wrap_content"
android:layout_height="40dp"
lht:leftBackground="@android:color/holo_blue_bright"
lht:leftTextColor="#FFFFFF"
lht:leftText="Back"
lht:rightBackground="@android:color/holo_blue_bright"
lht:rightTextColor="#FFFFFF"
lht:rightText="More"
lht:title="自定义标题"
lht:titleTextColor="#000000"
lht:titleTextSize="10sp">
</com.ht.animator.Topbar>
</RelativeLayout>