View详解

View在Android中是一切控件的基类,是所有控件的抽象,代表着一个控件。ViewGroup则是代表着一个控件组,包含了多个控件。在Android的设计中,ViewGroup也继承了View,这也就意味着View本身就可以是单个控件也可以是多个控件组成的一组控件,通过这种关系就形成了View树的结构。

Activity中View的结构

基础知识

View的位置参数

View的位置主要由它的四个顶点来决定。它的坐标是以父容器的左上角为基准,x轴方向向右,y轴方向向下。

  • top:左上角纵坐标
  • left:左上角横坐标
  • right:右下角横坐标
  • bottom:右下角纵坐标
  • X:左上角横坐标
  • Y:左上角纵坐标
  • TranslationX:左上角相对于父容器的偏移量
  • TranslationY:左上角相对于父容器的偏移量
//各个属性之间的关系
width = right - left;
height = bottom - top;
X = left + TranslationX;
Y = right + TranslationY;

需要注意的是这些参数在View被绘制出来之后才有确定的值,在之前则为0。也就是说如果直接在Activity的onCreate()方法中打印View的位置属性,则得到的结果是全部为0。Activity中有一个onWindowFocusChanged(boolean hasFocus)方法,会在这个焦点变化时被调用,这个时候View的宽高和位置信息都是已经确定的了。

View的事件

  • ACTION_DOWN: 手指刚接触屏幕
  • ACTION_MOVE: 手指在屏幕移动
  • ACTION_UP: 手指从屏幕上松开的一瞬间

正常情况下,一次触摸屏幕会触发一系列点击事件。

TouchSlop系统所能识别的最小滑动距离,也就是说当滑动距离小于这个值的时候,系统不认为这是滑动。这个值是一个常量,和设备有关,可以通过ViewConfiguration(getContext).getScaledTouchSlop()来获得。它存在的意义就是为我们在处理一些滑动的时候做过滤。

View的滑动

实现View滑动的的三种方式:

  1. View本身的scrollTo和scrollBy(这个方法移动的是View本身的位置,而我们看到的是View中内容的相对位移,这就是为什么我们希望的效果是右移却将移动值设置为负数的原因)
  2. 通过给View设置平移动画来实现(在动画中,移动的是View的影相而不是View这个对象)
  3. 改变View的Layout.Params是的View重新布局(使用复杂,就是在修改View的属性)

View的事件分发机制

//用来进行事件的分发,如果事件能够传递给当前的View,那么这个方法一定会被调用,返回值表示是否消耗当前事件
public boolean dispatchTouchEvent(MotionEvent ev)
//用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,
//此方法不会被再次调用,返回值表示是否拦截当前事件
public boolean onInterceptTouchEvent(MotionEvent event)
//在dispatchTouchEvent()方法中调用,用来处理点击事件
//返回值表示当前事件是否被消耗
public boolean onTouchEvent(MotionEvent event)

View的事件分发机制从用户点击屏幕开始,每一个事件都会由当前Activity获取接受然后开始事件分发流程。Activity -> PhoneWindow -> DecorView -> ViewGroup(布局文件实现的View)。每一个View(或者ViewGroup)对传递过来的事件都是执行相同的原则,即先判断是否拦截这个事件,如果拦截即消耗这个事件,否则不拦截则传递给子View。上面三个重要的方法,执行的伪代码如下所示。这三个方法的返回值都是boolean,如果返回true,则代表消耗这个事件,false则代表不拦截或者不消耗。

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev);
    }else{
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

重点知识点总结:

  • 同一个事件序列从down开始,以up结束,中间包含了数量不定的move事件。
  • 正常情况下,一个事件序列只能被一个View拦截且消耗。
  • 某个View一旦决定拦截,那么这个事件序列都只能由它来出来,并且它的onInterceptTouchEvent()不会再被调用。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(即onTouchEvent返回了false),那么统一事件的其它时间也不会再交给他来处理,并且时间交给它的父元素去处理(即父元素的onTouchEvent()会被调用)。
  • ViewGroup默认不拦截任何事件(即ViewGroup的onInterceptTouchEvent()的返回值默认为false)
  • View没有onInterceptTouchEvent(),一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认都会消耗事件(默认返回true)。除非它是不可点击的(即clickable和longClickable同时为false)。

滑动冲突

View的工作原理

ViewRoot和DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

DecorView作为顶级View,一般情况下它的内部会包含一个竖直方向的LinearLayout,分为上下两个部分,上面是标题栏,下面是内容栏(内容栏的id是content,android.R.id.content)。View的事件都会经过DecorView,然后再传递给我们的View。

View的绘制

image

View的Measure过程

MeasureSpec代表一个32位的int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(指在某种测量模式下的规格大小)。

SpecMode有三种模式:

  • UNSPECIFIED:父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部。
  • EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。对应着布局中的match_parent和具体数值两种模式。
  • AT_MOST:父容器制定了一个可用大小即SpecSize,View的大小不能大于这个值。对应着布局中的wrap_content。

通过父View获得子View的MeasureSpec方法源码

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;
    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    //noinspection ResourceType
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

子View通过父View和自身属性生成自己的MeasureSpec

父View的Mode 子View的属性 子View的Mode 子View的Size
EXACTLY 固定尺寸 EXACTLY 固定尺寸
EXACTLY match_parent EXACTLY 父Size
EXACTLY wrap_content AT_MOST 父Size
AT_MOST 固定尺寸 EXACTLY 固定尺寸
AT_MOST match_parent AT_MOST 父Size
AT_MOST wrap_content AT_MOST 父Size
UNSPECIFIED 固定尺寸 EXACTLY 固定尺寸
UNSPECIFIED match_parent UNSPECIFIED 父Size或者0
UNSPECIFIED wrap_content UNSPECIFIED 父Size或者0

父View与子View的尺寸关系

  1. 不管父View是何种模式,若子View有确切的数值,则子View大小就是其本身大小,且子View的Mode是EXACTLY
  2. 若子View是match_parent,则模式与父View相同,且大小同父View。
  3. 若子View是wrap_content,则模式是AT_MOST,大小同父View,表示不可超过父View大小。

View的Measure是一个从根布局开始遍历设置width和Height的过程,子View的尺寸由父View和本身属性共同决定。

ViewGroup中没有实现onMeasure()方法,因为不同的布局计算的方式是不同的,却实现了measureChildren()方法,用来测量布局中子View的尺寸。

View的Layout过程

View树在经过第一步的Measure过程之后,已经确定了每个View的大小,而Layout过程则是确定每一个View的位置的过程。这个位置是父布局中的相对位置,而不是相对于屏幕的绝对坐标系。

Layout过程图

在layout()方法中会执行对当前的View的位置信息操作(真正确定位置信息的方法是setFrame(),其返回值表示了当前View的位置信息有没有发生改变),而在onLayout()方法中则是确定当前View的子View的位置信息,对于View类并没有实现onLayout()方法,只有继承ViewGroup的布局类才会根据不同的规则来实现onLayout()方法。其伪代码如下。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (遍历子View) {
        /**
        根据如下数据计算。
            1、自己当前布局规则。比如垂直排放或者水平排放。
            2、子View的测量尺寸。
            3、子View在所有子View中的位置。比如位置索引,第一个或者第二个等。
        */
        计算每一个子View的位置信息; 
        
        child.layout(上面计算出来的位置信息);
    }        
}
设置子View位置的流程图

View的Draw过程

在经过Measure和Layout之后,每一个View都已经在确定了自己的大小和位置,Draw过程就是将这些View绘制出来。其中ViewGroup除了绘制自己,还要将子View给绘制出来。

绘制流程的六个步骤

  1. 对View的背景进行绘制
  2. 保存当前的图层信息(可跳过)
  3. 绘制View的内容
  4. 对View的子View进行绘制(如果有子View)
  5. 绘制View的褪色的边缘,类似于阴影效果(可跳过)
  6. 绘制View的装饰(例如:滚动条)

绘制子View的伪代码

protected void dispatchDraw(Canvas canvas) {
    if (需要绘制布局动画) {
    for (遍历子View) {
        绑定布局动画;
    }
    启动动画控制,通知动画开始;
    }

    for (遍历子View) {
    child.draw();
    }
}

你可能感兴趣的:(View详解)