【安卓小叙】详论View体系(二)

Activity加载布局过程

 加载布局是如何进一步显示的,书籍里大多贴以源码,文字说明却是云里雾里,这里就不对源码进行解读,贴出我做的图片,供大家在宏观局面了解一二。
纠错:下图黄色背景Activity.setCintentView应该为Activity.setContentView(..),并且它为入口。

 一个Activity包含一个Window对象(由PhoneWindow实现),PhoneWindow将DecorView作为整个应用的根View,DecorView又见布局划分为TextView做标题和给开发者嵌入自己的布局。

View的事件分发机制

  手指触摸屏幕,产生的事件就会被封装成“MotionEvent”,系统会将这个传递给View层级,这个传递过程就是事件分发。先提前了解些方法:

方法说明
dispatchTouchEvent(MotionEvent)用来进行事件的分发
onInterceptTouchEvent(MotionEvent)用来进行事件的拦截,在dispatchTouchEvent调用,注意View没有该方法
onTouchEvent(MotionEvent)处理点击事件,在dispatchTouchEvent进行调用

接下来我们看其分发机制:


总结一下,当ViewGroup要拦截事件时,后续的事件都要交给它处理,而不用调用onInterceptTouchEvent方法了。故:onInterceptTouchEvent不是每次事件都会调用的。

onInterceptTouchEvent

onInterceptTouchEvent的代码非常简单:

public boolean onInterceptTouchEvent(MotionEvent ev) {
	return false;
}

默认不进行拦截,如果要让ViewGroup拦截事件,应该在自定义的ViewGroup中重写这个方法。

dispatchTouchEvent

dispatchTouchEvent非常多,这里只写出核心代码,并做解释:

// 第一部分:
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--)
{
	[code...]
	// 第二部分:
	 if (
	 	 !child.canReceivePointerEvents()
      || !isTransformedTouchPointInView(x, y, child, null)
      ) { 
      	[code...]
      	continue; 
      }
      
      [code...]
      // 第三部分:
      if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
      { [code...] }

}

 第一部分,首先(倒序遍历,即从最上层的子View开始内层遍历)遍历ViweGroup的子元素,判断是否能接收到点击事件,若能,交给子元素处理。

 第二部分,判断触摸点的位置是否在子View的范围内,或是否在播放动画。若都不符合,则执行continue语句,表示这个子View不符合条件,开始遍历下个子View。

 第三部分,先看dispatchTransformedTouchEvent:


1. 如果有子控件,则调用其dispatchTouchEvent;若没有,则调用super.dispatchTouchEvent。ViewGroup继承自View,所以调用的也就是view的dispatchTouchEvent


2. view的dispatchTouchEvent:若OnTouchListener不为null且onTouch返回ntrue,则表示事件被消费,不再执行onTouchEvent方法。否则,会执行onTouchEvent。(OnTouchhListener的onTouch方法优先级高于onTouchEvent方法)


3. view的onTouchEvent方法:只要View的CLICKABLELONG_CLICKABLE(View代表可被点击和可被长按点击)有一个为true,即可消耗此事件。

CLICKABLELONG_CLICKABLE可以通过setClickablesetLongClickable方法来设置,也可以通过View的setOnClickListenersetOnLongClickListener来设置。

4. view的`onTouchEvent`方法接着在**ACTION_UP**事件中调用`performClick`方法。 5. `performClick`方法:如果View设置了OnClickListener,那么就会执行OnClickListener的onClick方法。 # View的事件传递规则 这三个重要关系,可以如下代码表示:

fun dispatchTouchEvent(ev : MotionEvent) : Boolean
{
	return if(onInterceptTouchEvent(ev)) {
		onTouchEvent(ev)
	} else {
		child_view.dispatchTouchEvent(ev)
	}
}

具体流程图如下:

点击事件由上自下传递给子控件,子控件自下而上消耗此事件。事件由下传递上返回规则是:若onTOuchEvent返回true,不继续向上传递;为false则不处理继续向上传。

View工作流程

 View工作流程指的是measurelayoutdraw。其中,measure用于测量view宽、高。layout确定view位置,draw则用来绘制view
  当DecorView被加载到Window中时,通过Activity的创建过程,当调用Activity的startActivity方法时,调用流程如下:
view流程

我们细看ViewRootImpl的方法performTraversals()(android 29):

private void performTraversals()
{
	// TODO: In the CL "ViewRootImpl: Fix issue with early draw report in
    // seamless rotation". We moved processing of RELAYOUT_RES_BLAST_SYNC
    // earlier in the function, potentially triggering a call to
    // reportNextDraw(). That same CL changed this and the next reference
    // to wasReportNextDraw, such that this logic would remain undisturbed
    // (it continues to operate as if the code was never moved). This was
    // done to achieve a more hermetic fix for S, but it's entirely
    // possible that checking the most recent value is actually more
    // correct here.
    if (!mStopped || wasReportNextDraw) {
          boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                 (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
          if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
               || mHeight != host.getMeasuredHeight() || dispatchApplyInsets ||
                  updatedConfiguration)
          {
              // 第一部分
              int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
              int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
              // Ask host how big it wants to be
              performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
          }
     }
     // 第二部分
	if (didLayout) {
	       performLayout(lp, mWidth, mHeight);
	       [code..]
	 }
	 // 第三部分
	 if (!cancelDraw) {
            performDraw();
     } 
}

我们只看第一部分childWidthMeasureSpec childHeightMeasureSpec ,这样来引出MeasureSpec。

MeasureSpec的理解

 MeasureSpec是View的内部类,其封装了一个View的规格尺寸,包括View宽高信息。他的作用是:在Measure过程中,系统将View的LayoutParams根据父容器所施加的规则转换成对应MeasureSpec,然后在onMeasure()方法根据这个MeasureSpec确定View的宽高
 先看measure的常量代码:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;
}

MeasureSpec 代表了32位的int值,最高2位代表specMode,剩余低30位代表了specSize
specMode: 测量模式,有三种:

specMode说明
UNSPECIFIED未指定模式,View大小不被父容器限制,想多大就多大,一般用于系统内部测量
EXACTLY精确模式, 对应match_parent属性和具体数值,父容器测量出view所需要大小,即specSize
AT_MOST最大模式,对应warp_parent属性,子view由父布局指定大小,并且不能超过这个值

 对于每一个view 都持有一个 保存了该view的尺寸规格的MeasureSpec。在View测量流程中,通过makeMeasureSpec来保存宽高信息。通过getModegetSize得到模式和宽高。

 MeasureSpec受自身LayoutParams父容器的MeasureSpec共同影响。

 作为顶层view DecorView,并没有父容器,是由自身LayoutParams窗口尺寸决定的。
我们再来看ViewRootImpl的performTraversals方法:

// 第一部分
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);       

getRootMeasureSpec会根据自身layoutParams来得到不同MeasureSpec。这样就能理解传入的参数是什么了。

performMeasure的代码很简单,就是调用子控件的measure:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
   if (mView == null) {
       return;
   }
   Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
   try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
   } finally {
       Trace.traceEnd(Trace.TRACE_TAG_VIEW);
   }
}

View 和 ViewGroup 的 measure流程

  measure用来测量View的宽和高。它的流程分为View流程ViewGroup流程;ViewGroup除了完成自己的测量之外,还要遍历调用子元素的measure方法。

View 的 measure流程

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

setMeasuredDimension方法是
用来设置view宽高的。而getDefaultSize方法如下:

   public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        // 测量模式
        int specMode = MeasureSpec.getMode(measureSpec);
        // view大小
        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;
    }

&emsp在AT_MOSTEXACTLY模式下,都返回specSize这个值,也就是这两种模式都取决于specSize,换而言之,对于一个直接继承View的自定义View来说,warp_parentmatch_parent属性效果都是一样的。因此,自定义view要加以区分两种模式,需要重写onMeasure方法,对warp_parent进行处理。
UNSPECIFIED模式下,返回的是getSuggestedMinimumWidth()/ getSuggestedMinimumHeight(),我们取其中一个分析:

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

大意是:设置没有背景,则取值为mMinWidth,这个字段可以由Android:minWidth或View.setMinimunWidth(int)设置值;不指定默认为0。
若有设置背景,则取值 max{ mMinWidth, mBackground.getMinimumWidth() }
mBackground.getMinimumWidth() 指的是 Drawable的方法,得到Drawable固有宽度,若宽小于0,返回0。

总结getSuggestedMinimumWidth(): 若view没有设置背景,返回mMinWidth,否则返回 mMinWidthDrawable最小宽度的最大那一个。

ViewGroup 的 measure流程

 不仅测量自身,还遍历调用子元素measure方法。ViewGroup 没有onMeasure方法,只定义了:measureChildren,在之中调用measureChild(..),如下所示:
viewgroup

getChildMeasureSpec中,有一点值得注意:若父容器MeasureSpec属性为AT_MOST子元素的LayoutParams属性为Wrap_Content,那么,子元素MeasureSpec属性也为AT_MOST,他的specSize的值为父容器的specSize - padding,也就是说这和子元素设置LayoutParams属性为match_parent效果是一样的

这个问题的解决思路是,在LayoutParams属性为WRAP_CONTENT指定默认宽高。

ViewGroup为了派生类有不同布局需要,没有提供onMeasure方法,我们这里以LinearLayout的measure流程为例,看看它的onMeasure方法:

	@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

以measureVertical为例来分析:
measureVertical方法定义了mTotalLength来存储LinearLayout在垂直方向高度,然后遍历子元素,根据子元素的MeasureSpec模式分别计算每一个子元素的高度。如果是WRAP_CONTENT,则将每一个子元素的高度和margin垂直高度、padding的值相加并给予mTotalLength,如果是具体的值,就加之。

view的layout流程

layout方法是用来确定元素的位置。ViewGroup中的,确定子元素位置;View中的,确定自身位置。我们依然以图为例。

View的draw流程

官方注释对此清楚的说明每一步做法:

  1. 如需要则绘制背景
  2. 保存当前canvas层
  3. 绘制view内容
  4. 绘制子view
  5. 如需要则绘制view边缘阴影
  6. 绘制装饰(滚动条之类)
  7. 绘制默认焦点突出(高版本)

第二步和第五步这里跳过,不做讲解。

1. 绘制背景

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }

        setBackgroundBounds();

        // Attempt to use a display list if requested.
        【code】

        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        // 第一部分
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

第一部分考虑了偏移参数,如果偏移值不为0,会在偏移后的canvas绘制背景。

3. 绘制view内容

oonDraw是个空方法,需要自己实现。

4. 绘制子view

调用dispatchDraw方法,这依旧是空方法。但viewGroup重写了这个方法,可见是对子view进行遍历,调用他们的drawChild方法,然后drawChild调用子view的draw方法
在view的draw方法里,步骤1-7都有调用,并且,优先加载缓存显示

6. 绘制装饰

绘制形如foreground, scrollbars装饰,并在视图内容上显示。

7. 绘制焦点

原代码是:

	/**
     * Draw the default focus highlight onto the canvas if there is one and this view is focused.
     * @param canvas the canvas where we're drawing the highlight.
     */
    private void drawDefaultFocusHighlight(Canvas canvas) {
        if (mDefaultFocusHighlight != null && isFocused()) {
            if (mDefaultFocusHighlightSizeChanged) {
                mDefaultFocusHighlightSizeChanged = false;
                final int l = mScrollX;
                final int r = l + mRight - mLeft;
                final int t = mScrollY;
                final int b = t + mBottom - mTop;
                mDefaultFocusHighlight.setBounds(l, t, r, b);
            }
            mDefaultFocusHighlight.draw(canvas);
        }
    }

小结

这样,我们就系统梳理了一遍view体系,至于具体实现,可以对症下药,再检索有关问题不迟。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值