1. 前言

一点一点学习自定义View,按照《Android开发艺术探索》中的说法,自定义View大致可以分为4类:

  1. 继承View重写onDraw方法;
  2. 继承ViewGroup派生特殊Layout;
  3. 继承特定View;
  4. 继承特定ViewGroup 看下第一种,制作一个简单的仪表盘进度条。

    2. 实现思路

  5. 继承View;

  6. 自定义属性值:arcColor,bgColor,arc_textColor,arc_textSize分别是前景色,背景色,进度文字颜色,进度文字字体大小;
  7. 确定弧形绘制位置和文字绘制位置
  8. 实现onDraw()方法

3. 效果

这里写图片描述

4. 知识点

4.1 MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode指的是测量模式,SpecSize指的是对应测量模式下的大小。

SpecMode有下面如下3中模式:

        /**
         * 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;
SpecMode 描述
UNSPECIFIED 父容器不对View有任何限制,系统涉及,平时很少涉及
EXACTLY 父容器已经检测出View所需要的精准大小,即SpecSize大小。对应match_parent和固定数值两种情况
AT_MOST 父容器指定一个View可用的大小。View的大小不能超过这值,具体是什么值要看View的具体实现。对应wrap_content情况

SpecSize大小单位px。

生成MeasureSpec方式如下:

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                          @MeasureSpecMode int mode) {
            if (sUseBrokenMakeMeasureSpec) {
                return size + mode;
            } else {
                return (size & ~MODE_MASK) | (mode & MODE_MASK);
            }
        }

获取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}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            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);
        }

4.2 View的工作流程

过程 描述
onMeasure 测量过程,确定View的宽度和高度,传入的参widthMeasureSpec,heightMeasureSpec
onLayout 布局过程,确定View的上下左右四个角的位置
onDraw 绘制过程,一般用来绘制特殊图形

继承View的自定义View基本只需要关注onDraw()和onMeasure()。由于没有子View所以不需要关注onLayout()。

4.3 图形绘制

具体请查看Canvas类和Paint类,这两个类内容比较多,可以自己研究一下,有很多有用的东西。

4.3.1 Canvas绘制方法

这里写图片描述

4.3.2 Paint属性值

Paint枚举值
这里写图片描述

设置属性方法
这里写图片描述

5. 关键代码

5.1 attrs.xml

    <declare-styleable name="ArcView">
        <attr name="arcColor" format="color"/>
        <attr name="bgColor" format="color"/>
        <attr name="arc_textColor" format="color"/>
        <attr name="arc_textSize" format="dimension"/>
    </declare-styleable>

5.2 ArcView

public class ArcView extends View {

    private final int MAX_SWEEP_ANGLE = 240;
    private final int START_SWEEP_ANGLE = 150;
    private final int DEFAULT_MAX_PROGRESS = 100;

    private final int DEFAULT_ARC_COLOR = Color.RED;
    private final int DEFAULT_BG_COLOR = Color.DKGRAY;
    private final int DEFAULT_TEXT_COLOR = Color.BLACK;
    private final int DEFAULT_TEXT_SIZE = 40;

    private int mArcColor = DEFAULT_ARC_COLOR;
    private int mBgColor = DEFAULT_BG_COLOR;
    private int mTextColor = DEFAULT_TEXT_COLOR;
    private int mTextSize = DEFAULT_TEXT_SIZE;

    private int progress = 0;
    private int mMaxProgress = DEFAULT_MAX_PROGRESS;

    private Paint mCirclePaint;
    private Paint mBgPaint;
    private Paint mTextPaint;

    private final Rect mTextBound = new Rect();

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

    public ArcView(Context context,
            @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ArcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray attributes = getContext().obtainStyledAttributes(attrs,
                R.styleable.ArcView);

        mArcColor = attributes
                .getColor(
                        R.styleable.ArcView_arcColor,
                        DEFAULT_ARC_COLOR);

        mBgColor = attributes
                .getColor(
                        R.styleable.ArcView_bgColor,
                        DEFAULT_BG_COLOR);

        mTextColor = attributes
                .getColor(
                        R.styleable.ArcView_arc_textColor,
                        DEFAULT_TEXT_COLOR);

        mTextSize = (int) attributes
                .getDimension(
                        R.styleable.ArcView_arc_textSize,
                        DEFAULT_TEXT_SIZE);
        attributes.recycle();
        init();
    }

    private void init() {
        mCirclePaint = new Paint();
        mCirclePaint.setColor(mArcColor);
        mCirclePaint.setStrokeWidth(8.0F);
        mCirclePaint.setDither(true);
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.STROKE);


        mBgPaint = new Paint();
        mBgPaint.setColor(mBgColor);
        mBgPaint.setStrokeWidth(20.0F);
        mBgPaint.setAntiAlias(true);
        mBgPaint.setStyle(Paint.Style.STROKE);
        mBgPaint.setStrokeCap(Paint.Cap.ROUND);

        mTextPaint = new Paint();
        mTextPaint.setStrokeWidth(4);

        //字体SP单位转换成PX
        int size =  (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                mTextSize, getResources().getDisplayMetrics());
        mTextPaint.setTextSize(size);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextAlign(Paint.Align.LEFT);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        int circleWidth = getWidth() - paddingLeft - paddingRight;
        int circleHeight = getHeight() - paddingTop - paddingBottom;

        int radius = Math.min(circleWidth, circleHeight) / 2;

        int left = getLeft() + paddingLeft;
        int right = left + radius * 2;
        int top = getTop() + getPaddingTop();
        int bottom = top + 2 * radius;
        canvas.drawArc(left, top, right, bottom, START_SWEEP_ANGLE, MAX_SWEEP_ANGLE, false,
                mBgPaint);

        int sweepArc = MAX_SWEEP_ANGLE * progress / mMaxProgress;
        canvas.drawArc(left, top, right, bottom, START_SWEEP_ANGLE, sweepArc, false, mCirclePaint);

        String text = String.valueOf(progress) + "%";
        mTextPaint.getTextBounds(text, 0, text.length(),
                mTextBound);
        canvas.drawText(text, (left + right) / 2 - mTextBound.width() / 2,
                (top + bottom) / 2 + mTextBound.height() / 2, mTextPaint);
    }

    /**
     * 设置进度大小
     */
    public void setProgress(int progress) {
        if (progress < 0 || progress > mMaxProgress) {
            return;
        }
        this.progress = progress;
        invalidate();
    }

    /**
     * 设置最大进度值
     */
    public void setMaxProgress(int maxProgress) {
        this.mMaxProgress = maxProgress;
    }
}

6. 源码

源码已上传Github,后续自定义View的学习Demo也会陆续上传

CustomViewDemo

results matching ""

    No results matching ""