Android View绘制及实践

概述

整个View树的绘图流程是在ViewRoot.java类的performTraversals()函数展开的,该函数做的执行过程可简单概况为:

- 判断是否需要重新计算视图大小(measure)

- 判断是否重新需要安置视图的位置(layout)

- 判断是否需要重绘(draw)

其整个流程图如下:

图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程

在Android中View的整个生命周期,调用invalidate和requestLayout会触发一系列的方法,如图所示

图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程

  • 当开发者调用requestLayout方法时,只会触发measure和layout过程
  • 当开发者调用invalidate方法时,会触发draw过程

Measure

  • 为整个View树计算实际大小,每个View的实际大小由父控件和其本身共同决定
  • measure方法调用onMeasure方法,onMeasure方法里通过setMeasuredDimension(注意padding和margin)设置View的大小
  • ViewGroup子类需要重写onMeasure去遍历测量其子View的大小
  • measure方法是final类型,不能被重写,需要重写的是onMeasure方法
  • 整个测量过程就是对View树的递归
  • 一个View一旦测量完成,即可通过getMeasuredWidth() 和 getMeasuredHeight()获得其宽度和高度
  • 自定义的ViewGroup只需实现measure和layout过程

MeasureSpec

一个MeasureSpec对象由size和mode组成,MeasureSpec类通过将其封装在一个int值中以减少对象的分配。其模式有以下三种,都为int型

- UNSPECIFIED

父视图不对子视图产生任何约束,如ListView,ScrollView

- EXACTLY

父视图为子视图指定一个确切的尺寸,子视图以这个确切的值作为大小,比如match_parent或具体值20dp

- AT_MOST

父视图为子视图指定一个最大尺寸,子视图必须在这个尺寸大小内,比如wrap_content

相关函数
  • makeMeasureSpec(int size, int mode) 根据size值和mode值封装成MeasureSpec
  • getSize(int measureSpec) 根据MeasureSpec值返回size值
  • getMode(int measureSpec) 根据MeasureSpec值返回mode值
  • 以上三个函数内部实现是用位运算实现,mode使用int的最高2位,size使用其余的30位,内部关键部分代码
public static class MeasureSpec {
    private static final int MODE_SHIFT = 30; //移位位数为30
    //int类型占32位,向右移位30位,该属性表示掩码值,用来与size和mode进行"&"运算,获取对应值。
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  

    //向右移位30位,其值为00 + (30位0)  , 即 0x0000(16进制表示)
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    //向右移位30位,其值为01 + (30位0)  , 即0x1000(16进制表示)
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    //向右移位30位,其值为02 + (30位0)  , 即0x2000(16进制表示)
    public static final int AT_MOST     = 2 << MODE_SHIFT;  

    //创建一个整形值,其高两位代表mode类型,其余30位代表长或宽的实际值。可以是WRAP_CONTENT、MATCH_PARENT或具体大小exactly size
    public static int makeMeasureSpec(int size, int mode) {
        return size + mode;
    }
    //获取模式,与运算
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    //获取长或宽的实际值,与运算
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }  

} 

Layout

  • 确定子视图在相当于父视图的位置(注意margin和padding)
  • ViewGroup的onLayout是抽象的,其子类必须实现
  • View的onLayout是空实现
  • 此时测量已完成,可通过getMeasuredWidth() 和 getMeasuredHeight()获得其宽度和高度
  • 不要在onDraw和onLayout中创建对象,因为这两个方法会被频繁调用

    到这里看一张总结性的图

    图片来自:Android 开源项目源码解析 公共技术点中的 View 绘制流程

LayoutParams

  • 它是一个ViewGroup的内部类
  • ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 的子类。比如 RelativeLayout 拥有的 ViewGroup.LayoutParams 的子类 RelativeLayoutParams
  • getLayoutParams() 方法得到是其所在父视图类型的 LayoutParams,比如 View 的父控件为 RelativeLayout,那么得到的 LayoutParams 类型为 RelativeLayoutParams
  • 有时我们需要使用 view.getLayoutParams() 方法获取一个视图 LayoutParams ,然后进行强转,但由于不知道其具体类型,可能会导致强转错误
  • 自定义View的margin等属性在LayoutParams 指定

Draw

  • 自定义View绘制过程需要重写onDraw方法
  • 自定义ViewGroup在dispatchDraw中发起对子视图的绘制,不应该对该函数重写
  • onDraw中调用相关绘制函数进行绘制

invalidate

  • 请求重绘View
  • 视图大小没有变化就不会调用layout过程
  • 只重新绘制那些调用了invalidate()方法的 View
  • 如果要在UI线程中重绘请使用postInvalidate()方法

requestLayout

  • 当布局变化的时候,比如方向变化,尺寸的变化,会调用该方法
  • 它会触发measure和layout过程,但不会进行 draw过程

最佳实践

以上都是理论知识,也差不多是对多篇文章的总结性内容。下面开始实现一个自定义View和ViewGroup

自定义View

其实自定义View的大部分逻辑都是在onDraw上,onLayout基本上无需重新,onMeasure需要实现测量逻辑。

下面是一个简单的毫无任何作用的自定义View,其唯一目的就是演示onDraw和onMeasure

package cn.edu.zafu.sourcedemo;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

/**
 * Created by lizhangqu on 2015/5/3.
 */
public class CustomView extends View {
    private Paint paint=null;
    private Rect rect=null;
    private  int bgColor=Color.parseColor("#673AB7");//写死背景色,实际是自定义属性
    private int minContentWidth=50;//最小内容宽度,不包含内边距
    private int minContentHeight=50;//最小内容高度,不包含内边距

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    /*初始化*/
    private void init() {
        paint=new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setColor(bgColor);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //其实所有逻辑可以简单调用resolveSize函数进行测量,这里自己实现一遍,理清思路
        //获得宽度和高度的mode和size
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //最终的宽高存在这两个变量中
        int width;
        int height;
        if (widthMode == MeasureSpec.EXACTLY) {
            // 父视图指定了大小
            width = widthSize;
        } else {
            //父视图指定必须在这个大小内
            //注意内边距,再加上自身需要的宽度
            width=getPaddingLeft()+getPaddingRight()+minContentWidth;
            if (widthMode == MeasureSpec.AT_MOST) {
                //如果是AT_MOST,必须在父控件指定的范围内,取width和widthSize中小的那个
                width = Math.min(width, widthSize);
            }
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            // 父视图指定了大小
            height = widthSize;
        } else {
            //父视图指定必须在这个大小内
            //注意内边距,再加上自身需要的高度
            height =getPaddingTop()+getPaddingBottom()+minContentHeight;
            if (heightMode == MeasureSpec.AT_MOST) {
                //如果是AT_MOST,必须在父控件指定的范围内,取width和widthSize中小的那个
                height = Math.min(height, heightSize);
            }
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        rect=new Rect(getPaddingLeft(),getPaddingTop(),getMeasuredWidth()-getPaddingRight(),getMeasuredHeight()-getPaddingBottom());//绘制的时候注意内边距
        canvas.drawRect(rect,paint);
    }
}

自定义Viewgroup

实现一个纵向排布子View的ViewGroup,效果如图所示,见代码,解释看注释

package cn.edu.zafu.sourcedemo;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by lizhangqu on 2015/5/3.
 */
public class CustomViewGroup extends ViewGroup {
    public CustomViewGroup(Context context) {
        this(context, null);
    }

    public CustomViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //重写onLayout抽象方法
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        MyLayoutParams lp = null;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //获得当前View
            lp = (MyLayoutParams) child.getLayoutParams();
            //获得LayoutParams,强制转换为MyLayoutParams
            child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y
                    + child.getMeasuredHeight());
            //调用当前View的layout方法进行布局
        }
    }

    //重写onMeasure实现测量逻辑
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = 0;
        int lastWidth = 0;
        int height = getPaddingTop();

        final int count = getChildCount();
        //获得子View个数
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //获得当前子View
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            //测量子View,必须调用
            MyLayoutParams lp = (MyLayoutParams) child.getLayoutParams();
            //获得LayoutParams
            width = Math.max(width, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            //比较当前View与之前的View宽度,取大者,注意这个宽度包含了margin
            lp.x = getPaddingLeft() + lp.leftMargin;
            //设置当前View的x左边
            lp.y = height + lp.topMargin;
            //设置当前View的y左边
            height = height + lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin;
            //累加高度
        }
        width=width+getPaddingLeft() + getPaddingRight();
        //加上左右内边距
        height = height + getPaddingBottom();
        //加上下边界
        setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
        //设置宽高,resolveSize方法会根据尺寸大小和MeasureSpec计算最佳大小

    }

    //重写生成LayoutParams的三个方法
    @Override
    public MyLayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    //重写生成LayoutParams的三个方法
    @Override
    protected MyLayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
    }

    //重写生成LayoutParams的三个方法
    @Override
    protected MyLayoutParams generateLayoutParams(LayoutParams p) {
        return new MyLayoutParams(p.width, p.height);
    }

    //继承MarginLayoutParams实现自己的LayoutParams,x,y代表控件的左边和上边左边
    public static class MyLayoutParams extends MarginLayoutParams {
        public int x;//左
        public int y;//上

        public MyLayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public MyLayoutParams(int w, int h) {
            super(w, h);
        }
    }
}

布局界面如下

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <cn.edu.zafu.sourcedemo.CustomViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="50dp"
        android:padding="20dp"
        android:background="#ffff00"
        >
        <Button
            android:layout_width="20dp"
            android:layout_height="20dp"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="5dp"
            android:layout_marginLeft="5dp"
            android:layout_marginRight="5dp"
            android:background="#ff0000"
            />
        <Button
            android:layout_width="24dp"
            android:layout_height="15dp"
            android:background="#00ff00"
            />
        <Button
            android:layout_width="10dp"
            android:layout_height="30dp"
            android:background="#0000ff"
            />
    </cn.edu.zafu.sourcedemo.CustomViewGroup>
</LinearLayout>

参考文章

  1. Android 开源项目源码解析 公共技术点中的 View 绘制流程 前面理论部分基本从这篇文章中提取来
时间: 2024-10-19 02:40:50

Android View绘制及实践的相关文章

简单研究Android View绘制一

2015-07-27 16:52:58 一.如何通过继承ViewGroup来实现自定义View?首先得搞清楚Android时如何绘制View的,参考Android官方文档:How Android Draws Views 以下翻译摘自:http://blog.csdn.net/linghu_java/article/details/23882681,这也是一片好文章,推荐大家看看- When an Activity receives focus, it will be requested to d

简单研究Android View绘制三 布局过程

2015-07-28 17:29:19 这一篇主要看看布局过程 一.布局过程肯定要不可避免的涉及到layout()和onLayout()方法,这两个方法都是定义在View.java中,源码如下: 1 /** 2 * Assign a size and position to a view and all of its 3 * descendants 4 * 5 * <p>This is the second phase of the layout mechanism. 6 * (The fir

[Android][转]Android View绘制13问13答

转自:http://www.androidchina.net/4458.html 1.view的绘制流程分几步,从哪开始?哪个过程结束以后能看到view? 答:从ViewRoot的performTraversals开始,经过measure,layout,draw 三个流程.draw流程结束以后就可以在屏幕上看到view了. 2.view的测量宽高和实际宽高有区别吗? 答:基本上百分之99的情况下都是可以认为没有区别的.有两种情况,有区别.第一种 就是有的时候会因为某些原因 view会多次测量,那

Android View 绘制过程

Android的View绘制是从根节点(Activity是DecorView)开始,他是一个自上而下的过程.View的绘制经历三个过程:Measure.Layout.Draw.基本流程如下图: performTraversals函数,具体的可以参考一下源代码: 1 private void performTraversals() { 2 final View host = mView; 3 ... 4 host.measure(childWidthMeasureSpec, childHeight

Android View 绘制流程(Draw) 完全解析

前言 前几篇文章,笔者分别讲述了DecorView,measure,layout流程等,接下来将详细分析三大工作流程的最后一个流程--绘制流程.测量流程决定了View的大小,布局流程决定了View的位置,那么绘制流程将决定View的样子,一个View该显示什么由绘制流程完成.以下源码均取自Android API 21. 从performDraw说起 前面几篇文章提到,三大工作流程始于ViewRootImpl#performTraversals,在这个方法内部会分别调用performMeasure

Android View绘制机制

------------------------------------------------------------------------------ GitHub:lightSky    微博:    light_sky, 即时分享最新技术,欢迎关注 ------------------------------------------------------------------------------ 前言 该篇文章来自一个开源项目android-open-project-analy

Android View绘制流程

框架分析 在之前的下拉刷新中,小结过触屏消息先到WindowManagerService(Wms)然后顺次传递给ViewRoot(派生自Handler),经decor view到Activity再传递给指定的View,这次整理View的绘制流程,通过源码可知,这个过程应该没有涉及到IPC(或者我没有发现),需要绘制时在UI线程中通过ViewRoot发送一个异步请求消息,然后ViewRoot自己接收并不处理这个消息. 在正式进入View绘制之前,首先需要明确一下Android UI的架构组成,偷图

简单研究Android View绘制二 LayoutParams

2015-07-28 17:23:20 本篇是关于LayoutParams相关 ViewGroup.LayoutParams文档解释如下: LayoutParams are used by views to tell their parents how they want to be laid out. See ViewGroup Layout Attributes for a list of all child view attributes that this class supports.

Android View绘制知识问答

1.View的绘制流程分几步,从哪开始?哪个过程结束以后能看到view? 答:从ViewRoot的performTraversals开始,经过measure,layout,draw 三个流程.draw流程结束以后就可以在屏幕上看到view了. 2.view的测量宽高和实际宽高有区别吗? 答:基本上百分之99的情况下都是可以认为没有区别的.有两种情况,有区别.第一种 就是有的时候会因为某些原因 view会多次测量,那第一次测量的宽高 肯定和最后实际的宽高 是不一定相等的,但是在这种情况下 最后一次