一个关于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; }
也就是说他算出来的子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的理解也深刻不少^_^