項目下載地址:http://download.csdn.net/detail/qq_26331127/9418430
github地址 :https://github.com/LoveIsReal/LWang
先看效果:
題外話:寫這個動畫撫慰一下自己和可憐的廣大單身狗們(不包括我)希望看着能開心
寫這個的想法來自於一篇Android開發中文站 的文章 地址:
他那個動畫的最後是畫了一個勾,但是覺得很普通 於是改成了現在這樣 覺得很nice的自覺底下頂下
既然是做動畫,不管什麼工具或方式,首先是設計動畫流程
我的動畫流程:
1 ,圓弧加載成整環,隨着進度值改變顏色 ,顏色區間是紅色到藍色。 這個並不是動畫,是通過一個線程不斷改變進度值從0 到 100 模擬了加載的過程
(所以當這個進度達到一百之後,程序後面的事就和加載無關了,純屬動畫)
2 ,圓環兩側平行於圓環圓心位置同時拋出方塊,運動到圓環正上方,此時距離圓環大小就是圓環半徑
3,將上一個動畫的兩個方塊 改爲一個圓球,並且改爲綠色,開始下落動畫 ,終點是圓心
4,同時開始嘴 和 眼睛的繪製的動畫。眼睛:圓心位置開始繪製兩個圓球 逐漸運動到最終位置 。 嘴:從左向右繪製圓弧
分析結束。。。。。。
代碼中最重要的知識點就是屬性動畫。
推薦看這篇文章 :http://blog.csdn.net/jdsjlzx/article/details/45558901
接下來的內容默認你看完上面那篇文章 知道什麼是 ValueAnimator Interpolator TypeEvaluator
接下來開始裝逼 ,額,錯了,開始講解代碼。。。。。
並沒有提供自定義屬性 就不用什麼attr了
繼承View :
public class LoadingView extends View {
標準的三個參數構造方法 ,最後一個構造方法做各種Paint的初始化操作
onMeasure() 、onLayout 方法 不需要重寫 因爲內部沒有子View xml 中設定的是固定寬高 。
看第一個流程 :
我們繪製圓環 繪製圓環用的是canvas.drawArc() 方法
裏面需要一個Rectf 參數 就是一個矩形區域 我們設置成成員變量 ,在onSizeChanged (onMeasure 之後 onDraw之前調用的一個方法)方法中初始化它
看張圖: 藍色的圓環即爲所需
然後就是進度值的設定:我在 Activity 中設置按鈕的監聽,啓動一個 Thread 去模擬加載進度的過程 ,
Thread.sleep(20);
每隔20毫秒 加載1%
View 類中 定義了一個 setProgress()
public void setProgress(int progress) {
if (progress == 0) {
// 由於ColorEvaluator 中的保存顏色值的靜態變量 所有每次重置進度 也需要重置這些變量
status = 0; // 每次重新運行 改變動畫的運行標誌位
currentPosition = ballRadius;
ColorEvaluator.resetColor();
}
float fraction = 1.0f * (progress) / 100;
String color_str = ColorEvaluator.evaluate(fraction, "#0000ff", "#ff0000");
int color = Color.parseColor(color_str);
Log.e("wxy", "asdasdasd " + fraction);
circlePaint.setColor(color);
this.progress = progress;
postInvalidate(); // 之所有在這裏使用postInvalidate 因爲我是開了個線程去更新
if (progress == 100) { // 開始方塊拋出動畫
status = drawFlyRect; // 先改變運行狀態 因爲在下面的post線程會調用onDraw
post(new Runnable() { // 這裏的post方法 其實就是View中專門設計的方法
// Causes the Runnable to be added to the message queue.
// The runnable will be run on the user interface thread 這兩行英文是它的官方解釋
// 這個post產生的Runnable將運行在主線程中 這就解釋了 爲什麼用了它 Mainactivity中就不用Looper.prepare()方法 ,
// 而且也滿足了 這個ValueAnimator必須用在主線程中
// 從源碼中看 View的 post 方法 開啓了一個mHandler.post(runnable);
@Override
public void run() {
animation_fly.start();
}
});
}
}
這個方法主要就是更新了成員變量progress 用postInvalidate() 方法去刷新View ;
看下drawArc方法的源碼 :
* @param oval The bounds of oval used to define the shape and size
* of the arc
* @param startAngle Starting angle (in degrees) where the arc begins
* @param sweepAngle Sweep angle (in degrees) measured clockwise
* @param useCenter If true, include the center of the oval in the arc, and
close it if it is being stroked. This will draw a wedge
* @param paint The paint used to draw the arc
*/
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
@NonNull Paint paint) {
drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter,
paint);
}
startAngle 是起始的角度 ; canvas的角度的規定是 X正半軸 爲0度 向下爲正 ,即順時針
sweepAngle 是圓弧掃過的角度 也有正負之分 正值表示順時針 負值表示逆時針
結束時是圓環重合 掃過的角度是 -360 , 起始角度是 -360 就是X軸正方向的角度
轉的過程就是依據進度值更新圓環的起始角度和掃過的角度 :
canvas.drawArc(mRectf, startAngle - 270 * percent, -60 - (300 * percent), false, circlePaint);
false參數表示是否繪製半徑 若爲true 會從橢圓重心開始繪製兩條半徑 和起點終點圍成一個封閉圖形 , percent 表示進度值
大家都應該注意到了這個顏色的變化 ,是自定義的Evaluator ,根據我傳入的進度值 返回相應的顏色 ,這個類不是我寫的,感興趣的看下:
public class ColorEvaluator {
private static int mCurrentRed = -1;
private static int mCurrentGreen = -1;
private static int mCurrentBlue = -1;
public static void resetColor() {
mCurrentBlue = -1;
mCurrentGreen = -1;
mCurrentRed = -1;
}
public static String evaluate(float fraction, String startColor, String endColor) {
int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
// 初始化顏色的值
if (mCurrentRed == -1) {
mCurrentRed = startRed;
}
if (mCurrentGreen == -1) {
mCurrentGreen = startGreen;
}
if (mCurrentBlue == -1) {
mCurrentBlue = startBlue;
}
// 計算初始顏色和結束顏色之間的差值
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff + greenDiff + blueDiff;
if (mCurrentRed != endRed) {
mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
fraction);
} else if (mCurrentGreen != endGreen) {
mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
redDiff, fraction);
} else if (mCurrentBlue != endBlue) {
mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
redDiff + greenDiff, fraction);
}
// 將計算出的當前顏色的值組裝返回
String currentColor = "#" + getHexString(mCurrentBlue)
+ getHexString(mCurrentGreen) + getHexString(mCurrentRed); // 這裏做了一些修改
// 正確的顏色組裝是 red green blue
return currentColor;
}
/**
* 根據fraction值來計算當前的顏色。
*/
private static int getCurrentColor(int startColor, int endColor, int colorDiff,
int offset, float fraction) {
int currentColor;
if (startColor > endColor) {
currentColor = (int) (startColor - (fraction * colorDiff - offset));
if (currentColor < endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (fraction * colorDiff - offset));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}
/**
* 將10進制顏色值轉換成16進制。
*/
private static String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}
}
看第二個流程:
需要注意的是 ,每次調用onDraw方法 之前在畫布上已經繪製的內容將全部清空 然後進行繪製
先看右側的方塊,因爲我們只需要知道方塊飛行時在圓弧上對應的扇形的角度 ,左側的方塊對應的角度是一樣的 。
drawLine方法:
/* @param startX The x-coordinate of the start point of the line
* @param startY The y-coordinate of the start point of the line
* @param paint The paint used to draw the line
*/
public void drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint) {
native_drawLine(mNativeCanvasWrapper, startX, startY, stopX, stopY, paint.mNativePaint);
}
---------------------------------------------------------------------------------------------------------------------------------------------
當然先得算出大圓弧的半徑 :設置圓環半徑爲R勾股定理:( R + X) ^2 + (2*R)^ 2 = (2*R + X )^ 2
得出 :X = R / 2 .
然後把座標系移到大圓弧圓心 以此爲座標原點 方便座標計算
canvas.save(); canvas.translate(radius / 2 + strokeWidth, 2 * radius + strokeWidth);
然後就需要用到ValueAnimator 屬性動畫, 去計算從0 度到 最高位置的角度endAngle
endAngle = (float) Math.atan(4f / 3);然後用我定義一個方法去初始化這個 ValueAnimator
public void initAnimatorFlyRect() {
animation_fly = ValueAnimator.ofFloat(0f, endAngle);
animation_fly.setDuration(1000);
animation_fly.setInterpolator(new DecelerateInterpolator()); // 定義了動畫變化的速率
// AccelerateDecelerateInterpolator表示 在開始和結束的時候減速 中間加速
// DecelerateInterpolator 表示逐漸減速
// AccelerateInterpolator 表示一直加速
// LinearInterpolator 表示勻速
animation_fly.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { // 角度值斷改變的監聽
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentAngle = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animation_fly.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) { // 動畫結束的監聽
super.onAnimationEnd(animation);
currentAngle = 0f;
status = drawBall;
circlePaint.setColor(Color.argb(255, 0, 150, 136)); // 把畫圓的Paint變成 綠色
postInvalidate();
post(new Runnable() {
@Override
public void run() {
initAnimationDown();
animation_down.start();
}
});
}
});
}
特別注意: ValueAnimator 必須在UI線程中 即主線程中啓動 ,但是我們當前是開了個子線程去更新UI 所以不滿足在UI線程中
那怎麼辦??
發現View類中 有個 post(Runnable action)方法 它API的解釋是這樣的
/** * Causes the Runnable to be added to the message queue. * The runnable will be run on the user interface thread. *用post方法開的線程會運行在UI線程中 問題解決
post方法內部是 用mHandler.post 方法 ,具體實現沒向下深究。。
if (progress == 100) { post(new Runnable() { @Override public void run() { animation_fly.start(); } }); }在進度值100 的 時候啓動這個Animator --- animation_fly
具體的方塊繪製代碼:
public void drawFlyRect(Canvas canvas) {
float bigX = getMeasuredWidth() / 2 - radius * 3 / 2 + strokeWidth;
float bigY = getMeasuredHeight() / 2;
canvas.save();
canvas.translate(bigX, bigY);//將座標移動到大圓圓心(方塊軌跡所在的那個大圓)
// 兩個參數分別是平移的 X 軸距離 和 Y 軸距離
float bigRadius = 5 * radius / 2;//大圓半徑
//方塊起始端座標 起始點座標就是currentAngle 對應在圓弧上的點 注意這裏我們規定緯度低的爲起始端
float x1 = (float) (bigRadius * Math.cos(currentAngle)); // 圓弧上的點在X軸上的投影長度
float x11 = (float) (3 * radius - strokeWidth * 2 - (bigRadius * Math.cos(currentAngle)));
float y1 = -(float) (bigRadius * Math.sin(currentAngle)); // 圓弧上的點在Y軸上的投影長度
//方塊末端座標 末端點最難定 它也是圓弧上的一個點 和起始點相連成的直線就是方快
// 所以起始點 和 末端點 構成的圓弧角度 我們不能用固定的值 畢竟希望這個方塊上升時長度在減小
float huAngle = (float) (0.15 * endAngle - 0.10 * endAngle * (currentAngle / endAngle)); // 確定方塊的那段弧);
float x2 = (float) (bigRadius * Math.cos(currentAngle + huAngle));
float x22 = (float) (3 * radius - strokeWidth * 2 - (bigRadius * Math.cos(currentAngle + huAngle)));
float y2 = -(float) (bigRadius * Math.sin(currentAngle + huAngle));
canvas.drawLine(x1, y1, x2, y2, rectPaint);//小方塊,其實是一條直線
canvas.drawLine(x11, y1, x22, y2, rectPaint);
canvas.restore();
}
到頂點的時候,即Animator結束的時候 對應方法 onAnimationEnd() , 裏面改變畫筆的顏色爲綠色 ,第二個過程結束。
看第三個流程:
倆方塊消失,取而代之的是一個綠色圓球 接着開始下落動畫 用ValueAnimator控制下落的縱座標,即高度 ,相信大家應該很熟悉了 ,通過ValueAnimator的ofFloat() 方法計算高度值
Canvas 有繪製圓的方法:
/* @param cx The x-coordinate of the center of the cirle to be drawn * @param cy The y-coordinate of the center of the cirle to be drawn * @param radius The radius of the cirle to be drawn * @param paint The paint used to draw the circle */ public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) { native_drawCircle(mNativeCanvasWrapper, cx, cy, radius, paint.mNativePaint); }
注意這個傳入的畫筆:
Paint ballPaint = new Paint(); ballPaint.setStyle(Paint.Style.FILL); ballPaint.setColor(Color.argb(255, 0, 150, 136)); // 綠色 ballPaint.setAntiAlias(true);它的Style是FILL 即實心圓球,如果是STROKE 就成了空心球
下落的球對應的Animator :
public void initAnimationDown() {
animation_down = ValueAnimator.ofFloat(ballRadius, radius * 2 + strokeWidth);
animation_down.setDuration(600);
animation_down.setInterpolator(new AccelerateInterpolator());
animation_down.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPosition = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animation_down.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
status = drawMouthAndEyes;
currentPosition = radius * 2 + strokeWidth;
initAnimatorTraslateBall();
initAnimatorMouth();
animation_traslate.start();
animation_mouth.start();
}
});
}
繪製球的代碼 , currentPosition 由估值器提供canvas.drawCircle(radius * 2 + strokeWidth, currentPosition, ballRadius, ballPaint);
看第四個流程:
嘴巴和眼睛同時繪製
上一個動畫留在了圓心處 所以這個動畫我的設計是 之前的墜落到圓心的小球變爲兩個同時向最終位置平移 並且畫筆寬度逐漸變大
這需要一個ValueAnimator 計算其中一個點(另一個點的值都可以對稱算出)X、Y軸座標 和當前畫筆寬度 三個值總不能用三次ofFloat方法吧
所以想到了ofObject 方法 把這些值存到一個對象中 就乾脆定義一個類
public class PointAndSizeOfEyes {
private float X;
private float Y;
private float eyeRadius;
public PointAndSizeOfEyes(){
}
public PointAndSizeOfEyes(float x, float y, float eyeRadius) {
X = x;
Y = y;
this.eyeRadius = eyeRadius;
}
...........// get set 方法省略
}
自定義一個 TypeEvaluator 去計算動畫過程的值: 就是重寫evaluate 方法就行 根據fraction進度值 , 去算出當前需要的值
public class PointAndSizeEvaluator implements TypeEvaluator {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
PointAndSizeOfEyes point_start = (PointAndSizeOfEyes) startValue;
PointAndSizeOfEyes point_end = (PointAndSizeOfEyes) endValue;
// 需要注意的是 平移後的座標X 還是 Y 都是變小的 所以 endValue < startValue
return new PointAndSizeOfEyes()
.setX(point_start.getX() - (point_start.getX() - point_end.getX()) * fraction)
.setY(point_start.getY() - (point_start.getY() - point_end.getY()) * fraction)
.setEyeRadius(point_start.getEyeRadius() + fraction * (point_end.getEyeRadius() - point_start.getEyeRadius()));
}
}
繪製眼睛的屬性動畫 :
public void initAnimatorTraslateBall() {
// Y軸眼睛的偏移量只有 1/4 radius , X軸 1/3 radius
animation_traslate = ValueAnimator.ofObject(new PointAndSizeEvaluator(), new PointAndSizeOfEyes(currentPosition, currentPosition, ballRadius), new PointAndSizeOfEyes(currentPosition - radius / 3, currentPosition - radius / 4, bigBallRadius));
animation_traslate.setDuration(1200);
animation_traslate.setInterpolator(new AccelerateDecelerateInterpolator());
animation_traslate.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (PointAndSizeOfEyes) animation.getAnimatedValue();
postInvalidate();
}
});
animation_traslate.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
});
}
繪製嘴巴的屬性動畫:
public void initAnimatorMouth() {
// 笑臉所在外切矩形的設定
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//
// 8/3 = 2 整型相除等於2 必須用 8f/3f = 2.667
//
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
mRectf_mouth.set(new RectF((4f / 3f) * radius + strokeWidth * 2, (4f / 3f) * radius + strokeWidth, (8f / 3f) * radius + strokeWidth * 2, (8f / 3f) * radius + strokeWidth));
animation_mouth = ValueAnimator.ofFloat(30 * 1.0f, 110 * 1.0f); // 這裏計算的是 嘴巴逆時針增長的角度 從小到大
animation_mouth.setDuration(1200);
animation_mouth.setInterpolator(new DecelerateInterpolator());
animation_mouth.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentMouthAngle = (float) animation.getAnimatedValue();
postInvalidate();
}
});
animation_mouth.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
});
}
然後 onDraw 方法中 繪製:
canvas.drawCircle(currentPoint.getX() + strokeWidth, currentPoint.getY(), currentPoint.getEyeRadius(), ballPaint); // 繪製左邊的眼睛 canvas.drawCircle(2 * currentPosition - currentPoint.getX() + strokeWidth, currentPoint.getY(), currentPoint.getEyeRadius(), ballPaint); // 繪製右邊的眼睛 canvas.drawArc(mRectf_mouth, 145, -currentMouthAngle, false, mouthPaint); // 繪製嘴巴
到此整個動畫完成 , 有疑問的可以留言,撤。。。。。