前言
系列文章:
Android Activity創(chuàng)建到View的顯示過程
Android Activity 與View 的互動思考
Android invalidate/postInvalidate/requestLayout-徹底厘清
Android 容易遺漏的刷新小細節(jié)
之前的文章斷斷續(xù)續(xù)有分析過刷新(requestLayout/invalidate)相關的知識,只是那會兒側重點不同锨推,主要是著眼于整體流程甘畅。本篇將著重分析刷新關聯的一些小細節(jié)亲怠。
通過本篇文章脑融,你將了解到:
1宫蛆、Measure/Layout/Draw 三者關聯茸俭。
2全谤、requestLayout/Invalidate 作用肤晓。
3、Measure/Layout/Draw 階段執(zhí)行requestLayout 會發(fā)生什么认然?
4补憾、Measure/Layout/Draw 階段執(zhí)行invalidate 會發(fā)生什么?
5卷员、如何監(jiān)聽 Measure/Layout/Draw 各個流程盈匾?
6、總結
1毕骡、Measure/Layout/Draw 三者關聯
Android 展示頁面簡略過程
以Activity 為例:
1削饵、創(chuàng)建Activity并關聯Window。
2未巫、構建ViewTree(View 布局形成的樹形結構)并關聯Window窿撬。
3、注冊監(jiān)聽屏幕刷新信號叙凡。
4劈伴、當屏幕刷新信號到來時執(zhí)行Measure/Layout/Draw 過程。
顯而易見握爷,我們應該從第4步著手跛璧。
Measure/Layout/Draw 內在聯系
假設頁面布局結構如下:
當屏幕刷新信號到來時會執(zhí)行ViewRootImpl.doTraversal()方法严里,于是先對整個ViewTree 執(zhí)行Measure過程,也即是:
上面的流程圖表示調用的時間順序赡模,并不代表有直接調用的關系田炭。
當ViewTree 完成Measure 過程,說明ViewTree里的每個View(ViewGroup) 的寬漓柑、高被確定了教硫。
此時再執(zhí)行Layout 過程,因為寬辆布、高已知瞬矩,因此只需要知道擺放的起點,那么終點也將確定锋玲。
當ViewTree 完成Layout 過程景用,說明ViewTree里的每個View(ViewGroup)的四個頂點值確定了(left、top惭蹂、right伞插、bottom)。
既然位置都確定了盾碗,下面的就交個Draw過程媚污,在確定的位置繪制內容即為Draw的主要工作。
三者關系:
1廷雅、Measure 為了確認布局的寬耗美、高。
2航缀、Layout 在Measure的基礎上確定了布局的起始點商架、終點。
3芥玉、Draw 在Layout 確定的布局邊界里繪制指定的內容蛇摸。
2、requestLayout/invalidate 作用灿巧。
requestLayout 作用
上面探討的是Activity 初次進入時頁面的顯示過程皇型,可以看出必然要經過Measure/Layout/Draw 過程,此時想要更新頁面里的某個元素砸烦,那么該如何操作呢弃鸦?
答案是:requestLayout。
還是以上面的圖為例幢痘,假若已經更改了View3的尺寸唬格,想要其生效只需要調用View3.requestLayout(),而后將會觸發(fā)View3.measure(xx),最后觸發(fā)View3.layout(xx)购岗。
此過程中汰聋,View3.onMeasure(xx)/View3.onLayout(xx) 將會被執(zhí)行。
此時重走了Measure/Layout 過程喊积,因為尺寸發(fā)生了改變烹困,因此將會走Draw過程,最終改變尺寸的View3 將會展示在屏幕上乾吻。
invalidate 作用
requestLayout 僅僅只是針對布局的寬髓梅、高改變、頂點位置發(fā)生變化后的刷新绎签,若是想要內容也要刷新枯饿,則需要借助invalidate。
當調用View3.invalidate()后诡必,最終將會執(zhí)行Draw 過程奢方,重新繪制內容。
此過程中爸舒,View3.onDraw(xx) 將會被執(zhí)行蟋字。
兩者區(qū)別:
1、想要重新測量扭勉、確定View 寬愉老、高,可以使用requestLayout剖效。
2、想要頁面內容重新刷新焰盗,可以使用invalidate璧尸。
3、調用 requestLayout 后若是發(fā)現寬熬拒、高發(fā)生變化爷光,那么將會觸發(fā)invalidate。
4澎粟、調用invalidate 則不會觸發(fā)reqeusetLayout(不走Measure/Layout 過程)蛀序。
3、Measure/Layout/Draw 階段執(zhí)行requestLayout 會發(fā)生什么活烙?
一個小例子
public class MyView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// requestLayout();
Log.d("fish1", "onMeasure called");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// requestLayout();
Log.d("fish1", "onLayout called");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
requestLayout();
Log.d("fish1", "onDraw called");
}
}
MyView 重寫了onMeasure(xx)/onLayout(xx)/onDraw(xx) 方法徐裸,分別在里面調用requestLayout()。
現在有兩個問題:
問題1:當在onMeasure(xx)/onLayout(xx) 里調用requestLayout()后啸盏,onMeasure(xx)/onLayout(xx) 還會被執(zhí)行嗎重贺?
從日志結果反饋,onLayout(xx)只在進入頁面的時候被執(zhí)行一次。
因此气笙,上面的答案是否定的次企。
問題2:當在onDraw(xx) 里調用requestLayout()后,onMeasure(xx)/onLayout(xx) 還會被執(zhí)行嗎潜圃?
日志如下:
從日志結果反饋缸棵,答案是肯定的。
刨根問底
先從onMeasure/onLayout 調用requestLayout()說起谭期。
為啥在onMeasure(xx)/onLayout(xx)里執(zhí)行requestLayout 沒效果呢堵第?這得從requestLayout 源碼說起。
#View.java
public void requestLayout() {
...
//PFLAG_FORCE_LAYOUT 表示需要執(zhí)行Layout 操作
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
//如果父布局Layout 請求已經完成崇堵,則可以再次Layout
mParent.requestLayout();
}
...
}
最頂級的mParent為ViewRootImpl.java:
#ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
//標記已經申請Layout
mLayoutRequested = true;
//開啟三大流程
scheduleTraversals();
}
}
關鍵之處在于mParent.isLayoutRequested():
#View.java
public boolean isLayoutRequested() {
return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}
問題的重點轉為:View 的PFLAG_FORCE_LAYOUT 標記啥時候添加與清除型诚?
View.requestLayout()時會添加PFLAG_FORCE_LAYOUT 標記,而移除的地方在View.layout里:
#View.java
public void layout(int l, int t, int r, int b) { ;
...
//設置4個頂點的值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//觸發(fā)子布局Layout
onLayout(changed, l, t, r, b);
...
}
final boolean wasLayoutValid = isLayoutValid();
//清除標記
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
...
}
結合添加與移除 標記如下圖:
簡要概括:
1鸳劳、首次執(zhí)行requestLayout狰贯,將會給ViewTree里的各個View 添加標記,表示需要做測量/布局動作赏廓。
2涵紊、當屏幕刷新信號到來后觸發(fā)三大流程,進行測量/布局 動作幔摸。
3摸柄、而我們此時重寫了onMeasure(xx)/onLayout(xx),在該方法里調用requestLayout既忆,因為Layout 過程未結束驱负,因此標記沒有被清除,最終requestLayout 里判斷mParent.isLayoutRequested()=true患雇,說明上一次的Layout 未完成跃脊,沒必要再次執(zhí)行。
再看onDraw 里調用requestLayout
當執(zhí)行onDraw(xx)時苛吱,說明Layout 過程已經結束酪术,PFLAG_FORCE_LAYOUT 標記已經被清除,表示Layout 過程已經結束翠储,可以重新開始新的一輪Measure/Layout 過程绘雁,因此在onDraw(xx)里執(zhí)行requestLayout 有效果。
4援所、Measure/Layout/Draw 階段執(zhí)行invalidate 會發(fā)生什么庐舟?
一個小例子
public class MyView extends View {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// requestLayout();
invalidate();
Log.d("fish1", "onMeasure called");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// requestLayout();
invalidate();
Log.d("fish1", "onLayout called");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// requestLayout();
invalidate();
Log.d("fish1", "onDraw called");
}
}
直接說結論,從日志反饋分析可知:
1住拭、onMeasure(xx)/onLayout(xx) 里執(zhí)行invalidate 不會觸發(fā)Draw 過程继阻。
2耻涛、在onDraw(xx) 里執(zhí)行invalidate 會觸發(fā)Draw 過程(這也是實現動畫的關鍵)。
尋本溯源
從View.invalidate()開始分析:
#View.java
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
...
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)) {
...
if (fullInvalidate) {
mLastIsOpaque = isOpaque();
//清空標記
mPrivateFlags &= ~PFLAG_DRAWN;
}
if (p != null && ai != null && l < r && t < b) {
...
//往上調用瘟檩,直至ViewRootImpl
p.invalidateChild(this, damage);
}
}
//不滿足抹缕,則不會開啟Draw 過程
}
重點查看PFLAG_DRAWN 標記,此處的判斷:若是該View 已經繪制過墨辛,那么先清除PFLAG_DRAWN標記卓研,然后再往上傳遞刷新意圖,最后執(zhí)行Draw過程睹簇。
若是判斷沒有PFLAG_DRAWN 標記奏赘,說明上一次的Draw 過程沒有結束,則無需再次刷新太惠。
問題重點轉到PFLAG_DRAWN 標記的清除與添加磨淌,其中清除過程已經明了,剩下的看標記啥時候添加上的凿渊。
Draw 執(zhí)行過程:
在View.draw(x1,x2,x3)里清空標記梁只。
結合添加與移除標記,如下圖:
簡要概括:
1埃脏、在onMeasure(xx)/onLayout(xx)里調用invalidate搪锣,因為此時還沒走Draw 過程,因此PFLAG_DRAWN 標記沒被添加彩掐,在invalidate()內部判斷的時候不會再向上傳遞動作构舟,因此最終沒有執(zhí)行Draw過程。
2堵幽、在onDraw(xx)里執(zhí)行invalidate狗超,因為在View.draw(x1,x2,x3)里已經將PFLAG_DRAWN 標記添加(而后會執(zhí)行onDraw(xx)),因此在invalidate()內部判斷通過朴下,最終將會觸發(fā)Draw 過程努咐。
5、如何監(jiān)聽 Measure/Layout/Draw 各個流程桐猬?
以上是我們通過重寫onMeasure(xx)/onLayout(xx)/onDraw(xx)來實現View三大流程監(jiān)聽,在很多時候我們并不需要重寫前兩者刽肠、甚至第三者溃肪,或者說我們只關注ViewTree 當前所處的過程而不需要知道具體某個View 所處的過程,因此需要一個外部的機制來監(jiān)聽音五。
ViewTreeObserver 提供了一系列的接口用來監(jiān)聽各個過程惫撰。
只需要添加對應的Listener 到ViewTreeObserver里,當ViewTree 走到對應的流程時將會回調給Listener躺涝,外界可以據此做一些操作厨钻。
比如,想要監(jiān)聽Layout 過程:
textView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
//可以拿到ViewTree 里所有布局的寬度、起點夯膀、終點
//invalidate()
//requestLayout()
}
});
大家有興趣可以猜猜此時執(zhí)行invalidiate()诗充、requestLayout() 會重走三大流程嗎?
如果你答對了诱建,說明對刷新的細節(jié)之處已經明了蝴蜓。
6、總結
可以看出Android 在設計刷新機制時是做了一些限制的俺猿,通過成對的標記來限制一些無意義的頻繁調用invalidate/requestLayout茎匠。
看完上面的內容,有些同學可能會說:"了解了這有啥用押袍?看了就容易忘~"诵冒。
確實,上面的細節(jié)分析以前我也接觸過一些谊惭,后面確實忘了汽馋。
后面寫了如下代碼:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (w > 500) {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
layoutParams.width = 1000;
setLayoutParams(layoutParams);
Log.d("fish1", "onDraw called");
}
}
本意是想在寬度>500時,將寬度提高一倍午笛,最終沒能如愿惭蟋。如果當時知道了這些細節(jié),就會明白setLayoutParams(xx)里調用了requestLayout()药磺,而此時調用requestLayout()是不會觸發(fā)Measure/Layout 過程的告组。
原因是:
View.Layout--->View.onSizeChanged(xx)--->View.onLayout(xx)--->OnLayoutChangeListener.onLayoutChange(xx)--->清除PFLAG_FORCE_LAYOUT 標記--->ViewTree的Layout 執(zhí)行完成后---> OnGlobalLayoutListener. onGlobalLayout()。
以上是各個流程調用的時序癌佩。
解決方法:當然是放到標記清除之后的步驟木缝,比如在OnGlobalLayoutListener. onGlobalLayout() 處理重設寬高的邏輯。
其實想表達的意思是:
雖然是一些不起眼的小細節(jié)围辙,若是提前知道了可能就會避坑我碟。
本文基于Android 10.0。
您若喜歡姚建,請點贊矫俺、關注,您的鼓勵是我前進的動力
持續(xù)更新中掸冤,和我一起步步為營系統厘托、深入學習Android
1、Android各種Context的前世今生
2稿湿、Android DecorView 必知必會
3铅匹、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5饺藤、Android事件分發(fā)全套服務
6包斑、Android invalidate/postInvalidate/requestLayout 徹底厘清
7流礁、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅動Handler-Message-Looper解析
9罗丰、Android 鍵盤一招搞定
10神帅、Android 各種坐標徹底明了
11、Android Activity/Window/View 的background
12丸卷、Android Activity創(chuàng)建到View的顯示過
13枕稀、Android IPC 系列
14、Android 存儲系列
15谜嫉、Java 并發(fā)系列不再疑惑
16萎坷、Java 線程池系列
17、Android Jetpack 前置基礎系列
18沐兰、Android Jetpack 易懂易學系列