攻略大全
1. 粘贴攻略
1.1 Android坐标体系
Android的坐标系定义为:
- 屏幕的左上角为坐标原点
- 向右为x轴增大方向
- 向下为y轴增大方向
1.2 View位置(坐标)描述
View的位置由4个顶点决定的(如下A、B、C、D)
4个顶点的位置描述分别由4个值决定:
(请记住:View的位置是相对于父控件而言的)
- Top:子View上边界到父view上边界的距离
- Left:子View左边界到父view左边界的距离
- Bottom:子View下边距到父View上边界的距离
- Right:子View右边界到父view左边界的距离
如下图:
1.3 View位置获取方式
- View的位置是通过view.getxxx()函数进行获取:
// 获取Top位置
public final int getTop() {
return mTop;
}
// 获取子View左上角距父View左侧的距离
getLeft();
// 获取子View右下角距父View顶部的距离
getBottom();
// 获取子View右下角距父View左侧的距离
getRight();
- 与MotionEvent中 get()和getRaw()的区别
// get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();
// getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();
具体如下图:
1.4 ViewGroup.LayoutParams
-
简介
布局参数类
- ViewGroup 的子类(RelativeLayout、LinearLayout)有其对应的 ViewGroup.LayoutParams 子类
- 如:RelativeLayout的 ViewGroup.LayoutParams子类 = RelativeLayoutParams
-
作用
指定视图View 的高度(height) 和 宽度(width)等布局参数。
-
具体使用
通过以下参数指定
参数 | 解释 |
---|---|
具体指 | dp/px |
fill_parent | 强制性使子视图的大小扩展至与父视图大小相等(不含 padding ) |
match_parent | 与fill_parent相同,用于Android 2.3 & 之后版本 |
wrap_content | 自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding ) |
1.5 MeasureSpec
1.5.1 简介
1.5.2 组成
测量规格(MeasureSpec) = 测量模式(mode) + 测量大小(size)
其中,测量模式(Mode)的类型有3种:UNSPECIFIED、EXACTLY 和 AT_MOST。具体如下:
1.5.3 具体使用
- MeasureSpec 是View类中的一个静态内部类
- MeasureSpec类 用1个变量封装了2个数据(size,mode):通过使用二进制,将测量模式(mode) & 测量大小(size)打包成一个int值来,并提供了打包 & 解包的方法
该措施的目的 = 减少对象内存分配
- 实际使用
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec = MeasureSpec.makeMeasureSpec(size, mode);
1.6 Window - DecorView - ViewRoot - Activity
1.6.1 Window
1.6.2 DecorView
- 定义:顶层View
即 Android 视图树的根节点;同时也是 FrameLayout 的子类
- 作用:显示 & 加载布局
View层的事件都先经过DecorView,再传递到View
-
特别说明
内含1个竖直方向的LinearLayout,分为2部分:上 = 标题栏(titlebar)、下 = 内容栏(content)
在Activity中通过 setContentView()所设置的布局文件其实是被加到内容栏之中的,成为其唯一子View = id为content的FrameLayout中.
// 在代码中可通过content得到对应加载的布局
// 1. 得到content
ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
// 2. 得到设置的View
ViewGroup rootView = (ViewGroup) content.getChildAt(0);
1.6.3 ViewRoot
-
定义
连接器,对应于ViewRootImpl类
-
作用
-
- 连接WindowManager 和 DecorView
- 完成View的三大流程: measure、layout、draw
-
特别注意
// 在主线程中,Activity对象被创建后:
// 1. 自动将DecorView添加到Window中 & 创建ViewRootImpll对象
root = new ViewRootImpl(view.getContent(),display);
// 3. 将ViewRootImpll对象与DecorView建立关联
root.setView(view,wparams,panelParentView)
1.6.4 Activity
1.6.5 总结
1.7 View的Measure过程
测量View的宽 / 高
- 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;
- 该情况下,measure过程后得到的宽 / 高可能不准确;
- 此处建议:在layout过程中onLayout()去获取最终的宽 / 高
1.8 View的Layout过程
计算视图(View)的位置
即计算View的四个顶点位置:Left、Top、Right 和 Bottom
1.9 View的Draw过程
绘制视图(View)
2. 造火箭攻略
2.1 MeasureSpec
子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:
即:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定。
2.2 getChildMeasureSpec()
-
注:区别于顶级View(即DecorView)的测量规格MeasureSpec计算逻辑:取决于 自身布局参数与窗口尺寸。
2.3 DecorView
2.4 View的Measure过程
实际作用的方法:getDefaultSize() = 计算View的宽/高值、setMeasuredDimension() = 存储测量后的View宽 / 高。
2.5 ViewGroup的Measure过程
-
原理
-
遍历 测量所有子View的尺寸
-
合并计算所有子View的尺寸,最终得到ViewGroup父视图的测量值
自上而下、一层层地传递下去,直到完成整个View树的measure()过程
-
- 流程
ViewGroup的measure过程如下:
2.6 View的Layout过程
2.7 ViewGroup的Layout过程
2.8 View的Draw过程
2.9 ViewGroup的Draw过程
3. 拧螺丝攻略
3.1 MeasureSpec的源码解析
public class MeasureSpec {
// 进位大小 = 2的30次方
// int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
private static final int MODE_SHIFT = 30;
// 运算遮罩:0x3为16进制,10进制为3,二进制为11
// 3向左进位30 = 11 00000000000(11后跟30个0)
// 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
// 通过高2位
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根据提供的size和mode得到一个详细的测量结果,即measureSpec
*/
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
// measureSpec = size + mode;此为二进制的加法 而不是十进制
// 设计目的:使用一个32位的二进制数,其中:32和31位代表测量模式(mode)、后30位代表测量大小(size)
// 例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100
}
/**
* getMode()方法
* 作用:通过measureSpec获得测量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:测量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
//原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
}
/**
* getSize方法
* 作用:通过measureSpec获得测量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
}
}
3.2 getChildMeasureSpec()的源码分析
/**
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
**/
public static int getChildMeasureSpec(int spec, int padding, int 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的大小
switch (specMode) {
// 当父view的模式为EXACITY时,父view强加给子view确切的值
//一般是父view设置为match_parent或者固定值的ViewGroup
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);
}
3.3 DecorView
3.3.1 DecorView的创建
/**
* 具体使用:Activity的setContentView()
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* 源码分析:Activity的setContentView()
*/
public void setContentView(int layoutResID) {
// getWindow() 作用:获得Activity 的成员变量mWindow ->>分析1
// Window类实例的setContentView() ->>分析2
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
/**
* 分析1:成员变量mWindow
*/
// 1. 创建一个Window对象(即 PhoneWindow实例)
// Window类 = 抽象类,其唯一实现类 = PhoneWindow
mWindow = new PhoneWindow(this, window);
// 2. 设置回调,向Activity分发点击或状态改变等事件
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
// 3. 为Window实例对象设置WindowManager对象
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
}
/**
* 分析2:Window类实例的setContentView()
*/
public void setContentView(int layoutResID) {
// 1. 若mContentParent为空,创建一个DecroView
// mContentParent即为内容栏(content)对应的DecorView = FrameLayout子类
if (mContentParent == null) {
installDecor(); // ->>分析3
} else {
// 若不为空,则删除其中的View
mContentParent.removeAllViews();
}
// 2. 为mContentParent添加子View
// 即Activity中设置的布局文件
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
//回调通知,内容改变
cb.onContentChanged();
}
}
/**
* 分析3:installDecor()
* 作用:创建一个DecroView
*/
private void installDecor() {
if (mDecor == null) {
// 1. 生成DecorView ->>分析4
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
// 2. 为DecorView设置布局格式 & 返回mContentParent ->>分析5
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
}
}
/**
* 分析4:generateDecor()
* 作用:生成DecorView
*/
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
/**
* 分析5:generateLayout(mDecor)
* 作用:为DecorView设置布局格式
*/
protected ViewGroup generateLayout(DecorView decor) {
// 1. 从主题文件中获取样式信息
TypedArray a = getWindowStyle();
// 2. 根据主题样式,加载窗口布局
int layoutResource;
int features = getLocalFeatures();
// 3. 加载layoutResource
View in = mLayoutInflater.inflate(layoutResource, null);
// 4. 往DecorView中添加子View
// 即文章开头介绍DecorView时提到的布局格式,那只是一个例子,根据主题样式不同,加载不同的布局。
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
// 5. 这里获取的是mContentParent = 即为内容栏(content)对应的DecorView = FrameLayout子类
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
return contentParent;
}
源码总结
- 创建Window抽象类的子类PhoneWindow类的实例对象;
- 为PhoneWindow类对象设置WindowManager对象;
- 为PhoneWindow类对象创建1个DecroView类对象(根据所选的主题样式增加);
- 为DecroView类对象中的content增加Activity中设置的布局文件。
此时,DecorView(即顶层View)已创建和添加Activity中设置的布局文件中,但目前仍未显示出来,即不可见。
3.3.2 DecorView的显示
/**
* 源码分析:主线程创建时,调用的handleResumeActivity()
*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* 源码分析:Activity的setContentView()
*/
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume) {
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
// 1. 获取Window实例中的Decor对象
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
// 2. DecorView对用户不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
// 3. DecorView被添加进WindowManager了
// 此时,还是不可见
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
// 4. 此处设置DecorView对用户可见
if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
// —>>分析1
}
}
}
/**
* 分析1:Activity.makeVisible()
*/
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
// 1. 将DecorView添加到WindowManager ->>分析2
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
// 2. DecorView可见
mDecor.setVisibility(View.VISIBLE);
}
/**
* 分析2:wm.addView
* 作用:WindowManager = 1个接口,由WindowManagerImpl类实现
*/
public final class WindowManagerImpl implements WindowManager {
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mDisplay, mParentWindow); ->>分析3
}
}
/**
* 分析3:WindowManagerGlobal 的addView()
*/
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
...
synchronized (mLock) {
// 1. 实例化一个ViewRootImpl对象
ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
// 2. WindowManager将DecorView实例对象交给ViewRootImpl 绘制View
root.setView(view, wparams, panelParentView);
// ->> 分析4
}
}
}
/**
* 分析4:ViewRootImpl.setView()
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
requestLayout(); // ->>分析5
}
/**
* 分析5:ViewRootImpl.requestLayout()
*/
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 1. 检查是否在主线程
checkThread();
mLayoutRequested = true;//mLayoutRequested 是否measure和layout布局。
// 2. ->>分析6
scheduleTraversals();
}
}
/**
* 分析6:ViewRootImpl.scheduleTraversals()
*/
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 通过mHandler.post()发送一个runnable,在run()方法中去处理绘制流程
// 与ActivityThread的Handler消息传递机制相似
// ->>分析7
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
/**
* 分析7:Runnable类的子类对象mTraversalRunnable
* 作用:在run()方法中去处理绘制流程
*/
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal(); // ->>分析8
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
/**
* 分析8:doTraversal()
*/
void doTraversal() {
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
performTraversals();
// 最终会调用performTraversals(),从而开始View绘制的3大流程:Measure、Layout、Draw
}
// 注:
// a. ViewRootImpl中W类是Binder的Native端,用于接收WmS处理操作
// b. 因W类的接收方法是在线程池中的,故可通过Handler将事件处理切换到主线程中
源码总结
- 将DecorView对象添加到WindowManager中;
- 创建ViewRootImpl对象;
- WindowManager将DecorView对象交给ViewRootImpl对象;
- ViewRootImpl对象通过Handler向主线程发送了一条触发遍历操作的消息:performTraversals();该方法用于执行View的绘制流程(measure、layout、draw)。
ViewRootImpl对象中接收的各种变化(如来自WMS的窗口属性变化、来自控件树的尺寸变化、重绘请求等都引发performTraversals()的调用及完成相关处理,并最终显示到可见的Activity中。整个流程如图下所示。
从上面的结论可以看出:
- 一次次performTraversals()的调用驱动着控件树有条不紊的工作;
- 一旦此方法无法正常执行,整个控件树都将处于僵死状态;
- 因此performTraversals()可以说是ViewRootImpl类对象的核心逻辑。而performTraversals()的后续逻辑,则是View绘制的三大流程:测量流程(measure)、布局流程(layout)、绘制流程(draw)。
3.4 View的Measure过程
/**
* 源码分析: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) {
// 计算视图大小 ->>分析1
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
// setMeasuredDimension() :获得View宽/高的测量值 ->>分析2
// 传入的参数通过getDefaultSize()获得 ->>分析3
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
* 分析2:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
*/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//参数说明:测量后子View的宽 / 高值
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
/**
* 分析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;
}
}
- 上面提到,当模式是UNSPECIFIED时,使用的是提供的默认大小(即第一个参数size);那么,提供的默认大小具体是多少呢?
- 答:在onMeasure()方法中,getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)中传入的默认大小是getSuggestedMinimumWidth()。
接下来我们继续看getSuggestedMinimumWidth()的源码分析
由于getSuggestedMinimumHeight()类似,所以此处仅分析getSuggestedMinimumWidth()
- 源码分析如下:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
从代码可以看出:
- 若 View 未设置背景,那么View的宽度 = mMinWidth
- mMinWidth = android:minWidth属性所指定的值;
- 若android:minWidth没指定,则默认为0。
- 若 View设置了背景,View的宽度为mMinWidth和mBackground.getMinimumWidth()中的最大值
那么,mBackground.getMinimumWidth()的大小具体指多少?继续看getMinimumWidth()的源码分析:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度
// 若无原始宽度,则为0
// 注:BitmapDrawable有原始宽度,而ShapeDrawable没有
总结:getDefaultSize()计算View的宽/高值的逻辑:
至此,单一View的宽/高值已经测量完成,即对于单一View的measure过程已经完成。
3.5 ViewGroup的的Measure过程
/**
* 源码分析:measure()
* 作用:基本测量逻辑的判断;调用onMeasure()
* 注:与单一View measure过程中讲的measure()一致
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 调用onMeasure()计算视图大小
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
...
}
}
/**
* 分析1:onMeasure()
* 作用:遍历子View & 测量
**/
为什么ViewGroup的measure过程不像单一View的measure过程那样对onMeasure()做统一的实现?(如下代码)
/**
* 分析:子View的onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
* 注:与单一View measure过程中讲的onMeasure()一致
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
// setMeasuredDimension() :获得View宽/高的测量值
// 传入的参数通过getDefaultSize()获得
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- 答:因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同
而onMeasure()的作用 = 测量View的宽/高值
因此,ViewGroup无法对onMeasure()作统一实现。这个也是单一View的measure过程与ViewGroup过程最大的不同。
- 即 单一View measure过程的onMeasure()具有统一实现,而ViewGroup则没有
- 注:其实,在单一View measure过程中,getDefaultSize()只是简单的测量了宽高值,在实际使用时有时需更精细的测量。所以有时候也需重写onMeasure()
在自定义ViewGroup中,关键在于:根据需求复写onMeasure()从而实现你的子View测量逻辑。复写onMeasure()的套路如下:
/**
* 根据自身的测量逻辑复写onMeasure(),分为3步
* 1. 遍历所有子View & 测量:measureChildren()
* 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值(自身实现)
* 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
**/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 定义存放测量后的View宽/高的变量
int widthMeasure ;
int heightMeasure ;
// 1. 遍历所有子View & 测量(measureChildren())
// ->> 分析1
measureChildren(widthMeasureSpec, heightMeasureSpec);
// 2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
void measureMerge{
... // 自身实现
}
// 3. 存储测量后View宽/高的值:调用setMeasuredDimension()
// 类似单一View的过程,此处不作过多描述
setMeasuredDimension(widthMeasure, heightMeasure);
}
// 从上可看出:
// 复写onMeasure()有三步,其中2步直接调用系统方法
// 需自身实现的功能实际仅为步骤2:合并所有子View的尺寸大小
/**
* 分析1:measureChildren()
* 作用:遍历子View & 调用measureChild()进行下一步测量
**/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:父视图的测量规格(MeasureSpec)
final int size = mChildrenCount;
final View[] children = mChildren;
// 遍历所有子view
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 调用measureChild()进行下一步的测量 ->>分析1
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* 分析2:measureChild()
* 作用:a. 计算单个子View的MeasureSpec
* b. 测量每个子View最后的宽 / 高:调用子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();
// 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
// 获取 ChildView 的 widthMeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
// 获取 ChildView 的 heightMeasureSpec
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
至此,ViewGroup的measure过程分析完毕。
3.6 View的Layout过程
/**
* 源码分析: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大小和位置是否发生了变化 & 返回
// ->>分析1、分析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过程
* a. 由于单一View是没有子View的,故onLayout()是一个空实现
* b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已完成了
**/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
}
3.7 ViewGroup的Layout过程
/**
* 源码分析: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大小和位置是否发生了变化 & 返回
// ->>分析1、分析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(),此处不作过多描述
}
}
3.8 View的Draw过程
/**
* 源码分析:draw()
* 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)。
* 注:
* a. 在调用该方法之前必须要完成 layout 过程
* b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
* c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
* d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
*/
public void draw(Canvas canvas) {
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
...// 仅贴出关键代码
int saveCount;
// 步骤1: 绘制本身View背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 若有必要,则保存图层(还有一个复原图层)
// 优化技巧:当不需绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
// 因此在绘制时,节省 layer 可以提高绘制效率
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 步骤3:绘制本身View内容
if (!dirtyOpaque)
onDraw(canvas);
// View 中:默认为空实现,需复写
// ViewGroup中:需复写
// 步骤4:绘制子View
// 由于单一View无子View,故View 中:默认为空实现
// ViewGroup中:系统已经复写好对其子视图进行绘制我们不需要复写
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
// 步骤6,绘制前景
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
// 步骤7,绘制默认焦点高亮
drawDefaultFocusHighlight(canvas);
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);
}
}
/**
* 步骤3:onDraw(canvas)
* 作用:绘制View本身的内容
* 注:
* a. 由于 View 的内容各不相同,所以该方法是一个空实现
* b. 在自定义绘制过程中,需由子类去实现复写该方法,从而绘制自身的内容
* c. 谨记:自定义View中 必须 且 只需复写onDraw()
*/
protected void onDraw(Canvas canvas) {
// 复写从而实现绘制逻辑
...
}
/**
* 步骤4: dispatchDraw(canvas)
* 作用:绘制子View
* 注:由于单一View中无子View,故为空实现
*/
protected void dispatchDraw(Canvas canvas) {
// 空实现
}
/**
* 步骤6: onDrawForeground(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);
}
}
至此,View的draw过程已分析完毕。
3.9 ViewGroup的Draw过程
/**
* 源码分析:draw()
* 与单一View的draw()流程类似
* 作用:根据给定的 Canvas 自动渲染 View(包括其所有子 View)
* 注:
* a. 在调用该方法之前必须要完成 layout 过程
* b. 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
* c. 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制
* d. 若自定义的视图确实要复写该方法,那么需先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制
*/
public void draw(Canvas canvas) {
// 与单一View的draw过程类似
}
由于drawBackground()、onDraw()、onDrawForeground()与单一View的draw过程类似,此处不作过多描述。
/**
* 源码分析: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);
至此,ViewGroup的draw过程已分析完毕。
3.10 View.setWillNotDraw()
/**
* 源码分析:setWillNotDraw()
* 定义:View 中的特殊方法
* 作用:设置 WILL_NOT_DRAW 标记位;
* 注:
* a. 该标记位的作用是:当一个View不需要绘制内容时,系统进行相应优化
* b. 默认情况下:View 不启用该标记位(设置为false);ViewGroup 默认启用(设置为true)
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
// 应用场景
// a. setWillNotDraw参数设置为true:当自定义View继承自 ViewGroup 且本身并不具备任何绘制时,设置为 true 后,系统会进行相应的优化。
// b. setWillNotDraw参数设置为false:当自定义View继承自 ViewGroup 且需要绘制内容时,那么设置为 false,来关闭 WILL_NOT_DRAW 这个标记位。