Android View 之 onDraw 深入介绍

1.Draw绘制过程

1.1 View的绘制过程遵循步骤

  • View的绘制过程遵循如下几步:
    • ①绘制背景 background.draw(canvas)
    • ②绘制自己(onDraw)
    • ③绘制Children(dispatchDraw)
    • ④绘制装饰(onDrawScrollBars)

1.2 查看源码

  • 从源码中可以清楚地看出绘制的顺序。

    • 无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    public void draw(Canvas canvas) {
    // 所有的视图最终都是调用 View 的 draw ()绘制视图( ViewGroup 没有复写此方法)
    // 在自定义View时,不应该复写该方法,而是复写 onDraw(Canvas) 方法进行绘制。
    // 如果自定义的视图确实要复写该方法,那么需要先调用 super.draw(canvas)完成系统的绘制,然后再进行自定义的绘制。
    ...
    int saveCount;
    if (!dirtyOpaque) {
    // 步骤1: 绘制本身View背景
    drawBackground(canvas);
    }

    // 如果有必要,就保存图层(还有一个复原图层)
    // 优化技巧:
    // 当不需要绘制 Layer 时,“保存图层“和“复原图层“这两步会跳过
    // 因此在绘制的时候,节省 layer 可以提高绘制效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {

    if (!dirtyOpaque)
    // 步骤2:绘制本身View内容 默认为空实现, 自定义View时需要进行复写
    onDraw(canvas);

    ......
    // 步骤3:绘制子View 默认为空实现 单一View中不需要实现,ViewGroup中已经实现该方法
    dispatchDraw(canvas);

    ........

    // 步骤4:绘制滑动条和前景色等等
    onDrawScrollBars(canvas);

    ..........
    return;
    }
    ...
    }

2.View绘制流程

  • View绘制流程:
    • img

3.onDraw实际案例分析

3.1 需求介绍

  • 绘制圆环,一个实心中心圆,还有一个外圆环
  • 此控件可以设置宽度和高度,可以设置颜色

3.2 思路介绍

  • 3.2.1 既然是绘制圆形,可以写一个继承View的自定义view
  • 3.2.2 重写onDraw方法,获取控件宽高,然后比较宽高值,取小值的一半作为圆的半径
  • 3.2.3 然后分别绘制选中状态和未选中状态的圆
  • 3.2.4 创建画笔Paint,并且设置相关属性,比如画笔颜色,类型等
  • 3.2.5 利用canvas绘制圆,然后再又用相同方法绘制外边缘
  • 3.2.6 自定义一个是否选中状态的方法,传入布尔值是否选中,然后调用view中invalidate方法

3.3 代码介绍

  • 具体代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    /**
    * 红点自定义控件
    */
    public class DotView extends View {

    private boolean isInit = false;
    private boolean isSelected = false;
    private float mViewHeight;
    private float mViewWidth;
    private float mRadius;
    private Paint mPaintBg = new Paint();
    private int mBgUnselectedColor = Color.parseColor("#1A000000");
    private int mBgSelectedColor = Color.parseColor("#FDE26E");
    private static final float mArcWidth = 2.0f;

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

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

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

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!isInit) {
    isInit = true;
    mViewHeight = getHeight();
    mViewWidth = getWidth();
    if (mViewHeight >= mViewWidth) {
    mRadius = mViewWidth / 2.f;
    } else {
    mRadius = mViewHeight / 2.f;
    }
    }

    //是否选中
    if (isSelected){
    drawSelectedDot(canvas);
    } else{
    drawUnSelectedDot(canvas);
    }
    }

    /**
    * 绘制选中指示器红点
    * @param canvas canvas
    */
    private void drawSelectedDot(Canvas canvas) {
    //设置paint相关属性
    mPaintBg.setAntiAlias(true);
    mPaintBg.setColor(mBgSelectedColor);
    mPaintBg.setStyle(Style.FILL);

    //绘制圆
    canvas.drawCircle(mViewWidth / 2.f, mViewHeight / 2.f, mRadius - 8.f, mPaintBg);

    mPaintBg.setStyle(Style.STROKE);
    float offset = 1.f + mArcWidth;
    RectF oval = new RectF(mViewWidth / 2.f - mRadius + offset, mViewHeight / 2.f - mRadius + offset,
    mViewWidth / 2.f + mRadius - offset, mViewHeight / 2.f + mRadius - offset);

    //绘制指定的弧线,该弧线将被缩放以适应指定的椭圆形。
    canvas.drawArc(oval, 0.f, 360.f, false, mPaintBg);
    }

    /**
    * 绘制未选中指示器红点
    * @param canvas canvas
    */
    private void drawUnSelectedDot(Canvas canvas) {
    mPaintBg.setAntiAlias(true);
    mPaintBg.setColor(mBgUnselectedColor);
    mPaintBg.setStyle(Style.FILL);
    canvas.drawCircle(mViewWidth / 2.f, mViewHeight / 2.f, mRadius - 8.f, mPaintBg);
    }


    /**
    * 设置是否选中
    * @param isSelected isSelected
    */
    public void setIsSelected(boolean isSelected) {
    this.isSelected = isSelected;
    //使整个视图无效。如果视图是可见的,则{@link#onDraw(android.Graphics.Canvas)}将在将来的某个时候被调用。
    //调用该方法,会进行重新绘制,也就是调用onDraw方法
    this.invalidate();
    }
    }

4.onDraw案例之绘制圆形ImageView

4.1 需求分析

  • 1.业务需求:可以设置圆角,可以设置圆形,如果是圆角则必须设置半径,默认圆角半径为10dp
  • 2.如果设置了圆形,则即使设置圆角也无效;如果设置非圆形,则圆角生效,同时需要判断圆角半径是否大于控件宽高,处理边界逻辑
  • 3.当设置圆形的时候,即使设置宽高不一样,那么取宽高中的最小值的一半为圆形半径

4.2 代码介绍

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    public class ARoundImageView extends AppCompatImageView {

    /*
    * Paint:画笔
    * Canvas:画布
    * Matrix:变换矩阵
    *
    * 业务需求:可以设置圆角,可以设置圆形,如果是圆角则必须设置半径,默认圆角半径为10dp
    */
    /**
    * 圆形模式
    */
    private static final int MODE_CIRCLE = 1;
    /**
    * 普通模式
    */
    private static final int MODE_NONE = 0;
    /**
    * 圆角模式
    */
    private static final int MODE_ROUND = 2;
    /**
    * 圆角半径
    */
    private int currRound = dp2px(10);
    /**
    * 画笔
    */
    private Paint mPaint;
    /**
    * 默认是普通模式
    */
    private int currMode = 0;

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

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

    public ARoundImageView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    obtainStyledAttrs(context, attrs, defStyleAttr);
    initViews();
    }

    private void obtainStyledAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ARoundImageView, defStyleAttr, 0);
    currMode = a.hasValue(R.styleable.ARoundImageView_type) ? a.getInt(R.styleable.ARoundImageView_type, MODE_NONE) : MODE_NONE;
    currRound = a.hasValue(R.styleable.ARoundImageView_radius) ? a.getDimensionPixelSize(R.styleable.ARoundImageView_radius, currRound) : currRound;
    a.recycle();
    }

    private void initViews() {
    //ANTI_ALIAS_FLAG 用于绘制时抗锯齿
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    }


    /**
    * 当模式为圆形模式的时候,我们强制让宽高一致
    */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (currMode == MODE_CIRCLE) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int result = Math.min(getMeasuredHeight(), getMeasuredWidth());
    // 此方法必须由{@link#onMeasure(int,int)}调用,以存储已测量的宽度和测量的高度。
    // 如果不这样做,将在测量时触发异常。
    setMeasuredDimension(result, result);
    } else {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
    //获取ImageView图片资源
    Drawable mDrawable = getDrawable();
    //获取Matrix对象
    Matrix mDrawMatrix = getImageMatrix();
    if (mDrawable == null) {
    return;
    }
    if (mDrawable.getIntrinsicWidth() == 0 || mDrawable.getIntrinsicHeight() == 0) {
    return;
    }
    if (mDrawMatrix == null && getPaddingTop() == 0 && getPaddingLeft() == 0) {
    mDrawable.draw(canvas);
    } else {
    final int saveCount = canvas.getSaveCount();
    canvas.save();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
    if (getCropToPadding()) {
    final int scrollX = getScrollX();
    final int scrollY = getScrollY();
    canvas.clipRect(scrollX + getPaddingLeft(), scrollY + getPaddingTop(),
    scrollX + getRight() - getLeft() - getPaddingRight(),
    scrollY + getBottom() - getTop() - getPaddingBottom());
    }
    }
    canvas.translate(getPaddingLeft(), getPaddingTop());
    switch (currMode){
    case MODE_CIRCLE:
    Bitmap bitmap1 = drawable2Bitmap(mDrawable);
    mPaint.setShader(new BitmapShader(bitmap1, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
    break;
    case MODE_ROUND:
    Bitmap bitmap2 = drawable2Bitmap(mDrawable);
    mPaint.setShader(new BitmapShader(bitmap2, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
    canvas.drawRoundRect(new RectF(getPaddingLeft(), getPaddingTop(),
    getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()),
    currRound, currRound, mPaint);
    break;
    case MODE_NONE:
    default:
    if (mDrawMatrix != null) {
    canvas.concat(mDrawMatrix);
    }
    mDrawable.draw(canvas);
    break;
    }
    canvas.restoreToCount(saveCount);
    }
    }

    /**
    * drawable转换成bitmap
    */
    private Bitmap drawable2Bitmap(Drawable drawable) {
    if (drawable == null) {
    return null;
    }
    Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    //根据传递的scaleType获取matrix对象,设置给bitmap
    Matrix matrix = getImageMatrix();
    if (matrix != null) {
    canvas.concat(matrix);
    }
    drawable.draw(canvas);
    return bitmap;
    }

    private int dp2px(float value) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
    value, getResources().getDisplayMetrics());
    }
    }