Android进阶之自定义控件二

来源:互联网 发布:淘宝的网商银行 编辑:程序博客网 时间:2024/05/15 23:48

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

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

自定义控件三大流程简介

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

measure

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

1、View的测量过程

@Overrideprotected 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的方法,同样的我们依次地大致阅读以下源码:

@Overrideprotected 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方法

@Overrideprotected 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

0 0
原创粉丝点击