Android自定義view實現動畫數字圓圈
來自: http://www.jcodecraeer.com//a/anzhuokaifa/androidkaifa/2014/0325/1603.html
我們要實現的是如下的效果,
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)); }
這兩個屬性只是動畫過程中要用到的臨時屬性,mText和mSweepAnglePer,
mCount
才是動畫結束之后表示扇形弧度和中間數值的真實值。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