自定义 View 三步骤

自定义View三步骤,即:onMeasure()(测量),onLayout()(布局),onDraw()(绘制)。

onMeasure()

首先我们需要弄清楚,自定义 View 为什么需要重新测量。正常情况下,我们直接在 XML 布局文件中定义好 View 的宽高,然后让自定义 View 在此宽高的区域内显示即可。但是为了更好地兼容不同尺寸的屏幕,Android 系统提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。它们分别代表自适应大小和填充父视图的大小,但是这两个属性并没有指定具体的大小,因此我们需要在 onMeasure 方法中过滤出这两种情况,真正的测量出自定义 View 应该显示的宽高大小。

/**
     * 测量
     * @param widthMeasureSpec 包含测量模式和宽度信息
     * @param heightMeasureSpec 包含测量模式和高度信息
     * int型数据,采用二进制,占32个bit。其中前2个bit为测量模式。后30个bit为测量数据(尺寸大小)。
     * 这里测量出的尺寸大小,并不是View的最终大小,而是父View提供的参考大小。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e("TAG","onMeasure()");

    }

MeasureSpec:

  • 测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽高。只是测量宽高,不一定等于实际宽高。
  • MeasureSpec代表一个32位int值(避免过多的对象内存分配),高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)。并提供了打包和解包方法。

SpecMode

说明

UNSPECIFIED

父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。这种情况一般用于系统内部,表示一种测量的状态。

EXACTLY

父容器已检测出View所需要的精确大小,就是SpecSize所指定的值。它对应于LayoutParams中的Match_parent和具体数值这两种模式。

AT_MOST

父容器指定SpecSize,View不能大于这个值。它对应于LayoutParams中的wrap_content。

MeasureSpec和LayoutParams的对应关系:

  • 在测量时,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,再根据MeasureSpec来确定View测量后宽高。(需要注意的是,决定MeasureSpec的有两点。即LayoutParams和父容器约束)
  • 对于顶级View(DecorView)和普通View,MeasureSpec的转换过程略有不同。除了自身的LayoutParams这点,前者由窗口的尺寸,后者由父容器的MeasureSpec来约束决定。MeasureSpec一定确定,onMeasure中就可以确定View的测量宽高。

当继承 View 或 ViewGroup 时,如果没有复写 onMeasure 方法时,默认使用父类也就是 View 中的实现,View 中的 onMeasure 默认实现如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  // setMeasuredDimension 是一个非常重要的方法,这个方法传入的值直接决定 View 的宽高,也就是说如果调用 setMeasuredDimension(100,200),最终 View 就显示宽 100 * 高 200 的矩形范围。
  // getDefaultSize 返回的是默认大小,默认为父视图的剩余可用空间。
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

查看 setMeasuredDimension 方法。其它现有控件的 onMeasure 方法的 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 经过一系列计算,最后也是调用到 setMeasuredDimension 方法。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

一种情况是:在 XML 中指定的是 wrap_content,但是实际使用的宽高值却是父视图的剩余可用空间,从 getDefaultSize 方法中可以看出是整个屏幕的宽高。解决方法只要复写 onMeasure,过滤出 wrap_content 的情况,并主动调用 setMeasuredDimension 方法设置正确的宽高即可:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // 判断是 wrap_content 的测量模式
        if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode){
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
//            int size = measuredWidth > measuredHeight ? measuredHeight : measuredWidth;
            // 将宽高设置为传入宽高的最小值
            int size = Math.min(measuredWidth, measuredHeight);
            // 设置 View 实际大小
            setMeasuredDimension(size,size);
        }
    }

ViewGroup 中的 onMeasure

如果自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己的宽高之前,需要先确定其内部子 View 的所占大小,然后才能确定自己的大小。比如 LinearLayout 的宽高为 wrap_content 表示由子控件的大小决定,那 LinearLayout 的最终宽度由其内部最大的子 View 宽度决定。

onLayout()

/**
     * 布局
     * @param changed 当前View的大小和位置改变了
     * @param left 左部位置(相对于父视图)
     * @param top 顶部位置(相对于父视图)
     * @param right 右部位置(相对于父视图)
     * @param bottom 底部位置(相对于父视图)
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.e("TAG","onLayout()");
        // 一般在自定义ViewGroup时使用,来定义子View的位置。

    }

这里扩展一些View位置相关知识点:

  • View的位置参数:
    由View的四个属性决定;left(左上角横坐标),right(右下角横坐标),top(左上角纵坐标),bottom(右下角纵坐标)。是一种相对坐标,相对父容器。
    四个参数对应View源码中的mLeft等四个成员变量,通过getLeft()等方法来获取。
  • View的宽高和坐标的关系:
    width=right-left;
    height=bottom-top;
  • 从Android3.0开始,新增额外的四个参数:
    x,y,translationX,translationY。前两者是View左上角坐标,后两者是View左上角相对于父容器的偏移量,并且默认值0。和四个基本位置参数一样,也提供了get/set方法。
  • 换算关系如下;
    x=left+translationX;
    y=top+translationY;
    注意;在View平移过程中,top和left表示的是原始左上角的位置信息,值并不会改变。发生改变的是;x,y,translationX,translationY这四个参数。

它是一个抽象方法,也就是说每一个自定义 ViewGroup 都必须主动实现如何排布子 View,具体就是遍历每一个子 View,调用 child.(l, t, r, b) 方法来为每个子 View 设置具体的布局位置。四个参数分别代表左上右下的坐标位置,一个简易的 FlowLayout 实现如下:

在大多数 App 的搜索界面经常会使用 FlowLayout 来展示历史搜索记录或者热门搜索项。
FlowLayout 的每一行上的 item 个数不一定,当每行的 item 累计宽度超过可用总宽度,则需要重启一行摆放 item 项。因此我们需要在 onMeasure 方法中主动的分行计算出 FlowLayout 的最终高度,如下所示:

public class FlowLayout extends ViewGroup {

    //存放容器中所有的View
    private List<List<View>> mAllViews = new ArrayList<List<View>>();
    //存放每一行最高View的高度
    private List<Integer> mPerLineMaxHeight = new ArrayList<>();

    public FlowLayout(Context context) {
        super(context);
    }

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

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

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        super.generateLayoutParams(p);
        return new MarginLayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    /**
    * 测量控件的宽和高
    *
    * onMeasure 方法的主要目的有 2 个:
    * 1.调用 measureChild 方法递归测量子 View;
    * 2.通过叠加每一行的高度,计算出最终 FlowLayout 的最终高度 totalHeight。
    */ 
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获得宽高的测量模式和测量值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //获得容器中子View的个数
        int childCount = getChildCount();
        //记录每一行View的总宽度
        int totalLineWidth = 0;
        //记录每一行最高View的高度
        int perLineMaxHeight = 0;
        //记录当前ViewGroup的总高度
        int totalHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //对子View进行测量
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            //获得子View的测量宽度
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //获得子View的测量高度
            int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (totalLineWidth + childWidth > widthSize) {
                //统计总高度
                totalHeight += perLineMaxHeight;
                //开启新的一行
                totalLineWidth = childWidth;
                perLineMaxHeight = childHeight;
            } else {
                //记录每一行的总宽度
                totalLineWidth += childWidth;
                //比较每一行最高的View
                perLineMaxHeight = Math.max(perLineMaxHeight, childHeight);
            }
            //当该View已是最后一个View时,将该行最大高度添加到totalHeight中
            if (i == childCount - 1) {
                totalHeight += perLineMaxHeight;
            }
        }
        //如果高度的测量模式是EXACTLY,则高度用测量值,否则用计算出来的总高度(这时高度的设置为wrap_content)
        heightSize = heightMode == MeasureSpec.EXACTLY ? heightSize : totalHeight;
        setMeasuredDimension(widthSize, heightSize);
    }

    //摆放控件
    //1.表示该ViewGroup的大小或者位置是否发生变化
    //2.3.4.5.控件的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        mAllViews.clear();
        mPerLineMaxHeight.clear();

        //存放每一行的子View
        List<View> lineViews = new ArrayList<>();
        //记录每一行已存放View的总宽度
        int totalLineWidth = 0;

        //记录每一行最高View的高度
        int lineMaxHeight = 0;

        /*************遍历所有View,将View添加到List<List<View>>集合中***************/
        //获得子View的总个数
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            int childWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (totalLineWidth + childWidth > getWidth()) {
                mAllViews.add(lineViews);
                mPerLineMaxHeight.add(lineMaxHeight);
                //开启新的一行
                totalLineWidth = 0;
                lineMaxHeight = 0;
                lineViews = new ArrayList<>();
            }
            totalLineWidth += childWidth;
            lineViews.add(childView);
            lineMaxHeight = Math.max(lineMaxHeight, childHeight);
        }
        //单独处理最后一行
        mAllViews.add(lineViews);
        mPerLineMaxHeight.add(lineMaxHeight);
        /************遍历集合中的所有View并显示出来*****************/
        //表示一个View和父容器左边的距离
        int mLeft = 0;
        //表示View和父容器顶部的距离
        int mTop = 0;

        for (int i = 0; i < mAllViews.size(); i++) {
            //获得每一行的所有View
            lineViews = mAllViews.get(i);
            lineMaxHeight = mPerLineMaxHeight.get(i);
            for (int j = 0; j < lineViews.size(); j++) {
                View childView = lineViews.get(j);
                MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
                int leftChild = mLeft + lp.leftMargin;
                int topChild = mTop + lp.topMargin;
                int rightChild = leftChild + childView.getMeasuredWidth();
                int bottomChild = topChild + childView.getMeasuredHeight();
                //四个参数分别表示View的左上角和右下角
                childView.layout(leftChild, topChild, rightChild, bottomChild);
                mLeft += lp.leftMargin + childView.getMeasuredWidth() + lp.rightMargin;
            }
            mLeft = 0;
            mTop += lineMaxHeight;
        }

    }
}

这样一个自定义布局就定义好了,接下来可以 根据需要添加相应样式的子 View。

onDraw()

onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这块画布上可以绘制各种类型的 UI 元素。

/**
     * 绘制
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.e("TAG","onDraw()");
    }

系统提供了一系列 Canvas 操作方法,如下:

void drawRect(RectF rect,Paint paint): // 绘制矩形区域
void drawOval(RectF oval,Paint paint): // 绘制椭圆
void drawCircle(float cx,float cy,float radius,Paint paint): // 绘制圆形
void drawArc(RectF oval,float startAngle,float sweepAngle,boolean useCenter,Paint paint): // 绘制弧形
void drawPath(Path path,Paint paint): // 绘制 Path 路径
void drawLine(float startX,float startY,float stopX,float stopY,Paint paint): // 绘制连线
void drawPoint(float x,float y,Paint paint): // 绘制点

Paint

Canvas 中每一个绘制操作都需要传入一个 Paint 对象。Paint 就相当于一个画笔,因为 Canvas(画布)本身只是呈现的一个载体,真正绘制出来的效果则取决于Paint(画笔)。可以通过设置画笔的各种属性,来实现不同绘制效果。

setStyle(Style style): // 设置绘制模式
setColor(int color) : // 设置颜色
setAlpha(int a): // 设置透明度
setShader(Shader shader): // 设置 Paint 的填充效果
setStrokeWidth(float width): // 设置线条宽度
setTextSize(float textSize): // 设置文字大小
setAntiAlias(boolean aa): // 设置抗锯齿开关
setDither(boolean dither): // 设置防抖动开关

例如 canvas.drawCircle(centerX, centerY, r, paint); 是在坐标 centerX 和 centerY 处绘制一个半径为 r 的圆,但具体圆是什么样子的则由 paint 来决定。

示例:绘制一个简易的圆形进度条控件。

public class PieImageView extends View {

    private static final int MAX_PROGRESS = 100;
    private Paint mArcPaint;
    private RectF mBound;
    private Paint mCirclePaint;
    private int mProgress = 0;

    public PieImageView(Context context) {
        this(context, null, 0);
    }

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

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

    public void setProgress(@IntRange(from = 0, to = MAX_PROGRESS) int mProgress) {
        this.mProgress = mProgress;
        ViewCompat.postInvalidateOnAnimation(this);
    }

    private void init() {
        mArcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mArcPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mArcPaint.setStrokeWidth(dpToPixel(0.1f, getContext()));
        mArcPaint.setColor(Color.RED);

        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setStyle(Paint.Style.STROKE);
        mCirclePaint.setStrokeWidth(dpToPixel(2, getContext()));
        mCirclePaint.setColor(Color.argb(120, 0xff, 0xff, 0xff));
        mBound = new RectF();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        // 判断是wrap_content的测量模式。
        // 如果没做处理,将 PieImageView 的宽高设置为 wrap_content(也就是自适应),PieImageView 不会正常显示。它会占满屏幕空间。
        if (MeasureSpec.AT_MOST == widthMode || MeasureSpec.AT_MOST == heightMode) {
            int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
            int measuredHeight = MeasureSpec.getSize(heightMeasureSpec);
            // 将宽高设置为传入宽高的最小值
            int size = measuredWidth > measuredHeight ? measuredHeight : measuredWidth;
            // 调用setMeasuredDimension设置View实际大小
            setMeasuredDimension(size, size);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int min = Math.min(w, h);
        int max = w + h - min;
        int r = Math.min(w, h) / 3;
        mBound.set((max >> 1) - r, (min >> 1) - r, (max >> 1) + r, (min >> 1) + r);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mProgress != MAX_PROGRESS && mProgress != 0) {
            float mAngle = mProgress * 360f / MAX_PROGRESS;
            canvas.drawArc(mBound, 270, mAngle, true, mArcPaint);
            canvas.drawCircle(mBound.centerX(), mBound.centerY(), mBound.height() / 2, mCirclePaint);
        }
    }

    private float scale = 0;

    private int dpToPixel(float dp, Context context) {
        if (scale == 0) {
            scale = context.getResources().getDisplayMetrics().density;
        }
        return (int) (dp * scale);
    }
}
public class PieImageActivity extends AppCompatActivity {

    PieImageView pieImageView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pie_image);

//        pieImageView = findViewById(R.id.pieImageView);
//        pieImageView.setProgress(45);
    }
}


示例

效果图:

完整代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:orientation="vertical"
    android:gravity="center"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/darker_gray"
    tools:context="com.example.xwxwaa.myapplication.MainActivity">

    <!--记录两个问题-->
    <!--1.这里的父布局是LinearLayout-->
    <!--如果换成RelativeLayout,效果还有问题。-->
    <!--2.MyCustomViewGroup的宽高如果是wrap_content-->
    <!--则子View的宽高设置成match_parent无效。-->

    <com.example.xwxwaa.myapplication.MyCustomViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@color/colorPrimaryDark"
        android:layout_marginLeft="2dp"
        android:layout_marginTop="2dp"
        android:paddingRight="5dp"
        android:paddingBottom="5dp">

        <TextView
            android:layout_marginLeft="2dp"
            android:layout_marginTop="2dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自定义View"
            android:background="@color/colorAccent"/>

        <!--app为命名空间,为了使用自定义属性-->
        <com.example.xwxwaa.myapplication.MyCustomView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="2dp"
            android:layout_marginTop="2dp"
            android:paddingRight="5dp"
            android:paddingBottom="5dp"
            app:default_size="100dp"
            app:default_color="@color/colorPrimaryDark"
            />
    </com.example.xwxwaa.myapplication.MyCustomViewGroup>

</LinearLayout>
public class MyCustomView extends View{

    private int defaultSize;
    private int defaultColor;
    private Paint paint ;

    /**
     * 需要两个构造参数
     * @param mContext
     */
    public MyCustomView(Context mContext){
        super(mContext);
        init();
    }

    public MyCustomView(Context mContext, AttributeSet attributeSet){
        super(mContext,attributeSet);
        // 通过它,取出在xml中,由命名空间定义的属性值
        // 第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        // 即属性集合的标签,在R文件中名称为R.styleable+name
        TypedArray a = mContext.obtainStyledAttributes(attributeSet, R.styleable.MyCustomView);

        //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defaultSize = a.getDimensionPixelSize(R.styleable.MyCustomView_default_size, 100);
        defaultColor = a.getColor(R.styleable.MyCustomView_default_color,Color.BLUE);

        //最后将TypedArray对象回收
        a.recycle();

        init();
    }
    private void init(){
        // 初始化Paint
        paint = new Paint();
        paint.setColor(defaultColor);
        paint.setStyle(Paint.Style.STROKE);//设置圆为空心
        paint.setStrokeWidth(3.0f);//设置线宽
    }
    /**
     * 测量
     * @param widthMeasureSpec 包含测量模式和宽度信息
     * @param heightMeasureSpec 包含测量模式和高度信息
     * int型数据占32个bit。其中前2个bit为测量模式。后30个bit为测量数据(尺寸大小)。
     * 这里测量出的尺寸大小,并不是View的最终大小,而是父View提供的参考大小。
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 定义宽高尺寸
        int width = getSize(widthMeasureSpec);
        int height = getSize(heightMeasureSpec);

        // 实现一个正方形,取小值
        int sideLength =Math.min(width,height);

        // 设置View宽高
        setMeasuredDimension(sideLength,sideLength);
    }

    private int getSize(int measureSpec){
        int mySize = defaultSize;

        // 可通过下面的方法,来获取测量模式和尺寸大小。
        // 注意这里的specSize值单位是px,而我们xml中一般为dp。
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode){
            case MeasureSpec.UNSPECIFIED:
                // 父容器不对View有任何限制,这种情况一般用于系统内部,表示一种测量的状态。
                // 一般也不需要我们处理。。可以看ScrollView或列表相关组件。
                Log.e("TAG","测量模式;MeasureSpec.UNSPECIFIED");
                break;
            case MeasureSpec.EXACTLY:
                // 父容器已检测出View所需要的精确大小,就是SpecSize所指定的值。
                // 当xml中,宽或高指定为match_parent或具体数值,会走这里。
                mySize = specSize;
                Log.e("TAG","测量模式;MeasureSpec.EXACTLY");
                break;
            case MeasureSpec.AT_MOST:
                // View的尺寸大小,不能大于父View指定的SpecSize。
                // 当xml中,宽或高指定为wrap_content时,会走这里。
                mySize = specSize/2;
                Log.e("TAG","测量模式;MeasureSpec.AT_MOST");
                break;
            default:
                break;
        }
        return mySize;
    }
    /**
     * 绘制
     * @param canvas 画布
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 接下来绘制一个正圆。
        // 需要知道,圆的半径,和圆点坐标。
        int r = getMeasuredWidth() ;
        int centerX ;
        int centerY ;

        int paddingL = getPaddingLeft();
        int paddingT = getPaddingTop();
        int paddingR = getPaddingRight();
        int paddingB = getPaddingBottom();

        // 计算View减去padding后的可用宽高
        int canUsedWidth = r - paddingL - paddingR;
        int canUsedHeight = r - paddingT - paddingB;

        // 圆心坐标
        centerX = canUsedWidth / 2 + paddingL;
        centerY = canUsedHeight / 2 + paddingT;
        // 取两者最小值作为圆的直径
        int minSize = Math.min(canUsedWidth, canUsedHeight);
        // 绘制一个圆
        canvas.drawColor(Color.WHITE);//设置画布颜色
        canvas.drawCircle(centerX,centerY,minSize / 2,paint);
    }
    /**
     * 布局
     * @param changed 当前View的大小和位置改变了
     * @param left 左部位置(相对于父视图)
     * @param top 顶部位置(相对于父视图)
     * @param right 右部位置(相对于父视图)
     * @param bottom 底部位置(相对于父视图)
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        // 一般在自定义ViewGroup时使用,来定义子View的位置。

    }
}
public class MyCustomViewGroup extends ViewGroup{

    // 内边距
    private int paddingL ;
    private int paddingT ;
    private int paddingR ;
    private int paddingB ;
    // 外边距
    private int marginL;
    private int marginT;
    private int marginR;
    private int marginB;

    public MyCustomViewGroup (Context mContext){
        super(mContext);

    }

    public MyCustomViewGroup(Context mContext, AttributeSet attributeSet){
        super(mContext,attributeSet);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //  宽高的测量模式和尺寸
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        // 获取内边距
        paddingL = getPaddingLeft();
        paddingT = getPaddingTop();
        paddingR = getPaddingRight();
        paddingB = getPaddingBottom();
        // 初始化外边距,因为测量不止一次。
        marginL = 0;
        marginT = 0;
        marginR = 0;
        marginB = 0;

        // 测量所有子View的宽高。它会触发每个子View的onMeasure()。
        // measureChildren(widthMeasureSpec,heightMeasureSpec);

        // measureChild是对单个View进行测量。
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            marginL = Math.max(0,lp.leftMargin);//在本例中找出最大的左边距
            marginT += lp.topMargin;//在本例中求出所有的上边距之和
            marginR = Math.max(0,lp.rightMargin);//在本例中找出最大的右边距
            marginB += lp.bottomMargin;//在本例中求出所有的下边距之和
        }

        if (childCount == 0){
            // 没有子View
            setMeasuredDimension(0,0);
        }else {
            // 最大宽度,加上内外边距
            int viewGroupWidth = paddingL + getChildMaxWidth() + paddingR +marginL+marginR;
            // 高度之和,加上内外边距
            int viewGroupHeight = paddingT + getChildTotalHeight() + paddingB+marginT+marginB;
            // 选小值
            int  resultWidth = Math.min(viewGroupWidth, widthSize);
            int  resultHeight = Math.min(viewGroupHeight, heightSize);

            if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
                // 如果父布局宽高都是wrap_content,只会走这个方法。
                // 宽高都是包裹内容,用于处理ViewGroup的wrap_content情况
                setMeasuredDimension(resultWidth,resultHeight);
            }else if (widthMode == MeasureSpec.AT_MOST){
                // 宽度是包裹内容
                setMeasuredDimension(resultWidth,heightSize);
            }else if (heightMode == MeasureSpec.AT_MOST){
                // 高度是包裹内容
                setMeasuredDimension(widthSize,resultHeight);
            }
            // 这里如果没进上面的条件判断中,super.onMeasure()会调用setMeasuredDimension()的,默认占满剩余可用空间。
        }
    }

    /**
     * 获取所有子View的最大宽度
     * @return
     */
    private int getChildMaxWidth(){
        int count = getChildCount();
        int maxWidth = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getMeasuredWidth() > maxWidth){
                maxWidth = child.getMeasuredWidth();
            }
        }
        return maxWidth;
    }

    /**
     * 获取所有子View的高度之和
     * @return
     */
    private int getChildTotalHeight(){
        int count = getChildCount();
        int totalHeight = 0;
        for (int i = 0; i < count; i++) {
            View view = getChildAt(i);
            totalHeight += view.getMeasuredHeight();
        }
        return totalHeight;
    }


    @Override
    protected void onLayout(boolean c, int l, int t, int r, int b) {
        int count = getChildCount();
        int coordHeight = paddingT;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams();
            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();
            int coordWidth = paddingL+ lp.leftMargin;
            coordHeight += lp.topMargin;
            child.layout(coordWidth,coordHeight,coordWidth+width,coordHeight+height);
            coordHeight+=height+lp.bottomMargin;
        }
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(),attrs);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--自定义属性-->
    <declare-styleable name="MyCustomView">
        <!--dimension是一个包含单位(dp、sp、px等)的尺寸,可用于定义视图的宽度、字号等。-->
        <attr name="default_size" format="dimension" />
        <attr name="default_color" format="color" />
    </declare-styleable>
</resources>

优化

比如重复绘制,还有大图长图优化。

加载长图大图优化:

  • 压缩图片
  • 沿着对角线缩放
  • 加载屏幕能够看见的区域
  • 复用上一个 bitmap 区域的内存
  • 处理滑动

对覆盖区域的 View ,一定要避免不要重复绘制。比如竞技棋牌类型的 APP 。打斗地主的时候,很多扑克都是覆盖的,那么就不能每张图片进行绘制,一定要先计算显示的区域,把不需要的截取,然后在绘制。


备注

参考资料:

Android 开发艺术探索