安卓开发学习之019 创建自定义视图

要创建一个新的控件,通常需要对View类或者SurfaceView类进行扩展。View类提供了一个Canvas对象和一系列绘制方法以及Paint类,因此可以使用它绘制一可视化的界面。之后可以重写像屏幕触摸或者按键按下这样的用户事件以提供交互。
要扩展View类,通常需要对onMeasure和onDraw方法进行重写。
在onMeasure方法中,新的视图将会计算出它在一系列给定的边界条件下占据的高度和宽度。onDraw方法用于在画布上进行绘图。
下面我们通过一个简单的例子来说明如何自定义视图

新建一个类继承自View类

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

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

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

在画布上绘制图案需要一个画笔(Paint对象),这里我们先提前定义它,然后在第二个构造函数里面对它进行创建和简单的设置

 private static final String TAG = MyView.class.getSimpleName();
 private Paint mPaint;

 public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint();
        //消除锯齿
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setTextSize(30);
        //描边宽度
        mPaint.setStrokeWidth(3);

    }

调整控件大小/重写onMeasure方法

当控件的父容器布局它的子控件的时候,就会调用onMeasure方法。它提出“你需要使用多大的空间?”这样的问题,同时传入两个参数:widthMeasureSpec和heightMeasureSpec。如下

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

在重写这个方法之前我们先看看google的TextView是怎么实现的(好效仿一下O(∩_∩)O~)

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        BoringLayout.Metrics boring = UNKNOWN_BORING;
        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;

        if (mTextDir == null) {
            mTextDir = getTextDirectionHeuristic();
        }

        int des = -1;
        boolean fromexisting = false;

        if (widthMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            width = widthSize;
        } else {
            if (mLayout != null && mEllipsize == null) {
                des = desired(mLayout);
            }

            if (des < 0) {
                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
                if (boring != null) {
                    mBoring = boring;
                }
            } else {
                fromexisting = true;
            }

            if (boring == null || boring == UNKNOWN_BORING) {
                if (des < 0) {
                    des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
                }
                width = des;
            } else {
                width = boring.width;
            }

            final Drawables dr = mDrawables;
            if (dr != null) {
                width = Math.max(width, dr.mDrawableWidthTop);
                width = Math.max(width, dr.mDrawableWidthBottom);
            }

            if (mHint != null) {
                int hintDes = -1;
                int hintWidth;

                if (mHintLayout != null && mEllipsize == null) {
                    hintDes = desired(mHintLayout);
                }

                if (hintDes < 0) {
                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
                    if (hintBoring != null) {
                        mHintBoring = hintBoring;
                    }
                }

                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
                    if (hintDes < 0) {
                        hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
                    }
                    hintWidth = hintDes;
                } else {
                    hintWidth = hintBoring.width;
                }

                if (hintWidth > width) {
                    width = hintWidth;
                }
            }

            width += getCompoundPaddingLeft() + getCompoundPaddingRight();

            if (mMaxWidthMode == EMS) {
                width = Math.min(width, mMaxWidth * getLineHeight());
            } else {
                width = Math.min(width, mMaxWidth);
            }

            if (mMinWidthMode == EMS) {
                width = Math.max(width, mMinWidth * getLineHeight());
            } else {
                width = Math.max(width, mMinWidth);
            }

            // Check against our minimum width
            width = Math.max(width, getSuggestedMinimumWidth());

            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }
        }

        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
        int unpaddedWidth = want;

        if (mHorizontallyScrolling) want = VERY_WIDE;

        int hintWant = want;
        int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();

        if (mLayout == null) {
            makeNewLayout(want, hintWant, boring, hintBoring,
                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
        } else {
            final boolean layoutChanged = (mLayout.getWidth() != want) ||
                    (hintWidth != hintWant) ||
                    (mLayout.getEllipsizedWidth() !=
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight());

            final boolean widthChanged = (mHint == null) &&
                    (mEllipsize == null) &&
                    (want > mLayout.getWidth()) &&
                    (mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));

            final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);

            if (layoutChanged || maximumChanged) {
                if (!maximumChanged && widthChanged) {
                    mLayout.increaseWidthTo(want);
                } else {
                    makeNewLayout(want, hintWant, boring, hintBoring,
                            width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
                }
            } else {
                // Nothing has changed
            }
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            // Parent has told us how big to be. So be it.
            height = heightSize;
            mDesiredHeightAtMeasure = -1;
        } else {
            int desired = getDesiredHeight();

            height = desired;
            mDesiredHeightAtMeasure = desired;

            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(desired, heightSize);
            }
        }

        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
        }

        /*
         * We didn't let makeNewLayout() register to bring the cursor into view,
         * so do it here if there is any possibility that it is needed.
         */
        if (mMovement != null ||
            mLayout.getWidth() > unpaddedWidth ||
            mLayout.getHeight() > unpaddedHeight) {
            registerForPreDraw();
        } else {
            scrollTo(0, 0);
        }

        setMeasuredDimension(width, height);
    }

晕倒,一个简单的TextView的onMeasure代码好长,我们细心分析下这个方法,大致分了三部,首先通过MeasureSpec这个类得到长宽的mode和size,然后根据一系列判断得到实际的长和宽,最后调用setMeasuredDimension(width, height)方法设置控件的实际宽和高。
这个MeasureSpec又是什么东西呢?看来我们有必要去了解这个类

MeasureSpec类介绍

它是View中的一个内部类,定义如下

    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        /**
         * Creates a measure specification based on the supplied size and mode.
         *
         * The mode must always be one of the following:
         * 
    *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* *

Note: On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.

* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */
public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } /** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */ public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } /** * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } /** * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } /** * Returns a String representation of the specified measure * specification. * * @param measureSpec the measure specification to convert to a String * @return a String with the following format: "MeasureSpec: MODE SIZE" */ public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }


这个工具类主要有四个方法和三个常量:

UNSPECIFIED:0左移30位,最高两位为00
EXACTLY:1左移30位,最高两位为01
AT_MOST:2左移30为,最高两位为10

//这个是由我们给出的尺寸大小和模式生成一个包含这两个信息的int变量,这里这个模式这个参数,传三个常量中的一个。
public static int makeMeasureSpec(int size, int mode)

//这个是得到这个变量中表示的模式信息,返回三个常量之一。
public static int getMode(int measureSpec)

//这个是得到这个变量中表示的尺寸大小的值。
public static int getSize(int measureSpec)

//把这个变量里面的模式和大小组成字符串返回来,方便打日志
public static String toString(int measureSpec)

从TextView中的onMeasure方法中可以看到

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

一个int型变量,如何能既得到模式又得到尺寸值呢?
诀窍就在于一个int型整数有32位,将它的最高2位表示模式,后30位表示大小值
如何得到一个整数的最高两位数呢,
通过与11000000000000000000000000000000按位与得到

最高两位是00的时候表示”未指定模式”。即MeasureSpec.UNSPECIFIED
最高两位是01的时候表示”’精确模式”。即MeasureSpec.EXACTLY
最高两位是11的时候表示”最大模式”。即MeasureSpec.AT_MOST

那么如何取后30位的数值呢?
通过与00111111111111111111111111111111按位与得到


那么三种模式具体又代表什么呢?
UNSPECIFIED:父布局没有给子布局任何限制,子布局可以任意大小。如控件ScrollView/HorizontalScrollView就是
EXACTLY:父布局决定子元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小.一般是layout_width设置了明确的值或者是MATCH_PARENT
AT_MOST:子布局可以根据自己的大小选择任意大小。一般layout_width为WARP_CONTENT

对MeasureSpec类有一个基本了解后,我们开始动手来重写onMeasure方法

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取真正的尺寸
        int measuredWidth = measureWidth(widthMeasureSpec);
        int measuredHeight = measureHeight(heightMeasureSpec);
        //必须调用setMeasuredDimension,否则在布局控件的时候会造成运行时异常
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    private int measureHeight(int heightMeasureSpec) {
        //设置默认高度
        int result = 200;
        //获取控件上下间距
        int padding = getPaddingTop() + getPaddingBottom();
        //获取模式
        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        //获取值大小
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        //打印模式和值大小
        Log.i(TAG,"测量高度 " + MeasureSpec.toString(heightMeasureSpec));

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                break;
            case MeasureSpec.AT_MOST:
                //result = specSize;
                //因为在ondraw里面只是简单的画一个文本。所以只需要文本的高度即可
                result = Math.min(heightMeasureSpec,measureTextHeight()+padding);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    private int measureWidth(int widthMeasureSpec) {
        //设置默认宽度
        int result = 2000;
        //获取控件左右间距
        int padding = getPaddingLeft() + getPaddingRight();
        //获取模式
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        //获取值大小
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        //打印模式和值大小
        Log.i(TAG, "测量宽度 " + MeasureSpec.toString(widthMeasureSpec));
        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(widthMeasureSpec,specSize+padding);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }
 //获取文本高度
    private int measureTextHeight() {
        Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
//        System.out.println("fontMetrics = " + fontMetrics);
        return fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading;
    }

重写onDraw方法

本文只是简单的在画布上写一串字符串,比较简单

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawText("MyView", 0, getHeight(), mPaint);
    }

使用自定义视图并验证三种模式

在XML中添加四个自定义视图


<LinearLayout
    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:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="com.antex.myview.MainActivity"
    tools:showIn="@layout/activity_main">

    <HorizontalScrollView android:layout_width="wrap_content"
                          android:layout_height="wrap_content">
        <com.antex.myview.MyView
            android:layout_width="wrap_content"
            android:layout_height="60dp"
            android:text="Hello World!"/>
    HorizontalScrollView>

    <com.antex.myview.MyView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="20dp"
        android:text="Hello World!"/>
    <com.antex.myview.MyView
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:text="Hello World!"/>
    <com.antex.myview.MyView
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:text="Hello World!"/>
LinearLayout>

第一个视图包裹在HorizontalScrollView中,父视图的宽度是不定的,那么第一个视图的宽度模式应该是UNSPECIFIED
第二个视图的layout_width=”wrap_content”,那么它的宽度应该是被限制在父视图的宽度内,宽度模式应该是AT_MOST
第三个和第四个视图的layout_width指定为match_parent何确切的值,那么
其宽度模式应该是EXACTLY

下面运行下程序来验证下这个结论
输出日志为

12-19 23:13:54.822 2101-2101/? I/MyView: 测量宽度 MeasureSpec: UNSPECIFIED 0
12-19 23:13:54.822 2101-2101/? I/MyView: 测量高度 MeasureSpec: EXACTLY 158
12-19 23:13:54.822 2101-2101/? I/MyView: 测量宽度 MeasureSpec: AT_MOST 996
12-19 23:13:54.822 2101-2101/? I/MyView: 测量高度 MeasureSpec: AT_MOST 1489
12-19 23:13:54.822 2101-2101/? I/MyView: 测量宽度 MeasureSpec: EXACTLY 996
12-19 23:13:54.822 2101-2101/? I/MyView: 测量高度 MeasureSpec: EXACTLY 158
12-19 23:13:54.822 2101-2101/? I/MyView: 测量宽度 MeasureSpec: EXACTLY 263
12-19 23:13:54.822 2101-2101/? I/MyView: 测量高度 MeasureSpec: EXACTLY 158

通过日志可以发现与我们设想的一致~~~~

好了本文就介绍到这里,开始动手创建属于自己的视图吧。
后续讲解自定义View的事件交互与Canvas详解

开发工具:Android Studio1.5
SDK: Android 6.0
API 23

代码下载:019_MyView_onMeasure

你可能感兴趣的:(android)