Android进阶之自定义控件二

了解自定义控件的三大流程(measure、layout、draw)

在上一篇博客中我们大致介绍了一下View和ViewGroup,接下来我们就学习一下自定义控件的三大流程,为我们打下夯实的基础。(本博客主要参考《Android群英传》和《Android开发艺术探索》,大家也可以去阅读这两本书籍)

自定义控件三大流程简介

什么是自定义控件的三大流程,相信正在阅读这篇博客的你肯定接触过自定义控件,也见过onMeasure()、onLayout()和onDraw()这三个方法,自定义控件的三大流程就是这三个方法了,下面就让我们循序渐进的了解一下这三个方法。

measure

onMeasure方法的作用通俗点讲就是确认View位置,而对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各子元素再递归去执行这个过程。下面就依次来讲解:

1、View的测量过程

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

我们在onMeasure上按住ctrl后鼠标左击,进入 super.onMeasure的源码,如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

这里我们大致的可以明白onMeasure方法是通过setMeasuredDimension来控制控件大小的,我们不需要深入的去了解,我们在看一下getDefaultSize方法返回的是什么?如何来控制控件大小的,代码如下:

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这段代码相信大家很容易理解,当然我们首先需要了解一下MeasureSpec这个类,MeasureSpec其实是一个32位的int值,高2位为测量模式,低30位为测量的大小,内部封装了一些获取测量模式和测量大小的位运算。测量模式分一下三种:

UNSPECIFIED

中文翻译为未特别指定(规定)的,既父容器不对View有任何限制,View想要多大就多大

EXACTLY

中文翻译为精确地,即精确值模式,但我们将控件的layout_width或者layout_height指定为固定值,如“10dp”,或者为match_parent时使用该模式

AT_MOST

最大值模式,当控件宽高指定为wrap_content时,使用改模式

通过上面的分析我们可以很轻松的写出我们自己想要的测量方法,下面给出一个示例:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
    }

    public static int getMeasureSize(int measureSpec) {
        int size = 200;
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

2、ViewGroup的measure过程

ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但它提供了一个measureChildren的方法,同样的我们依次地大致阅读以下源码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }


相信大家也能很容易的读懂,原理就是遍历ViewGroup内所有的View去调用View的measure方法。

layout

Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中子view的onLayout方法有会被调用,这段话大家可能看的云里雾里,下面给出LinearLayout的onLayout的源码,相信大家就一目了然了:

1、写一个继承LinearLayout的类,重写onLayout方法

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
     super.onLayout(changed, l, t, r, b);
 }

2、进入super.onLayout方法

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

3、从源码很明显可以看出线性布局分垂直和水平方向,我们以垂直方向为例,进入layoutVertical方法:

这里代码量很大,大家没必要看的非常仔细,首先找到for (int i = 0; i < count; i++)这个for循环,大家肯定明白这是遍历LinearLayout中的子View,之后调用setChildFrame这个方法确定子View的位置

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               // mTotalLength contains the padding already
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

               // mTotalLength contains the padding already
           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

4、接下来进入setChildFrame方法看如何实现确定子View的位置

private void setChildFrame(View child, int left, int top, int width, int height) {        
        child.layout(left, top, left + width, top + height);
    }

到这里大家就稍微明白了,原来是调用子View的layout方法来确定子View的位置

5、进入layout方法一探究竟

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

这里代码量也有些多,相信仔细看了的肯定也能读懂,没懂也没关系,不用看得那么仔细,首先找到boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
setOpticalFrame和setFrame从参数我们就大致能猜到是确定子View的位置的,之后再找到onLayout(changed, l, t, r, b);这个方法,这样就到了子View的onLayout方法中,若子View是一个ViewGroup的话又可以确定子View的位置了,这样就可以确定一个View树上所有View的位置。

相信看到这里大家都恍然大悟了,希望大家在看博客的时候也打开eclipse或者as简单的阅读以下源码,参考源码我相信大家聪慧的大脑,肯定能玩转自定义View的layout。

draw

当View的位置确定好之后我们就要开始绘制View了,这里我们就需要了解一下Paint和Canvas这两个对象了,相信大家已经非常熟悉了,这里稍微做一下总结:

1.Paint(画笔)类
要绘制图形,首先得调整画笔,按照自己的开发需要设置画笔的相关属性。Pain类的常用属性设置方法如下:

  setAntiAlias(); //设置画笔的锯齿效果

  setColor(); //设置画笔的颜色

  setARGB(); //设置画笔的A、R、G、B值

  setAlpha(); //设置画笔的Alpha值

  setTextSize(); //设置字体的尺寸

  setStyle(); //设置画笔的风格(空心或实心)

  setStrokeWidth(); //设置空心边框的宽度

  getColor(); //获取画笔的颜色
  
2.Canvas(画布)类

  画笔属性设置好之后,还需要将图像绘制到画布上。Canvas类可以用来实现各种图形的绘制工作,如绘制直线、矩形、圆等等。Canvas绘制常用图形的方法如下:

  绘制直线:canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);

  绘制矩形:canvas.drawRect(float left, float top, float right, float bottom, Paint paint);

  绘制圆形:canvas.drawCircle(float cx, float cy, float radius, Paint paint);

  绘制字符:canvas.drawText(String text, float x, float y, Paint paint);

  绘制图形:canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);

示例:简单的实现一下音频条效果

public class CustomView extends View {

    private Paint mPaint = null;
    private int count = 80;
    private int mRectWidth = 10;
    private int offset = 2;
    private float mRectHight = 400;
    private float mCurrentHight = 400;

    public customView(Context context) {
        super(context);
        init();
    }

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

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

    private void init() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
    }

    public static int getMeasureSize(int measureSpec) {
        int size = 200;
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                result = size;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(size, specSize);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LinearGradient mLinearGradient = new LinearGradient(
                0,
                0,
                mRectWidth,
                mRectHight,
                Color.YELLOW,
                Color.BLUE,
                Shader.TileMode.CLAMP
        );

        mPaint.setShader(mLinearGradient);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < count; i++){
            double mRandom = Math.random();
            mCurrentHight = (float) (mRectHight * mRandom);
            canvas.drawRect((float)(mRectWidth * i + offset),
                    mRectHight-mCurrentHight,
                    (float)(mRectWidth * (i+1)),
                    mRectHight,
                    mPaint);

        }
        postInvalidateDelayed(300);
    }
}

这里写图片描述

这个示例博主这里就不做解释了,实现方法有很多种,我只是提供一种思路,更多的还是希望读者亲自去敲一遍试试效果,遇到不懂的baidu或者google,这样收获会更多,感谢您的阅读,下一篇将继续讨论自定义View的滑动和事件分发机制,欢迎大家进一步学习!自定义控件的时间分发、拦截、处理http://blog.csdn.net/u010083327/article/details/60874681

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值