一篇文章搞定《CoordinatorLayout完成电商首页》
本文前言
像事件的分发一样,View的绘制流程我也分成了三部分来讲
分别为:1、怎样到达ViewRootImpl。2、到达ViewRootImpl做了什么。3、View的最终绘制
之后我会利用一个自定义View的实例来反刍这篇文章,像《一篇文章搞定搞定事件分发》一样。
怎样到达ViewRootImpl
过程如下:
1、首先View是在Activity的onCreate阶段通过setContentView解析xml加载进去的。也就是View是在OnCreate阶段加到缓存中的。
2、加在哪个缓存中了呢:这个View对象会被添加到ActivityThread的ActivityClientRecord对象中(后面可以看到取出来添加到DecorView中),并放入ActivityThread的mActivities集合中缓存当前Activity对象的状态信息。
3、ActivityThread.handleResumeActivity()是在Activity的生命周期中的onResume阶段调用的,用于将Activity设置为resumed状态。
4、ActivityThread.handleResumeActivity()会调用WindowManager的addView()方法来把Activity的Window视图添加到窗口上。源码如下:
@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
.....
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
.....
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (!a.mWindowAdded) {
......
a.mWindowAdded = true;
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
5、可以看到ActivityThread.handleResumeActivity()方法中,会先通过Activity的getWindow()方法获取到当前Activity的Window,设置相关的Window参数,接着调用WindowManager的addView方法将Window的视图加入到窗口中。
6、这里面ActivityClientRecord只是Activity的一个信息类,记录一下Activity的状态、所属进程等相关信息。
7、WindowManager实现类为WindowManagerImpl,WindowManagerImpl中addView()方法又会调用WindowManagerGlobal的addView()方法。源码如下:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
······
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
//即将开始流程绘制
root.setView(view, wparams, panelParentView);
·······
}
8、可以看到他会创建出我们熟知的ViewRootImpl。并将我们的View Set给ViewRootImpl进行管理。
上面的这个过程可以理解为Android ManagerService的功劳,通过管理Activity的生命周期从而加载我们的View。
流程图小结:
到达ViewRootImpl做了什么
像事件的分发一样,最终View都交给了ViewRootImpl来进行管理。这也是ViewRootImpl代码上万行的原因。经过不断的迭代,View的体积已经很庞大了。这也许是Jetpack Compose出现的原因吧,来替换掉这庞大的View体系,毕竟庞大就难以维护。
下面看看ViewRootImpl对View的绘制做了哪些关键的步骤。
第一步:setView()
首先上面讲到了ViewRootImpl的setView。那setView做了什么呢?
我们知道ViewRootImpl中的mView就是我们的根布局DecorView这个在事件的分发中也有提到。这里再说一遍吧:
其实上面认真读代码的人已经发现了,就在第一步handleResumeActivity方法中。我们先是创建了window之后获取了decor,并通过windowsManager的wm.addView(decor, l)设置给了下一步。最终到达ViewRootImpl。
下面是ViewRootImpl的setView方法。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
if (mView == null) {
mView = view;
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
}
}
可以看到ViewRootImpl首先将window给他的view和自己的mView进行了绑定。毕竟自己家的小孩才能管,对吧。
第二步:performTraversals()
下一步就来到了View绘制的关键方法performTraversals()。
上面可以看到执行了requestLayout()方法,requestLayout()方法中会异步执行performTraversals()方法,View的三大流程都是在该方法中执行的。
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
到这儿我们算是明白View的绘制流程是从哪儿开始的,接下来分析这个过程到底是怎么做的。
直接简化一下performTraversals的代码便于理解(简化把----嘻嘻)
private void performTraversals() {
//计算DecorView根View的MeasureSpecint childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
performLayout(lp, mWidth, mHeight);
performDraw();
}
真正的奥秘就在performMeasure()、performLayout()、performDraw()大家看着熟悉吗是不是想到了,说烂嘴的onMeasure()、onLayout()、onDraw()方法。
没错昂、看来你很有学习Android的脑瓜。他最终就调用了我们的mView也就是DecorView的三大绘制方法。
第三步:DecorView中的Measure()、Layout()、Draw()
让我们来看一下:
performMeasure()
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
performLayout()
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
final View host = mView;
if (host == null) {
return;
}
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
performDraw()
这个方法中没有直接调用mView的Draw。是先调用draw方法从而调用drawSoftware。最后还是一样会调用Draw。
源码如下:都被简化的了方便理解
private boolean performDraw() {
try {
boolean canUseAsync = draw(fullRedrawNeeded, usingAsyncReport && mSyncBuffer);
....
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
try {
....
mView.draw(canvas);
....
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
最后可以看到调用了mView的Measure、Layout、Draw方法。
我们点进去发现,在View的Measure、Layout、Draw会调用我们熟知的onMeasure()、onLayout()、onDraw()方法。这个源码就不粘贴出来了。大家可以点进去看一下。
View的最终绘制
所谓View的最终绘制就是。简单来说就是View是如果通过onMeasure()、onLayout()、onDraw(),最终绘制的界面上的。
下面会对这三个重要方法进行知识解析,随后我会以两篇自定义View的文章去用例子来反刍这个知识点。
View的层级和View树
1、首先我们之前有说到我们Set的XML布局是我们的DecorView。这个前面已经证实过了。
2、通过源码我们还知道DecorView是存放在Window中的,而PhoneWindow是Window唯一的实现类。
3、每个Activity都有一个对应的Window。当一个Activity启动时,它会创建一个Window实例。所以说Activity包含着Window
所以我们基础的层级为Activity->PhoneWindow->DecorView
其次就是我们Set的布局了(这个就不细说了,我们的xml布局其实就是ViewGroup和View的组合组成的树状的View)
盗的树的层级图如下:
这边只讲这三个方法的定义和理论、下一篇会结合实战来讲。放在这一篇太冗余了。
首先要知道这三个是做什么的,简单来说就是,画多大、画在哪里、画什么。
之后三个方法都会以:什么作用?注意点!怎么用!三个方面来说一下。
ps:这里定义看着不太理解也没关系,读一遍之后打开下一篇文字对照着去走一遍自定义View的例子,就很明确了,不然谁来看定义都是比较懵的。
onMeasure()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
什么作用:
他会测量我们View树中所有View的宽和高,以DecorView根View为起点,遍历计算所有孩子View的大小。从而计算每个控件的大小进行绘制我们的布局。
主要分为两种情况如下
有可能是用父布局来确定子View大小:这里既然都需要来确认子布局的大小,那么其实只有我们的子View是wrap_content需要我们来次测量并设置我们View的大小。
有可能是用子View大小来确定父布局大小:那么我们其实就需要遍历所有的子View大小,从而计算我们的父View大小。既然是通过一堆的View确定那么我们的父View其实大多数就是以ViewGroup的形式出现的。
注意什么:
那就不得不说他的MeasureSpec参数了,所有的测量相关内容都在这个参数中。可以看到onMeasure函数中的两个参数widthMeasureSpec和heightMeasureSpec。那么他们代表什么呢?有什么作用。
MeasureSpec可以理解为是View的测量依据,它是系统将View的参数根据父容器的规则转换而成的,之后根据它来测量出View的宽和高。
可以看到他为一个Int值他存储着父View的测量模式和测量大小。
那么是怎么存储的呢?
他是利用了Int为32bit位来去存储相关内容的。
首先测量模式为三种:
模式 | 作用 |
---|---|
EXACTLY精确模式00 | 表示父容器已经检测出View所需要的精确大小。这时View的最终大小就是SpecSize的值,它对应于View参数中的match_parent和具体大小数值这两种模式。 |
AT_MOST最大模式01 | 表示父容器指定了一个可用大小的数值,记录在SpecSize中,View的大小不能大于它,但具体的值还是看View的具体实现。它对应于View参数中的wrap_content。该模式下父控件无法确认子View的尺寸,只能通过子控件的情况来确定大小。 |
UNSPECIFIED未指定模式11 | 父容器不对View有任何限制,给它想要的任何尺寸。自定义View一般用不到,一般用于系统中的一些控件比如ListView、ScrollView。 |
怎么用:
主要API,最常用的就是这三个API了。完整例子在下一篇整出来。
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
比如如果我们设置了wrapcontent,不想使用父类的剩余宽高,我们可以在onMeasure做判断,通过setMeasureDimension然后自己设置宽高。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200)
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(100, height)
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, 100)
}
}
问:上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
答:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。
总结来说所有的View都是由父View的MeasureSpec和view本身的LayoutParams来决定。
onLayout()
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
}
什么作用:
用于设置子View在父View中的布局位置,以及设置子View的大小等相关布局信息。
当一个View的onLayout方法被调用时,表示该View已经完成了测量阶段的工作,并且所有的子View都已经完成了测量和布局工作,此时可以计算出子View在父View中的位置和大小。
其中他需要调用layout用来确定自身位置,自身位置确定后,调用onlayout确定子view的位置。
也就是说layout方法确定view本身的位置,onLayout确定所有子元素的位置。
注意什么:
上面说到layout方法确定view本身的位置,当确认之后会调用onLayout。那么就是说当回调onLayout时,他的的坐标已经是确认的了。
那么他的onLayout的四个位置参数即为当前View在父布局中的位置和大小,具体如下:
left - 当前View的左侧边缘在父布局中的X坐标,相对于父View的左边距离(单位为像素)。
top -当前View的顶部边缘在父布局中的Y坐标。
right - 当前View的右侧边缘在父布局中的X坐标。
bottom - 当前View的底部边缘在父布局中的Y坐标。
这些参数表示了当前View的位置和大小,并且相对于父布局的坐标系。
事实上,这些位置参数可以用于计算当前View的其他相关属性和位置,如宽度、高度、中心点坐标等。
因此,onLayout方法的四个参数也可以称为位置和大小参数。
所以要熟悉我们的坐标系:直接上图(屏幕坐标系和视图坐标系)
View获取自身宽高
- getHeight():获取View自身高度
- getWidth():获取View自身宽度
获得View到其父控件(ViewGroup)的距离
- getTop():获取View自身顶边到其父布局顶边的距离
- getLeft():获取View自身左边到其父布局左边的距离
- getRight():获取View自身右边到其父布局左边的距离
- getBottom():获取View自身底边到其父布局顶边的距离
MotionEvent提供的方法(点击事件需要的坐标,蓝色圆圈为点击点)
- getX():获取点击事件距离控件左边的距离,即视图坐标
- getY():获取点击事件距离控件顶边的距离,即视图坐标
- getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
- getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标
怎么用:
小例子:获取宽与高的最大值用于设置正方形 View 的边长
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
// 使用宽高的最大值设置边长
var width = right - left
var height = bottom - top
var size = Math.max(width, height);
super.layout(left, top, left + size, top + size)
}
onDraw()
这个在后面实战中细讲,想对它有个概念理解。
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
什么作用:
Draw过程比较简单,主要作用是将View绘制到屏幕上面。
他的过程是:
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispathDraw)
- 绘制装饰(onDrawScrollBars)
View绘制过程的传递是通过dispatchDraw来实现的,dispatchDraw会便利调用所有子元素的draw方法,如此draw事件就被一层层的传递下去了。
注意什么:
1、不要在onDraw方法中进行耗时操作,否则会导致界面卡顿,影响用户体验。
2. 不要在onDraw方法中创建对象,尽量复用已有对象。因为onDraw在绘制过程中会频繁的调用,会造成JVM不断的创建、回收对象。从而造成内存抖动,进而卡顿。
3. 如果需要在View外部修改View的属性,比如设置背景颜色等,最好在View的构造函数中设置。不然会不起作用。
怎么用:
在onDraw方法中使用Canvas对象绘制需要的图形
在需要更新View的时候,调用View的invalidate方法,触发系统重新调用onDraw方法。
class CustomView : View {
private var canvas : Canvas? = null
private lateinit var paint : Paint
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr){
//初始化
canvas = Canvas() //< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
paint = Paint()
paint.color = Color.parseColor("#F50808")
}
override fun onDraw(canvas: Canvas) {
//画一个圆
canvas.drawCircle(100f, 100f, 50f, paint)
}
}
总结
总结就是文章内容过多,分批阅读。自定义View需要多实战。
虽然大多数时候我们都是copy其他人的自定义View去修改。但是想要提升自己,还是一定要去自己去实现一些自定义View的
后面会更新1-2篇的自定义View的实战。