自定义View的绘制流程基础分析

发表于:2018-3-01 10:47

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:anAngryAnt    来源:掘金

  自定义View的绘制流程基础分析
  一个基本的自定义View应该做的事情:
  从绘制角度来说,一共有三点 测量,摆放,绘画他们本身以及子views(针对于ViewGroup而言)。
  保存UI状态。
  处理触摸事件。
  今天先从绘制流程开始学习吧,绘制流程:constructor()->onMeasure()->onLayout()->onDraw()
  在开始之前,我们先来看看Android 默认视图的层级:
  1. 通过onMeasure()方法,根据父容器的尺寸大小和约束,能知道一个View要占多大的地方。这是一个自下而上执行的方法,也就是说,先从最下层的RootView开始执行测量,然后分发测量viewGroup中众多的子Views。
  要搞清楚onMeasure(),我们先来看一下方法原型吧。
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {....}
  刚刚读源码的童鞋可能并不知道widthMeasureSpec,heightMeasureSpec这两个参数的作用是什么。这两个参数实际上可以通过MeasureSpec.getSize(),MeasureSpec.getMode()这两个方法来判断该view被测量后的尺寸。那么,为什么还要通过MeasureSpec.getMode()呢?这就是接下来要说明的重点。
  以下是MeasureSpec中剔除部分代码之后的getMode()实现方式。
  private static final int MODE_SHIFT = 30;
  private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
  //我们通过位运算可以知道,MODE_MASK实际上等于11(二进制)左移30位的二进制,在Java中int占32位,所以这里就表示的是int中最高位为11,其余30位为0的二进制数。
  public static final int UNSPECIFIED = 0 << MODE_SHIFT;
  public static final int EXACTLY     = 1 << MODE_SHIFT;
  public static final int AT_MOST     = 2 << MODE_SHIFT;
  ....
  public static int getMode(int measureSpec) {
  //measureSpec就是我们传进来的 widthMeasureSpec 或者 heightMeasureSpec
  //它与上measureSpec,低30位就全部都清零了。
  //将返回值与UNSPECIFIED、EXACTLY、AT_MOST比较就能得到measureSpec的mode了。

  return (measureSpec & MODE_MASK);
  }

  通过以上的注释style分析,我们知道了MeasureSpec是如何计算mode了,那么为什么要计算这些mode呢?与size又有什么关系呢? View的MeasureSpec Mode有三种,分别为:UNSPECIFIED,EXACTLY,AT_MOST,他们的规则如下(要注意一点的是,在子view中计算的mode,是parent view告诉它的):
  UNSPECIFIED:parent view对子view没有任何的约束,子view可以任一尺寸,但必须要有。发生场景: 对于一个ScorllView来说,它通常不会去约束子view高度的,也就是说,子view的高度加起来有多高,那么ScrollView就有多高。在这种情况下,子view的mode就会为UNSPECIFIED。
  EXACTLY:parent view已经有了确切的size,子view的大小不能够超过parent的大小。发生场景: 在parent view指定大小或者为match_parent的时候,子view会得到这个mode。
  AT_MOST:在这种模式下,子view能够尽可能大的达到设定的尺寸或者原尺寸。发生场景: 在parent view指定大小或者match_parent的情况下,子view为wrap_content,那么mode将会为AT_MOST。
  通过以上对mode的规则分析,我们就能够知道,在onMeasure中要搞到尺寸,是需要根据mode来搞的。
  于是我们可以根据规则写出如下代码:
  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;//如果不指定size,那么我们就使用默认的size
  break;
  case MeasureSpec.AT_MOST:
  case MeasureSpec.EXACTLY:
  result = specSize;//这两种mode都允许我们直接使用测量出来的size
  break;
  }
  return result;
  }
  2. 通过onLayout()方法知道这个控件应该放在哪个位置。*同样,是一个自下而上的方法。*一般我们只有在重写ViewGroup的时候需要自己处理onLayout()方法,因为该方法主要是ViewGroup用于摆放子view位置的(如:水平摆放或者垂直摆放,在这里同学们可以参考一下LinearLayout的onLayout()方法的实现),一般我们只继承View来定制我们的自定义View的时候,都不需要重写该方法。不过需要注意的一点是,子view的margin属性是否生效就要看parent是否在自身的onLayout方法进行处理,而view得padding属性是在onDraw方法中生效的。
  以下是重头戏!!!
  3. 通过onDraw(Canvas canvas)方法将这个控件绘画出来。主要是通过Paint和参数中的canvas,还有各种Animation以及invalidate()、postInvalidate()这两个方法去进行视图的重绘,实现动态效果。canvas中画各种图形的方法,比如说:rect(矩形),circle(圆形) 等等。当然你可以使用Path,PathMeasure去完成更加细腻的动画。下面结合一个很的效果实例源码,来说明动态自定义View。
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  //根据模式来赋值with 和 height
  int widthMode = MeasureSpec.getMode(widthMeasureSpec);
  if( widthMode == MeasureSpec.EXACTLY )
  width = MeasureSpec.getSize(widthMeasureSpec);
  else width = ViewGroup.LayoutParams.MATCH_PARENT;
  int heightMode = MeasureSpec.getMode(heightMeasureSpec);
  if( heightMode == MeasureSpec.EXACTLY )
  height = MeasureSpec.getSize(heightMeasureSpec);
  else height = ViewGroup.LayoutParams.MATCH_PARENT;

  //左端点的X
  leftX = margin;
  //右端点的X
  rightX = width - margin;
  y = height/2.0f;

  //线段的长度
  distance = width - ( 2 * margin + 2 * radius  );
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }
  @Override
  protected void onDraw(Canvas canvas) {
  //绘制左端点
  canvas.drawCircle( leftX, y , radius * factor , mPaint);
  //绘制右端点
  canvas.drawCircle( rightX , y , radius * ( 1 - factor ), mPaint);
  mPaint.setStrokeWidth(5);
  //绘制线段
  canvas.drawLine(margin , y , margin + radius+ ((radius + distance)*(1-factor)) , y, mPaint);
  }

  public void startAnimation(){
  mAnimator = ValueAnimator.ofFloat(1, 0);
  mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  @Override
  public void onAnimationUpdate(ValueAnimator animation) {
  factor = (float) animation.getAnimatedValue();
  postInvalidate();
  }
  });
  mAnimator.setDuration(1500);
  // 重复次数 无限循环
  mAnimator.setRepeatCount(ValueAnimator.INFINITE);
  // 重复模式, RESTART: 重新开始 REVERSE:恢复初始状态再开始
  mAnimator.setRepeatMode(ValueAnimator.REVERSE);
  mAnimator.start();
  }

  效果图
  没错!就是这么皮!!╭(╯^╰)╮
  requestLayout()、postInvalidate()、invalidate()的区别
  1.实际上,后两者的作用是一样的,只不过postInvalidate内部会将重绘操作放入子线程中,而invalidate则是在调用线程中重绘view。
  2.requestLayout在什么时候用呢?当view本身的测量属性改变了的时候,就可以调用该方法去让parent view去重新调用view的onMeasure,onLayout方法,去重新评估view的大小和所在位置。
  Enjoy Android :) 如果有误,轻喷,欢迎指正。


上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号