对于这个问题并不是一件容易的事,但是如果你掌握了基本的原理其实很简单。依我的习惯还是先复习一些相关的知识,这样可以保证你在阅读过程中没有阻碍。
【复习或者预习部分 Begin 】
先看看官方文档,其中Dev Guide -->User Interface -->How Android Draws Views. 具体文档内容如下(当然你可以直接在官方上看,刚刚网上找链接的时候发现改版了,自己找找吧,我用的是离线的):
When an Activity receives focus, it will be requested to draw its layout. The Android framework will handle the procedure for drawing, but the Activity must provide the root node of its layout hierarchy.
Drawing begins with the root node of the layout. It is requested to measure and draw the layout tree. Drawing is handled by walking the tree and rendering each View that intersects the invalid region. In turn, each View group is responsible for requesting each of its children to be drawn (with the
method) and each View is responsible for drawing itself. Because the tree is traversed in-order, this means that parents will be drawn before (i.e., behind) their children, with siblings drawn in the order they appear in the tree.draw()
The framework will not draw Views that are not in the invalid region, and also will take care of drawing the Views background for you.
You can force a View to draw, by calling
.invalidate()
Drawing the layout is a two pass process: a measure pass and a layout pass. The measuring pass is implemented in
and is a top-down traversal of the View tree. Each View pushes dimension specifications down the tree during the recursion. At the end of the measure pass, every View has stored its measurements. The second pass happens in measure(int, int)
and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.layout(int, int, int, int)
When a View's measure()
method returns, its
and getMeasuredWidth()
values must be set, along with those for all of that View's descendants. A View's measured width and measured height values must respect the constraints imposed by the View's parents. This guarantees that at the end of the measure pass, all parents accept all of their children's measurements. A parent View may call getMeasuredHeight()
measure()
more than once on its children. For example, the parent may measure each child once with unspecified dimensions to find out how big they want to be, then call measure()
on them again with actual numbers if the sum of all the children's unconstrained sizes is too big or too small (i.e., if the children don't agree among themselves as to how much space they each get, the parent will intervene and set the rules on the second pass).
To initiate a layout, call
. This method is typically called by a View on itself when it believes that is can no longer fit within its current bounds.requestLayout()
The measure pass uses two classes to communicate dimensions. The View.MeasureSpec
class is used by Views to tell their parents how they want to be measured and positioned. The base LayoutParams class just describes how big the View wants to be for both width and height.
【复习或者预习部分 End 】
这一部分网上有翻译过来的,不过这也比较简单,在此就自己翻译或者google一下。下面开始切入主题,正式讲解。
关键部分:Drawing the layout is a two pass process: a measure pass and a layout pass. 所以一个view执行OnDraw时最关键的是measure和layout。其实这很好理解的,一个view需要绘制出来,那么必须知道他要占多大的空间也就是measure,还得知道在哪里绘制,也就是把view放在哪里即layout。把这两部分掌握好也就可以随意自定义view了。至于viewGroup中如何绘制就参考上面官方文档,其实就是一个分发绘制,直到child是一个view自己进行绘制。
说了这么多看着挺累的,举两个例子给大家。
1、重写一个View。
引用一下网上的一个例子http://labs.ywlx.net/?p=284 ,代码如下:
public class RotateTextView extends TextView { private static final String NAMESPACE = “http://www.ywlx.net/apk/res/easymobi”; private static final String ATTR_ROTATE = “rotate”; private static final int DEFAULTVALUE_DEGREES = 0; private int degrees ; public RotateTextView(Context context, AttributeSet attrs) { super(context, attrs); degrees = attrs.getAttributeIntValue(NAMESPACE, ATTR_ROTATE, DEFAULTVALUE_DEGREES); } @Override protected void onDraw(Canvas canvas) { canvas.rotate(degrees,getMeasuredWidth()/2,getMeasuredHeight()/2); super.onDraw(canvas); } }
使用自定义RotateTextView如下:
<cn.easymobi.application.memorytest.RotateTextView android:layout_width=”wrap_content” android:layout_height=”wrap_content” android:padding=”8dip” android:gravity=”center” android:id=”@+id/tvBottom_color” android:textSize=”15dip” android:textColor=”@color/black” easymobi:rotate=”10″ android:layout_marginTop=”468dip” />
个人觉得这个例子挺不错的,简单易懂,而且涉及的内容比较单一。需要更详细的讲解看文章http://labs.ywlx.net/?p=284 。
2、重写一个ViewGroup。
再次引用网上的一个例子http://blog.csdn.net/arui319/article/details/5868466 ,代码如下:
public class MyViewGroup extends ViewGroup { public MyViewGroup(Context context) { super(context); this.initOtherComponent(context); } private void initOtherComponent(Context context) { Button aBtn = new Button(context); // set id 1 aBtn.setId(1); aBtn.setText("a btn"); this.addView(aBtn); Button bBtn = new Button(context); // set id 2 bBtn.setId(2); bBtn.setText("b btn"); this.addView(bBtn); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); switch (child.getId()) { case 1: // 1 is aBtn Log.d("MyViewGroup", "btn1 setting"); child.setVisibility(View.VISIBLE); child.measure(r - l, b - t); child.layout(0, 0, child.getMeasuredWidth(), child .getMeasuredHeight()); break; case 2: // 2 is bBtn Log.d("MyViewGroup", "btn2 setting"); child.setVisibility(View.VISIBLE); child.measure(r - l, b - t); child.layout(0, 50, child.getMeasuredWidth(), child .getMeasuredHeight() + 50); break; default: // } } } }
这个例子主要是说明layout 和Measure的使用。
3、两个例子的总结。
重写一个view一般情况下只需要重写OnDraw方法。那么什么时候需要重写OnMeasure、OnLayout、OnDraw方法呢,这个问题只要把这几个方法的功能弄清楚你就应该知道怎么做了。在此我也简单的讲一下(描述不正确请拍砖,欢迎交流)。
①如果需要改变View绘制的图像,那么需要重写OnDraw方法。(这也是最常用的重写方式。)
②如果需要改变view的大小,那么需要重写OnMeasure方法。
③如果需要改变View的(在父控件的)位置,那么需要重写OnLayout方法。
④根据上面三种不同的需要你可以组合出多种重写方案,你懂的。
说了这么多如果有没有讲清楚的,请再看看那文档。希望对你理解view的绘制原理和重写view有所帮助。欢迎留言交流。