ViewGroup学习之绘制过程

Android UI界面由以下树形结构组成, 从图中可以看出, UI界面是有View与ViewGroup两大类控件组成,在下面树形图中不管是View还是ViewGroup都是从android.view.View中派生, 而ViewGroup作为容器, 它可以装载和管理其下的一些列由android.view.View派生出来的元素(View和ViewGroup):

       

      由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);
        }
复制代码

下面是效果图:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值