由android.view.View派生出来的单一控件元素常见的有TextView, Button, ImageView等, 派生出的容器有LinearLayout, FrameLayout 等, 也有一些由ViewGroup派生出来的控件做为单一控件元素使用的, 比如说ListView, 当然我们也可以把ListView当做容器使用。Android通过布局可以完成很多有创意富有美感的界面, ViewGroup的作用很大,这里单独拿出来研究。
ViewGroup实现了android.view.ViewParent和android.view.ViewManager两个接口, 赋予其装载子控件和管理子控件的能力。这篇主要讲Android控件如何绘制到界面上的。
控件显示到界面上主要分三个流程, 如下图。这是一个非常自然的想法, 得到大小后才可以布局, 布局好了才可以绘制; 这三个流程都是按照上图树形结构递归的。对于这三个流程,只要对Android控件稍有研究的人都
会发现, 每一个控件都有measure(), layout(), draw()方法, 下面分别分析其作用:
measure 递归:
1、判断是否需要重新计算大小
2、调用onMeasure, 如果是ViewGroup类型, 则遍历所有子控件的measure方法,计算出子控件大小,
3、使用setMeasuredDimension(int, int)确定自身计算的大小
由于第二步会调用子控件的measure方法, 在子控件的大小计算当中也会经历这三步动作, 直到整个树遍历完, 此时此控件及其子控件的大小都确定了, 在这里强调控件的大小是由父控件和自身决定的,当然取决在于父控件, 控件自身只提供参考值, 这是因为控件的measure方法是由父控件调用的, 而父控件的控件有限,可能不完全按照你的申请要求给出, 这里留待以后讨论关于布局参数问题。
在android.view.View对于measure流程已经实现了一部分:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); ... }
对于android.view.View来说它不需要遍历子控件了, 下面贴出一个我实现的一个onMeasure :
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //获取mode和size, 方便给children分配空间 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); //TODO 这里可以检查你的大小, 或者mode final int count = getChildCount(); for(int i = 0; i < count; i++) { final View view = getChildAt(i); //这里只是举一个例子, 这里给child多少大小根据实际来定 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY); view.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 得出自己计算出的大小, 这里也是一个例子, 可以根据所有子控件占多大空间 // 给出, 这里也根据要实现的效果看, 这部分建议看LinearLayout等容器的源码 setMeasuredDimension(widthSize, heightSize); }
layout 递归:
1、设置自身相对父控件的位置并判断是否需要重新布局,使用setFrame(left, top, right, bottom);
2、调用onLayout()布局子控件
在android.view.View也实现了此流程的一部分:
public void layout(int l, int t, int r, int b) { ... onLayout(changed, l, t, r, b); ... }
下面我也简单的实现了第二步:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int widthSpan = 0; int heightSpan = 0; for(int i = 0; i < count; i++) { final View child = getChildAt(i); child.layout(widthSpan, heightSpan, child.getMeasuredWidth(), child.getMeasuredHeight()); widthSpan += child.getMeasuredWidth(); heightSpan += child.getMeasuredHeight(); } }
这是一个简陋的Grid布局。
draw递归:
1、绘制背景
2、调用onDraw()绘制控件内容
3、调用dispatchDraw()绘制所有的子控件
4、绘制渐变边界等
5、绘制装饰品, 比如滑动条等
draw递归在android.view.View已经有完整的实现, 自定义ViewGroup时一般只需要重写onDraw实现如何绘制内容就够了, 当然所有的流程都可以重写, 如果需要的话。下面看一下android.view.View里面draw递归的原型:
public void draw(Canvas canvas) { // Step 1, draw the background, if needed ...// Step 2, draw the content onDraw(canvas); // Step 3, draw the children dispatchDraw(canvas); // Step 4, draw the fade effect and restore layers ... //Step 5, draw decorations onDrawScrollBars(canvas); }
上面三个递归, 解决了一颗控件树的显示问题, 现在大家会很奇怪, 到底是谁发起这个递归, 即最上层的父控件到底是谁, 查看源码可以看到, 在android.view下面有一个ViewRoot(更新后变成ViewRootImpl)隐藏类, 在其performTraversals()方法中发起这三个递归,这个类没有研究太深入, 以后补上。在performTraversals()中大概的流程是:
private void performTraversals() { final View host = mView; ... host.measure(); ... host.layout(); ... host.draw(); ... }
这样就实现了一个大的递归, 把完整的界面给绘制出来了。下面我自己写一个实现ViewGroup的Demo:
package com.ui.viewgroup; import android.content.Context; import android.graphics.Canvas; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; public class ViewGroupImpl extends ViewGroup { public class LayoutParams extends ViewGroup.LayoutParams { public int left = 0; public int top = 0; public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(int left, int top, int width, int height) { super(width, height); this.left = left; this.top = top; } } public ViewGroupImpl(Context context) { this(context, null); } public ViewGroupImpl(Context context, AttributeSet attrs) { super(context, attrs); } public void addInScreen(View child, int left, int top, int width, int height) { addView(child, new LayoutParams(left, top, width, height)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 检测控件大小是否符合要求 if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) { throw new IllegalArgumentException("不合法的MeasureSpec mode"); } // 计算子控件大小 final int count = getChildCount(); for(int i = 0; i < count; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams)child.getLayoutParams(); //确定大小的 final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } // 设置计算的控件大小 setMeasuredDimension(widthSize, heightSize); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); LayoutParams lp; for(int i = 0; i < count; i++) { final View child = getChildAt(i); lp = (LayoutParams)child.getLayoutParams(); //相对父控件坐标 child.layout(lp.left, lp.top, lp.left + lp.width, lp.top + lp.width); } } // draw递归 不需要我们接管, @Override public void draw(Canvas canvas) { super.draw(canvas); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); } }
Activity:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ViewGroupImpl viewGroupImpl = new ViewGroupImpl(this); setContentView(viewGroupImpl, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); // 因为此时无法获取viewGroupImpl的实际大小, 所以只好假设一个大小 final int parentWidth = 400; final int parentHeight = 700; final int maxWidthSize = parentWidth / 4; final int maxHeightSize = parentHeight / 4; Random random = new Random(); for(int i = 0; i < 50; i++) { int left = random.nextInt(parentWidth) - 10; int top = random.nextInt(parentHeight) - 10; int width = random.nextInt(maxWidthSize) + 10; int height = random.nextInt(maxHeightSize) + 10; ImageView child = new ImageView(this); child.setImageResource(R.drawable.ic_launcher); viewGroupImpl.addInScreen(child, left, top, width, height); }
下面是效果图: