自定义View与ViewGroup
一、概述:
(一)、View和ViewGroup的职责
1、ViewGroup的职责是什么?
ViewGroup相当于一个放置View的容器,在写布局xml的时候,会告诉容器(凡是以layout开头的属性,都是为用于告诉容器),容器宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity),还有margin等。因此ViewGroup的职能为:给childView计算出建议的宽和高和测量模式 ,决定childView的位置。
为什么只是建议的宽和高,而不是直接确定呢,因为childView的宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
2、View的职责是什么?
View的职责,根据测量模式和ViewGroup给出的建议宽和高,计算出自己的宽和高;另外还有个更重要的职责是:在ViewGroup为其指定的区域内绘制自己的形状。
3、ViewGroup和LayoutParams之间的关系?
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。
(二)、View的三种测量模式
ViewGroup会为childView指定测量模式,下面简单介绍下三种测量模式:
1、EXACTLY:表示设置了精确的值,一般当childView设置其宽、高为精确值、match_parent时,ViewGroup会将其设置为EXACTLY;
2、AT_MOST:表示子布局被限制在一个最大值内,一般当childView设置其宽、高为wrap_content时,ViewGroup会将其设置为AT_MOST;
3、UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中;此种模式比较少见。
【备注】:
上面的每一行都有一个一般,意思上述不是绝对的,对于childView的mode的设置还会和ViewGroup的测量mode有一定的关系;当然了,这是第一篇自定义ViewGroup,而且绝大部分情况都是上面的规则,所以为了通俗易懂,暂不深入讨论其他内容。
(三)、从API角度进行浅析
View根据ViewGroup传人的测量值和模式,对自己宽高进行确定(onMeasure中完成),然后在onDraw中完成对自己的绘制。
ViewGroup需要给View传入view的测量值和模式(onMeasure中完成),而且对于此ViewGroup的父布局,自己也需要在onMeasure中完成对自己宽和高的确定。此外,需要在onLayout中完成对其childView的位置的指定。
二、自定义View:
(一)、简介:
1、关键部分:
Drawing the layout is a two pass process: a measure pass and a layout pass.
所以一个view执行OnDraw时最关键的是measure和layout。其实这很好理解的,一个view需要绘制出来,那么必须知道他要占多大的空间也就是measure,还得知道在哪里绘制,也就是把view放在哪里即layout。把这两部分掌握好也就可以随意自定义view了。至于viewGroup中如何绘制就参考官方文档,其实就是一个分发绘制,直到child是一个view自己进行绘制。
2、重写一个view一般情况下只需要重写onDraw()方法。那么什么时候需要重写onMeasure()、onLayout()、onDraw() 方法呢,这个问题只要把这几个方法的功能弄清楚就应该知道怎么做了。
①、如果需要绘制View的图像,那么需要重写onDraw()方法。(这也是最常用的重写方式。)
②、如果需要改变view的大小,那么需要重写onMeasure()方法。
③、如果需要改变View的(在父控件的)位置,那么需要重写onLayout()方法。
④、根据上面三种不同的需要你可以组合出多种重写方案。
3、按类型划分,自定义View的实现方式可分为三种:自绘控件、组合控件、以及继承控件。
4、如何让自定义的View在界面上显示出来? 只需要像使用普通的控件一样来使用自定义View就可以了。
(二)、自绘控件:
1、概念:自绘控件的意思就是,这个View上所展现的内容全部都是自己绘制出来的。
2、自绘控件的步骤:
1、绘制View :
绘制View主要是onDraw()方法中完成。通过参数Canvas来处理,相关的绘制主要有drawRect、drawLine、drawPath等等。
Canvas绘制的常用方法:
drawColor() 填充颜色
drawLine() 绘制线
drawLines() 绘制线条
drawOval() 绘制圆
drawPaint()
drawPath() 绘制路径
drawPicture() 绘制图片
drawPoint() 绘制点
drawPoints() 绘制点
drawRGB() 填充颜色
drawRect() 绘制矩形
drawText() 绘制文本
drawTextOnPath() 在路径上绘制文本
2、刷新View :(刷新view的方法这里主要有:)
invalidate(int l,int t,int r,int b)
刷新局部,四个参数分别为左、上、右、下
invalidate()
整个view刷新。执行invalidate类的方法将会设置view为无效,最终重新调用onDraw()方法。
invalidate()是用来刷新View的,必须是在UI线程中进行工作。在修改某个view的显示时,调用invalidate()才能看到重新绘制的界面。invalidate()的调用是把之前的旧的view从主UI线程队列中pop掉。
invalidate(Rect dirty)
刷新一个矩形区域
3、控制事件:(例如处理以下几个事件)
onSaveInstanceState() 处理屏幕切换的现场保存事件
onRestoreInstanceState() 屏幕切换的现场还原事件
onKeyDown() 处理按键事件
onMeasure() 当控件的父元素正要放置该控件时调用
3、案例核心代码:
(三)、组合控件:
1、概念:
组合控件的意思就是,不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
2、例如:标题栏就是个很常见的组合控件,很多界面的头部都会放置一个标题栏,标题栏上会有个返回按钮和标题,点击按钮后就可以返回到上一个界面。那么下面我们就来尝试去实现这样一个标题栏控件。
3、案例核心代码——实现图片右上角显示红色圆圈,圈内显示数字
(四)、继承控件:
1、概念:
继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。
这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能,比如 Android PowerImageView实现,可以播放动画的强大ImageView 就是一个典型的继承控件。
2、例如:对ListView进行扩展, 加入在ListView上滑动就可以显示出一个删除按钮,点击按钮就会删除相应数据的功能。
3、核心代码——实现ListView的item手势侧滑,显示删除按钮
【备注:】
1、getWidth(): View在设定好布局后整个View的宽度。
2、getMeasuredWidth():
对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调用measure(0,0);(measure中的参数的值你自己可以定义),否则你得到的结果和getWidth()得到的结果是一样的。
自定义View:
1、 绘制控件
a) 创建类,继承View及View的子类,并提供相关的构造方法
b) 准备画笔,
// 创建画笔
//mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint = new Paint();
// 抗锯齿
mPaint.setAntiAlias(true);
// 设置画笔颜色,红色
mPaint.setColor(Color.RED);
// 设置描边
mPaint.setStyle(Paint.Style.STROKE);
c) 在画布中通过画笔来画出UI
1, 重写onDraw()方法,实现绘制特定内容
2, 在此方法中,通过画布,来绘制
3, 在自定义View中调用invalidate() ,会调用onDraw方法,重新绘制
2、组合控件
a) 继承现有布局,RelativeLayout LinearLayout
LayoutInflater i = LayoutInflater.from(
1, 创建布局文件
2,创建自定义ViewGroup,继承现有的ViewGroup(RelativeLayout LinearLayout)
3, 通过LayoutInflater加载布局,获取View组件实例
4,把自定义ViewGroup加入到布局中
< 类名
。。。。。/>
class = "类名" 。。。。。/> 5, 在自定义ViewGroup中添加行为 b) 继承ViewGroup,ViewGroup中嵌套其他组件 继承ViewGroup类,需要重写两个方法 1. onMeasure(int widthMeasureSpec, int heightMeasureSpec) -- measure计算方法 measure(int widthMeasureSpec, int heightMeasureSpec) measureChildren(widthMeasureSpec, heightMeasureSpec) 计算子视图大小,并且保存到子视图 -- MeasureSpec类有以下方法: MeasureSpec对象,封装了layout规格说明,并且从父view传递给子view。 每个MeasureSpec对象代表了width或height的规格 MeasureSpec对象包含一个size和一个mode,其中mode可以取以下三个数值之一: > EXACTLY,0 [0x0],精确的,表示父view为子view确定精确的尺寸。 > AT_MOST,-2147483648 [0x80000000],子view可以在指定的尺寸内尽量大。 > UNSPECIFIED,1073741824 [0x40000000],未加规定的,表示没有给子view添加任何规定。 public static int getMode (int measureSpec) 从传入的值中获取属于MeasureSpec定义的哪一种模式 public static int getSize (int measureSpec) 从传入的值中获取大小 public static int makeMeasureSpec(int size, int mode) 创建MeasureSpec -- setMeasuredDimension(int measuredWidth, int measuredHeight) 存储ViewGroup测量得到的宽度和高度值 2. onLayout(boolean changed,int l, int t, int r, int b) 在View给其孩子设置尺寸和位置时被调用。 数changed表示view有新的尺寸或位置;参数l表示相对于父view的Left位置; 参数t表示相对于父view的Top位置;参数r表示相对于父view的Right位置;参数b表示相对于父view的Bottom位置 调用孩子各自的layout(int, int, int, int)方法, ------- 实现步骤: 1) 继承ViewGroup,实现onLayout(boolean changed,int l, int t, int r, int b) 2)同时重写 onMeasure 3) 组件定位,onLayout方法 4, 自定义View属性 1) 在attr.xml中定义属性 2)在布局文件中 xmlns:myview="http://schemas.android.com/apk/res-auto" xmlns:myview="http://schemas.android.com/apk/ android:layout_width="100dp" android:layout_height="55dp" myview:contentBackground="#0022ff" myview:labelText="@string/add" myview:labelTextSize="12sp" myview:labelTextColor="@android:color/holo_red_light"/> 3) 解析属性 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyViewWithAttrs); contentBackground = array.getColor( R.styleable.MyViewWithAttrs_contentBackground,contentBackground); labelText = array.getString( R.styleable.MyViewWithAttrs_labelText); labelTextColor = array.getColor( R.styleable.MyViewWithAttrs_labelTextColor,labelTextColor); labelTextSize = array.getDimension( R.styleable.MyViewWithAttrs_labelTextSize,labelTextSize); array.recycle(); /** * mode共有三种情况,取值分别为 * MeasureSpec.UNSPECIFIED, * MeasureSpec.EXACTLY, * MeasureSpec.AT_MOST。 * * MeasureSpec.EXACTLY是精确尺寸, * 当我们将控件的layout_width或layout_height指定为具体数值时 * 如andorid:layout_width="50dip",或者为FILL_PARENT是,都是控件大小已经确定的情况,都是精确尺寸。 * * MeasureSpec.AT_MOST是最大尺寸, * 当控件的layout_width或layout_height指定为WRAP_CONTENT时 * ,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可 * 。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。 * * MeasureSpec.UNSPECIFIED是未指定尺寸,这种情况不多,一般都是父控件是AdapterView, * 通过measure方法传入的模式。 */ 今天的分享结束了,再见~