自定义View

179 阅读4分钟

「这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

1. 自定义View的分类

自定义View的分类.png

2. 自定义View的构造函数

构造函数调用场景
public View(Context context)View是在Java代码里面new的
public View(Context context, @Nullable AttributeSet attrs)View是在.xml里声明的
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr)View有style属性时
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)View有style属性时、API21之后才使用

3. ViewGroup.LayoutParams

指定视图View的宽高等布局参数

参数说明
fill_parent强制性使子视图的大小扩展至与父视图大小相等(不含 padding )
match_parent与fill_parent相同,用于Android 2.3 & 之后版本
wrap_content自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )
XX dip精确设置高度值

4. MeasureSpec

  • 是测量View大小的依据,每一个MeasureSpec代表一组宽度(widthMeasureSpec)/高度(heightMeasureSpec)的测量规格
  • 宽测量规格 + 高测量规格 决定了视图View的大小
  • 根据View的类型分为两种情况:

5. View绘制流程

View的绘制基本由onMeasure()、onLayout()、onDraw()三个函数组成

6. onMeasure()

View类型measure过程
单一View只测量自身
ViewGroup对ViewGroup视图中所有的子View都进行测量
单一View的measure过程

Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
  ...
  int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
  if (cacheIndex < 0 || sIgnoreMeasureCache) {
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
  } else {
    ...
  }
  ...
}
  • 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  • 存储测量后的View宽/高:setMeasuredDimension()
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  • 存储测量后的View宽/高
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
  boolean optical = isLayoutModeOptical(this);
  if (optical != isLayoutModeOptical(mParent)) {
    Insets insets = getOpticalInsets();
    int opticalWidth  = insets.left + insets.right;
    int opticalHeight = insets.top  + insets.bottom;
    measuredWidth  += optical ? opticalWidth  : -opticalWidth;
    measuredHeight += optical ? opticalHeight : -opticalHeight;
  }
  setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
  • 根据View宽/高的测量规格计算View的宽/高值
  • UNSPECIFIED: 使用提供的默认大小
  • AT_MOST/EXACTLY: 使用View测量后的宽/高值 = measureSpec中的Size
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;
}
ViewGroup的measure过程
  • 从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性
  • onMeasure()方法的作用是测量View的宽/高值,而不同的ViewGroup(如LinearLayout、RelativeLayout、自定义ViewGroup子类等)具备不同的布局特性,这导致它们的子View测量方法各有不同,所以onMeasure()的实现也会有所不同。因此,ViewGroup无法对onMeasure()作统一实现。这个也是单一View的measure过程与ViewGroup的measure过程最大的不同
  • 针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步: (1)遍历所有子View及测量:measureChildren();(2)合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现;(3)存储测量后View宽/高的值:setMeasuredDimension()

7. View位置

View的位置由4个顶点决定

  • Left:子View左边界到父view左边界的距离
  • Top: 子View上边界到父view上边界的距离
  • Right:子View右边界到父view左边界的距离
  • Bottom:子View下边距到父View上边界的距离

8. onLayout()

单一View的onLayout过程
  • 由于单一View是没有子View的,故onLayout()是一个空实现
ViewGroup的onLayout过程

从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性

  • 定义为抽象方法,需重写,因为子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
  • 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

9. onDraw()

单一View的onDraw()过程

根据给定的 Canvas 自动渲染View包括其所有子 View)

  • View绘制自身(含背景、内容)
  • 绘制装饰(滚动指示器、滚动条、和前景)
ViewGroup的onDraw()过程
  • ViewGroup绘制自身(含背景、内容)
  • ViewGroup遍历子View & 绘制其所有子View
  • l ViewGroup绘制装饰(滚动指示器、滚动条、和前景)