自定义控件——初识自定义控件
本文出自 “阿敏其人” 简书博客,转载或引用请注明出处。
开发的时候,因为业务需求或者封装需要,我们会进行自定义控件。
说在前面,本篇涉及到一些东西
- onMeasure
- onLayout
- onDraw
- MeasureSpec (32位二进制数,头两位模式(Mode),后两位大小(Size))
- onFinishInflate
�* ViewGroup的getViewAt 方法 - MeasureSpec的makeMeasureSpec方法,getSize,getMode,和AT_MOST、EXACTLY、UNSPECIFIFD三个常量。
- getMeasuredWidth和getMeasuredHeight
一、控件的简单分类
简单粗暴来说,控件可以分为2种,View和ViewGroup
View 单独的控件,里面不能存放控件,继承自View。比如Imageview Button TextView。
TextView继承自View.pngViewGroup 能存放控件的容器,比如FrameLayout、RelativeLayout 和LinearLayout等。
FrameLayout继承自ViewGroup.png二、自定义控件的分类
自定义控件,类型各有各的分法,本人的对其进行大概如下分类:
- 自制控件 : 该自定义控件继承自View或者ViewGroup,自己绘制。
- 组合控件 : 利用系统已提供的控件,组合成一个新的控件
- 拓展控件 : 继承自系统已提供的控件并且加上新的功能或者特性。
既然如此,我们就开始来吧,从自制控件说起。
(本文的最后会附上关于三类自定义控件的博文链接)
三、自制控件
如果用最简单接地气的语言来描述自制控件,那么就是
1、继承自View或者ViewGroup
2、利用 onMeasure(测量)、onLayout(摆放)和onDraw(绘制) 三大步骤弄来弄去把View搞出来。(三者不必同时用到)
3、在利用事件的触发机制等调一调onTouchEvent等方法监听一下,然后利用Scroller或者ViewDragHelper等做做动画之类,合适怎么整就怎么整。
4、写写接口,调调回调,该谁干活谁就干活。
说成一句话:继承自View或者ViewGroup然后测量摆放绘制,然后做做动画,最后写写接口给调用者调用。
小二,来一个最简单的自制控件吧
1、 需要先认识的简单知识
提示:如果觉得琐碎可以直接从三.2的实例看起,最后再回过头来看这个三.1的内容。效果可能更佳。
我们的ViewGroup继承自View,在View里面有这么几个重要方法
首先三个我们已经提到的
1.1、measure和onMeasure(),layout()和onLayout(),draw()和onDraw()
- measure() View的测量的入口,辗转调用真正工作的onMeasure()
- layout() View的摆放的入口,辗转调用真正工作的onLayout()
- draw() View的绘制的入口,辗转调用真正工作的onDraw()
其中,measure确定View的宽高,layout确定View的四个顶点的位置,而draw将View绘制在屏幕上。
measure()方法是final类型的方法,子类无法复写,而layout()和draw()子类就可以复写
- onMeasure() 回调该方法对控件进行测量
- onLayout() 回调该方法对控件位置进行摆放
- onDraw() 回调该方法对控件进行绘制
measure和onMeasure()
关于measure(),需要分两种情况讨论,
情况1,只是View,那么通过View的measure() 可以完成对View的测量,而measure() 会去调用 onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
情况2,是ViewGroup,measure()除了完成对容器自身的测量之外,接着还会遍历去调用子元素(子元素可以使View或者ViewGroup)的measure(),各个子元素再去递归调用自身子元素的measure。注意是递归,递归,递归,重要的说三遍。
在ViewGroup的measure()会去调用onMeasure(),然后我们需要在onMeasure()里面对容器里的孩子进行 孩子.measure() 对孩子进行测量。
layout()和onLayout()
对于layout
如果是View,View调用layout可以指定View的位置
如果是ViewGroup,那么onLayout() 里面进行 孩子.layout 可以精确摆放孩子的位置
draw()和onDraw()
对于draw
这个就 比较简单了,他是按照如下几个流程走的
1、绘制背景 backgroud.draw(canvas)
2、绘制本身( onDraw )
3、绘制孩子( dispatchDraw )
4、绘制装饰 ( onDrawScrollBars )
其实实际开发中我们常用的,关心的也就是onDraw方法。
其实说了这么多,大概就是需要明白的是:
一个View的从无到有需要经过measure(),layout(),draw()三个步骤,measure()会辗转调用measure(),layout()会辗转调用onLayout,draw会转转调用onDraw().
如果是View,那么就写写onDraw,onLayout,至于onMeasure一般不需要。
如果是ViewGroup,那么些在onMeasure里面测量自身后接着进行对孩子的测量,在onLayout里面进行孩子的位置摆放。
.
.
1.2、onFinishInflate方法
我们知道,我们在Xml写的布局文件最终会在通过Pull解析的方式转成代码的。
onFinishInflate的作用,就是在xml加载组件完成后调用的。这个方法一般在自制ViewGroup的时候调用。
/**
* Finalize inflating a view from XML. This is called as the last phase
* of inflation, after all child views have been added.
*
* <p>Even if the subclass overrides onFinishInflate, they should always be
* sure to call the super method, so that we get called.
*/
@CallSuper
protected void onFinishInflate() {
}
.
.
1.3、ViewGroup的 getChildAt(int position) 方法
Returns the view at the specified position in the group.
返回该组中指定位置的视图。
这个位置按照我们include的顺序排列,索引从0开始。
/**
* Returns the view at the specified position in the group.
*
* @param index the position at which to get the view from
* @return the view at the specified position or null if the position
* does not exist within the group
*/
public View getChildAt(int index) {
if (index < 0 || index >= mChildrenCount) {
return null;
}
return mChildren[index];
}
.
.
1.4 onMeasure和measure的参数
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
孩子.measure(32位的MeasureSpec宽,32位的MeasureSpec高);
}
但看起来他们是一个int值,这两个参数可以是宽高的意思,但是不完全是,这宽和高都是32由 32位的二进制码组成的,并不是随随便便穿进去一个int类型的值就完事了的。
既然不能随随便便,那我们应该传什么?——我们应该传一个32位的二进制数。
那么我们计算出或者拿到这个数?—— 使用View类里面的MeasureSpec这个静态内部类。
1.5、 View类里面的静态内部类 MeasureSpec
我们首先需要看一个类,MeasureSpec
MeasureSpec.png从上图我们可以看到,MeasureSpec这个类给我们提供了4个方法和3个常量
makeMeasureSpec和32位的二进制int值
先看看几个方法
- makeMeasureSpec(int size,int mode)
返回值是int,我们就是用MeasureSpec的makeMeasureSpec方法制造32位的二进制数给measure(宽,高)这个方法。
其中,size占30位,mode占2位
这个方法的2个参数
size(大小)怎么指定?
mode怎么确定?
** 利用LayoutParams得到size **
我们的View有多大谁知道?——我们在写xml的布局文件的时候就知道了有多大了。
但是怎么获取文件的在xml布局时的大小呢?
利用View的getLayoutParams()方法可以得到一个LayoutParams类型的值,利用
layoutParams.width 和
layoutParams.height
就可以获得View在布局时的宽高。
** 怎么确定mode **
mode有3种模式,分别是EXACTLY:精确模式;AT_MOST�最大值模式,UNSPECIFIED : 未指定的模式,
MeasureSpec.EXACTLY,精确模式
当我们的 layout_width 和 layout_height 设定为
填充父窗体 match_parent 或者
指定具体数值 比如 android:layout_width="130dp" 时
就可以采用EXACTLY这种模式
.
MeasureSpec.AT_MOST,最大值模式
当我们的 layout_width 和 layout_height 设定为 warp_content 时,控件大小随着内容大小的变化而变化,就是采用AT_MOST这种模式。
.
MeasureSpec.UNSPECIFIED,未指定模式
父容器没有给子布局任何限制,子布局可以任意大小。比较奇怪,少用。
1.6、getMeasuredWidth和getMeasuredHeight
getMeasuredWidth 获取测量后的宽度
getMeasuredHeight 获取测量后的高度
这两个如果孩子没有被measure有效地测量过,那么返回的值是0。
getMeasuredWidth
/**
* Like {@link #getMeasuredWidthAndState()}, but only returns the
* raw width component (that is the result is masked by
* {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured width of this view.
*/
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
.
.
getMeasuredHeight
/**
* Like {@link #getMeasuredHeightAndState()}, but only returns the
* raw width component (that is the result is masked by
* {@link #MEASURED_SIZE_MASK}).
*
* @return The raw measured height of this view.
*/
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
.
.
2、继承自View的简单demo
上菜之前,我们新建一个名字为MyDiyView 的类,当我们继承自View,会被系统要求写上构造方法。
2.1、重载构造方法
我们一般重载2个或者3个即可
系统提示.png构造方法
public class MyDiyView extends View {
// java代码创建视图的时候被调用
public MyDiyView(Context context) {
super(context);
}
// xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
public MyDiyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
//xml创建视图并且指定StyleAttr是调用,一般不需需要
public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
// 参数多指定了一个,这个方法是API 21以上才有的,所以一般不需要
public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}
只要有一个构造方法就不会报错了,一般我们会写上头两个构造方法。
为什么系统或有这样要求呢,因为这几个构造方法是必不可少的。根据参数的不同,其应用场合的也不同。我们在代码备注里面已经说得很清楚。
现在这个自定义VIew就能用了。只是当前是一片空白。
2.2、给view,鲜艳上色
一篇空白的View我们不需要,我们还是画上一点东西吧
�对于自制控件来说,有三个方法是必须掌握的,分别是
onMeasure (测量)
onLayout (摆放)
onDraw (绘制)
我们当前既然是最简单,直接调用一下用于绘制的onDraw就好
复写一下onDraw方法,发现onDraw里面一个Canvas类型的参数可以拿来用
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
利用Canvas,我们可以画 线条,文字,图形,bitmap等等。我们先来画一个线条。(关于Canvas的相关知识网上很多,不了解的可以自行参考)
Canvas可以画出来很多东西
Canvas可以画出来很多东西.png
我们画一条线看看
MyDiyView
public class MyDiyView extends View {
private Paint paint;
// java代码创建视图的时候被调用
public MyDiyView(Context context) {
super(context);
paint = new Paint(); // 此处创建可避免多次创建对象,浪费内存
}
// xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
public MyDiyView(Context context, AttributeSet attrs) {
super(context, attrs);
paint = new Paint();
}
//xml创建视图并且指定StyleAttr是调用,一般不需需要
public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(20); // 笔划大小
// canvas 是帆布的意思,也可以理解为绘画板
// 我们现在来画一条线
/**
public void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
两点连成线
startX和startY确定开始的点
stopX和stopY确定结束的点
*/
canvas.drawLine(30, 30, 120, 70, paint); // 利用canvas画一条线
super.onDraw(canvas);
}
}
顺便佐证下构造函数的作用
佐证1、利用第一个构造函数直接在Java代码创建出来视图
public class MainActivity extends Activity {
private TextView mTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
setContentView(new MyDiyView(MainActivity.this));
}
}
运行界面:
2016-02-28_23-03-33.png如上已证
佐证2、在xml代码里面调用我们的自定义控件
在MainActivity的activity_main的xml布局里面利用MyDiyView的全路径名,引用我们的自定义控件
activity_main
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.amqr.simpleviewtest.MainActivity">
<com.amqr.simpleviewtest.MyDiyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
MainActivity
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
运行,发现效果如上图的运行效果。
3、继承自ViewGroup的简单demo
3.1、 重载构造方法和复写onLayout
我们新建一个类,继承ViewGroup
发现,系统不单单要需要弄出来几个构造方法,还要求实现复写onLayout方法。这就不报错了。
其实这也可以理解,毕竟是可以存放孩子的容器嘛。
像下面这样就不错报了
public class MyDiyViewGroup extends ViewGroup{
public MyDiyViewGroup(Context context) {
super(context);
}
public MyDiyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
3.2、把View塞进去ViewGroup
接下来我们怎么把View塞进ViewGroup里面呢?假设我们要塞进去的是两个View
1、新建两个xml布局文件,一个放上一个TextView,一个放上我们刚刚写好的自定义控件MyDiyView,在MainActivity的xml中的MyViewGroup里面include这两个布局
item_text
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mLlDelete"
android:layout_width="130dp"
android:layout_height="60dp"
android:background="#ff0000"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文本"
android:textSize="22dp"
android:textColor="#ffffff"
/>
</LinearLayout>
.
.
item_mdv
<?xml version="1.0" encoding="utf-8"?>
<com.amqr.simpleviewtest.MyDiyView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="60dp">
</com.amqr.simpleviewtest.MyDiyView>
.
.
MyViewGroup里面include这两个布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.amqr.simpleviewtest.MainActivity">
<com.amqr.simpleviewtest.MyDiyViewGroup
android:id="@+id/mMyDiyViewGroup"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->
<!--TextView,系统自带的-->
<include layout="@layout/item_text"/>
<!--自定义控件-->
<include layout="@layout/item_mdv"/>
</com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>
.
.
2、在onFinishInflate方法中使用getChild(int position)方法加载获取这两个View
public class MyDiyViewGroup extends ViewGroup{
private View myDiyView;
private View mViewText;
public MyDiyViewGroup(Context context) {
super(context);
}
public MyDiyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mViewText = getChildAt(0);
myDiyView = getChildAt(1);
}
}
到了这里总算把View塞进ViewGroup了。但是此时如果直接运行程序,我们会发现还看到什么效果。
3.3、把让进ViewGroup的View显示出来
这时候,我们就需要利用到onMeasure和onLayout了。
首先进行复写onMeasure,在onMeasure里面进行 孩子.measure(measureSpec的32位的宽,measureSpec的32位的高)
这个32的值的size利用 LayoutParams 得到
至于mode,因为我们xml都是指定大小的,所以用MeasureSpec.EXACTLY
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LayoutParams layoutParams = mViewText.getLayoutParams();
int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
mViewText.measure(mViewTextWidth,mViewTextHeight);
LayoutParams lp = myDiyView.getLayoutParams();
int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);
myDiyView.measure(myDiyViewWidth, myDiyViewHeight);
}
.
.
在onLayout里面进行 孩子.layout(l,t,r,b)
利用 孩子View.getMeasuredWidth和孩子View.getMeasuredHeight获取的孩子的宽和高
因为之前我们已经对孩子进行 孩子View.measure(宽,高)了,所以此时调用getMeasuredWidth和getMeasuredHeight能够顺利获得孩子的宽和高。(如果没进行measure那么这两个获取的值就是0)
为什么不用 孩子View.getWidth() 和 孩子View.getHeight()?
因为在haiziView还没进行 孩子View.layout(l,t,r,b)之前,如果使用 孩子View.getWidth(),那么获取出来的值是0。
摆放孩子的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewTextWidth = mViewText.getMeasuredWidth();
int mViewTextHeight = mViewText.getMeasuredHeight();
Log.d("VG", "mViewText.getMeasuredWidth(): " + mViewText.getMeasuredWidth());
Log.d("VG", "mViewText.getMeasuredHeight(): " + mViewText.getMeasuredHeight());
mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);
int myDiyViewWidth = myDiyView.getMeasuredWidth();
int myDiyViewHeight = myDiyView.getMeasuredHeight();
myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
Log.d("VG", "layout之后 mViewText.getWidth(): " + mViewText.getWidth());
}
此时运行程序,发现我们这个ViewGroup已经成功显示在屏幕上了。
ViewGroup成功显示.png附上完整的MyViewGroup代码
MyViewGroup
public class MyDiyViewGroup extends ViewGroup{
private View myDiyView;
private View mViewText;
public MyDiyViewGroup(Context context) {
super(context);
}
public MyDiyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
LayoutParams layoutParams = mViewText.getLayoutParams();
int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
mViewText.measure(mViewTextWidth,mViewTextHeight);
LayoutParams lp = myDiyView.getLayoutParams();
int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);
myDiyView.measure(myDiyViewWidth, myDiyViewHeight);
Log.d("VG", "layout之前 mViewText.getWidth(): " + mViewText.getWidth());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int mViewTextWidth = mViewText.getMeasuredWidth();
int mViewTextHeight = mViewText.getMeasuredHeight();
Log.d("VG", "mViewText.getMeasuredWidth(): " + mViewText.getMeasuredWidth());
Log.d("VG", "mViewText.getMeasuredHeight(): " + mViewText.getMeasuredHeight());
mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);
int myDiyViewWidth = myDiyView.getMeasuredWidth();
int myDiyViewHeight = myDiyView.getMeasuredHeight();
myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
Log.d("VG", "layout之后 mViewText.getWidth(): " + mViewText.getWidth());
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mViewText = getChildAt(0);
myDiyView = getChildAt(1);
}
}
MainActivity
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
tools:context="com.amqr.simpleviewtest.MainActivity">
<com.amqr.simpleviewtest.MyDiyViewGroup
android:id="@+id/mMyDiyViewGroup"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->
<!--TextView,系统自带的-->
<include layout="@layout/item_text"/>
<!--自定义控件-->
<include layout="@layout/item_mdv"/>
</com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>
.
.
最后附上证明在layout之前孩子的getWidth是无法获取宽度的log
02-29 02:40:45.952 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 0
02-29 02:40:45.952 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 0
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredWidth(): 260
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredHeight(): 120
02-29 02:40:46.024 5073-5073/? D/VG: layout之后 mViewText.getWidth(): 260
02-29 02:40:46.032 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredHeight(): 120
02-29 02:40:46.036 5073-5073/? D/VG: layout之后 mViewText.getWidth(): 260
关于最简单的自制控件到此结束。
下面的链接是本人关于三类自定义控件的相关博文,欢迎链接查看。
自制控件1 开关按钮
自制控件2 —— 自制控件 仿qq侧滑菜单
组合控件1—— 设置框
组合控件2——海贼王选项菜单
拓展控件——拓展TextView
本篇完。