本文为过往笔记整理, 在此只作记录,不做严谨的技术分享。
UI绘制的起点
Activity启动
Activity加载流程
章节中,handleResumeActivity将DecorView添加到ViewRootImpl,并调用ViewRootImpl#requestLayout方法,并最终调用ViewRootImpl#performTraversals方法。在performTraversals最终调用View#measure/layout/draw三大流程。
//ActivityThread
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
//UI绘制在onResume之后
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
//...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l); // 调用addView方法
}
//...
}
}
}
//WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
mViews.add(view);
mRoots.add(root);
}
try {
root.setView(view, wparams, panelParentView); //调用ViewRootImpl的requestLayout
}
}
复制代码
//ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//TraversalRunnable
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
performTraversals();
}
}
private void performTraversals() {
...
if (!mStopped) {
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); // 1
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
}
if (!cancelDraw && !newSurface) {
if (!skipDraw || mReportNextDraw) {
performDraw();
}
}
}
复制代码
View.requestLayout、invalidate
深度分析requestLayout、invalidate与postInvalidate
requestLayout
//从源码注释可以看出,如果当前View在请求布局的时候,View树正在进行布局流程的话,
//该请求会延迟到布局流程完成后或者绘制流程完成且下一次布局发现的时候再执行。
public void requestLayout() {
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
//为当前view设置标记位 PFLAG_FORCE_LAYOUT
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//向父容器请求布局
mParent.requestLayout();
}
}
复制代码
//ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
复制代码
scheduleTraversals()最终会调用ViewRootImpl#performTraversals(),在这个方法内部分别调用View#measure/layout/draw三大流程。
首先是判断一下标记位,如果当前View的标记位为PFLAG_FORCE_LAYOUT,那么就会进行测量流程,调用onMeasure,对该View进行测量,接着最后为标记位设置为PFLAG_LAYOUT_REQUIRED,这个标记位的作用就是在View的layout流程中,如果当前View设置了该标记位,则会进行布局流程。
//View
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
...
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
}
public void layout(int l, int t, int r, int b) {
...
//判断标记位是否为PFLAG_LAYOUT_REQUIRED,如果有,则对该View进行布局
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//onLayout方法完成后,清除PFLAG_LAYOUT_REQUIRED标记位
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
//最后清除PFLAG_FORCE_LAYOUT标记位
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
复制代码
invalidate
该方法不进行测量、布局流程,会引起View树的重绘(只绘制需要重绘的视图),常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候调用该方法。
public void invalidate() {
invalidate(true);
}
void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
if (mGhostView != null) {
mGhostView.invalidate(true);
return;
}
//这里判断该子View是否可见或者是否处于动画中
if (skipInvalidate()) {
return;
}
//根据View的标记位来判断该子View是否需要重绘,假如View没有任何变化,那么就不需要重绘
if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
|| (invalidateCache &&
(mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
|| (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
|| (fullInvalidate && isOpaque() != mLastIsOpaque)) {
//设置PFLAG_DIRTY标记位
mPrivateFlags |= PFLAG_DIRTY;
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
//把需要重绘的区域传递给父容器
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
//调用父容器的方法,向上传递事件
p.invalidateChild(this, damage);
}
...
}
}
//求得父容器和子View需要重绘的区域的并集(dirty)
ViewGroup#invalidateChild -->
ViewGroup#invalidateChildInParent -->
ViewRootImpl#invalidateChildInParent -->
//触发View的工作流程,由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始, 并且只绘制需要绘制的视图
ViewRootImpl#scheduleTraversals
复制代码
postInvalidate
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
//View
public void postInvalidate() {
postInvalidateDelayed(0);
}
public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}
//ViewRootImpl
public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}
final ViewRootHandler mHandler = new ViewRootHandler();
final class ViewRootHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_INVALIDATE:
((View) msg.obj).invalidate();
break;
...
}
}
}
复制代码
测量过程
MeasureSpec
一种压缩数据类型,保存mode和size
View根据该mode、size,来确定自己的大小(有默认规则,但也可自定义规则来设置大小)
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
复制代码
specSize
View的大小
- 确切的数字
- LayoutParams.MATCH_PARENT(-1)
- LayoutParams.WRAP_CONTENT(-2)
specMode
父View给子View的大小限制“建议”
- UNSPECIFIED:父控件没有给子view任何限制,子View可以设置为任意大小
- EXACTLY:父控件已经确切的指定了子View的大小
- AT_MOST:子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小
合成
父View为子View设置Spec
假定父容器是300dp*300dp的尺寸,下面是子View的几种场景
<!--场景1-->
//希望子View的尺寸要是300dp*300dp。
android:layout_width="match_parent"
android:layout_height="match_parent"
<!--场景2-->
//希望子View的尺寸要是100dp*100dp。
android:layout_width="100dp"
android:layout_height="100dp"
<!--场景3-->
//希望子View的尺寸可以按照自己需求的尺寸来确定,但是最好不要超过300dp*300dp。
android:layout_width="wrap_content"
android:layout_height="wrap_content"
复制代码
那么在给子View设置Spec时,父容器怎么把这些要求告诉子View呢?
注意:View的android:layout_xxx属性(不仅限于宽高属性),是给父View用的
- 父View自己的Spec.mode + 剩余空间
- 子View的layoutParams对象中的宽高值
- 两者结合ViewGroup中默认的MeasureSpec创建规则,给子View合成Spec
<!--#ViewGroup-->
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//父容器(也就是本ViewGroup)的MeasureSpec和子view的params的宽高的类型去合成给子view的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
//合成新的MeasureSpec给子View的measure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
复制代码
创建MeasureSpec规则:
//参数:1:父容器的MeasureSpec。2.父容器剩余的空间。3.子view的宽高类型。
//这三者结合创建新的MeasureSpec给子View,这是在父容器中完成的。
<!--#ViewGroup-->
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) {
//父容器确定自己的大小
case MeasureSpec.EXACTLY:
//子view的宽高设置的是精确值,如200dp
if (childDimension >= 0) {
//明确知道子View的大小,size是子View中设置的大小。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子View希望自己足够大,size是父容器剩余的大小
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子View不确定自己的大小,size是父容器大小。
//注意:AT_MOST表明,你最大不要超过resultSize,并不是说子View的大小就是resultSize。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父容器也不知道自己的大小
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//只要子View是精确值,父是任何Mode,子都能确定自己大小。size是自己设置的值。
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 父不确定,子填充它,虽然给子resultSize为父剩余大小。
// AT_MOST表明是最大不能超过,而不是给子View大小设置为size。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//父子都不确定,最大不能超过size。
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局对子View没有做任何限制
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view有自己的尺寸,则使用自己的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//合成新的Spec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
使用
子View中接收Spec,设置自己大小
- 接收的Spec完全是View自己的信息,不包括父View的信息
- View如果设置为wrap_width/match_width时,都是充满父控件
//父容器中传递过来的Spec信息
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//View的onMeasure中并没有处理“不能超过size”的这种建议,而是把大小设置为最大值。
//(因为AT_MOST时子View没有可用size值)
// 所以,当自定义View设置为match/wrap时,显示的都是最大宽高
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
复制代码
总结MeasureSpec的作用:
是父控件提供给子View的一个参数,作为设定自身大小参考。
但是只是个参考,要多大,还是View自己说了算。
- MeasureSpec是父容器调用子View的measure方法时,传递给子View的一个信息,告诉它应该怎么设置自己的大小
- 子View在自己的measure中接收到的这个MeasureSpec是自己的信息,此时已经和父View无关了
- 子View根据这个MeasureSpec得到自己的信息,决定如何设置自己的大小
UNSPECIFIED
The parent has not imposed any constraint on the child. It can be whatever size it wants.
父类不对子类施加任何约束,它可以是任意想要的大小(不同的ViewGroup或View有不同的处理规则)
父View何时设置:
//RecyclerView
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
int childDimension, boolean canScroll) {
if (canScroll) {
if (childDimension >= 0) {
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//可滑动、子View为wrap时:无论父View的mode是什么,强制设置为UNSPECIFIED
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
} else {
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
- RecyclerView可滑动、子View为wrap时:无论父View的mode是什么,强制设置为UNSPECIFIED
- 正常情况下:子View为wrap则mode应为AT_MOST(不能超过父View尺寸嘛)
- 设置为UNSPECIFIED是因为父View可滑动,无需在意子View尺寸,想多大就多大
场景:子View可在父View中滑动(ScrollView、LinearLayout、RecyclerView…)
子View接收如何处理:
既然不受限制,那就看View想要的默认行为了。
可以给size设置一个默认值,也可以设置为0,也可以有多大就展示多大
ViewRootImpl#performMeasure
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
复制代码
ViewGroup
ViewGroup的测量和View测量不同,View只考虑自己的尺寸计算,只需要测量自己就可以了。而ViewGroup测量自己没有固定规则,完全按该容器的功能来定义如何给自己和子View设置大小。
所以,ViewGroup中没有复写onMeasure方法,需要我们在自定义ViewGroup时主动复写该方法进行测量。
ViewGroup不同的测量策略:
- 宽高不固定:先测量子View,再决定父View大小
比如LinearLayout,自己的高度等于子View的高度之和
- 宽高固定:先计算父View,再决定子View大小
比如标签类容器,宽高固定,子View的大小由子View数量决定
计算自己尺寸
ViewGroup虽然没有提供测量自己的方法,但是系统提供了封装好的测量子View的方法
<!--测量子View-->
//遍历子View进行测量(上面不同的测量策略中,如 1 时可直接调用)
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//单独测量子View(上面不同的测量策略,如 2 时可直接调用,每次传入ViewGroup中剩余的尺寸信息Spec)
protected void measureChild(View child,
int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight,
lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//单独测量子View(上面不同的测量策略,如 2 时可直接调用,每次传入ViewGroup中剩余可用大小)
//计算的时候算上了子View的margin信息
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed,
lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed,
lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//创建MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//为子View合成MeasureSpec的规则,上面分享MeasureSpec时提到
}
<!--测量自己-->
/**
* 1. 宽高不固定:测量子View,遍历子View宽高之和,确定ViewGroup尺寸
* 2. 宽高固定:直接使用onMeasure中传入的信息
*/
复制代码
保存自己尺寸
- 修正尺寸:resolveSize、resolveSizeAndState
- 保存尺寸: setMeasuredDimension、setMeasuredDimensionRaw
计算出自己的尺寸后,我们得到的是一个精确的值。保存的时候按说是可以直接setMeasuredDimension保存的。
但是这时也要考虑到ViewGroup的父view对自己所做的限制,有可能它给的最大尺寸不满足我们计算得出的尺寸呢?这时候就需要resolveSize来修正一下我们得出的结果了。
<!--修正尺寸-->
public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
<!--保存自己的尺寸,同View-->
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//使用视觉边界布局
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
...
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
}
复制代码
View
计算自己尺寸
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
}
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
//计算尺寸
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
复制代码
保存自己尺寸
//保存尺寸
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//使用视觉边界布局
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
...
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
复制代码
布局过程
HenCoder UI 2-3 定制 Layout 的内部布局
ViewRootImpl#performLayout
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
int numViewsRequestingLayout = mLayoutRequesters.size();
if (numViewsRequestingLayout > 0) {
if (validLayoutRequesters != null) {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
}
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
mInLayout = false;
复制代码
ViewGroup
父View给自己的坐标,已经在自己的layout中保存了。
但是和View不同的是,作为ViewGroup需要给自己的子View.layout中设置参数。
所以,ViewGroup中复写onLayout方法,给子View设置坐标。
ViewGroup中onLayout默认abstract方法,复写onLayout,此时已经知道了自己和子View的大小;
由ViewGroup功能特性,来决定子View的摆放
<!--例如这种实现,下面自定义View中有详解-->
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
//在onMeasure计算ViewGroup尺寸时,已经顺便记录了子View坐标信息,在onLayout中直接使用
Rect rect = childRects.get(i);
getChildAt(i).layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
复制代码
View
父View给自己的坐标,已经在layout中保存了。onLayout可以什么都不做。
绘制过程
ViewRootImpl#performDraw
private void performDraw() {
//...
final boolean fullRedrawNeeded = mFullRedrawNeeded;
try {
draw(fullRedrawNeeded);
} finally {
mIsDrawing = false;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
//...
}
private boolean draw(boolean fullRedrawNeeded) {
if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
//开启了硬件加速,则执行该方法,内部最终还是会执行到view 的 draw 方法
if (mAttachInfo.mThreadedRenderer != null &&
mAttachInfo.mThreadedRenderer.isEnabled()) {
// Draw with hardware renderer.
mIsAnimating = false;
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
} else {
//未开启硬件加速,则执行该方法,直接调用 view 的 draw 方法
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
}
if (animating) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return useAsyncReport;
}
复制代码
View#draw
由于ViewGroup没有重写draw方法,因此所有的View都是调用View#draw方法
//View.java
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* 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 lay
* 6. Draw decorations
*
* 1. 画背景 [drawBackgound(canvas),不可复写]
* 2. 保存画布 [如果需要,保存画布层(Canvas.saveLayer)为淡入或淡出做准备]
* 3. 画主体 [onDraw(canvas)]
* 4. 画子View [dispatchDraw]
* 5. 恢复画布 [如果需要,绘制淡入淡出的相关内容并恢复之前保存的画布层(layer)]
* 6. 画前景 [装饰、滚动条, onDrawForeground]
*/
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//将默认的焦点高亮显示到画布上
drawDefaultFocusHighlight(canvas);
return;
}
// Step 2, save the canvas' layers
saveCount = canvas.getSaveCount();
canvas.save...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
canvas.draw...
canvas.restoreToCount(saveCount);
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
复制代码
一个 View(ViewGroup)的绘制不会这几项全都包含,但必然逃不出这几项,并且一定会严格遵守这个顺序:
绘制内容写在不同的方法中或者super.xxx前后,都会有不同的效果,如下表(可对比上图流程):
ViewGroup#dispatchDraw
调用关系
- View.draw(1) –> View.onDraw(x): 只有draw(1)被调用,onDraw才有可能被调用
- ViewGroup.dispatchDraw –> View.draw(x,x,x) –> View.draw(x)
- 只有在ViewRootImpl中是直接调用的View.draw(x)
- 后面遍历的子View中,都是通过父View的dispatchDraw调用的draw(x,x,x),然后draw(x)
//ViewGroup.java
protected void dispatchDraw(Canvas canvas) {
...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
}
...
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
...
}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
复制代码
//View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
...
if (drawingWithRenderNode) {
//开启硬件加速
renderNode = updateDisplayListIfDirty();
if (!renderNode.hasDisplayList()) {
renderNode = null;
drawingWithRenderNode = false;
}
}
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {//硬件加速
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((RecordingCanvas) canvas).drawRenderNode(renderNode);
} else {
// 跳过自身绘制
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
}
...
return more;
}
//硬件加速
public RenderNode updateDisplayListIfDirty() {
try {
if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
canvas.translate(-mScrollX, -mScrollY);
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
// 跳过自身绘制
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
}
} else {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
}
return renderNode;
}
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
// Step 1, draw the background, if needed
// Step 2, save the canvas' layers
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
// Step 6, draw decorations (foreground, scrollbars)
}
复制代码
自定义ViewGroup.onDraw()不被调用?
onDraw没被调用,也就是draw(x)没被调用。从上图和“调用时机”逻辑中可以看到,无论是否开启硬件加速,满足是否绘制自身的条件,都是相同的:PFLAG_SKIP_DRAW(如果有该标记,则跳过自身绘制)
一共有两个地方会影响到 PFLAG_SKIP_DRAW是否标记:
WILL_NOT_DRAW
- ViewGroup在初始化时默认设置WILL_NOT_DRAW,进而通过setFlags设置PFLAG_SKIP_DRAW标记
- 可以调用setWillNotDraw(false)清除WILL_NOT_DRAW标记
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
}
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
复制代码
PFLAG_SKIP_DRAW
- 如果设置了WILL_NOT_DRAW,继续检查mBackgroud、mDefaultFocusHighlight、mForegroundInfo,如果三者任一有值,则清除PFLAG_SKIP_DRAW标记,否则添加
- 如果没有设置,则直接清除PFLAG_SKIP_DRAW标记
vew.java setFlags方法
if ((mViewFlags & WILL_NOT_DRAW) != 0) {
if (mBackground != null
|| mDefaultFocusHighlight != null
|| (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;//清除标记
} else {
mPrivateFlags |= PFLAG_SKIP_DRAW;//添加标记
}
} else {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;//清除标记
}
requestLayout();
invalidate(true);
复制代码
设置背景、前景、获取焦点时高亮这三种Drawable会直接清除PFLAG_SKIP_DRAW标记:
view.java
public void setBackgroundDrawable(Drawable background) {
if (background != null) {
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
requestLayout = true;
}
}
}
public void setForeground(Drawable foreground) {
if (foreground != null) {
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
}
}
private void setDefaultFocusHighlight(Drawable highlight) {
mDefaultFocusHighlight = highlight;
mDefaultFocusHighlightSizeChanged = true;
if (highlight != null) {
if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) {
mPrivateFlags &= ~PFLAG_SKIP_DRAW;
}
}
}
复制代码
原因
至此,通过上面的分析知道,ViewGroup.onDraw没有执行是因为:
- ViewGroup初始化默认设置WILL_NOT_DRAW,进而设置了PFLAG_SKIP_DRAW标记
- 没有设置背景、前景、获取焦点高亮,这三种背景中的任一个
自定义View流程
常见步骤(ViewGroup)
自定义View没有模板步骤可言,但大概都是这个思路
测量 –> 布局 –> 绘制 + 动画 + 事件处理
- onMeasure
- 测量子View,计算自己大小;或根据自己大小测量子View
- 保存子View的位置信息
需要靠子View来确定容器本身的大小,在计算的过程中方便保存子View位置信息。当然也可以在其它步骤计算位置信息。
- 修正、保存自己尺寸
- onLayout
- 遍历子View,layout传入各自位置信息
注意:这里是layout而不是子View的onLayout
- 遍历子View,layout传入各自位置信息
- onDraw、动画、触摸事件等,视情况而定
以自定义一个布局容器为例:
- 容器根据各个子View的大小,计算出自己的大小
- 修正并保存自己大小
- 布局子View
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量子View
measureChildren(widthMeasureSpec, heightMeasureSpec);
//自己宽度
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int linesCount = 0;
int useWidth = getPaddingLeft();
int useHeight = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//获去已测量完毕的子View信息
int cw = child.getMeasuredWidth();
int ch = child.getMeasuredHeight();
//计算自己高度
if((useWidth + cw + tableSpacingHorizontal) > wSize){
linesCount ++;
useWidth = getPaddingLeft();
useHeight += (ch + tableSpacingVertical);
}
useWidth += (cw + tableSpacingHorizontal);
//保存child位置信息
Rect rect = new Rect();
childRects.add(rect);
rect.left = useWidth;
rect.top = useHeight;
rect.right = useWidth;
rect.bottom = useHeight + ch;
}
//setMeasuredDimension(wSize, useHeight);
//修正、保存尺寸
setMeasuredDimension(resolveSize(wSize, widthMeasureSpec),
resolveSize(useHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
//onMeasure中记录的子View位置信息
Rect rect = childRects.get(i);
getChildAt(i).layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
复制代码
自定义属性
在Android中,命名空间可分为3种:
1.xmlns:android=”schemas.android.com/apk/res/and…
2.xmlns:tools=”schemas.android.com/tools”
3.xmlns:app=”schemas.android.com/apk/res-aut…
(红框1是空间名,可是任意字符串
;红框2是URI定位资源的)
命名空间里存放的是特定属性的集合,当我们自定义属性时,可以直接在xml布局根节点写上命名空间:
**xmlns:app=”schemas.android.com/apk/res-aut…
但是,当命名空间有冲突时:
- 重命名
- 使用**xmlns:app=”schemas.android.com/apk/res/包名”…
其它回调方法
View 的关键生命周期为 : 构造View –> onFinishInflate –> onAttachedToWindow –> onMeasure –> onSizeChanged –> onLayout –> onDraw –> onDetackedFromWindow
onFinishInflate
当xml中的View标签被映射成View对象时,会调用该View的onFinishInflate方法
- 该方法只有在布局文件中加载View时会回调,如果直接new View实例则不会被回调
- 在Activity初始化过程中:加载xml是在setContent中被调用的,也就是由ActivityThread.performLaunchActivity触发的
- 所以onFinishInflate方法的触发时机在onCreate –> onFinishInflate –> onResume中
- 此时还没有进行View的测量、布局、绘制,所以无法得到宽高、坐标等信息
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate){
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
} else if (TAG_TAG.equals(name)) {
} else if (TAG_INCLUDE.equals(name)) {
} else if (TAG_MERGE.equals(name)) {
} else {
//生成子View实例对象。
final View view = createViewFromTag(parent, name, context, attrs);
//子View宽高LayoutParams
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
//1: 递归解析子View下层的View,注意第四个参数是true,也就是下文的finishInflate=true
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
//2: 通知View加载完成
parent.onFinishInflate();
}
}
复制代码
- 注释1:递归+while循环中,不断遍历子View直到最深的View
- 注释2:递归中,无论有没有子View都会被调用;从里往外层触发onFinishInflate
onSizeChanged
初始化或尺寸有变化时触发
- setFrame
- setLeft/top/right/bottom
onFocusChanged
当View获取或失去焦点时触发
attached/detached Window
onAttachedToWindow
当view被附着到一个窗口时触发
onDetachedFromWindow
当view离开附着的窗口时触发
状态保存与恢复
View 之 onSaveInstanceState 深入理解
当Activity调用了onSave/InstoreInstanceState/方法后,便会对它的View Tree进行保存,而进一步对每一个子View调用其onSave/InstoreInstanceState()方法来保存状态。
onSaveInstanceState
Activity的save与View的关系:
//Activity
protected void onSaveInstanceState(@NonNull Bundle outState) {
//mWindow保存View Hierarchy(View层级)
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
//...
dispatchActivitySaveInstanceState(outState);
}
//PhoneWindow
public Bundle saveHierarchyState() {
Bundle outState = new Bundle();
SparseArray<Parcelable> states = new SparseArray<Parcelable>();
//触发有id的子View:保存自己的状态
mContentParent.saveHierarchyState(states);
//states在上句代码中保存了有id的子View
outState.putSparseParcelableArray(VIEWS_TAG, states);
//...
return outState;
}
复制代码
mContentParent.saveHierarchyState(states)有id的子View和自己:保存状态、记录id(恢复状态时使用)
//ViewGroup
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
//保存自己的状态
super.dispatchSaveInstanceState(container);
for (int i = 0; i < count; i++) {
View c = children[i];
if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
//递归循环,保存子View状态
c.dispatchSaveInstanceState(container);
}
}
}
//View
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
//2
Parcelable state = onSaveInstanceState();
if (state != null) {
//View有id才会保存
container.put(mID, state);
}
}
}
复制代码
注释2:View的onSaveInstanceState被调用
自定义View中处理onSaveInstanceState:
//View
@Override
protected Parcelable onSaveInstanceState() {
//获取默认保存的一些参数
Parcelable parcelable = super.onSaveInstanceState();
SavedState ss = new SavedState(parcelable);
//把当前的位置保存进SavedState
ss.currentPosition = mCurrentPosition;
return ss;
}
//继承View内部类BaseSavedState
static class SavedState extends BaseSavedState{
//当前的ViewPager的位置
int currentPosition;
public SavedState(Parcel source) {
super(source);
currentPosition = source.readInt();
}
public SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(currentPosition);
}
public static final Parcelable.Creator<SavedState> CREATOR = new
Parcelable.Creator<SavedState>(){
@Override
public SavedState createFromParcel(Parcel source) {
return new SavedState(source);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
复制代码
onRestoreInstanceState
**Activity的restore与View的关系: **
//Activity
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//mWindow恢复View Hierarchy
mWindow.restoreHierarchyState(windowState);
}
}
}
//PhoneWindow
public void restoreHierarchyState(Bundle savedInstanceState) {
SparseArray<Parcelable> savedStates =
savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
if (savedStates != null) {
mContentParent.restoreHierarchyState(savedStates);
}
// restore the focused view
int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID);
if (focusedViewId != View.NO_ID) {
View needsFocus = mContentParent.findViewById(focusedViewId);
if (needsFocus != null) {
needsFocus.requestFocus();
}
}
//...
}
//View
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) {
//根据id恢复状态
Parcelable state = container.get(mID);
if (state != null) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
onRestoreInstanceState(state);
}
}
}
复制代码
**自定义View中处理onRestoreInstanceState: **
@Override
protected void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
//调用别的方法,把保存的数据重新赋值给当前的自定义View
mViewPager.setCurrentItem(ss.currentPosition);
}
复制代码
辅助类
Paint
Canvas
save
save()方法会保存当前Canvas的状态到一个私有栈中,调用了restore()方法,会将canvas恢复至save之前的状态。save()方法是可以多次调用的,每调用一次,栈的深度就会+1,可以通过getSaveCount()方法获取当前栈的深度。restore()和restoreToCount()都可以返回到save()之前的Canvas的状态,差异性也很明显,restore()只能一次弹出一个save()方法的状态栈,但是restoreToCount()可以一次性弹出saveCount个栈,当然了saveCount也是有限制的,saveCount不能<1,其次不能大于已保存的栈的深度。
一般save和restore方法成对出现,当然了也可以不成对出现,但是必须是save的操作比restore的次数多,如果restore调用次数比save多,会引发IllegalStateException
public MyCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.RED);
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);
rect = new Rect(10, 10, 400, 400);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw: " + canvas.getSaveCount());// 1
canvas.drawRect(rect, paint);
int count = canvas.save();
Log.d(TAG, "onDraw count: " + count);// 1
canvas.translate(100,100);
paint.setColor(Color.BLUE);
canvas.drawRect(rect, paint);
Log.d(TAG, "onDraw: " + canvas.getSaveCount());// 2
canvas.restore();
canvas.translate(150,150);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);
Log.d(TAG, "onDraw: " + canvas.getSaveCount());// 1
}
复制代码
如下是使用save、restore方法和不使用的区别:
saveLayer
Canvas在一般的情况下可以看作是一张画布,所有的绘图操作都发生在这张画布上。但是如果需要实现一些相对复杂的绘图操作,比如多层动画,地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)。Canvas提供了图层(Layer)支持,缺省情况可以看作是只有一个图层Layer。如果需要按层次来绘图,Android的Canvas可以使用saveLayer、restore 来创建一些中间层,对于这些Layer是按照“栈结构“来管理的:
saveLayer()类似save()方法,但是它会生成一个离屏缓冲(offscreen bitmap)
,之后的所有操作都是在这个新的offscreen bitmap上。savelayer()这是一个非常耗费性能
的方法,会导致绘制相同的内容渲染时耗费两倍多的资源。
public MyCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
paint = new Paint();
paint.setAntiAlias(true);
paint.setColor(Color.RED);
rect = new RectF(0, 0, 400, 400);
bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.image);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(TAG, "onDraw: " + canvas.getSaveCount());// 1
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
saveCount = canvas.saveLayer(rect, paint);
} else {
saveCount = canvas.saveLayer(rect, paint, Canvas.ALL_SAVE_FLAG);
}
Log.d(TAG, "onDraw: " + canvas.getSaveCount() + " " + saveCount);
canvas.drawColor(Color.RED);
canvas.drawBitmap(bm, 200, 200, paint);
canvas.restore();
canvas.drawBitmap(bm, 400, 400, paint);
Log.d(TAG, "onDraw: " + canvas.getSaveCount());// 1
}
复制代码
如下是使用saveLayer方法和不使用的区别:
saveLayer与save的不同点
- saveLayer()是生成一个独立的图层,而save()只是保存了一下画布的状态,这里说的画布的状态类似一个还原点。
- saveLayer()由于会生成一个新的图层,所以更加耗费内存,需要慎用。
- saveLayer()可以保存特定的区域。
- 在使用混合模式setXfermode时会产生不同的影响。
Path
Matrix
Tips
自定义View的默认宽高
自定义View时,如果未复写onMeasure方法,无论设置其宽度为 MATCH_PARENT 还是 WRAP_CONTENT,显示的大小都为填充父控件
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
//...
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
/...
}
==>
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/**
如果不是非常确定下面的处理包含了所有情况,尽量带上super。
什么叫处理了所有情况?就是没有在下面流程if/else中包含的时候,也会有调用setMeasuredDimension的地方,否则会报错。
下面就处理了所有情况,即无论什么条件,setMeasuredDimension都会被调用,并且有默认或正确的值传入,所以去掉super也可以。
*/
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//默认值不要比给的建议值大
int defaultWidth = mWidth >= widthSpecSize ? widthSpecSize : mWidth;
int defaultHeight = mHeight >= heightSpecSize ? heightSpecSize : mHeight;
int shouldWidth = widthSpecSize;
int shouldHeight = heightSpecSize;
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
shouldWidth = defaultWidth;
shouldHeight = defaultHeight;
}else if(widthSpecMode == MeasureSpec.AT_MOST){
shouldWidth = defaultWidth;
shouldHeight = heightSpecSize;
}else if(heightSpecMode == MeasureSpec.AT_MOST){
shouldWidth = widthSpecSize;
shouldHeight = defaultHeight;
}
//即使不在if/else里也会有默认值(此处未修正结果,非必须)
setMeasuredDimension(shouldWidth, shouldHeight);
}
复制代码
获取宽高信息/时机
getMeasureWidth和getWidth
-
getMeasureWidth 和 getWidth在大多数情况下都是相等的
-
除非ViewGroup在布局子View时,由于其它功能限制,没有完全按测量到的宽高计算View.layout相关坐标。以至于设置到layout中的(right – left)或 (bottom – top) 和测量的宽高不一致
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getWidth() {
return mRight - mLeft;
}
/**
* layout中调用setFrame
* 而传入layout中的坐标参数,一般情况下都是由测量宽高等信息得出的
* 类似于:left - right == mMeasuredWidth
*/
protected boolean setFrame(int left, int top, int right, int bottom) {
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
}
复制代码
何时能获取到宽高
-
View内部:onMeasure测量以后
-
View外部:
-
view.post(): 可在任何时机调用 (例如:逻辑需要代码必须写在onCreate中时)
为什么View.post方法可以用来获取视图尺寸 -
onWindowFocusChanged:当前Activity获取/失去焦点时触发
-
ViewTreeObserver监听
-
View.post获取原理
- mAttachInfo != null : 已经完成绘制,直接获取宽高
- mAttachInfo == null: 放入队列,等待时机被执行action中的逻辑
//View
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
//直接执行
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
//1 放入队列
getRunQueue().post(action);
return true;
}
//ViewRootImpl
private void performTraversals() {
final View host = mView;
if (mFirst) {
//给View中的mAttachInfo赋值
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
//2 执行队列中的任务
getRunQueue().executeActions(attachInfo.mHandler);
...
performMeasure();
...
performLayout();
...
performDraw();
}
//HandlerActionQueue
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
}
}
复制代码
但是有一点疑问:注释1放入队列后,在注释2处直接遍历执行了队列中的action,但是此时 performMeasure还没执行啊?
注意:
- executeActions中的actions是放入到了
mHandler
中执行 - 触发performTraversals的mTraversalRunnable这个Ruunable也是放到了
mHandler
中执行 - 所以,
当执行getRunQueue().executeActions时,是身处在mTraversalRunnable这个执行消息之中的
- 所以,一定会是performTraversals执行完之后(测量、布局、绘制),才会有机会取出后面的消息执行
- 此时,执行到executeActions中的消息时,一定是已经测量完成了
- 所以View.post可以在任何时间点获取宽高(只不过不是马上有结果)
//ViewRootImpl
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
//Choreographer
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
复制代码
requestLayout、invalidate
Android View 深度分析requestLayout、invalidate与postInvalidate
requestLayout和invalidate区别
requestLayout
触发 测量、布局、绘制操作
- 递归调用父窗口的requestLayout,直到ViewRootImpl ,然后触发peformTraversals,
- 会导致onMeasure和onLayout被调用
- 不一定会触发OnDraw
- 触发onDraw可能是因为在在layout过程中发现l,t,r,b和以前不一样,那就会触发一次invalidate,也可能是因为别的原因导致mDirty非空(比如在跑动画)
invalidate
触发绘制View树,不会触发测量、布局
postInvalidate
在非UI线程调用
动画
- 属性变化 –> 调用invalidate()/requestLayout
- 在onDraw(多数情况下)中动态处理
- 开启硬件加速或离屏缓冲,提高性能
View中做初始化操作
- 构找函数
- onAttachedToWindow:初始化
- onDetachedFromWindow:回收资源
View绘制在onResume之后
//ActivityThread
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
//调用onResume
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
//...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l); //触发requestLayout-> performTraversals(绘制View)
}
//...
}
}
}
复制代码
子线程更新UI
子线程可以更新UI,甚至有些UI主线程却不能更新。能否更新UI完全取决于:创建View的线程和当前操作线程是否是同一个,跟是否为主线程无关
为什么不能?
requestLayout、invalidate、postInvalidate最终都会调用checkThread来检查当前线程是否和创建ViewRootImpl的操作线程是否是同一个,我们通常所说的“子线程不能更新UI”,是因为大多数情况下window创建添加ViewRootImpl是在主线程进行的。
//ViewRootImpl
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
...
invalidateRectOnScreen(dirty);
return null;
}
final Thread mThread = Thread.currentThread();
void checkThread() {
//检查的并不是“是否是主线程”,对比的是生成ViewRootImpl时的操作线程
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views."
);
}
}
复制代码
何时可以?
onResume之前
此时还没创建ViewRootImpl;requestLayout/invalidate时mParent为null,根本就不会走到checkThread
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) {
//...
ActivityClientRecord r = performResumeActivity(token, clearHide);
if (r != null) {
final Activity a = r.activity;
//...
if (r.window == null && !a.mFinished && willBeVisible) {
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l); // 创建ViewRootImpl
}
//...
}
}
}
public void requestLayout() {
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
}
复制代码
子线程内自己玩
checkThread检查的是“当前线程和操作线程是否相同”
平时我们都在UI线程中创建Window/View/ViewRootView,所以只能在主线程中更新UI,但是如果我们从头到尾都是在子线程中操作(在子线程创建ViewRootImpl添加到window),那就是没问题的。
下面这段代码,就可以完美运行:
new Thread(new Runnable() {
@Override
public void run() {
//ViewRootImpl创建时有Handler实例生成,需要prepare
Looper.prepare();
MyDialog(MainActivity.this).show();
// or Toast.show()
Looper.loop();
}
复制代码