自定义view实现动画数字圆圈

我们要实现的是如下的效果,

1.该view在设置属性之后时候会有数字和圆圈不断增长的效果

2.该view在按下和放开状态下显示不同的样式。

这种效果逻辑上并不复杂,底层灰色圆圈和蓝色扇形圆圈都是用canvas.drawArc()绘制出来的,中间的数字用drawtext绘制,数字不断增长的效果用了继承Animation的动画类;在按下和放开状态下显示不同的样式是重写了View 的setPressed()方法。

先贴出所有代码,再一一解释

import com.jcodecraeer.util.MyUtils;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;
public class CircleBar extends View {
    private RectF mColorWheelRectangle = new RectF();
    private Paint mDefaultWheelPaint;
    private Paint mColorWheelPaint;
    private Paint textPaint;
    private float mColorWheelRadius;
    private float circleStrokeWidth;
    private float pressExtraStrokeWidth;
    private String mText;
    private int mCount;
    private float mSweepAnglePer;
    private float mSweepAngle;
    private int mTextSize;
    BarAnimation anim;
    public CircleBar(Context context) {
        super(context);
        init(null, 0);
    }
    public CircleBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }
    public CircleBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }
    private void init(AttributeSet attrs, int defStyle) {
        circleStrokeWidth = MyUtils.dip2px(getContext(), 10);
        pressExtraStrokeWidth = MyUtils.dip2px(getContext(), 2);
        mTextSize = MyUtils.dip2px(getContext(), 40);
        mColorWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mColorWheelPaint.setColor(0xFF29a6f6);
        mColorWheelPaint.setStyle(Paint.Style.STROKE);
        mColorWheelPaint.setStrokeWidth(circleStrokeWidth);
        mDefaultWheelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDefaultWheelPaint.setColor(0xFFeeefef);
        mDefaultWheelPaint.setStyle(Paint.Style.STROKE);
        mDefaultWheelPaint.setStrokeWidth(circleStrokeWidth);
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
        textPaint.setColor(0xFF333333);
        textPaint.setStyle(Style.FILL_AND_STROKE);
        textPaint.setTextAlign(Align.LEFT);
        textPaint.setTextSize(mTextSize);
        mText = "0";
        mSweepAngle = 0;
        anim = new BarAnimation();
        anim.setDuration(2000);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawArc(mColorWheelRectangle, -90, 360, false, mDefaultWheelPaint);
        canvas.drawArc(mColorWheelRectangle, -90, mSweepAnglePer, false, mColorWheelPaint);
        Rect bounds = new Rect();
        String textstr=mCount+"";
        textPaint.getTextBounds(textstr, 0, textstr.length(), bounds);
        canvas.drawText(
                textstr+"",
                (mColorWheelRectangle.centerX())
                        - (textPaint.measureText(textstr) / 2),
                mColorWheelRectangle.centerY() + bounds.height() / 2,
                textPaint);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height = getDefaultSize(getSuggestedMinimumHeight(),
                heightMeasureSpec);
        int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        int min = Math.min(width, height);
        setMeasuredDimension(min, min);
        mColorWheelRadius = min - circleStrokeWidth -pressExtraStrokeWidth ;
        mColorWheelRectangle.set(circleStrokeWidth+pressExtraStrokeWidth, circleStrokeWidth+pressExtraStrokeWidth,
                mColorWheelRadius, mColorWheelRadius);
    }
    @Override
    public void setPressed(boolean pressed) {
        Log.i(TAG,"call setPressed ");
        if (pressed) {
            mColorWheelPaint.setColor(0xFF165da6);
            textPaint.setColor(0xFF070707);
            mColorWheelPaint.setStrokeWidth(circleStrokeWidth+pressExtraStrokeWidth);
            mDefaultWheelPaint.setStrokeWidth(circleStrokeWidth+pressExtraStrokeWidth);
            textPaint.setTextSize(mTextSize-pressExtraStrokeWidth);
        } else {
            mColorWheelPaint.setColor(0xFF29a6f6);
            textPaint.setColor(0xFF333333);
            mColorWheelPaint.setStrokeWidth(circleStrokeWidth);
            mDefaultWheelPaint.setStrokeWidth(circleStrokeWidth);
            textPaint.setTextSize(mTextSize);
        }
        super.setPressed(pressed);
        this.invalidate();
    }
    public void startCustomAnimation(){
        this.startAnimation(anim);
    }
    public void setText(String text){
        mText = text;
        this.startAnimation(anim);
    }
    public void setSweepAngle(float sweepAngle){
        mSweepAngle = sweepAngle;
    }
    public class BarAnimation extends Animation {
        /**
         * Initializes expand collapse animation, has two types, collapse (1) and expand (0).
         * @param view The view to animate
         * @param type The type of animation: 0 will expand from gone and 0 size to visible and layout size defined in xml.
         * 1 will collapse view and set to gone
         */
        public BarAnimation() {
        }
        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            super.applyTransformation(interpolatedTime, t);
            if (interpolatedTime < 1.0f) {
                mSweepAnglePer =  interpolatedTime * mSweepAngle;
                mCount = (int)(interpolatedTime * Float.parseFloat(mText));
            } else {
                mSweepAnglePer = mSweepAngle;
                mCount = Integer.parseInt(mText);
            }
            postInvalidate();
        }
    }
}

属性变量及其说明

   private RectF mColorWheelRectangle = new RectF();圆圈的矩形范围
private Paint mDefaultWheelPaint;  绘制底部灰色圆圈的画笔
private Paint mColorWheelPaint; 绘制蓝色扇形的画笔
private Paint textPaint; 中间文字的画笔
private float mColorWheelRadius; 圆圈普通状态下的半径
private float circleStrokeWidth; 圆圈的线条粗细

   private float pressExtraStrokeWidth;按下状态下增加的圆圈线条增加的粗细  

   private String mText;中间文字内容
private int mCount; 为了达到数字增加效果而添加的变量,他和mText其实代表一个意思
private float mSweepAnglePer;  为了达到蓝色扇形增加效果而添加的变量,他和mSweepAngle其实代表一个意思  

   private float mSweepAngle; 扇形弧度
private int mTextSize;文字颜色
BarAnimation anim;动画类

构造方法调用之后,第一个调用的是init方法,在该方法中初始化了各种画笔的颜色,风格等,字体大小和线条粗细则使用了我自己定义的工具函数dip2px(),这样做的目的是在不同分辨率的手机上,相同数值的最终显示效果差别不大,比如字体大小mTextSize的初始化:

mTextSize = MyUtils.dip2px(getContext(), 40);

还定义了动画对象以及动画持续时间:

anim = new BarAnimation();
anim.setDuration(2000);

其中BarAnimation为自定义的动画类:

public class BarAnimation extends Animation {
    /**
     * Initializes expand collapse animation, has two types, collapse (1) and expand (0).
     * @param view The view to animate
     * @param type The type of animation: 0 will expand from gone and 0 size to visible and layout size defined in xml.
     * 1 will collapse view and set to gone
     */
    public BarAnimation() {
    }
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);
        if (interpolatedTime < 1.0f) {
            mSweepAnglePer =  interpolatedTime * mSweepAngle;
            mCount = (int)(interpolatedTime * Float.parseFloat(mText));
        } else {
            mSweepAnglePer = mSweepAngle;
            mCount = Integer.parseInt(mText);
        }
        postInvalidate();
    }
}

这个动画类利用了applyTransformation参数中的interpolatedTime参数(从0到1)的变化特点,实现了该View的某个属性随时间改变而改变。原理是在每次系统调用animation的applyTransformation()方法时,改变``mSweepAnglePer,`mCount`的值,然后调用postInvalidate()不停的绘制view。``

if (interpolatedTime < 1.0f) {
     mSweepAnglePer =  interpolatedTime * mSweepAngle;
     mCount = (int)(interpolatedTime * Float.parseFloat(mText));
 }

````` ``` `` mSweepAnglePer,`mCount` `` ```这两个属性只是动画过程中要用到的临时属性,mText和```` ``` `` `mSweepAngle` `` ``` ````才是动画结束之后表示扇形弧度和中间数值的真实值。 `````

绘制方法

在onDraw方法中我们绘制了圆圈、扇形以及文字,但是绘制需要用到的一些坐标值是经过计算得出的,比如绘制扇形:

canvas.drawArc(mColorWheelRectangle, -90, mSweepAnglePer, false, mColorWheelPaint);

mColorWheelRectangle是一个矩形,这个矩形的上下左右边界都是在onMeasure方法中根据控件所分配的大小得出来的。

具体计算方式在onMeasure的实现中:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int height = getDefaultSize(getSuggestedMinimumHeight(),
            heightMeasureSpec);
    int width = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
    int min = Math.min(width, height);
    setMeasuredDimension(min, min);
    mColorWheelRadius = min - circleStrokeWidth -pressExtraStrokeWidth ;
    mColorWheelRectangle.set(circleStrokeWidth+pressExtraStrokeWidth, circleStrokeWidth+pressExtraStrokeWidth,
            mColorWheelRadius, mColorWheelRadius);
}

从setMeasuredDimension(min, min)可以看出我们强制该View为正方形。上面说到的`mColorWheelRectangle`矩形区域比控件的实际边界要小,这样做的目的是在按下状态下状态下让圆圈的线条变大之后也并不会超出矩形区域。

按下松开view样式改变的实现

改变样式很简单,只需改变画笔的样式就可以了,关键是在什么地方改变。我们都知道设置背景成selector就能是按下松开状态下背景改变,但是直接设背景不满足这里的要求,因为这是个圆圈,如果设置背景那肯定不会紧贴着圆圈边缘,但是我们可以在不同状态下更改画笔然后重绘达到相同的效果。如何检测到按下与松开呢?

看了view的源码知道setPressed()方法可以满足我们的要求:

@Override
public void setPressed(boolean pressed) {
    Log.i(TAG,"call setPressed ");
    if (pressed) {
        mColorWheelPaint.setColor(0xFF165da6);
        textPaint.setColor(0xFF070707);
        mColorWheelPaint.setStrokeWidth(circleStrokeWidth+pressExtraStrokeWidth);
        mDefaultWheelPaint.setStrokeWidth(circleStrokeWidth+pressExtraStrokeWidth);
        textPaint.setTextSize(mTextSize-pressExtraStrokeWidth);
    } else {
        mColorWheelPaint.setColor(0xFF29a6f6);
        textPaint.setColor(0xFF333333);
        mColorWheelPaint.setStrokeWidth(circleStrokeWidth);
        mDefaultWheelPaint.setStrokeWidth(circleStrokeWidth);
        textPaint.setTextSize(mTextSize);
    }
    super.setPressed(pressed);
    this.invalidate();
}

每次按下或者松开setPressed都会被调用,我们重写该方法,但要注意调用super.setPress()不然长按放开之后boolean pressed参数仍然为true,这样松开之后样式就保持按下的状态。具体原因还需要多阅读view的源码。

总结

其实这里最主要的是要有耐心了解canvas的一些方法,还有就是要根据自己的需求有针对性的分析view的源码。

demo下载地址:http://pan.baidu.com/s/1c0zjC3I