Android~自定义View和事件分发

Android 同时被 2 个专栏收录
24 篇文章 2 订阅
23 篇文章 1 订阅

老生常谈自定义View,我们去查阅安卓相关书籍总是会有那么一章讲述自定义View的原理。说明这是高级UI的基础,高级UI自然范围也很大,本篇文章总结一下自定义View的套路,因为实际开发中我们时不时会需要自定义View,目的是加速开发。

两种坐标系

Android坐标系,左上角为原点,触控事件中的getRawX()和getRawY()获取的就是该坐标系下的值。
安卓坐标系
视图坐标系,描述的是子视图和在父视图的位置。可以获取到自身宽高,自身坐标。
View坐标系

事件分发介绍

首先我们先要知道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中调用。
    ViewGroup事件分发源码

点击事件传递的规则,用伪代码表示如下:

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>
  1. attrs.xml文件declare-styleable标签定义及相关属性
  2. 在布局文件中导入自定义的属性集。两种方法
<!-- 方法1 com.example 是应用的清单文件的包名 -->
xmlns:custom="http://schemas.android.com/apk/res/com.example"
<!-- 方法2 -->
xmlns:custom="http://schemas.android.com/apk/res-auto"
  1. 代码中如何获取自定义属性值
TypedArray arry = context.obtainStyledAttributes(attrs, R.styleable.CustomView);
float size = arry.getDimension(R.styleable.CustomView_size,10f);
// to use
ViewGroup绘制流程

View和ViewGroup绘制流程基本相同,只是ViewGroup除了绘制自己还需要绘制子控件。绘制流程分为测量 ——布局——绘制 主要对应下面三个函数:

  1. onMeasure():测量当前控件大小并为布局提供建议
  2. onLayout():使用layout()函数对所有子控件进行布局
  3. onDraw():根据测量布局的位置绘图
measure流程和MeasureSpec

MeasureSpec是int型数字,但它由两部分组成mode+size,它转换为二进制前两位代表模式后30位代表数值,它有三种模式。MeasureSpec是View的内部类,作用是在Measure的过程中,将View的LayoutParams根据父容器所分发的规则转换成对应的MeasureSpec,最后在onMeasure根据该值确定View的宽高。

  1. UNSPECIFIED: 未指定模式,子View不受父View的限制,子View可以设置任意大小。一般用于系统内部的测量。
  2. EXACTLY:精确模式,对应于match_parent和具体数值,子元素被限定于给定的边界。
  3. 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流程
  1. 如果需要绘制背景
  2. 保存当前canvas层
  3. 绘制View的内容,即onDraw()方法 是一个空实现由我们自己实现
  4. 绘制子View,调用dispatchDraw()对子View遍历,子View绘制
  5. 如果需要绘制View的褪色边缘,类似于阴影效果
  6. 绘制装饰,如滚动条。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。我们再接到需求是就应判断最接近那种实现,那种实现方便后续维护。

展开阅读全文
  • 1
    点赞
  • 2
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:马嘣嘣 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值