[Android开发艺术探索]第四章学习笔记

ViewRoot 与 DecorView

ViewRoot 是连接 WindowManager 和 DecorView 的纽带,其实现类是 ViewRootImpl 类。View 的三大流程均由ViewRoot 完成。Activity 被创建完成后,会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象,并与DecorView 建立关联。

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);

View 的绘制流程从 ViewRoot 的 performTraversals 方法开始:

[Android开发艺术探索]第四章学习笔记_第1张图片
performTraversals 方法

performMeasure、performLayout、performDraw 三个方法分别完成顶级 View 的measure、layout、draw 这三大流程。其中,performMeasure 中调用 measure 方法,measure 中又调用 onMeasure 方法,onMeasure中会对所有子元素进行 measure 过程。measure 流程就是这样从父元素传递到子元素中,子元素会重复这个过程,从而完成整个 View树的遍历。performLayout、performDraw 的传递流程与此类似(draw方法中通过 dispatchDraw 来实现传递过程)。

  • measure 过程决定 View 的宽高,完成后可通过 getMeasuredWidthgetMeasuredHeight 方法获取测量后的宽高,一般此宽高就是 View 的最终宽高(特殊情况下例外)。
  • layout 过程决定 View 的四个顶点和实际宽高,完成后可通过 getTopgetBottongetLeftgetRight 拿到 View 的四个顶点坐标,且可以通过 getWidthgetHeight 方法拿到最终宽高。
    *draw 过程决定了 View 的显示。

DecorView作为顶级View,其实是一个 FrameLayout ,它包含一个竖直方向的 LinearLayout ,这个LinearLayout 分为标题栏和内容栏两个部分。

[Android开发艺术探索]第四章学习笔记_第2张图片
DecorView

Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是 R.android.id.content ,通过 ViewGroup content = findViewById(R.android.id.content);可以得到这个contentView。 content.getChildAt(0)可以获得我们设置的 View。

MeasureSpec

MeasureSpec 参与 View 的 measure 的过程,类似于一个测量规格的概念。其创建过程受父容器影响。测量过程中,系统根据 View 的 LayoutParams 根据父容器所施加规则转换成对应MeasureSpec,根据这个 measureSpec 得到View 的测量宽高。
MeasureSpec 代表一个 32 位 int 值,高 2 位代表 SpecMode,低 30 位 代表 SpecSize。
MeasureSpec 通过 makeMeasureSpec(int size , int mode) 方法将模式和尺寸打包成一个 int 值,且可以通过getModegetSize 方法解包原始值(此处 MeasureSpec 指其代表 int 值,而非对象本身)。

SpecMode有三类:

  • UNSPECIFIED 父容器不对View有限制,要多大给多大,系统内部用。
  • EXACTLY 父容器已检测出 View 所需精确大小。View 最终大小就是 SpecSize 指定值,对应
    match_parent 和具体数值。
  • AT_MOST 父容器指定了一个可用大小即 SpecSize,View 的大小不能大于这个值,具体值要看 View 的具体实现。对应 wrap_content。

DecorView 的 MeasureSpec 由窗口尺寸及自身参数共同决定。
普通 View 的 MeasureSpec 则由父容器与自身参数共同决定。

[Android开发艺术探索]第四章学习笔记_第3张图片
普通 View 的 MeasureSpec 的创建规则

这里的parentSize 实际上是父容器剩余的可用空间,要减去父容器 padding、自身 margin、和父容器已经使用的部分。

View 的工作流程

measure

View 的 measure 过程
[Android开发艺术探索]第四章学习笔记_第4张图片
View 的 measure 过程
  • getSuggestedMinimumSize() 方法中,若有设置背景,会获取背景(Drawable)的原始宽高作为备选的返回值。Drawable若无原始宽高会默认为0。ShapeDrawable 无原始宽高,BitmapDrawable 有原始宽高(图片的尺寸)。

  • 直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 wrap_content 时的自身大小,否则
    wrap_content 效果等同 matct_parent。原因通过阅读源码可知,getDefaultSize 方法中只判断了是否为 UNSPECIFIED 模式,对EXACTLY 和 AT_MOST 处理是一样的,即返回 MeasureSpec 的 size 作为 View 的测量宽高。通过View 的 MeasureSpec 的创建规则表可知,size 为父容器剩余的可用空间。

  //设置 wrap_content 时的自身大小
  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    //mWidth, mHeight 为自定义的默认内部宽高 
    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(widthSpecSize, mHeight);
    }
  }
ViewGroup 的 measure 过程

ViewGroup 除了完成自身的 measure 过程外,应该去遍历所有子 View 的measure 方法,各个子元素再去递归执行这个过程。但是 ViewGroup 是一个抽象类,并没有重写 onMeasure 方法。所以自定义 ViewGroup 时应该根据所需重写 onMeasure 方法。

VIewGroup 虽然没有重写 onMeasure 方法,但是相应提供了一些自定义时可以直接使用的方法:

  • measureChild() :取出子 View 的LayoutParams 和本身的MeasureSpec 来创建子 View 的 MeasureSpec(通过getChildMeasureSpec 方法),直接传递给子 View 的measure 方法进行测量。

  • measureChildren() :作用是遍历所有子 View,对所有可见不为 GONE 的 View 使用 measureChild()方法。直接放到ViewGroup 的onMeasure 中可以实现类似无 wrap_content 功能的 FrameLayout 的效果。

  • resolveSizeAndState :主要是根据期望值(根据业务逻辑在重写的 onMeasure 中计算的尺寸)和ViewGroup 的 MeasureSpec 的模式和尺寸,判断最终返回的测量尺寸,一般用于重写 onMeasure 方法最后 setMeasureDimension 的参数处理。防止出现超过父容器剩余空间等情况。

一般重写 ViewGroup 的onMeasure 方法的思路是,遍历子元素进行 measure,按需求保存某方向尺寸的累加值或者最大值,加上padding、子View 的 margin等。最终使用这个值设定 VIewGroup 的测量尺寸。

具体实现可以看看这篇文章,自定义ViewGroup。

获取 View 的宽高

measure 过程完成后,理论上可以通过 getMeasureWidth/Height 方法获取 View 的测量宽高,但是极端情况下,系统可能需要多次 measure 才能确定最终测量宽高,这种情况下获取数值可能出错。

应该在 onLayout 方法中去获取 VIew 的测量宽高或者最终宽高

由于 View 的 measure 过程和 Activity 的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。想要在 Activity 启动的时候就获取一个View的宽高的方法有以下 4 种:

  • Activity / View # onWindowFocusChanged :这个方法的含义是:View 已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当 Activity 的窗口得到焦点和失去焦点均会被调用。
  • view.post(runnable) :通过 post 将一个 runnable 投递到消息队列的尾部,当 Looper 调用此 runnable 的时候,View 也初始化好了。
  • ViewTreeObserver : 使用 ViewTreeObserver 的众多回调可以完成这个功能,比如 OnGlobalLayoutListener 这个接口,当 View 树的状态发送改变或 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取 View 宽高的好时机。需要注意的是,伴随着 View 树状态的改变, onGlobalLayout 会被回调多次。
  • view.measure(int widthMeasureSpec,int heightMeasureSpec) :手动对view进行measure。需要根据View的layoutParams分情况处理。
    为 match_parent 时,无法 measure 出具体的宽高,因为不知道父容器的剩余空间,无法测量出 View 的大小。
    为具体的数值时( dp/px):
  //假设宽高为 100
  int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
  view.measure(widthMeasureSpec,heightMeasureSpec);

为 wrap_content 时:

  int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  // View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,我们用View理论上能支持的最大
  //值去构造MeasureSpec是合理的
  int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
  view.measure(widthMeasureSpec,heightMeasureSpec);

layout

layout 过程比较简单,layout 的方法大致流程是:setFrame 方法设定 4 个顶点位置(初始化 mLeft、mRight、mTop、mBottom 4 个值),然后调用 onLayout 方法(View 中为空实现、ViewGroup 为抽象方法),基本实现思路是:遍历所有子 View,获取其测量宽高,和根据需求计算其中两个顶点,根据这 4 个值对子 View 使用 layout 方法。

getWidth/Height 与getMeasureWidth/Height 方法的区别
public final int getHeight() {
    return mBottom - mTop;
  }

public final int getWidth() {
    return mRight - mLeft;
  }

在View 的默认实现中,VIew 的测量宽高等于最终宽高,但是测量宽高的形成时间先于最终宽高。在日常开发中,一般来说两者相等。但是存在特殊情况导致两者不等。如在 layout 方法中操作 setFrame 方法参数。另外一些情况下,View 需要多次 measure 才能确定自己的测量宽高,这会导致在前几次测量之后得出的测量宽高与最终宽高不等。

draw

比较简单大致流程如下:

  • 绘制背景 background.draw(canvas)
  • 绘制自己(onDraw)
  • 绘制 children (dispatchDraw)
  • 绘制装饰 (onDrawScrollBars)

需要注意两个方法 :
dispatchDraw 方法会遍历调用所有子元素的 draw 方法。
setWillNotDraw 操作一个标记位,在 View 默认为 false,ViewGroup 中默认为 true。作用是View本身不需要绘制任何内容时(绘制自己,即 onDraw 方法没干啥事),系统会进行相应优化。所以,当一个 ViewGroup 需要通过 onDraw 来绘制内容时(重写了 onDraw 方法时),需要显示的关闭该标记位。

自定义View

自定义 View 的分类与须知

自定义 VIew 的 4 类:

  • 继承View 重写 onDraw 方法:主要用于实现不规则效果,往往需要静态或动态绘制一些不规则图形。需要自己实现支持 wrap_content 与 padding。
  • 继承 ViewGroup 派生特殊的 Layout:用于实现自定义布局,实现比较复杂,需要处理自身和子元素的 measure、layout 两个过程。
  • 继承特定 View:较常见做法,一般用于扩展已有 View 的功能。不需要自己实现支持 wrap_content 与 padding。
  • 继承特定 ViewGroup:较常见做法,用于实现 多种 View 组合的效果。不需自己处理自身和子元素的 measure、layout 。

自定义 View 须知:

  • 直接继承 View 或者 ViewGroup 时要自己处理支持 wrap_content 效果。
  • 直接继承 View 的控件,需要在 draw 方法中处理 padding。直接继承 ViewGroup 的控件需要在 onMeasure 和 onLayout 中考虑 padding、子元素 margin 的影响。
  • 尽量不要在 View 中使用 Handler,因为 View 本身提供 post 系列方法。
  • View 中如果有线程或者动画需要停止,考虑在 onDetachedFromWindow 中处理。该方法当包含此 View 的 Activity 退出或者 View 被 remove 时会被调用。
  • View 带有嵌套滑动情形时,需要处理好滑动冲突。
自定义 View 示例

直接继承 View 的示例,主要 3 个点:

  • 支持 padding:onDraw 里面获取 padding,并进行处理。
  • 支持 wrap_content:onMeasure 里面判断 AT_MOST 模式,使用需要的大小
  • 支持自定义属性:写一个自定义属性集合的 xml 文件,在构造方法中加载、解析即可。

以下是实现上述 3 点,作用为画一个圆形的自定义 View 代码:



  
  
    
    
  

public class CircleView extends View {

  private int mColor = Color.RED;
  private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

  public CircleView(Context context) {
    super(context);
    init();
  }

  public CircleView(Context context, @Nullable AttributeSet attrs) {
    //使用自定义属性时 应该为调用另一个构造方法
    this(context, attrs, 0);
  }

  public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    //加载自定义属性集合 CircleView
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
    //解析 CircleView 属性集合 中 id 为 circleView_circle_color 的属性
    mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
    a.recycle();
    init();
  }

  private void init() {
    mPaint.setColor(mColor);
  }

  //支持padding
  @Override protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int pL = getPaddingLeft();
    int pR = getPaddingRight();
    int pT = getPaddingTop();
    int pB = getPaddingBottom();
    int w = getWidth() - pL - pR;
    int h = getHeight() - pT - pB;
    int radius = Math.min(w, h) / 2;
    //把圆心定在左和上方除去padding后的位置加上设定宽高的一半的位置
    canvas.drawCircle(w / 2 + pL, h / 2 + pT, radius, mPaint);
  }

  //支持 wrap_content 即当宽高设为 wrap_content 时默认为 200 pt
  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, 200);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(200, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
      setMeasuredDimension(wSpecSize, 200);
    }
  }
}

直接继承 ViewGroup 示例:
这种方法实现比较复杂,以下是一个带有水平滑动效果的类似 LinearLayout(水平方向上)的自定义View,基本思路是 onMeasure 里,当判断模式为 wrap_content 时,获取子 View 的宽高,进行相应处理,onLayout 中,使用一个变量保存宽度累加值,实现水平排列。另外此例实现了内容滑动效果,且解决了滑动冲突。

public class HorizontalScrollViewEx extends ViewGroup {

  private int mChildrenSize;  //子View数量
  private int mChildWidth;    //子 View 宽度 本例默认子View大小是一样的
  private int mChildIndex;    //处于屏幕可见最左边的子 View 序号

  private int mLastX;
  private int mLastY;
  private int mLastXInterceptTouchEvent;
  private int mLastYInterceptTouchEvent;

  private Scroller mScroller;
  private VelocityTracker mVelocityTracker;

  public HorizontalScrollViewEx(Context context) {
    super(context);
    init();
  }

  public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
    super(context, attrs);
    init();
  }

  public HorizontalScrollViewEx(Context context, AttributeSet attrs,
      int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  //实例化 Scroller 和 VelocityTracker(速度追踪)
  private void init() {
    if (mScroller == null) {
      mScroller = new Scroller(getContext());
      mVelocityTracker = VelocityTracker.obtain();
    }
  }

  //包含此 View 的 Activity 退出或者 View 被 remove 时会被调用 回收资源
  @Override protected void onDetachedFromWindow() {
    mVelocityTracker.recycle();
    super.onDetachedFromWindow();
  }

  @Override public boolean onInterceptTouchEvent(MotionEvent ev) {
    boolean intercepted = false;
    int x = (int) ev.getX();
    int y = (int) ev.getY();

    switch (ev.getAction()) {
      case MotionEvent.ACTION_DOWN:
        intercepted = false;
        if (!mScroller.isFinished()) {
          //滑动动画效果未完成时 再次传来事件序列 则中止滑动动画 且拦截事件
          mScroller.abortAnimation();
          intercepted = true;
        }
        break;
      case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastXInterceptTouchEvent;
        int deltaY = y - mLastYInterceptTouchEvent;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
          intercepted = true;
        } else {
          //如果 Y 轴滑动距离大于 X 轴,不拦截事件
          intercepted = false;
        }
        break;
      case MotionEvent.ACTION_UP:
        intercepted = false;
        break;
      default:
        break;
    }
    mLastX = x;
    mLastY = y;
    mLastXInterceptTouchEvent = x;
    mLastYInterceptTouchEvent = y;

    return intercepted;
  }

  //自身的滑动事件
  @Override public boolean onTouchEvent(MotionEvent event) {
    mVelocityTracker.addMovement(event);
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
        if (!mScroller.isFinished()) {
          mScroller.abortAnimation();
        }
        break;
      case MotionEvent.ACTION_MOVE:
        int deltaX = x - mLastX;
        int deltaY = y = mLastY;
        scrollBy(-deltaX, 0);
        break;
      case MotionEvent.ACTION_UP:
        //获取当前View边缘与View内容左边缘的距离
        int scrollX = getScrollX();
        mVelocityTracker.computeCurrentVelocity(1000);
        float xVelocity = mVelocityTracker.getXVelocity();
        //mChildIndex代表在屏幕最左边的子View的序号 0开始
        if (Math.abs(xVelocity) > 50) {
          //如果是快速滑动 判断滑动方向 使最左边一个子View的序号加减1
          mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
        } else {
          //当前View边缘与View内容左边缘的距离加上半个子 View 的宽度 除去子View宽,使滑动超过半个子View宽情况下
          // 滑动到下个子View 否则只滑动整数个子View宽度
          mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
        }
        //使mChildIndex不会超过实际子View数,以及防止负数出现
        mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
        //在smoothScrollBy调用前 实际状态是滑动距离是 正scrollX 所以减去
        // 使得手指离开后一直处于滑动整数个子View的状态
        int dx = mChildIndex * mChildWidth - scrollX;
        smoothScrollBy(dx, 0);
        mVelocityTracker.clear();
        break;
      default:
        break;
    }
    mLastX = x;
    mLastY = y;
    return true;
  }

  //缓慢滑动 需要 computeScroll 配合
  private void smoothScrollBy(int dx, int dy) {
    mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
    invalidate();
  }

  @Override public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
      scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
      postInvalidate();
    }
  }

  @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = 0;
    int measuredHeight = 0;
    final int childCount = getChildCount();
    //遍历子View 进行measure
    measureChildren(widthMeasureSpec, heightMeasureSpec);

    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

    //若无子元素,设置宽高为0
    if (childCount == 0) {
      setMeasuredDimension(0, 0);
    } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
      //若宽高都使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一个子View的宽乘以数量  其他需求可以考虑改写 measureChildren 等方法
      measuredWidth = childView.getMeasuredWidth() * childCount;
      //使用第一个子View的高
      measuredHeight = childView.getMeasuredHeight();
      setMeasuredDimension(measuredWidth, measuredHeight);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
      //若高度使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一个子View的高度
      measuredHeight = childView.getMeasuredHeight();
      setMeasuredDimension(widthSpecSize, measuredHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
      //若宽度使用了 wrap content
      final View childView = getChildAt(0);
      //使用第一个子View的宽乘以数量  其他需求可以考虑改写 measureChildren 等方法
      measuredWidth = childView.getMeasuredWidth() * childCount;
      setMeasuredDimension(measuredWidth, heightSpecSize);
    }
  }

  @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;

    for (int i = 0; i < childCount; i++) {
      final View childView = getChildAt(i);
      if (childView.getVisibility() != View.GONE) {
        final int childWidth = childView.getMeasuredWidth();
        mChildWidth = childWidth;
        childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
        //累加所有子View的宽度
        childLeft += childWidth;
      }
    }
  }
}

以上代码存在两个不规范之处,一是,没有子元素时不应该直接设宽高为 0。应该根据 LayoutParams 进行处理。二是,在measure 和 layout 中都没有根据自身的 padding 和子 View 的 margin 进行处理。

你可能感兴趣的:([Android开发艺术探索]第四章学习笔记)