一個關於View/ViewGroup onMeasure() onLayout()被呼叫了但是onDraw()沒有被呼叫的問題分析
好,問題的背景大家應該很熟悉,Android 的ViewGroup中可以通過addview()的方式加入View或者viewGroup,然後通常一個view要正常顯示在螢幕上它的onMeasure(), onLayout()和onDraw至少要被呼叫一次。
現在我遇到的一個需求是SystemUI,顯示狀態列的列表,當列表比較小時顯示全部,當列表比較大時顯示一定的高度,其他的通過滾動的方式顯示。效果如下圖:
這個顯然想到的是ScrollView。但是單單ScrollView是實現不了的,不能實現動態的高度調整。於是我採用如下的佈局:
<com.android.systemui.statusbar.tablet.RestrictedContainerLayout android:layout_width="478dp" android:orientation="vertical" > <LinearLayout android:id="@+id/content_frame" android:layout_width="478dp" android:layout_height="wrap_content" android:layout_above="@id/system_bar_notification_panel_bottom_space" android:layout_alignParentEnd="true" android:layout_marginTop="250dp" android:background="@drawable/notification_panel_bg" android:orientation="vertical" android:paddingBottom="8dp" > <ScrollView android:id="@+id/notification_scroller" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="1" android:overScrollMode="always" > <com.android.systemui.statusbar.policy.NotificationRowLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true" android:descendantFocusability="afterDescendants" android:focusable="true" android:gravity="center_horizontal|bottom" systemui:rowHeight="@dimen/notification_row_min_height" /> </ScrollView> </LinearLayout> </com.android.systemui.statusbar.tablet.RestrictedContainerLayout> </com.android.systemui.statusbar.tablet.NotificationPanel>
通過RestrictedContainerLayout來限制子View的高度,然後思路就這樣劈里啪啦程式碼寫好了:
public class RestrictedContainerLayout extends ViewGroup { public RestrictedContainerLayout(Context context, AttributeSet attrs) { super(context, attrs); setWillNotDraw(false); setTag("RestrictedContainerLayout"); // TODO Auto-generated constructor stub } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int maxHeight = 200; switch (heightMode){ case MeasureSpec.EXACTLY: break; case MeasureSpec.AT_MOST: case MeasureSpec.UNSPECIFIED: heightSize = Math.min(maxHeight,heightSize); break; } int spec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); int n = getChildCount(); for(int i = 0 ; i < n ; i++){ View view = getChildAt(i); measureChild(view, widthMeasureSpec, spec); int height = view.getMeasuredHeight(); int min = Math.max(height, heightSize); spec = MeasureSpec.makeMeasureSpec(min, heightMode); } super.onMeasure(widthMeasureSpec,spec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub int n = getChildCount(); for(int i = 0 ; i < n ; i++){ View view = getChildAt(i); view.layout(l, t, r, b); } } @Override public void draw(Canvas canvas) { // TODO Auto-generated method stub super.draw(canvas); } }
似乎要很順利,可是真正的問題才剛剛開始,什麼啥東西都沒有顯示出來...
只能出示看家本領debug和logcat,既然沒有顯示,或者顯示出問題了,極有可能是onMeasure(), onLayout()或者onDraw其中出問題了,通過除錯
很快定位到時RestricetedContainerLayout的onMeasure(), onLayout()和onDraw都被呼叫了,但是他的子View,LinearLayout的onDraw沒有被呼叫但是onMeasure(), onLayout()被呼叫了。好,問題的表面原因找到了,就是onDraw()沒有被呼叫。但是立刻陷入了一個更大的漩渦:為什麼onDraw()會沒有被呼叫?
當然也許你眼睛犀利可以一下子發現我程式碼中的問題,當然如果這樣子的話就沒必要寫這篇部落格。為了找到onDraw()沒有被呼叫的原因,我決定進入framework進行除錯,其實思路很簡單,既然RestricetedContainerLayout的onDraw被呼叫了而LinearLayout是被RestricetedContainerLayout的onDraw呼叫的,原因肯定是RestricetedContainerLayout的onDraw在呼叫子類的draw()過程中有個判斷沒有過去。
如下圖是ViewGroup及其子View的呼叫過程:
ViewGroup也就是我們這裡的RestricetedContainerLayout的draw函式被呼叫,進而呼叫父類View的draw函式,進而呼叫view的onDawe()函式,這裡draw()和onDraw()都可能被ViewGroup過載,如果這樣自然呼叫的就是ViewGroup過載後的函數了。然後呼叫ViewGroupd的dispatchDraw()函式,進而呼叫drawChild()。最後呼叫draw(Canvas canvas, ViewGroup parent, long drawingTime)和draw(Canvas canvas)現在我們的問題就轉換為:第一步:1.draw()我們知道是成功的,而第七步:7.draw()沒有被呼叫。那肯定是這個呼叫鏈出了問題。
我的辦法是:
1.在建立View的時候設定tag:
<span style="white-space:pre"> </span>Log.e("luoziorng11","ViewGroupe drawChild "+" tag:"+this.getTag());
2.在這個鏈路的各個函式的關鍵節點加上log,就是在ViewGroup和View的draw,onDraw()等函式加上log,例如:
<span style="white-space:pre"> </span>Log.e("luoziorng11","ViewGroupe drawChild "+" tag:"+this.getTag());
3.編譯framework得到framework.jar.ext.jar等檔案push到system/framework目錄下。
4.啟動並執行程式,觀察log:
adb logcat | grep -E '(luoziorng11.*RestrictLinearLayout|RestrictLinearLayout.*luoziorng11)'
注意:我這裡其實利用了一個很簡單的技巧給我們的View設定tag,這樣子可以過濾出只針對特定Veiw的在View.java和ViewGroup.java的log。
追蹤不久,發現問題出現在boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)函式中的如下程式碼:
if (!concatMatrix &&
(flags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
(mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
if (DBG_DRAW) {
Xlog.d(VIEW_LOG_TAG, "view draw1 quickReject, this =" + this
+ ", mLeft = " + mLeft + ", mTop = " + mTop
+ ", mBottom = " + mBottom + ", mRight = " + mRight);
}
Log.e("luoziorng11","View draw 3 parameter"+" tag"+this.getTag()+"802");
return more;
}
直接返回了.....進一步除錯發現罪魁禍首是:canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW)
也就是說他算出來的子View的位置,在給他的畫布的顯示區域之外。那為什麼呢?其實原因就在onLayout()裡:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO Auto-generated method stub
int n = getChildCount();
for(int i = 0 ; i < n ; i++){
View view = getChildAt(i);
view.layout(l, t, r, b);
}
}
子View layout()時傳入的上下左右的引數應該是相對父類的座標而不是相對於父類的父類的座標更不是全域性座標,所以程式碼做如下修改就好了:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO Auto-generated method stub
Log.e("luoziorng11", " l:"+l+" t:"+t+" r:"+r+" b:"+b);
int n = getChildCount();
for(int i = 0 ; i < n ; i++){
View view = getChildAt(i);
view.layout(0, 0, r-l, b-t);
}
}
總結:這個問題出現的原因是我對layout的引數的理解出現問題,實屬初級錯誤,但是這個解決問題的過程我覺得可以解決各種類似的複雜問題。這樣子,感覺就不是作為一個簡單搬用工,靠記概念解決,而且容易問題變了就不知道怎麼解了,同時這樣子在framework走一遭對framework的理解也深刻不少^_^