关于作者
郭孝星,程序员,吉他手,主要从事Android平台基础架构方面的工作,欢迎交流技术方面的问题,可以去我的Github提issue或者发邮件至guoxiaoxingse@163.com与我交流。
文章目录
- 一 View的生命周期
- 二 View的测量流程
- 三 View的布局流程
- 四 View的绘制流程
- 五 View事件分发机制
This class represents the basic building block for user interface components. A Viewoccupies a rectangular area on the screen and is
responsible for drawing and event handling.
View是屏幕上的一块矩形区域,负责界面的绘制与触摸事件的处理,它是一种界面层控件的抽象,所有的控件都继承自View。
View是Android显示框架中较为复杂的一环,首先是它的生命周期会随着Activity的生命周期进行变化,掌握View的生命周期对我们自定义View有着重要的意义。另一个方面View从ViewRoot.performTraversals()开始
经历measure、layout、draw三个流程最终显示在用户面前,用户在点击屏幕时,点击事件随着Activity传入Window,最终由ViewGroup/View进行分发处理。今天我们就围绕着这些主题进行展开分析。
一 View生命周期
在View中有诸多回调方法,它们在View的不同生命周期阶段调用,常用的有以下方法。
我们写一个简单的自定义View来观察View与Activity的生命周期变化。
public class CustomView extends View {
private static final String TAG = "View";
public CustomView(Context context) {
super(context);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
Log.d(TAG, "CustomView()");
}
public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
Log.d(TAG, "CustomView()");
}
/**
* View在xml文件里加载完成时调用
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
Log.d(TAG, "View onFinishInflate()");
}
/**
* 测量View及其子View大小时调用
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "View onMeasure()");
}
/**
* 布局View及其子View大小时调用
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.d(TAG, "View onLayout() left = " + left + " top = " + top + " right = " + right + " bottom = " + bottom);
}
/**
* View大小发生改变时调用
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "View onSizeChanged() w = " + w + " h = " + h + " oldw = " + oldw + " oldh = " + oldh);
}
/**
* 绘制View及其子View大小时调用
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "View onDraw()");
}
/**
* 物理按键事件发生时调用
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyDown() event = " + event.getAction());
return super.onKeyDown(keyCode, event);
}
/**
* 物理按键事件发生时调用
*/
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.d(TAG, "View onKeyUp() event = " + event.getAction());
return super.onKeyUp(keyCode, event);
}
/**
* 触摸事件发生时调用
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "View onTouchEvent() event = " + event.getAction());
return super.onTouchEvent(event);
}
/**
* View获取焦点或者失去焦点时调用
*/
@Override
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
Log.d(TAG, "View onFocusChanged() gainFocus = " + gainFocus);
}
/**
* View所在窗口获取焦点或者失去焦点时调用
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
Log.d(TAG, "View onWindowFocusChanged() hasWindowFocus = " + hasWindowFocus);
}
/**
* View被关联到窗口时调用
*/
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
Log.d(TAG, "View onAttachedToWindow()");
}
/**
* View从窗口分离时调用
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Log.d(TAG, "View onDetachedFromWindow()");
}
/**
* View的可见性发生变化时调用
*/
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
Log.d(TAG, "View onVisibilityChanged() visibility = " + visibility);
}
/**
* View所在窗口的可见性发生变化时调用
*/
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
Log.d(TAG, "View onWindowVisibilityChanged() visibility = " + visibility);
}
}
Activity与View的生命周期变化一目了然。
Activity create
Activity pause
Activity resume
Activity destory
我们来总结一下View的声明周期随着Activity生命周期变化的情况。
我们了解这些生命周期方法有什么作用呢?🤔
其实这些方法在我们自定义View的时候发挥着很大的作用,我们来举几种应用场景。
场景1:在Activity启动时获取View的宽高,但是在onCreate、onStart和onResume均无法获取正确的结果。这是因为在Activity的这些方法里,Viewed绘制可能还没有完成,我们可以在View的生命周期方法里获取。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
场景2:在Activity生命周期发生变化时,View也要做响应的处理,典型的有VideoView保存进度和恢复进度。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if(visibility == VISIBLE){
}
//Activity onPause()
else {
}
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
//TODO do something if activity lifecycle changed if necessary
//Activity onResume()
if (hasWindowFocus) {
}
//Activity onPause()
else {
}
}
场景3:释放线程、资源
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//TODO release resources, thread, animation
}
二 View的测量流程
View是一个矩形区域,它有自己的位置、大小与边距。
View位置
View位置:有左上角坐标(getLeft(), getTop())决定,该坐标是以它的父View的左上角为坐标原点,单位是pixels。
View大小
View大小:View的大小有两对值来表示。getMeasuredWidth()/getMeasuredHeight()这组值表示了该View在它的父View里期望的大小值,在measure()方法完成后可获得。
getWidth()/getHeight()这组值表示了该View在屏幕上的实际大小,在draw()方法完成后可获得。
View内边距
View内边距:View的内边距用padding来表示,它表示View的内容距离View边缘的距离。通过getPaddingXXX()方法获取。需要注意的是我们在自定义View的时候需要单独处理
padding,否则它不会生效,这一块的内容我们会在View自定义实践系列的文章中展开。
View外边距
View内边距:View的内边距用margin来表示,它表示View的边缘离它相邻的View的距离。
Measure过程决定了View的宽高,该过程完成后,通常都可以通过getMeasuredWith()/getMeasuredHeight()获得宽高。
理解了上面这些概念,我们接下来来看看详细的测量流程。
View的测量流程看似复杂,实际遵循着简单的逻辑。
在做测量的时候,measure()方法被父View调用,在measure()中做一些准备和优化工作后,调用onMeasure()来进行实际的自我测量。对于onMeasure(),View和ViewGroup有所区别:
- View:View 在 onMeasure() 中会计算出自己的尺寸然后保存;
- ViewGroup:ViewGroup在onMeasure()中会调用所有子View的measure()让它们进行自我测量,并根据子View计算出的期望尺寸来计算出它们的实际尺寸和位置然后保存。同时,它也会
根据子View的尺寸和位置来计算出自己的尺寸然后保存.
在介绍测量流程之前,我们先来介绍下MeasureSpec,它用来把测量要求从父View传递给子View。我们知道View的大小最终由子View的LayoutParams与父View的测量要求公共决定,测量要求指的
就是这个MeasureSpec,它是一个32位int值。
- 高2位:SpecMode,测量模式
- 低30位:SpecSize,在特定测量模式下的大小
测量模式有三种:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//父View不对子View做任何限制,需要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//父View已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值,它对应LayoutParams中的match_parent和具体数值这两种模式
public static final int EXACTLY = 1 << MODE_SHIFT;
//父View给子VIew提供一个最大可用的大小,子View去自适应这个大小。
public static final int AT_MOST = 2 << MODE_SHIFT;
}
日常开发中我们接触最多的不是MeasureSpec而是LayoutParams,在View测量的时候,LayoutParams会和父View的MeasureSpec相结合被换算成View的MeasureSpec,进而决定View的大小。
View的MeasureSpec计算源码如下所示:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}
该方法用来获取子View的MeasureSpec,由参数我们就可以知道子View的MeasureSpec由父容器的spec,父容器中已占用的的空间大小
padding,以及子View自身大小childDimension共同来决定的。
通过上述方法,我们可以总结出普通View的MeasureSpec的创建规则。
- 当View采用固定宽高的时候,不管父容器的MeasureSpec是什么,resultSize都是指定的宽高,resultMode都是MeasureSpec.EXACTLY。
- 当View的宽高是match_parent,当父容器是MeasureSpec.EXACTLY,则View也是MeasureSpec.EXACTLY,并且其大小就是父容器的剩余空间。当父容器是MeasureSpec.AT_MOST
则View也是MeasureSpec.AT_MOST,并且大小不会超过父容器的剩余空间。 - 当View的宽高是wrap_content时,不管父容器的模式是MeasureSpec.EXACTLY还是MeasureSpec.AT_MOST,View的模式总是MeasureSpec.AT_MOST,并且大小都不会超过芙蓉的剩余空间。
了解了MeasureSpec的概念之后,我就就可以开始分析测量流程了。
- 对于顶级View(DecorView)其MeasureSpec由窗口的尺寸和自身的LayoutParams共同确定的。
- 对于普通View其MeasureSpec由父容器的Measure和自身的LayoutParams共同确定的。
View的绘制会先调用View的measure()方法,measure()方法用来测量View的大小,实际的测量工作是由ziView的onMeasure()来完成的。我们来看看
onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法的实现。
关键点1:View.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
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);
//measureSpec指的是View测量后的大小
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
//MeasureSpec.UNSPECIFIED一般用来系统的内部测量流程
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//我们主要关注着两种情况,它们返回的是View测量后的大小
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果View没有设置背景,那么返回android:minWidth这个属性的值,这个值可以为0
//如果View设置了背景,那么返回android:minWidth和背景最小宽度两者中的最大值。
protected int getSuggestedMinimumHeight() {
int suggestedMinHeight = mMinHeight;
if (mBGDrawable != null) {
final int bgMinHeight = mBGDrawable.getMinimumHeight();
if (suggestedMinHeight < bgMinHeight) {
suggestedMinHeight = bgMinHeight;
}
}
return suggestedMinHeight;
}
}
View的onMeasure()方法实现比较简单,它调用setMeasuredDimension()方法来设置View的测量大小,测量的大小通过getDefaultSize()方法来获取。
如果我们直接继承View来自定义View时,需要重写onMeasure()方法,并设置wrap_content时的大小。为什么呢?🤔
通过上面的描述我们知道,当LayoutParams为wrap_content时,SpecMode为AT_MOST,而在
关于getDefaultSize(int size, int measureSpec) 方法需要说明一下,通过上面的描述我们知道etDefaultSize()方法中AT_MOST与EXACTLY模式下,返回的
都是specSize,这个specSize是父View当前可以使用的大小,如果不处理,那wrap_content就相当于match_parent。
如何处理?🤔
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "widthMeasureSpec = " + widthMeasureSpec + " heightMeasureSpec = " + heightMeasureSpec);
//指定一组默认宽高,至于具体的值是多少,这就要看你希望在wrap_cotent模式下
//控件的大小应该设置多大了
int mWidth = 200;
int mHeight = 200;
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpec