項目下載下傳位址:http://download.csdn.net/detail/qq_26331127/9418430
github位址 :https://github.com/LoveIsReal/LWang
先看效果:
題外話:寫這個動畫撫慰一下自己和可憐的廣大單身狗們(不包括我
)希望看着能開心
寫這個的想法來自于一篇Android開發中文站 的文章 位址:
http://mp.weixin.qq.com/s?__biz=MzA4NDM2MjAwNw==&mid=400894996&idx=1&sn=586b373aad44c03f881ff688d8daae0f&scene=0#wechat_redirect
他那個動畫的最後是畫了一個勾,但是覺得很普通 于是改成了現在這樣 覺得很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); // 繪制嘴巴
到此整個動畫完成 , 有疑問的可以留言,撤。。。。。