自定义View以及自定义ViewGroup

转载自:http://www.linuxidc.com/Linux/2014-12/110164.htm

Android开发中偶尔会用到自定义View,一般情况下,自定义View都需要继承View类的onMeasure方法,那么,为什么要继承onMeasure()函数呢?什么情况下要继承onMeasure()?系统默认的onMeasure()函数行为是怎样的 ?本文就探究探究这些问题。

首先,我们写一个自定义View,直接调用系统默认的onMeasure函数,看看会是怎样的现象:

 1 package com.titcktick.customview;
 2  
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.view.View;
 6  
 7 public class CustomView extends View {
 8     
 9     public CustomView(Context context) {
10         super(context); 
11     }
12  
13     public CustomView(Context context, AttributeSet attrs) {
14         super(context, attrs);      
15     }
16     
17     @Override
18     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        
19         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
20     }
21  
22 }

1. 父控件使用match_parent,CustomView使用match_parent

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:orientation="vertical">
 6  
 7     <com.titcktick.customview.CustomView
 8         android:layout_width="match_parent"
 9         android:layout_height="match_parent"
10         android:layout_margin="10dp"
11         android:background="@android:color/black"/>
12  
13 </LinearLayout>

这里加了10dp的margin并且把View的背景设置为了黑色,是为了方便辨别我们的CustomView,效果如下:

Android开发实践:为什么要继承onMeasure()

我们可以看到,默认情况下,如果父控件和CustomView都使用match_parent,则CustomView会充满父控件。

2.  父控件使用match_parent,CustomView使用wrap_content

把layout文件中,CustomView的layout_width/layout_height替换为wrap_content,你会发现,结果依然是充满父控件。

3.  父控件使用match_parent,CustomView使用固定的值

把layout文件中,CustomView的layout_width/layout_height替换为50dp,你会发现,CustomView的显示结果为50dpx50dp,如图所示:

Android开发实践:为什么要继承onMeasure()

4.  父控件使用固定的值,CustomView使用match_parent或者wrap_content

那么,如果把父控件的layout_width/layout_height替换为50dp,CustomView设置为match_parent或者wrap_content,你会发现,CustomView的显示结果也是为50dpx50 dp。

5  结论

如果自定义的CustomView采用默认的onMeasure函数,行为如下:

(1) CustomView设置为 match_parent 或者 wrap_content 没有任何区别,其显示大小由父控件决定,它会填充满整个父控件的空间。

(2) CustomView设置为固定的值,则其显示大小为该设定的值。

如果你的自定义控件的大小计算就是跟系统默认的行为一致的话,那么你就不需要重写onMeasure函数了。

6. 怎样编写onMeasure函数

系统默认的onMeasure函数的行为就讨论到这,下面也说说怎样重写onMeasure函数,以及onMeasure函数的基本原理,关键部分在代码中以注释的形式给出了,仅供参考:

 1 package com.titcktick.customview;
 2  
 3 import android.content.Context;
 4 import android.util.AttributeSet;
 5 import android.view.View;
 6  
 7 public class CustomView extends View {
 8     
 9     private static final int DEFAULT_VIEW_WIDTH = 100;
10     private static final int DEFAULT_VIEW_HEIGHT = 100;
11     
12     public CustomView(Context context) {
13         super(context); 
14     }
15  
16     public CustomView(Context context, AttributeSet attrs) {
17         super(context, attrs);      
18     }
19     
20     @Override
21     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
22         
23         int width  = measureDimension(DEFAULT_VIEW_WIDTH, widthMeasureSpec);
24         int height = measureDimension(DEFAULT_VIEW_HEIGHT, heightMeasureSpec);
25         
26         setMeasuredDimension(width, height);                
27     }
28     
29     protected int measureDimension( int defaultSize, int measureSpec ) {
30         
31         int result = defaultSize;
32         
33         int specMode = MeasureSpec.getMode(measureSpec);
34         int specSize = MeasureSpec.getSize(measureSpec);
35                 
36         //1. layout给出了确定的值,比如:100dp
37         //2. layout使用的是match_parent,但父控件的size已经可以确定了,比如设置的是具体的值或者match_parent
38         if (specMode == MeasureSpec.EXACTLY) {      
39             result = specSize; //建议:result直接使用确定值
40         } 
41         //1. layout使用的是wrap_content
42         //2. layout使用的是match_parent,但父控件使用的是确定的值或者wrap_content
43         else if (specMode == MeasureSpec.AT_MOST) {            
44             result = Math.min(defaultSize, specSize); //建议:result不能大于specSize
45         } 
46         //UNSPECIFIED,没有任何限制,所以可以设置任何大小
47         //多半出现在自定义的父控件的情况下,期望由自控件自行决定大小
48         else {      
49             result = defaultSize; 
50         }
51         
52         return result;
53     }
54 }

这样重载了onMeasure函数之后,你会发现,当CustomView使用match_parent的时候,它会占满整个父控件,而当CustomView使用wrap_content的时候,它的大小则是代码中定义的默认大小100x100像素。当然,你也可以根据自己的需求改写measureDimension()的实现。

前一篇文章主要讲了自定义View为什么要重载onMeasure()方法(见 http://www.linuxidc.com/Linux/2014-12/110164.htm),那么,自定义ViewGroup又都有哪些方法需要重载或者实现呢 ?

Android开发中,对于自定义View,分为两种,一种是自定义控件(继承View类),另一种是自定义布局容器(继承ViewGroup)。如果是自定义控件,则一般需要重载两个方法,一个是onMeasure(),用来测量控件尺寸,另一个是onDraw(),用来绘制控件的UI。而自定义布局容器,则一般需要实现/重载三个方法,一个是onMeasure(),也是用来测量尺寸;一个是onLayout(),用来布局子控件;还有一个是dispatchDraw(),用来绘制UI。

本文主要分析自定义ViewGroup的onLayout()方法的实现。

ViewGroup类的onLayout()函数是abstract型,继承者必须实现,由于ViewGroup的定位就是一个容器,用来盛放子控件的,所以就必须定义要以什么的方式来盛放,比如LinearLayout就是以横向或者纵向顺序存放,而RelativeLayout则以相对位置来摆放子控件,同样,我们的自定义ViewGroup也必须给出我们期望的布局方式,而这个定义就通过onLayout()函数来实现。

我们通过实现一个水平优先布局的视图容器来更加深入地了解onLayout()的实现吧,效果如图所示(黑色方块为子控件,白色部分为自定义布局容器)。该容器的布局方式是,首先水平方向上摆放子控件,水平方向放不下了,则另起一行继续水平摆放。

Android开发实践:自定义ViewGroup的onLayout()分析

1.  自定义ViewGroup的派生类

第一步,则是自定ViewGroup的派生类,继承默认的构造函数。

 1 public class CustomViewGroup extends ViewGroup {
 2   
 3   public CustomViewGroup(Context context) {
 4       super(context);    
 5     }
 6   
 7   public CustomViewGroup(Context context, AttributeSet attrs) {
 8       super(context, attrs);    
 9     }
10     
11   public CustomViewGroup(Context context, AttributeSet attrs, intdefStyle) {
12       super(context, attrs, defStyle);
13     }
14 }

2.  重载onMeasure()方法

为什么要重载onMeasure()方法这里就不赘述了,上一篇文章已经讲过,这里需要注意的是,自定义ViewGroup的onMeasure()方法中,除了计算自身的尺寸外,还需要调用measureChildren()函数来计算子控件的尺寸。

onMeasure()的定义不是本文的讨论重点,因此这里我直接使用默认的onMeasure()定义,当然measureChildren()是必须得加的。

1 @Override
2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3     measureChildren(widthMeasureSpec, heightMeasureSpec);
4     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
5 }

3.  实现onLayout()方法
onLayout()函数的原型如下:

//@param changed 该参数指出当前ViewGroup的尺寸或者位置是否发生了改变
//@param left top right bottom 当前ViewGroup相对于其父控件的坐标位置
protected void onLayout(boolean changed,int left, int top, int right, int bottom);

由于我们希望优先横向布局子控件,那么,首先,我们知道总宽度是多少,这个值可以通过getMeasuredWidth()来得到,当然子控件的宽度也可以通过子控件对象的getMeasuredWidth()来得到。

这样,就不复杂了,具体的实现代码如下所示:

 1 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 2     
 3     int mViewGroupWidth  = getMeasuredWidth();  //当前ViewGroup的总宽度      
 4  
 5     int mPainterPosX = left;  //当前绘图光标横坐标位置
 6     int mPainterPosY = top;  //当前绘图光标纵坐标位置  
 7     
 8     int childCount = getChildCount();        
 9     for ( int i = 0; i < childCount; i++ ) {
10         
11         View childView = getChildAt(i);
12  
13         int width  = childView.getMeasuredWidth();
14         int height = childView.getMeasuredHeight();            
15                     
16         //如果剩余的空间不够,则移到下一行开始位置
17         if( mPainterPosX + width > mViewGroupWidth ) {              
18             mPainterPosX = left; 
19             mPainterPosY += height;
20         }                    
21         
22         //执行ChildView的绘制
23         childView.layout(mPainterPosX,mPainterPosY,mPainterPosX+width, mPainterPosY+height);
24         
25         //记录当前已经绘制到的横坐标位置 
26         mPainterPosX += width;
27     }      
28 }

4. 布局文件测试

下面我们就尝试写一个简单的xml文件,来测试一下我们的自定义ViewGroup,我们把子View的背景颜色都设置为黑色,方便我们辨识。

 1 <com.titcktick.customview.CustomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent">
 5  
 6     <View
 7         android:layout_width="100dp"
 8         android:layout_height="100dp"
 9         android:layout_margin="10dp"
10         android:background="@android:color/black"/>
11     
12     <View
13         android:layout_width="100dp"
14         android:layout_height="100dp"
15         android:layout_margin="10dp"
16         android:background="@android:color/black"/>    
17         
18     <View
19         android:layout_width="100dp"
20         android:layout_height="100dp"
21         android:layout_margin="10dp"
22         android:background="@android:color/black"/>
23     
24     <View
25         android:layout_width="100dp"
26         android:layout_height="100dp"
27         android:layout_margin="10dp"
28         android:background="@android:color/black"/>    
29  
30 </com.titcktick.customview.CustomViewGroup>

5. 添加layout_margin

为了让核心逻辑更加清晰,上面的onLayout()实现我隐去了margin的计算,这样就会导致子控件的layout_margin不起效果,所以上述效果是子控件一个个紧挨着排列,中间没有空隙。那么,下面我们来研究下如何添加margin效果。

其实,如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。

ViewGroup.MarginLayoutParams的定义关键部分如下,它记录了子控件的layout_margin值:

1 public static class MarginLayoutParams extends ViewGroup.LayoutParams {        
2     public int leftMargin;
3     public int topMargin;
4     public int rightMargin;
5     public int bottomMargin;
6 }

你可以跟踪源码看看,其实XML文件中View的layout_xxx参数都是被传递到了各种自定义ViewGroup.LayoutParams派生类对象中。例如LinearLayout的LayoutParams定义的关键部分如下:

 1 public class LinearLayout extends ViewGroup {
 2  
 3 public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 4  
 5     public float weight;
 6     public int gravity = -1;
 7  
 8     public LayoutParams(Context c, AttributeSet attrs) {
 9  
10             super(c, attrs);
11  
12             TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
13             weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
14             gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
15  
16             a.recycle();
17         }
18     }
19  
20     @Override
21     public LayoutParams generateLayoutParams(AttributeSet attrs) {
22         return new LinearLayout.LayoutParams(getContext(), attrs);
23     }
24 }

这样你大概就可以理解为什么LinearLayout的子控件支持weight和gravity的设置了吧,当然我们也可以这样自定义一些属于我们ViewGroup特有的params,这里就不详细讨论了,我们只继承MarginLayoutParams来获取子控件的margin值。

 1 public class CustomViewGroup extends ViewGroup {
 2  
 3     public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 4         public LayoutParams(Context c, AttributeSet attrs) {
 5             super(c, attrs);            
 6         }      
 7     }
 8  
 9     @Override  
10     public LayoutParams generateLayoutParams(AttributeSet attrs) {  
11         return new CustomViewGroup.LayoutParams(getContext(), attrs);  
12     }
13  
14 }

这样修改之后,我们就可以在onLayout()函数中获取子控件的layout_margin值了,添加了layout_margin的onLayout()函数实现如下所示:

 1 @Override
 2 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 3  
 4     int mViewGroupWidth  = getMeasuredWidth();  //当前ViewGroup的总宽度
 5     int mViewGroupHeight = getMeasuredHeight(); //当前ViewGroup的总高度
 6  
 7     int mPainterPosX = left; //当前绘图光标横坐标位置
 8     int mPainterPosY = top;  //当前绘图光标纵坐标位置  
 9     
10     int childCount = getChildCount();        
11     for ( int i = 0; i < childCount; i++ ) {
12         
13         View childView = getChildAt(i);
14  
15         int width  = childView.getMeasuredWidth();
16         int height = childView.getMeasuredHeight();            
17  
18         CustomViewGroup.LayoutParams margins = (CustomViewGroup.LayoutParams)(childView.getLayoutParams());
19         
20         //ChildView占用的width  = width+leftMargin+rightMargin
21         //ChildView占用的height = height+topMargin+bottomMargin
22         //如果剩余的空间不够,则移到下一行开始位置
23         if( mPainterPosX + width + margins.leftMargin + margins.rightMargin > mViewGroupWidth ) {              
24             mPainterPosX = left; 
25             mPainterPosY += height + margins.topMargin + margins.bottomMargin;
26         }                    
27         
28         //执行ChildView的绘制
29         childView.layout(mPainterPosX+margins.leftMargin, mPainterPosY+margins.topMargin,mPainterPosX+margins.leftMargin+width, mPainterPosY+margins.topMargin+height);
30         
31         mPainterPosX += width + margins.leftMargin + margins.rightMargin;
32     }      
33 }

 既然谈到了View以及ViewGroup的自定义问题,那有必要研究一下View的绘制流程,对于上面涉及的一些参数以及函数也可以有个更加清晰的认识:请参看接下来的一片博文:

 http://www.cnblogs.com/CoolRandy/articles/4558581.html

了解了view的自定义,那究竟view是如何显示到窗口上的呢?这就要从源码入手了

ActivityThread#handleResumeActivity方法

 1 final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
 2             boolean reallyResume) {
 3 ……
 4 ActivityClientRecord r = performResumeActivity(token, clearHide);
 5     ……
 6 if (r.window == null && !a.mFinished && willBeVisible) {
 7                 r.window = r.activity.getWindow();
 8                 View decor = r.window.getDecorView();//前面分析过,这个是Window对象所维护的装饰窗口,最顶层的窗口
 9                 decor.setVisibility(View.INVISIBLE);
10                 ViewManager wm = a.getWindowManager();//获取WindowManager,继承自ViewManager,不可实例化,是个接口
11                 WindowManager.LayoutParams l = r.window.getAttributes();
12                 a.mDecor = decor;
13                 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
14                 l.softInputMode |= forwardBit;
15                 if (a.mVisibleFromClient) {
16                     a.mWindowAdded = true;
17                     wm.addView(decor, l);// 通过WindowManager添加到窗口
18                 }
19     ……
20 }

可以看到最顶层的装饰窗口在activity resume的时候通过windowManager#addView方法添加。

WindowManagerImpl# addView

 

 1 public void addView(View view, ViewGroup.LayoutParams params) {
 2         mGlobal.addView(view, params, mDisplay, mParentWindow);
 3 }
 4 
 5     public void addView(View view, ViewGroup.LayoutParams params,
 6             Display display, Window parentWindow) {
 7 
 8         ViewRootImpl root;
 9         View panelParentView = null;
10         ......
11             root = new ViewRootImpl(view.getContext(), display); // 创建一个ViewRoot对象
12 
13             view.setLayoutParams(wparams);
14 
15             if (mViews == null) {
16                 index = 1;
17                 mViews = new View[1];
18                 mRoots = new ViewRootImpl[1];
19                 mParams = new WindowManager.LayoutParams[1];
20             } else {
21                 index = mViews.length + 1;
22                 Object[] old = mViews;
23                 mViews = new View[index]; 
24                 System.arraycopy(old, 0, mViews, 0, index-1);
25                 old = mRoots;
26                 mRoots = new ViewRootImpl[index];
27                 System.arraycopy(old, 0, mRoots, 0, index-1);
28                 old = mParams;
29                 mParams = new WindowManager.LayoutParams[index];
30                 System.arraycopy(old, 0, mParams, 0, index-1);
31             }
32             index--;
33 
34             mViews[index] = view;
35             mRoots[index] = root;// 将view和ViewRootImp关联起来  ViewRootImp是链接View和WindowManagerService的桥梁
36             mParams[index] = wparams;
37         }
38         try {
39             root.setView(view, wparams, panelParentView);// 调用ViewRoot的setView方法
40         } 
41         ......
42     }

setView:

 1 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {  
 2     requestLayout(); // 请求UI开始绘制重新绘制View树
 3     ......
 4         try {
 5            mOrigWindowType = mWindowAttributes.type;
 6            mAttachInfo.mRecomputeGlobalAttributes = true;
 7            collectViewAttributes();
 8     // 通知WindowManagerService添加一个窗口  会调用到addWindow方法
 9            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
10                      getHostVisibility(), mDisplay.getDisplayId(),
11                      mAttachInfo.mContentInsets, mInputChannel);
12         }
13 }

两种情况会导致调用到requestLayout,改变视图显示属性,比如setVisibility,是直接或者间接调用该函数。

 1 @Override
 2     public void requestLayout() {
 3         checkThread();// 本次调用是否是在UI线程调用的。
 4         mLayoutRequested = true;
 5         scheduleTraversals();
 6     }
 7 
 8     void scheduleTraversals() {
 9         if (!mTraversalScheduled) {
10             mTraversalScheduled = true;
11             mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
12 //分发一个异步消息,处理函数中调用performTraversals()对View进行重新遍历。
13             mChoreographer.postCallback(
14                     Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);// mTraversalRunnable是一个Runnable对象
15             scheduleConsumeBatchedInput();
16         }
17     }
18 
19     final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
20 
21         final class TraversalRunnable implements Runnable {
22         @Override
23         public void run() {
24             doTraversal();//执行doTraversal()
25         }
26 }
27 
28     void doTraversal() {
29         if (mTraversalScheduled) {
30             mTraversalScheduled = false;
31             mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);
32     ……
33             try {
34                 performTraversals();
35             } finally {
36                 Trace.traceEnd(Trace.TRACE_TAG_VIEW);
37             }
38         ……
39         }
40 }

View树遍历的核心函数  measure—layout--draw

 1 private void performTraversals() {
 2     ......
 3     // Ask host how big it wants to be
 4     performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 5     ......
 6 performLayout();
 7 ……
 8 performDraw();
 9 ……
10      mIsInTraversal = false;
11 }
12 
13 
14 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
15         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
16         try {
17             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
18         } finally {
19             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
20         }
21     }

mView.measure(childWidthMeasureSpec,childHeightMeasureSpec);就执行到了熟悉onMeasure(childWidthMeasureSpec,childHeightMeasureSpec);里面。

posted @ 2015-06-07 15:59  CoolRandy  阅读(349)  评论(0)    收藏  举报