1、前言
自定义控件在我们的开发过程中占据了很大一部分,可以说如果你不会自定义控件,那么你就不是一个合格的Android工程师。要知道系统给我们提供的控件很可能在样式和效果上跟我们想要实现和呈现给用户的不相符合,这个时候如果你不会自定义控件,那么就只能茫然四顾啦。
我的这篇博客并不是特指讲解某个控件的自定义,要介绍的是自定义控件的基础,如自定义控件的流程这样,更多的是一种编程模式,在我看来,无论是哪个控件的自定义都是大同小异的,毕竟都是继承的View嘛,所以同根同源,我们要学习的正是自定义控件的实现步骤。
2、使用场景
通过前言我相信大家都应该知道为什么要使用自定义控件了,这里我来总结下自定义控件都在什么时候使用。
在我看来如果有下面几点要求的时候,就去自定义控件:
- 特定的显示风格 比如我们想要实现一个效果但系统并没有提供给我们这样一个控件,那我们就可以自己设计显示风格
- 处理特有的用户交互 比如Touch Remove这样做一些其它的操作,于是为了实现对应的交互,我们选择自定义控件
- 优化布局 这个可以体现在我们的 ListView 中,大家都知道 ListView 是通过调用 getView() 方法来渲染item,那么当用户快速滑动我们的ListView的时候,getView() 方法就会被频繁地调用。这时如果我们的item的布局比较复杂的话,那么渲染的速度可能就会变得缓慢,用户在快滑的时候就会感觉到卡顿。所以我们可以通过自定义控件去实现复杂的item布局,极高地提升渲染的效率。
- 封装 这是比较单纯的就是为了封装和复用
3、自定义流程
在我看来,自定义控件的步骤可以分为以下几步:
- 自定义属性的声明与获取
- 测量onMeasure
- [布局onLayout(ViewGroup)]
- 绘制onDraw
- onTouchEvent
- [onInterceptTouchEvent(ViewGroup)]
- 状态的恢复和保存
带了[ ]的,第三步因为是自定义ViewGroup时需要对子View的位置进行设置,而第六步则是对子View的Touch事件,如ScrollView,需要监听子View上是不是触发move事件,如果用户有上下移动的手势,ScrollView就会将其拦截,然后调用自己的onIntercepTouchEvent方法,实现上下滑动的效果。所以是针对于ViewGroup的,如果你要自定义的是View控件,这两步就可以不做啦。
第七步状态的恢复和保存,拿进度条来举例,当用户正在下载的时候,如果下载时间比较久的话,用户就可能不会在页面等待,也许会切到别的界面去玩会儿游戏,聊会儿天,做别的事情,一会儿之后再回到我们的app。但是因为内存的原因,我们的在后台可能会被系统杀死,但用户第二次进入我们的app时,Activity可能已经被重建了,于是我们的View也会被销毁和重建,如果下载到75%,这个时候我们的进度条即使被重建也应该显示为75%,否则用户就会疑惑下载的内容去了哪里,所以就需要就涉及到状态的恢复和保存。
这些步骤只是总结而已,在实际开发中,你可以根据自己的需要选择步骤去处理,这些步骤并不是一定必须的。
4、自定义属性
在自定义属性之前,我们首先应该先了解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的文件用来配置我们需要的属性。
既然是要自定义控件,我们就写个例子来演示一下过程,接下来控件的重写也是这个例子。
我们大家都用过很多app,也自己写过不少,应该发现很多app的都有一个topbar,形式也大致相同,就拿QQ来说。
我们可以看到在QQ中几乎每个界面的标题栏都不一样,如果每增加一个界面就要多写一个TopBar,那我们肯定都要疯啦,所以在这里我们完全可以用自定义来实现这样一个可以修改像文字图片这样属性的控件。
最终就是完成这种可以改变属性的样式。
看到我们的样式和QQ上的标题栏,我们可以知道是把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"/>
像上面那样就把我们的自定义属性设计好了,我们就可以在控件中调用它们啦,是不是觉得很简单。
5、自定义属性获取
属性已经定义好了,因为我们并不是要在已有的控件中添加属性,所以我们不是在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设置为居中。
以上,就完成了我们自定义属性的获取。
6、子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>
在这里我们首先要声明我们的属性,否则是使用不了的。大家可以想想我们是如何使用系统给我们的控件的,android:layout_width,这个android就是xml的引用,我们写Java代码需要import导入包,xml中就是用xmlns实现。
xmlns:lht="http://schemas.android.com/apk/res-auto"
仿照系统的写法,加上这样的一句话就可以使用我们的自定义属性了,Android Studio中是用res-auto,eclipse是在res下加上包名。
其实我们的TopBar写到这里就已经实现了,因为我们的控件继承的是RelativeLayout,所以它本身就提供了许多方法,实现了很多功能,像addView()这样的方法,让我们不用重写它的onLayout(),onMeasure()还有onDraw()方法就可以实现我们想要的效果。在实际开发中,我们也不是总要继承View或者ViewGroup,完全设计一个控件,像RelativeLayout它已经满足了我们想要的很多要求,只是要添加几个效果就没必要去继承ViewGroup来实现了。
不过我们这篇博客是要介绍自定义控件的整个过程,如果忽略了上面提到的这几个方法,那可不算学会了自定义控件,所以在下篇博客中我还会介绍使用这些方法。
结束语:本文仅用来学习记录,参考查阅。