Android学习- View的工作机制

自定义View##

作者:桂志宏

在Android中做界面是一件较为轻松的事情,例如想要添加一个按钮,你可以用xml也可以在代码中设置相关的属性,下面是用xml添加一个按钮的脚本

  <Button
    android:id="@+id/buttonDemo"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Button" 
    android:layout_gravity="center_horizontal"/>

上述代码就能在界面上添加一个按钮,如下
这里写图片描述

上面添加的是Android原生态的组件,显然这种界面不太好看,对用户不友好。平时我们用各种app时,会看到很多很漂亮的界面甚至有一些炫酷的效果,而原生态的组件是做不到这种效果,要实现各式各样的精美的界面可就没那么容易了。这时就需要我们自定义View才能实现各种美观的界面,因此我们首先要先知道Android的View是如何工作的,为此,本文详细讨论View的工作原理,在讨论View之前,需要介绍一下Android的事件体系,因为View就是用来和用户交互的,当用户点击app上的某个按钮,这就是一个触摸事件,这时app需要作出相应的响应,从而不断更新界面。

事件体系

事件

当用户触摸屏幕,随即触发触摸事件序列,一个完整的事件序列一般由如下事件组成:

  • ACTION_DOWN事件:用户点击屏幕触发
  • ACTION_MOVE事件:用户在屏幕上滑动触发
  • ACTION_UP事件:用户手指离开屏幕触发

其中一个事件序列可以有若干个ACTION_MOVE事件(或0个,即Click),但是一定会有1个ACTION_DOWN和ACTION_UP.

当触发了触摸事件序列,它是如何分发呢?

事件分发

触摸事件首先传递给当前Activity,由Activity的dispatchTouchEvent方法进行事件分发,事件分发给Window(抽象类,唯一实现为PhoneWindow),Window进一步分发事件给DecorView,而DecorView继承FrameLayout,它是所有组件的顶级View,更准确地说,DecorView是ViewGroup(DecorView一般为竖直方向的Linearlayout,包括titlebar和content,所有的控件以及View都是显示在content中),接着DecorView将事件分发给它的子View.上述过程可由下图表示

当触摸事件传递给DecorView(ViewGroup)后,它如何分发给相应的子View?

事件分发涉及如下三个重要的方法:

protected boolean dispatchTouchEvent(MotionEvent ev)

只要触摸事件分发到当前View,必定会调用该View的上述方法。该方法返回true,表示事件被该View或者其子View处理;返回false,表示该View和其所有的子View都没有处理该事件,换句话说该View及其所有的子View的onTouchEvent()方法都返回false,这时,事件会向父View传递,请求父View处理。

protected boolean onInterceptTouchEvent(MotionEvent ev)

决定当前View是否拦截触摸事件,在dispatchTouchEvent中被调用,返回true,表示拦截触摸事件,然后事件交给当前View的onTouchEvent方法处理;返回false,表示不拦截触摸事件,事件进一步分发给其子View。

 protected boolean onTouchEvent(MotionEvent ev)

对当前View的触摸事件进行处理,返回true,表示事件被消耗,返回false,表示事件未被处理,因此调用其父View的onTouchEvent方法进行处理。

这里需要指出的是onTouch和onClick
当View设置了OnClickListener(),则onClick方法就会被调用,当View设置了OnTouchListener,则onTouch方法就会被调用,当View同时设置了OnClickListener和OnTouchListener,onTouch方法会在onClick方法之前被调用,如下图

我们可以从View的源码验证这一点,下面是View的dispatchTouchEvent方法的部分代码:

    public boolean dispatchTouchEvent(MotionEvent event) {

                     ...

           if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }

        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

                ...
      }

由源码可知,该方法会先检查onTouchListener是否实例化并进一步调用onTouch方法,接着去调用onTouchEvent方法,这里需要注意一点,如果OnTouchListener实例化并且其onTouch方法返回true,由于result为true,因此onTouchEvent方法不会被调用,从而被屏蔽,而onClick方法是在onTouchEvent方法中执行的(可以查看View的onTouchEvent方法源码),因此onClick方法也会被屏蔽。 总之,onTouch的优先级大于onTouchEvent,onTouchEvent方法的优先级又大于onClick

View和ViewGroup的工作原理

View主要有三个工程,measure,layoutdraw,其中measure是测量View的宽和高,layout是将View放置在相应的位置,而draw是将相应的View绘制出来。

Measure

测量过程调用View的measure方法,如下

    public final void measure(int widthMeasureSpec, int heightMeasureSpec)

进一步,在measure中调用onMeasure方法

  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

可知,View的meausre方法是final类型,子类不能重写,自定义View只用重写OnMeasure方法.

View的onMeasure源码逻辑如下:

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

由源码可知,onMeasure方法的参数是View的宽和高的MeasureSpec对象,其中setMeasuredDimension方法用来保存当前View测量的宽高,onMeasure方法中测量完View的宽和高,必须调用setMeasuredDimension方法,否则会抛出异常

       protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)

其参数就是当前View的测量的宽和高。上面onMeasure方法中setMeasuredDimension方法的两个参数是getDefaultSize方法的返回值,该方法的源码如下

    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;
}

该方法的逻辑很简单,当测量模式为UNSPECIFIED,返回size,而这个size是系统设定的最小宽度或View背景最小宽度(getSuggestedMinimumWidth方法逻辑,这里就不展开其源码),当测量模式为ATMOST(wrap_content)或EXACTLY(match_parent)时,这时就返回View测量的值。

上面给出了View的测量过程,而ViewGroup(继承View)不仅需要测量自己的大小,还得测量子View的大小。由于不同ViewGoup子类有不同的布局方式(例如LinearLyaout和HorizontalScrollView等),因此其测量的方式不同,故ViewGroup中没有实现onMeasure方法,其不同子类有不同的实现方法。

由于ViewGroup不仅需要测量自身的大小,还要测量子View的大小,因此ViewGroup中提供如下方法逻辑来测量子View

 /* @param widthMeasureSpec The width requirements for this view
 * @param heightMeasureSpec The height requirements for this view
 */
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);
        }
    }
}

上述代码逻辑很清晰,逐个测量子View的大小,其中measureChild方法的源码如下:

  / * @param child The child to measure
    * @param parentWidthMeasureSpec The width requirements for this view
    * @param parentHeightMeasureSpec The height requirements for this view
  */
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);
}

上述代码的逻辑是根据父View的MeasureSpec来确定子View的MeasureSpec,利用方法getChildMeasureSpec返回相应子View的MeasureSpec,然后进入子View的measure过程(上面已详述View的测量过程),其中方法getChildMeasureSpec其源代码较长,如下:

 /* @param spec The requirements for this view
 * @param padding The padding of this view for the current dimension and
 *        margins, if applicable
 * @param childDimension How big the child wants to be in the current
 *        dimension
 * @return a MeasureSpec integer for the child
 */
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;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述方法的源代码的逻辑是根据当前View的MeasureSpec(参数spec)和Padding(参数padding)得到其剩余的空间,同时由当前View的子View请求的大小(高度或宽度的值,参数childDimension)来确定子View实际分配的尺寸大小以及其相应的测量模式,最后返回子View的MeasureSpec对象。 这里提一句,ViewGroup在measure时,一定记得处理padding和margin属性,因为涉及到分配给子View的空间大小,下图是padding和margin的关系

需要说明的是,上述是View和ViewGroup的measure过程的总体思想,对于具体的View或ViewGroup类,需要覆盖其中的测量方法,一般覆盖onMeasure方法,实现具体的测量细节

Layout

当测量完View的尺寸后,接下就要将View放置在相应的位置上,调用layout方法,对ViewGroup而言,调用layout是确定当前View的四个顶点,也即确定当前View在其父容器的位置,接着在layout中调用onLayout方法来确定其子View在当前View中位置,和onMeasure一样,onLayout具体实现和具体布局相关,因此不同的子View需要重写onLayout方法来确定其子View在其中的位置排布。我们以LinearLayout为例,其onLayout方法实现如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}

由上述源码,onLayout首先确定线性布局是竖向还是横向的,然后调用相应的方法来放置其子View,具体的代码这里就不做展开

Draw

当将View放置在相应的位置后,接下来就是将它们画出来.具体形状的绘制这里没什么好说的,就是几何图形计算。其中几个比较重要的的接口是Canvas,Paint和Path,Canvas相当于画板,Paint相当于画笔,Path是绘制路径。关于Path的适用参考网址: http://blog.csdn.net/leejizhou/article/details/51565057

下面是一个自定义View,实现和ScrollView相似的功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值