Android~自定义View和事件分发
作者:互联网
老生常谈自定义View,我们去查阅安卓相关书籍总是会有那么一章讲述自定义View的原理。说明这是高级UI的基础,高级UI自然范围也很大,本篇文章总结一下自定义View的套路,因为实际开发中我们时不时会需要自定义View,目的是加速开发。
两种坐标系
Android坐标系,左上角为原点,触控事件中的getRawX()和getRawY()获取的就是该坐标系下的值。
视图坐标系,描述的是子视图和在父视图的位置。可以获取到自身宽高,自身坐标。
事件分发介绍
首先我们先要知道Activity中View的层级,是自上而下的,具体我们可以去参考Activity的setContentView()跟踪源码。即:
Activity ——PhoneWindow——DectorView——rootViewGroup——子View
一个完整的事件流程是从Down开始的,UP结束,我们称作这为一个事件序列。某一事件序列经过触摸屏传递各个View,由各个view来处理这一事件的过程,即为事件分发。事件分发的三个重要方法:
- dispatchTouchEvent(MotionEvent ev) :用来进行事件的分发
- onInterceptTouchEvent(MotionEvent ev) :用来进行事件的拦截,dispatchTouchEvent中调用该方法,view中未提供该方法。
- onTouchEvent(MotionEvent ev) :用来处理Touch事件,dispatchTouchEvent中调用。
点击事件传递的规则,用伪代码表示如下:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean res = false;
if(onInterceptTouchEvent(ev)) { // 拦截后自己处理
res = onTouchEvent(ev);
}else {
res = child.dispatchTouchEvent(ev); // 分发
}
return res;
}
自定义属性
<resources><!-- resource是跟标签,可以在里面定义若干个declare-styleable -->
<declare-styleable name="CustomView"> <!-- 属性集名称-->
<attr name="color" format="color" /> <!-- 属性名称-->
<attr name="size" format="dimension" />
<!--每一个发生要定义format指定其类型,类型包括
reference 表示引用,参考某一资源ID
string 表示字符串
color 表示颜色值
dimension 表示尺寸值
boolean 表示布尔值
integer 表示整型值
float 表示浮点值
fraction 表示百分数
enum 表示枚举值
flag 表示位运算
-->
<attr name="background" format="reference|color" />
<!-- 注:属性可以有多种类型 -->
</declare-styleable>
- attrs.xml文件declare-styleable标签定义及相关属性
- 在布局文件中导入自定义的属性集。两种方法
<!-- 方法1 com.example 是应用的清单文件的包名 -->
xmlns:custom="http://schemas.android.com/apk/res/com.example"
<!-- 方法2 -->
xmlns:custom="http://schemas.android.com/apk/res-auto"
- 代码中如何获取自定义属性值
TypedArray arry = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
float size = arry.getDimension(R.styleable.CustomView_size,10f);
// to use
ViewGroup绘制流程
View和ViewGroup绘制流程基本相同,只是ViewGroup除了绘制自己还需要绘制子控件。绘制流程分为测量 ——布局——绘制 主要对应下面三个函数:
- onMeasure():测量当前控件大小并为布局提供建议
- onLayout():使用layout()函数对所有子控件进行布局
- onDraw():根据测量布局的位置绘图
measure流程和MeasureSpec
MeasureSpec是int型数字,但它由两部分组成mode+size,它转换为二进制前两位代表模式后30位代表数值,它有三种模式。MeasureSpec是View的内部类,作用是在Measure的过程中,将View的LayoutParams根据父容器所分发的规则转换成对应的MeasureSpec,最后在onMeasure根据该值确定View的宽高。
- UNSPECIFIED: 未指定模式,子View不受父View的限制,子View可以设置任意大小。一般用于系统内部的测量。
- EXACTLY:精确模式,对应于match_parent和具体数值,子元素被限定于给定的边界。
- AT_MOST:最大模式,对应于wrap_content,父控件给子控件分配的SpecSize。
作为顶层的View,它没有父容器。DecorView的getRootMeasureSpec方法第一个参数windowSize是指窗口尺寸,它的MeasureSpec由自身的LayoutParams和窗口尺寸大小决定。
特别需要注意的是,wrap_content对应AT_MOST,当布局文件中配置为EXACTLY模式时,我们就直接使用该值即可,当模式为AT_MOST,我们还需要将大小设置为我们计算的值,该值应该是包含控件最大值。
View的measure流程 : 先判断有无背景,取mMinWidth和背景的最小宽度的最大值; 再通过measureSpec获取默认大小
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
ViewGroup源码中无onMeasure方法,它的measure流程 :直接遍历测量子View的MeasureSpec,measureChild中则是先获取自己的LayoutParams ,再计算自己的getChildMeasureSpec。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
layout流程
View中的layout是用来确定自身的位置。调用层级是layout调用setFrame确定该View在父容器中的位置,最后才调用onLayout。
ViewGroup的layout则是用来确定子元素的位置,不通的布局有不同的摆放规则,但都离不开最终调用setChirdFrame方法,调用子View的layout方法确定子View的位置。为了满足多种需求,我们有时还需要获取子View的MarginLayoutParams和重写generateLayoutParams提取Margin值。
draw流程
- 如果需要绘制背景
- 保存当前canvas层
- 绘制View的内容,即onDraw()方法 是一个空实现由我们自己实现
- 绘制子View,调用dispatchDraw()对子View遍历,子View绘制
- 如果需要绘制View的褪色边缘,类似于阴影效果
- 绘制装饰,如滚动条。onDrawForeground()方法
注:getMeasuredWidth()和getWidth()函数的区别
他们大多时候是相同的,但含义是不一样的。getMeasuredWidth一般被调用在layout中,getWidth则被调用在onDraw中。我们时常会在onDraw混用两个方法,切记用错。
- getMeasuredWidth()函数在measure过程结束后就可以获取到,而getWidth()需要layout结束后才能获取到。
- getMeasuredWidth()的值时通过setMeasuredDimension()进行设置的,getWidth()是通过layout(left,top,right,bottom)函数设置。
总结
对于ViewGroup我们需要重点关注measure和layout。获取子控件的margin方法。对于View则需要关注measure darw,无需关注layout。自定义控件分为继承View和继承系统控件,继承ViewGroup和继承系统特定的ViewGroup。我们再接到需求是就应判断最接近那种实现,那种实现方便后续维护。
标签:控件,MeasureSpec,layout,自定义,int,Android,View 来源: https://blog.csdn.net/Bluechalk/article/details/114105489