Android - 自定义view
为什么要自定义view?
在Android开发中有很多业务场景,原生的控件无法满足需求,并且经常也会遇到一个UI在多处重复使用情况,于是可以通过自定义View的方式来实现这些UI效果。
自定义view的分类
自定义属性
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
作为顶层视图容器,负责加载和展示活动的布局资源。
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);
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 会负责测绘自身。
View的三大流程
- measure:测量view的宽高
- layout:确定view在父容器中的位置
- draw:负责将view绘制在屏幕上
measure
自定义view的测量方法,是用于测量 View 大小的重要方法。
在这个方法中,需要根据传入的宽度和高度测量规格,计算并设置 View 的宽度和高度。
ViewGroup.LayoutParams
布局参数类,用于指定视图View
的高度(height)
和 宽度(width)
等布局参数。
ViewGroup
的子类(RelativeLayout、LinearLayout)
有其对应的ViewGroup.LayoutParams
子类- 如:
RelativeLayout
的ViewGroup.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);
}
注意:
区别于顶级View
(即DecorView
)的测量规格MeasureSpec,
计算逻辑:取决于 自身布局参数 & 窗口尺寸
掌握 Rect 类
Rect
是 Android 中表示矩形的一个类,它有四个主要的属性:left
、top
、right
和 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种情况:
单一View
/*** 源码分析: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的属性。即:
- 遍历测量所有子View的尺寸(宽/高);
- 合并所有子View的尺寸(宽/高),最终得到ViewGroup父视图的测量值。
layout
用于计算视图(View)
的位置,即计算View
的四个顶点位置:Left
、Top
、Right
和 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)