当前位置: 首页 > news >正文

Android - 自定义view

为什么要自定义view?

在Android开发中有很多业务场景,原生的控件无法满足需求,并且经常也会遇到一个UI在多处重复使用情况,于是可以通过自定义View的方式来实现这些UI效果。

自定义view的分类

f0a77d115fdc4c2aa260972bc5d5c471.png自定义属性

Window

  • window是一个抽象类,它的具体实现是PhoneWindow类。

  • Android中的所有视图都是通过window来实现的。

  • 每一个window都对应着一个 view 和 ViewRootImpl,window和 view 通过ViewRootImpl来建立联系,因此,window并不是实际存在的,view是window存在的实体。

ViewRootImpl

ViewRootImpl 是 ViewSystem 和 SurfaceSystem 的桥梁,负责处理事件分发、布局、绘制等,是连接应用逻辑和系统底层的关键组件。

PhoneWindow 中创建的 DecorView对象会通过 setView 的方式设置给它,因为View 的实现是 ViewTree的形式,所以根据 DecorView遍历到所有的 View list。

ViewRootImpl 内部会获取到 Choreographer 对象,根据 Choreographer 提供的节奏 调用 View 的三大方法

Choreographer 对象:负责协调 UI 刷新和动画的执行时机,提高界面的流畅度。

  • windowManager是外界访问window的入口,windowManager常用的方法有:添加window中的view、更新view、删除view

过程总结:

在创建一个 View 时,首先会通过 WindowManager 来初始化一个窗口中的视图,其中会在WindowManagerGlobal 中创建 ViewRootImpl 。

在 PhoneWindow(窗口实现类)中,将创建一个 DecorView,DecorView 作为顶层视图容器,负责加载和展示活动的布局资源。

852ef4f9eddb478280472f7de6587636.png

DecorView

DecorView 作为顶级view,即 Android 视图树的根节点;同时也是 FrameLayout 的子类。

一般内部会包含一个竖直的LinearLayout,分为2部分:上-标题栏(titlebar)、下-内容栏(content)

作用:

显示 & 加载布局:View层的事件都先经过DecorView,再传递到View。 

在Activity中通过 setContentView()  所设置的布局文件实际是被加到内容栏之中的,成为 id = content 的 FrameLayout的唯一子view。

如何得到content?

ViewGroup content = findViewById(R.android.id.content)

如何得到我们设置的view?

content.getChildAt(0)

ViewGroup content = findViewById(android.R.id.content);
View v  = content.getChildAt(0);

7d7cc6d80c724020a3f1957b38428a4f.png

ViewRoot 

ViewRoot 是window和DectorView的连接器。它对应 ViewRootImpl 类。view的三大流程都是通过ViewRoot来实现的。

view的绘制流程是从ViewRoot的 performTraversals() 方法开始的。

performTraversals 会依次调用 performMeasure performLayout performDraw 三大方法,这三个方法会依次完成顶级view 的measure、layout、draw三大流程。

View的绘制流程从顶级View(DecorView)的ViewGroup开始,一层一层从ViewGroup至子View遍历测绘。

自上而下遍历、由父视图到子视图、每一个 ViewGroup 负责测绘它所有的子视图,而最底层的 View 会负责测绘自身。

c4f66e3f6c044e33b232055c4dd2b05e.png399da250e07041b981be81ef3d327953.png

 

View的三大流程

  • measure:测量view的宽高
  • layout:确定view在父容器中的位置
  • draw:负责将view绘制在屏幕上

measure

自定义view的测量方法,是用于测量 View 大小的重要方法。

在这个方法中,需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。

ViewGroup.LayoutParams 

布局参数类,用于指定视图View 的高度(height) 和 宽度(width)等布局参数。

  • ViewGroup 的子类(RelativeLayout、LinearLayout)有其对应的 ViewGroup.LayoutParams 子类
  • 如:RelativeLayoutViewGroup.LayoutParams子类
    = RelativeLayoutParams

通过以下参数指定

android:layout_height="wrap_content"   //自适应大小  
android:layout_height="match_parent"   //与父视图等高  
android:layout_height="fill_parent"    //与父视图等高  
android:layout_height="100dip"         //精确设置高度值为 100dip  

MeasureSpec

测量规格(MeasureSpec)是由测量模式(mode)和测量大小(size)组成,共32位(int类型),其中:

  • 测量模式(mode):占测量规格(MeasureSpec)的高2位;
  • 测量大小(size):占测量规格(MeasureSpec)的低30位。

MeasureSpec 类型 

MeasureSpec.AT_MOST

  • 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式

MeasureSpec.EXACTLY

  • View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 、设置了精确的尺寸、fill.parent时,会使用这种模式

MeasureSpec.UNSPECIFIED

  • 表示 View 的大小可以任意扩展,不受限制。父视图不约束子视图view
  • 通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式,如:ListView、ScrollView

MeasureSpec.getMode(xxx) 获取宽高的模式

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);

计算逻辑

View的 MeasureSpec 值计算取决于两个因素:

  • View自身的布局参数(LayoutParams)
  • 父容器的测量规格(MeasureSpec)

即View的大小是由 自身布局参数(LayoutParams) 和 父容器的测量规格(MeasureSpec) 共同决定的。

MeasureSpec值的具体计算逻辑封装在getChildMeasureSpec()里,具体计算逻辑如下源码所示。

/*** 源码分析:getChildMeasureSpec()* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定**/public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  // 参数说明// * @param spec 父view的详细测量值(MeasureSpec) // * @param padding view当前尺寸的的内边距和外边距(padding,margin) // * @param childDimension 子视图的布局参数(宽/高)//父view的测量模式int specMode = MeasureSpec.getMode(spec);     //父view的大小int specSize = MeasureSpec.getSize(spec);     //通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)   int size = Math.max(0, specSize - padding);  //子view想要的实际大小和模式(需要计算)  int resultSize = 0;  int resultMode = 0;  //通过父view的MeasureSpec和子view的LayoutParams确定子view的大小  // 当父view的模式为EXACITY时,父view强加给子view确切的值//一般是父view设置为match_parent或者固定值的ViewGroup switch (specMode) {  case MeasureSpec.EXACTLY:  // 当子view的LayoutParams>0,即有确切的值  if (childDimension >= 0) {  //子view大小为子自身所赋的值,模式大小为EXACTLY  resultSize = childDimension;  resultMode = MeasureSpec.EXACTLY;  // 当子view的LayoutParams为MATCH_PARENT时(-1)  } else if (childDimension == LayoutParams.MATCH_PARENT) {  //子view大小为父view大小,模式为EXACTLY  resultSize = size;  resultMode = MeasureSpec.EXACTLY;  // 当子view的LayoutParams为WRAP_CONTENT时(-2)      } else if (childDimension == LayoutParams.WRAP_CONTENT) {  //子view决定自己的大小,但最大不能超过父view,模式为AT_MOST  resultSize = size;  resultMode = MeasureSpec.AT_MOST;  }  break;  // 当父view的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content)  case MeasureSpec.AT_MOST:  // 道理同上  if (childDimension >= 0) {  resultSize = childDimension;  resultMode = MeasureSpec.EXACTLY;  } else if (childDimension == LayoutParams.MATCH_PARENT) {  resultSize = size;  resultMode = MeasureSpec.AT_MOST;  } else if (childDimension == LayoutParams.WRAP_CONTENT) {  resultSize = size;  resultMode = MeasureSpec.AT_MOST;  }  break;  // 当父view的模式为UNSPECIFIED时,父容器不对view有任何限制,要多大给多大// 多见于ListView、GridView  case MeasureSpec.UNSPECIFIED:  if (childDimension >= 0) {  // 子view大小为子自身所赋的值  resultSize = childDimension;  resultMode = MeasureSpec.EXACTLY;  } else if (childDimension == LayoutParams.MATCH_PARENT) {  // 因为父view为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0  resultSize = 0;  resultMode = MeasureSpec.UNSPECIFIED;  } else if (childDimension == LayoutParams.WRAP_CONTENT) {  // 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0  resultSize = 0;  resultMode = MeasureSpec.UNSPECIFIED;  }  break;  }  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
}  

2000fb88c60c473f94ba1588826d7fab.png

注意:

区别于顶级View(即DecorView)的测量规格MeasureSpec,计算逻辑:取决于 自身布局参数 & 窗口尺寸

format,png

掌握 Rect 类

Rect 是 Android 中表示矩形的一个类,它有四个主要的属性:lefttopright 和 bottom。使用 Rect 可以方便地进行碰撞检测和区域计算。

  • Rect 主要用于需要整数值的场合,比如简单矩形区域的定义。
Rect(left: Int, top: Int, right: Int, bottom: Int)

使用指定的坐标创建一个新矩形。注意:不执行范围检查,因此调用者必须确保 left <= right 且 top <= Bottom。 

Rect(r: Rect?)

创建一个新矩形,使用指定矩形中的值进行初始化(保持不变)。 

掌握 RectF 类

RectF 保存矩形的四个浮点坐标,这使得 RectF 可以更精确地表示矩形,尤其是在涉及到小数点运算时。

  • 在图形绘制中,尤其是涉及到曲线(如圆弧)时,使用 RectF 可以提供更平滑的效果,因为浮点数能够更好地捕捉精细的变化。

矩形由其 4 个边(左、上、右、下)的坐标表示。这些字段可以直接访问。使用 width() 和 height() 检索矩形的宽度和高度。注意:大多数方法不会检查坐标是否正确排序(即左 <= 右且顶部 <= 底部)。

  • RectF()

创建一个新的空 RectF。

  • RectF(float left, float top, float right, float bottom)

使用指定的坐标创建一个新矩形。

  • RectF(RectF r)

创建一个新矩形,使用指定矩形中的值进行初始化(保持不变)。

measure过程

measure过程 根据View的类型分为2种情况:

 

format,png

单一View

5e0aa55317014d9c85e35ee00036b9c9.png

/*** 源码分析:measure()* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法* 作用:基本测量逻辑的判断*/ public final void measure(int widthMeasureSpec, int heightMeasureSpec) {// 参数说明:View的宽 / 高测量规格...int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :mMeasureCache.indexOfKey(key);if (cacheIndex < 0 || sIgnoreMeasureCache) {onMeasure(widthMeasureSpec, heightMeasureSpec);// 计算视图大小 ->>分析1} else {...}
/*** 分析1:onMeasure()* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()*      b. 存储测量后的View宽 / 高:setMeasuredDimension()*/ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  // 参数说明:View的宽 / 高测量规格setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  // setMeasuredDimension() :获得View宽/高的测量值 ->>分析2// 传入的参数通过getDefaultSize()获得 ->>分析3
}
/*** 分析2:setMeasuredDimension()* 作用:存储测量后的View宽 / 高* 注:该方法即为我们重写onMeasure()所要实现的最终目的*/protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  //参数说明:测量后子View的宽 / 高值// 将测量后子View的宽 / 高值进行传递mMeasuredWidth = measuredWidth;  mMeasuredHeight = measuredHeight;  mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  } // 由于setMeasuredDimension()的参数是从getDefaultSize()获得的// 下面继续看getDefaultSize()的介绍
/*** 分析3:getDefaultSize()* 作用:根据View宽/高的测量规格计算View的宽/高值*/public static int getDefaultSize(int size, int measureSpec) {  // 参数说明:// size:提供的默认大小// measureSpec:宽/高的测量规格(含模式 & 测量大小)// 设置默认大小int result = size; // 获取宽/高测量规格的模式 & 测量大小int specMode = MeasureSpec.getMode(measureSpec);  int specSize = MeasureSpec.getSize(measureSpec);  switch (specMode) {  // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Sizecase MeasureSpec.UNSPECIFIED:  result = size;  break;  // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Sizecase MeasureSpec.AT_MOST:  case MeasureSpec.EXACTLY:  result = specSize;  break;  }  // 返回View的宽/高值return result;  }    

ViewGroup 

测量原理

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

  1. 遍历测量所有子View的尺寸(宽/高);
  2. 合并所有子View的尺寸(宽/高),最终得到ViewGroup父视图的测量值。

layout

用于计算视图(View)的位置,即计算View的四个顶点位置:LeftTopRight 和 Bottom。

单一view

/*** 源码分析起始点:layout()* 作用:确定View本身的位置,即设置View本身的四个顶点位置*/ public void layout(int l, int t, int r, int b) {  // 当前视图的四个顶点int oldL = mLeft;  int oldT = mTop;  int oldB = mBottom;  int oldR = mRight;  // 1. 确定View的位置:setFrame() / setOpticalFrame()// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 // setFrame() ->分析1// setOpticalFrame() ->分析2boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);// 2. 若视图的大小 & 位置发生变化// 会重新确定该View所有的子View在父容器的位置:onLayout()if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  onLayout(changed, l, t, r, b);  // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现 ->分析3// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需自定义重写实现(下面的章节会详细说明)
}  /*** 分析1:setFrame()* 作用:根据传入的4个位置值,设置View本身的四个顶点位置* 即:最终确定View本身的位置*/ protected boolean setFrame(int left, int top, int right, int bottom) {// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点// 从而确定了视图的位置mLeft = left;mTop = top;mRight = right;mBottom = bottom;mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);}/*** 分析2:setOpticalFrame()* 作用:根据传入的4个位置值,设置View本身的四个顶点位置* 即:最终确定View本身的位置*/ private boolean setOpticalFrame(int left, int top, int right, int bottom) {Insets parentInsets = mParent instanceof View ?((View) mParent).getOpticalInsets() : Insets.NONE;Insets childInsets = getOpticalInsets();// 内部实际上是调用setFrame()return setFrame(left   + parentInsets.left - childInsets.left,top    + parentInsets.top  - childInsets.top,right  + parentInsets.left + childInsets.right,bottom + parentInsets.top  + childInsets.bottom);}// 回到调用原处/*** 分析3:onLayout()* 注:对于单一View的laytou过程*    1. 由于单一View是没有子View的,故onLayout()是一个空实现*    2. 由于在layout()中已经对自身View进行了位置计算:setFrame() / setOpticalFrame()*    3. 所以单一View的layout过程在layout()后就已完成了*/ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {// 参数说明// changed 当前View的大小和位置改变了 // left 左部位置// top 顶部位置// right 右部位置// bottom 底部位置
}  

viewGroup

/*** 源码分析:layout()* 作用:确定View本身的位置,即设置View本身的四个顶点位置* 注:与单一View的layout()源码一致*/ public void layout(int l, int t, int r, int b) {  // 当前视图的四个顶点int oldL = mLeft;  int oldT = mTop;  int oldB = mBottom;  int oldR = mRight;  // 1. 确定View的位置:setFrame() / setOpticalFrame()// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 // setFrame() ->分析1// setOpticalFrame() ->分析2boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);// 2. 若视图的大小 & 位置发生变化// 会重新确定该View所有的子View在父容器的位置:onLayout()if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  onLayout(changed, l, t, r, b);  // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->分析3...}  /*** 分析1:setFrame()* 作用:确定View本身的位置,即设置View本身的四个顶点位置*/ protected boolean setFrame(int left, int top, int right, int bottom) {...// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点// 从而确定了视图的位置mLeft = left;mTop = top;mRight = right;mBottom = bottom;mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);}/*** 分析2:setOpticalFrame()* 作用:确定View本身的位置,即设置View本身的四个顶点位置*/ private boolean setOpticalFrame(int left, int top, int right, int bottom) {Insets parentInsets = mParent instanceof View ?((View) mParent).getOpticalInsets() : Insets.NONE;Insets childInsets = getOpticalInsets();// 内部实际上是调用setFrame()return setFrame(left   + parentInsets.left - childInsets.left,top    + parentInsets.top  - childInsets.top,right  + parentInsets.left + childInsets.right,bottom + parentInsets.top  + childInsets.bottom);}// 回到调用原处} 
/*** 分析3:onLayout()* 作用:计算该ViewGroup包含所有的子View在父容器的位置()* 注: *    a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现*    b. 在自定义ViewGroup时必须复写onLayout()!!!!!*    c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())*/ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {// 参数说明// changed 当前View的大小和位置改变了 // left 左部位置// top 顶部位置// right 右部位置// bottom 底部位置// 1. 遍历子View:循环所有子Viewfor (int i=0; i<getChildCount(); i++) {View child = getChildAt(i);   // 2. 计算当前子View的四个位置值// 2.1 位置的计算逻辑...// 需自己实现,也是自定义View的关键// 2.2 对计算后的位置值进行赋值int mLeft  = Leftint mTop  = Topint mRight = Rightint mBottom = Bottom// 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数// 即确定了子View在父容器的位置child.layout(mLeft, mTop, mRight, mBottom);// 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述}}

draw

paint 类

Paint 类包含有关如何绘制几何图形、文本和位图的样式和颜色信息。

构造函数

Paint() 使用默认设置创建新油漆。

Paint(int flags) 使用指定的标志创建一个新的油漆。

Paint(Paint paint)

常用方法

setColor(int),设置画笔的颜色

setAlpha(int),设置画笔的透明度

setARGB(int a, int r, int g, int b),设置画笔的颜色,a代表透明度,r,g,b代表颜色值

setAntiAlias(boolean),设置是否使用抗锯齿功能,设置后会平滑一些

setDither(boolean),设定是否使用图像抖动处理,设置后图像更加清晰

setStyle(Style),设置画笔的风格

  • Style.FILL,实心
  • Style.FILL_AND_STROKE,同时显示实心和空心
  • Style.STROKE,空心

setStrokeJoin(Join),设置连接 

getFontMetricsInt 返回给定文本的字体规格值

Paint.FontMetricsInt fontMetricsInt = myPaint.getFontMetricsInt();

Canvas类

Canvas主要用于2D绘图,它提供了很多相应的drawXxx()方法,Canvas的获取方式有三种

  • 重写View的onDraw(Canvas)方法
@Override
protected void onDraw(Canvas canvas) {}
  • SurfaceView通过lockCanvas()方法获取Canvas
@Override
public void surfaceCreated(SurfaceHolder holder) {Canvas canvas = holder.lockCanvas();holder.unlockCanvasAndPost(canvas);
}

 

  • 通过Canvas(Bitmap)或setBitmap(Bitmap)自定义Canvas
Bitmap bitmap = Bitmap.createBitmap(600, 800, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);

常用方法

  • 绘制背景

// a为透明度,`r`,`g`,`b`代表颜色值
drawARGB(int a, int r, int g, int b)
drawColor(int color)
drawRGB(int r, int g, int b)
  • 绘制点

// 根据给定的坐标,绘制点
drawPoint(float x, float y, Paint)// 两个一组组成坐标,pts必须是二的倍数
drawPoints(float[] pts, Paint)// offset是起始,count是数量,必须是二的倍数
drawPoints(float[] pts, int offset, int count, Paint)
  • 绘制线

线由两个点连接而成

// 根据给定的坐标,绘制线
drawLine(float, float, float, float, Paint)// 四个一组组成连线,pts必须是四的倍数
drawLines(float[] pts, Paint)// offset是起始,count是数量,必须是四的倍数
drawLines(float[] pts, int offset, int count, Paint)
  • 绘制矩形

// 绘制矩形,rect指定左右上下
drawRect(Rect rect, Paint)
drawRect(RectF, Paint)
drawRect(float, float, float, float, Paint)
  • 圆角矩阵

// 绘制圆角矩形,rx是横向,ry是纵向
drawRoundRect(RectF, float rx, float ry, Paint)
drawRoundRect(float, float, float, float, float, float, Paint)
  • 绘制椭圆

// 绘制椭圆,rect指定左右上下
drawOval(RectF rect, Paint)
drawOval(float, float, float, float, Paint)
  • 绘制圆

// cx和cy是圆的中心,radius是圆的半径
drawCircle(float cx, float cy, float radius, Paint)
  • 绘制弧形

// startAngle是圆弧开始角度,sweepAngle是圆弧经过的角度,useCenter设置圆弧是否经过中心
drawArc(RectF, float startAngle, float sweepAngle, boolean useCenter, Paint)
drawArc(float, float, float, float, float, float, boolean, Paint)
  • 绘制文字 

 

baseLine基线计算

单一view

/*** 源码分析:draw()* 作用:根据给定的 Canvas 自动渲染View包括其所有子 View)。* 绘制过程:*   1. 绘制view背景*   2. 绘制view内容*   3. 绘制子View*   4. 绘制装饰(渐变框,滑动条等等)* 注:*    a. 在调用该方法之前必须要完成 layout 过程*    b. 所有的视图最终都是调用 View 的 draw()绘制视图( ViewGroup 没有复写此方法)*    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制*    d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制*/ public void draw(Canvas canvas) {...// 仅贴出关键代码int saveCount;// 步骤1: 绘制本身View背景if (!dirtyOpaque) {drawBackground(canvas); // ->分析1}// 若有必要,则保存图层(还有一个复原图层)// 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过// 因此在绘制时,节省 layer 可以提高绘制效率final int viewFlags = mViewFlags;if (!verticalEdges && !horizontalEdges) {// 步骤2:绘制本身View内容if (!dirtyOpaque) onDraw(canvas);// 单一View中:默认为空实现,需复写// ViewGroup中:需复写// ->分析2// 步骤3:绘制子View// 由于单一View无子View,故View中:默认为空实现// ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写dispatchDraw(canvas);// ->分析3// 步骤4:绘制装饰,如滑动条、前景色等等onDrawScrollBars(canvas);// ->分析4return;}...    
}/*** 分析1:drawBackground(canvas)* 作用:绘制View本身的背景*/ private void drawBackground(Canvas canvas) {// 获取背景 drawablefinal Drawable background = mBackground;if (background == null) {return;}// 根据在 layout 过程中获取的 View 的位置参数,来设置背景的边界setBackgroundBounds();...// 获取 mScrollX 和 mScrollY值 final int scrollX = mScrollX;final int scrollY = mScrollY;if ((scrollX | scrollY) == 0) {background.draw(canvas);} else {// 若 mScrollX 和 mScrollY 有值,则对 canvas 的坐标进行偏移canvas.translate(scrollX, scrollY);// 调用 Drawable 的 draw 方法绘制背景background.draw(canvas);canvas.translate(-scrollX, -scrollY);}} /*** 分析2:onDraw(canvas)* 作用:绘制View本身的内容* 注:*   a. 由于 View 的内容各不相同,所以该方法是一个空实现*   b. 在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容*   c. 谨记:自定义View中 必须且只需复写onDraw()*/protected void onDraw(Canvas canvas) {... // 复写从而实现绘制逻辑}/*** 分析3: dispatchDraw(canvas)* 作用:绘制子View* 注:由于单一View中无子View,故为空实现*/protected void dispatchDraw(Canvas canvas) {... // 空实现}/*** 分析4: onDrawScrollBars(canvas)* 作用:绘制装饰,如滚动指示器、滚动条、和前景等*/public void onDrawForeground(Canvas canvas) {onDrawScrollIndicators(canvas);onDrawScrollBars(canvas);final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;if (foreground != null) {if (mForegroundInfo.mBoundsChanged) {mForegroundInfo.mBoundsChanged = false;final Rect selfBounds = mForegroundInfo.mSelfBounds;final Rect overlayBounds = mForegroundInfo.mOverlayBounds;if (mForegroundInfo.mInsidePadding) {selfBounds.set(0, 0, getWidth(), getHeight());} else {selfBounds.set(getPaddingLeft(), getPaddingTop(),getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());}final int ld = getLayoutDirection();Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);foreground.setBounds(overlayBounds);}foreground.draw(canvas);}}

viewGroup

/*** 源码分析:draw()* 与单一View的draw()流程类似* 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)* 绘制过程:*   1. 绘制view背景*   2. 绘制view内容*   3. 绘制子View*   4. 绘制装饰(渐变框,滑动条等等)* 注:*    a. 在调用该方法之前必须要完成 layout 过程*    b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)*    c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制*    d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制*/ public void draw(Canvas canvas) {...// 仅贴出关键代码int saveCount;// 步骤1: 绘制本身View背景if (!dirtyOpaque) {drawBackground(canvas);}// 若有必要,则保存图层(还有一个复原图层)// 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过// 因此在绘制时,节省 layer 可以提高绘制效率final int viewFlags = mViewFlags;if (!verticalEdges && !horizontalEdges) {// 步骤2:绘制本身View内容if (!dirtyOpaque) onDraw(canvas);// View 中:默认为空实现,需复写// ViewGroup中:需复写// 步骤3:绘制子View// ViewGroup中:系统已复写好对其子视图进行绘制,不需复写dispatchDraw(canvas);// 步骤4:绘制装饰,如滑动条、前景色等等onDrawScrollBars(canvas);return;}...    
}
/*** 源码分析:dispatchDraw()* 作用:遍历子View & 绘制子View* 注:*   a. ViewGroup中:由于系统为我们实现了该方法,故不需重写该方法*   b. View中默认为空实现(因为没有子View可以去绘制)*/ protected void dispatchDraw(Canvas canvas) {......// 1. 遍历子Viewfinal int childrenCount = mChildrenCount;......for (int i = 0; i < childrenCount; i++) {......if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||transientChild.getAnimation() != null) {// 2. 绘制子View视图 ->>分析1more |= drawChild(canvas, transientChild, drawingTime);}....}}/*** 分析1:drawChild()* 作用:绘制子View*/protected boolean drawChild(Canvas canvas, View child, long drawingTime) {// 最终还是调用了子 View 的 draw ()进行子View的绘制return child.draw(canvas, this, drawingTime);}

View的常见回调方法

构造函数

构造函数1:会在代码中创建对象 new 时使用

public Custom_Textview(Context context)

构造函数2:会在layout布局中使用

public Custom_Textview(Context context, @Nullable AttributeSet attrs)

构造函数3:会在layout布局中使用,但是有style

public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr)

构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。

public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)

View的弹性滑动

 

View的滑动冲突

MotionEvent

实例

自定义一个view 继承自viewGroup能不能出效果?

不能。默认的viewGroup不会调用onDraw方法。因为它是用了责任链模式。

主要实现绘制功能的是:

自定义TextView

1.创建一个自定义TextView 类继承自 View

package com.example.androidstudiostudy.customview;import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;public class Custom_Textview extends View {// 构造函数1:会在代码中创建对象 new 时使用public Custom_Textview(Context context) {super(context);Log.d("自定义textView","new 对象");}// 构造函数2:会在layout布局中使用public Custom_Textview(Context context, @Nullable AttributeSet attrs) {super(context, attrs);Log.d("自定义textView","在layout布局中使用");}// 构造函数3:会在layout布局中使用,但是有stylepublic Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);Log.d("自定义textView","在layout布局中使用,但是有style");}// 构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}// 自定义view的测量方法,用于测量 View 大小的重要方法。在这个方法中,你需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// MeasureSpec.getMode(xxx) 获取宽高的模式int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);// MeasureSpec.AT_MOST 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式// MeasureSpec.EXACTLY  View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 或者设置了精确的尺寸时,会使用这种模式// MeasureSpec.UNSPECIFIED 表示 View 的大小可以任意扩展,不受限制。通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式if(widthMode == MeasureSpec.UNSPECIFIED){}}
}

2.创建自定义属性

在res/values下创建一个名为arrs.xml的资源文件

<?xml version="1.0" encoding="utf-8"?>
<resources><!-- name:自定义view的名字--><declare-styleable name="CustomTextview"><!-- name:属性名称 format:格式--><attr name="custom_text" format="string"/><attr name="custom_textColor" format="color"/><!-- 字体大小格式:dimension--><attr name="custom_textSize" format="dimension"/><!-- background:自定义view都是继承自view 所以不用自定义管理--><attr name="custom_background" format="reference|color"/><attr name="custom_inputType"><enum name="number" value="1"/><enum name="string" value="2"/>、</attr></declare-styleable>
</resources>

3.获取自定义属性

public class Custom_Textview extends View {private String text;private int textColor = Color.BLACK;private int fontSize;private Paint myPaint;// 构造函数1:会在代码中创建对象 new 时使用public Custom_Textview(Context context) {// super(context);// 修改构造函数,无论使用哪个构造函授都调用第三个this(context, null);Log.d("自定义textView", "new 对象");}// 构造函数2:会在layout布局中使用public Custom_Textview(Context context, @Nullable AttributeSet attrs) {// super(context, attrs);this(context, attrs, 0);// hLog.d("自定义textView", "在layout布局中使用");}// 构造函数3:会在layout布局中使用,但是有stylepublic Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);Log.d("自定义textView", "在layout布局中使用,但是有style");// 获取自定义属性TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CustomTextview);text = array.getString(R.styleable.CustomTextview_custom_text);textColor = array.getColor(R.styleable.CustomTextview_custom_textColor, textColor);fontSize = array.getDimensionPixelSize(R.styleable.CustomTextview_custom_textSize, fontSize);// 回收自定义属性array.recycle();}// 构造函数4:Android 5.0 (API 21) 之后新增的,用于支持更复杂的样式定制。public Custom_Textview(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}// 自定义view的测量方法,用于测量 View 大小的重要方法。在这个方法中,你需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。// int widthMeasureSpec, int heightMeasureSpec 是父类传过来的@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);}}

使用 

<com.example.androidstudiostudy.customview.Custom_Textviewapp:custom_text = "自定义文本"app:custom_textColor = "@color/p6"app:custom_textSize = "14sp"android:background="@color/black"android:layout_width="wrap_content"android:layout_height="wrap_content"/>

3.测量view的宽高

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width,height;// MeasureSpec.getMode(xxx) 获取宽高的模式int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);// MeasureSpec.AT_MOST 表示 View 的大小可以最大到父 View 允许的尺寸,但不能超过这个尺寸。通常情况下,在使用 View.WRAP_CONTENT 且父 View 设置了最大尺寸时,会使用这种模式// MeasureSpec.EXACTLY  View 的大小已经被精确地确定了,通常是父 View 已经为它指定了确切的尺寸。例如,在使用 View.MATCH_PARENT 或者设置了精确的尺寸时,会使用这种模式// MeasureSpec.UNSPECIFIED 表示 View 的大小可以任意扩展,不受限制。通常情况下,在使用 View.WRAP_CONTENT 时,会使用这种模式// 计算宽度// 1. 确定的值,这时候不需要计算,给多少是多少width = MeasureSpec.getSize(widthMeasureSpec);// 2.给的是wrap_content 需要计算if (widthMode == MeasureSpec.AT_MOST) {Log.e("测量模式", "AT_MOST");// 计算的宽度与字体长度、字体大小有关 ----- 需要使用画笔来测量Rect bounds = new Rect();myPaint.getTextBounds(text, 0, text.length(), bounds); // 获取文本的rectwidth = bounds.width();} else if (widthMode == MeasureSpec.EXACTLY) {Log.e("测量模式", "EXACTLY");} else if (widthMode == MeasureSpec.UNSPECIFIED) {Log.e("测量模式", "UNSPECIFIED");}// 计算高度// 1. 确定的值,这时候不需要计算,给多少是多少height = MeasureSpec.getSize(heightMeasureSpec);// 2.给的是wrap_content 需要计算if(heightMode == MeasureSpec.AT_MOST){Rect bounds = new Rect();myPaint.getTextBounds(text, 0, text.length(), bounds); // 获取文本的rectheight = bounds.height();}// 设置控件的宽高setMeasuredDimension(width,height);}

4.绘制view

Carson带你学Android:自定义View 绘制过程(Draw) - 简书 (jianshu.com)

Carson带你学Android:自定义View 布局过程(Layout) - 简书

Carson带你学Android:一文梳理自定义View工作流程 - 简书 (jianshu.com)

 

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • OpenCV几何图像变换(5)旋转和缩放计算函数getRotationMatrix2D()的使用
  • Linux安装Miniconda3
  • 数学理论在编程中的核心应用与实践(上)
  • EmguCV学习笔记 VB.Net 6.3 轮廓外接多边形
  • C#(asp.net)乡镇中学宿舍管理系统---附源码 97861
  • OSPF路由原理详解与关键点
  • 20L水箱植保无人机技术详解
  • NVM安装管理node.js版本(简单易懂)
  • 微信左滑删除聊天记录怎么恢复?记录找回秘籍,第一种更有效!
  • 基于java图书销售管理系统设计与实现
  • Aurora IP核 —— NFC功能
  • Unity3D 屏幕空间阴影的简单优化详解
  • 【MySQL数据库管理问答题】第6章 管理 MySQL 用户
  • windows vs2022 MFC使用webview2嵌入网页
  • 大数据-96 Spark 集群 SparkSQL Scala编写SQL操作SparkSQL的数据源:JSON、CSV、JDBC、Hive
  • 【前端学习】-粗谈选择器
  • 2017 前端面试准备 - 收藏集 - 掘金
  • CentOS 7 防火墙操作
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • extjs4学习之配置
  • JavaScript 一些 DOM 的知识点
  • Java方法详解
  • Java面向对象及其三大特征
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • leetcode98. Validate Binary Search Tree
  • Linux gpio口使用方法
  • Netty 框架总结「ChannelHandler 及 EventLoop」
  • Python语法速览与机器学习开发环境搭建
  • Spark in action on Kubernetes - Playground搭建与架构浅析
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 三栏布局总结
  • 深入 Nginx 之配置篇
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 国内开源镜像站点
  • ​​​​​​​开发面试“八股文”:助力还是阻力?
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • (二)Eureka服务搭建,服务注册,服务发现
  • (二开)Flink 修改源码拓展 SQL 语法
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (附源码)ssm高校运动会管理系统 毕业设计 020419
  • (离散数学)逻辑连接词
  • (三)终结任务
  • (译)计算距离、方位和更多经纬度之间的点
  • .class文件转换.java_从一个class文件深入理解Java字节码结构
  • .gitignore
  • .Net Core中的内存缓存实现——Redis及MemoryCache(2个可选)方案的实现
  • .NET Micro Framework 4.2 beta 源码探析
  • .Net插件开发开源框架
  • //解决validator验证插件多个name相同只验证第一的问题
  • @Autowired自动装配
  • @GetMapping和@RequestMapping的区别
  • @Resource和@Autowired的区别
  • [ Python ]使用Charles对Python程序发出的Get与Post请求抓包-解决Python程序报错问题
  • [100天算法】-实现 strStr()(day 52)