为什么要自定义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时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case 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() ->分析2
boolean 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() ->分析2
boolean 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:循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 2. 计算当前子View的四个位置值
// 2.1 位置的计算逻辑
...// 需自己实现,也是自定义View的关键
// 2.2 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int 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);
// ->分析4
return;
}
...
}
/**
* 分析1:drawBackground(canvas)
* 作用:绘制View本身的背景
*/
private void drawBackground(Canvas canvas) {
// 获取背景 drawable
final 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. 遍历子View
final int childrenCount = mChildrenCount;
......
for (int i = 0; i < childrenCount; i++) {
......
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
// 2. 绘制子View视图 ->>分析1
more |= 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布局中使用,但是有style
public 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 的宽度和高度。
@Override
protected 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);
// h
Log.d("自定义textView", "在layout布局中使用");
}
// 构造函数3:会在layout布局中使用,但是有style
public 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 是父类传过来的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
使用
<com.example.androidstudiostudy.customview.Custom_Textview
app: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); // 获取文本的rect
width = 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); // 获取文本的rect
height = bounds.height();
}
// 设置控件的宽高
setMeasuredDimension(width,height);
}
4.绘制view
Carson带你学Android:自定义View 绘制过程(Draw) - 简书 (jianshu.com)
Carson带你学Android:自定义View 布局过程(Layout) - 简书
Carson带你学Android:一文梳理自定义View工作流程 - 简书 (jianshu.com)