github:https://github.com/qintong91/InsLoadingAnimation
前段時間發(fā)現(xiàn)instagram點擊用戶頭像的加載小視頻動畫畴栖,效果如下:
對哈打,就是轉(zhuǎn)圈圈的這個碌廓,這么酷炫,我也要做一個唧瘾!在整理代碼和總結時候,神奇的事情發(fā)生了,在我日常刷微博的時候點開微博客戶端時候突然發(fā)現(xiàn):
緣分啊岸裙,發(fā)現(xiàn)了微博Android客戶端也上線了類似動畫!等等速缆,不是類似降允,這是特么是除了顏色和ins的一毛一樣啊艺糜!
既然這個動畫效果這么火剧董,那還不趕快把我實現(xiàn)分享出來
如下就是我實現(xiàn)的效果:
工程鏈接:https://github.com/qintong91/InsLoadingAnimation
(你如果覺得不錯就不要控制自己,點進去star一下~)
下文破停,分別為整理介紹翅楼,使用,具體實現(xiàn)與總結真慢。
1.介紹
InsLoadingView繼承自ImageView,其對應的image顯示為圓形毅臊。InsLoadingView有三種狀態(tài):LOADING/UNCLICKED/CLICKED,Loading時候輪廓有不斷循環(huán)的動畫晤碘,如上圖(下文分析源碼時候會詳細闡明其過程)褂微。UNCLICKED時外側輪廓為靜態(tài)的彩色圈功蜓,CLICKED外層為靜態(tài)的灰色圈。此外宠蚂,在其被點擊時還有控件收縮的動畫效果式撼。注意:由于狀態(tài)是與應用中的情況相關的利赋,所以狀態(tài)變化需要用戶手動去設置易猫。
整體效果如下(感謝家里的喵主子~)
2.使用
如果你想在自己的項目中使用的話,可以按如下幾步進行:
Step 1
在build.gradle增加依賴:
dependencies {
compile 'com.qintong:insLoadingAnimation:1.0.1'
}
Step 2
InsLoadingView繼承自ImageView, 所以最基本的后专,可以按照ImageView的用法使用InsLoadingView:
<com.qintong.library.InsLoadingView
android:layout_centerInParent="true"
android:id="@+id/loading_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/pink"/>
Step 3
設置狀態(tài):
您可以手動設置其狀態(tài)呀癣,來對應在您應用中的當前狀態(tài)美浦。InsLoadingView的狀態(tài)有:
LOADING: 表示InsLoadingView被點擊之后正在加載內(nèi)容(未加載完畢之前),該狀態(tài)下動畫正在執(zhí)行项栏。
UNCLICKED: 該InsLoadingView被點擊之前的狀態(tài)浦辨,此狀態(tài)下動畫停止。
CLICKED: 表示InsLoadingView被點擊和加載過沼沈,此狀態(tài)下動畫停止切圓圈的顏色為灰色流酬。
默認的狀態(tài)是LOADING。
可以通過一下代碼設置狀態(tài):
xml:
app:status="loading" //or "clicked",or "clicked"
java:
mInsLoadingView.setStatus(InsLoadingView.Status.LOADING); //Or InsLoadingView.Status.CLICKED, InsLoadingView.Status.UNCLICKED
設置顏色
設置start color和start color列另,InsLoadingView的圓圈會顯示兩個顏色間的過渡芽腾。
可以按如下代碼設置:
xml:
app:start_color="#FFF700C2" //or your color
app:end_color="#FFFFD900" //or your color
java:
mInsLoadingView.setStartColor(Color.YELLOW); //or your color
mInsLoadingView.setEndColor(Color.BLUE); //or your color
默認的start color和start color為#FFF700C2和#FFFFD900。
設置速度
通過設置環(huán)繞動畫的時間和整體旋轉(zhuǎn)的時間來改變速度:
xml:
app:circle_duration="2000"
app:rotate_duration="10000"
java:
mInsLoadingView.setCircleDuration(2000);
mInsLoadingView.setRotateDuration(10000);
默認的時間為2000ms和10000ms页衙。
2.實現(xiàn)
完整的代碼請見https://github.com/qintong91/InsLoadingAnimation
下面就對代碼進行分析摊滔。
InsLoadingView繼承自ImageView,動畫效果主要通過重寫onDraw()函數(shù)重新繪制店乐。所以可以先看onDraw()方法:
@Override
protected void onDraw(Canvas canvas) {
canvas.scale(mScale, mScale, centerX(), centerY());
drawBitmap(canvas);
Paint paint = getPaint(getColor(0), getColor(360), 360);
switch (mStatus) {
case LOADING:
drawTrack(canvas, paint);
break;
case UNCLICKED:
drawCircle(canvas, paint);
break;
case CLICKED:
drawClickedircle(canvas);
break;
}
}
drawBitmap()為實現(xiàn)顯示圓形圖片重新完成了繪制圖片的過程艰躺。之后根據(jù)當前status繪制圖片外的圈:status為LOADING時候繪制時是動畫,其他兩種情況繪制是靜態(tài)的圓圈响巢。
(1) 動畫繪制:
LOADING時候的動畫是項目中最核心的部分描滔。從動畫效果中可以看出棒妨,圓弧的兩端都在運動:運動較慢的一端其實反應了外圈的整體旋轉(zhuǎn)(連同顏色)踪古,較快一端的旋轉(zhuǎn)還有兩個過程:圓弧向外“伸展”一圈和向回“收縮”一圈的過程。
degress和cricleWidth是實時變化的券腔,他們的值由ValueAnimator設置伏穆,這兩個值分別表示整個動畫整體旋轉(zhuǎn)的角度(也就是動畫中轉(zhuǎn)速較慢一端)和轉(zhuǎn)速較快的圓弧的動畫。兩個變量的單位都是度degress范圍為0-360纷纫,cricleWidth范圍為-360到360枕扫。cricleWidth圓弧向回“收縮”和向外“伸展”的過程,分別對應代碼中的a和b過程,對應的circleWidth范圍為-360—0度和0—360度。
在a過程中辱魁,cricleWidth + 360換算得到成正的adjustCricleWidth烟瞧,adjustCricleWidth到360度繪制一個扇形圓弧诗鸭,adjustCricleWidth到0度,依次向后每隔12度畫小的扇形圓弧参滴,圓弧的寬度遞減强岸。
b過程中:從0到cricleWidth:最前端繪制4個小扇形圓弧,其后到0度繪制一個長圓弧砾赔。從360度到cricleWidth蝌箍,每間隔12度依次繪制小圓弧,其寬度遞減暴心。
private void drawTrack(Canvas canvas, Paint paint) {
canvas.rotate(degress, centerX(), centerY());
canvas.rotate(ARC_WIDTH, centerX(), centerY());
RectF rectF = new RectF(getWidth() * (1 - circleDia), getWidth() * (1 - circleDia),
getWidth() * circleDia, getHeight() * circleDia);
if (DEBUG) {
Log.d(TAG, "cricleWidth:" + cricleWidth);
}
if (cricleWidth < 0) {
//a
float startArg = cricleWidth + 360;
canvas.drawArc(rectF, startArg, 360 - startArg, false, paint);
float adjustCricleWidth = cricleWidth + 360;
float width = 8;
while (adjustCricleWidth > ARC_WIDTH) {
width = width - arcChangeAngle;
adjustCricleWidth = adjustCricleWidth - ARC_WIDTH;
canvas.drawArc(rectF, adjustCricleWidth, width, false, paint);
}
} else {
//b
for (int i = 0; i <= 4; i++) {
if (ARC_WIDTH * i > cricleWidth) {
break;
}
canvas.drawArc(rectF, cricleWidth - ARC_WIDTH * i, 8 + i, false, paint);
}
if (cricleWidth > ARC_WIDTH * 4) {
canvas.drawArc(rectF, 0, cricleWidth - ARC_WIDTH * 4, false, paint);
}
float adjustCricleWidth = 360;
float width = 8 * (360 - cricleWidth) / 360;
if (DEBUG) {
Log.d(TAG, "width:" + width);
}
while (width > 0 && adjustCricleWidth > ARC_WIDTH) {
width = width - arcChangeAngle;
adjustCricleWidth = adjustCricleWidth - ARC_WIDTH;
canvas.drawArc(rectF, adjustCricleWidth, width, false, paint);
}
}
}
(2) 點擊View收縮效果:
在onDraw()方法中有:
canvas.scale(mScale, mScale, centerX(), centerY());
控制了View在點擊后的整體收縮效果妓盲,mScale參數(shù)由ValueAnimator和觸摸事件控制。在onTouchEvent()中我們要分析event专普,ACTION_DOWN時候按下mScale開始變小悯衬,從當前值向最向0.9變化(中間值由ValueAnimator生成),在ACTION_UP和ACTION_CANCEL時候手指抬起檀夹,mScale由當前值向1變化甚亭。
這里值得注意的是,在重寫onTouchEvent()時候击胜,有兩點要注意:1.要保證super.onTouchEvent(event)被調(diào)用亏狰,否則該View的OnClickListener和OnLongClickListener將不會響應(具體可見事件傳遞機制,OnClickListener/OnLongClickListener層級最低)偶摔。2.在處理ACTION_DOWN時候要保證返回值為True暇唾,否則同次動作的ACTION_UP等事件將不會再響應,這也是事件傳遞機制的內(nèi)容辰斋。為保證這兩點策州,此處代碼如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = false;
if (DEBUG) {
Log.d(TAG, "onTouchEvent: " + event.getAction());
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
startDownAnim();
result = true;
break;
}
case MotionEvent.ACTION_UP: {
startUpAnim();
break;
}
case MotionEvent.ACTION_CANCEL: {
startUpAnim();
break;
}
}
return super.onTouchEvent(event) || result;
}
private void startDownAnim() {
mTouchAnim.setFloatValues(mScale, 0.9f);
mTouchAnim.start();
}
private void startUpAnim() {
mTouchAnim.setFloatValues(mScale, 1);
mTouchAnim.start();
}
(3) ValueAnimator:
該項目用到了三個ValueAnimator:分別控制前文的degress,cricleWidth以及mScale宫仗,繪制圓弧的過程中是減速的過程够挂,所以用了減速插值器,其他兩個過程用的都是線性插值器藕夫。此外孽糖,還需要判斷當前是繪制圓弧向外伸展還是向內(nèi)伸縮,所以用了個boolean值isFirstCircle進行判斷毅贮,在動畫Repeat時候?qū)ζ渲捣崔D(zhuǎn)办悟。代碼如下:
private void onCreateAnimators() {
mRotateAnim = ValueAnimator.ofFloat(0, 180, 360);
mRotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
degress = (float) animation.getAnimatedValue();
postInvalidate();
}
});
mRotateAnim.setInterpolator(new LinearInterpolator());
mRotateAnim.setDuration(mRotateDuration);
mRotateAnim.setRepeatCount(-1);
mCircleAnim = ValueAnimator.ofFloat(0, 360);
mCircleAnim.setInterpolator(new DecelerateInterpolator());
mCircleAnim.setDuration(mCircleDuration);
mCircleAnim.setRepeatCount(-1);
mCircleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (isFirstCircle) {
cricleWidth = (float) animation.getAnimatedValue();
} else {
cricleWidth = (float) animation.getAnimatedValue() - 360;
}
postInvalidate();
}
});
mCircleAnim.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
isFirstCircle = !isFirstCircle;
}
});
mTouchAnim = new ValueAnimator();
mTouchAnim.setInterpolator(new DecelerateInterpolator());
mTouchAnim.setDuration(200);
mTouchAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mScale = (float) animation.getAnimatedValue();
postInvalidate();
}
});
startAnim();
}
(4) 繪制圓形圖片:
由于與ImageView不同,這里圖片要顯示成圓形滩褥,所以這里我們通過Drawble拿到Bitmap對象后病蛉,將其BitmapShader修剪成正方形,paint的shader設置為其BitmapShader,再用該paint畫圓:
private void drawBitmap(Canvas canvas) {
Paint bitmapPaint = new Paint();
setBitmapShader(bitmapPaint);
RectF rectF = new RectF(getWidth() * (1 - bitmapDia), getWidth() * (1 - bitmapDia),
getWidth() * bitmapDia, getHeight() * bitmapDia);
canvas.drawOval(rectF, bitmapPaint);
}
private void setBitmapShader(Paint paint) {
Drawable drawable = getDrawable();
Matrix matrix = new Matrix();
if (null == drawable) {
return;
}
Bitmap bitmap = drawableToBitmap(drawable);
BitmapShader tshader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
float scale = 1.0f;
int bSize = Math.min(bitmap.getWidth(), bitmap.getHeight());
scale = getWidth() * 1.0f / bSize;
matrix.setScale(scale, scale);
if (bitmap.getWidth() > bitmap.getHeight()) {
matrix.postTranslate(-(bitmap.getWidth() * scale - getWidth()) / 2, 0);
} else {
matrix.postTranslate(0, -(bitmap.getHeight() * scale - getHeight()) / 2);
}
tshader.setLocalMatrix(matrix);
paint.setShader(tshader);
}
private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
return bitmapDrawable.getBitmap();
}
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
(5) 顏色:
在onDraw()中,getPaint()得到了從mStartColor到mEndColor的過渡的顏色:
Paint paint = getPaint(mStartColor, mEndColor, 360);
其中:
private Paint getPaint(int startColor, int endColor, double arcWidth) {
Paint paint = new Paint();
Shader shader = new LinearGradient(0f, 0f, (float) (getWidth() * circleDia * (arcWidth - ARC_WIDTH * 4) / 360),
getHeight() * strokeWidth, startColor, endColor, CLAMP);
paint.setShader(shader);
setPaintStroke(paint);
return paint;
}
(6) 重寫onMeasure():
因為該控件是圓形铺然,所以還需要重寫onMeasure()方法俗孝,使其最后長和高一致,并針對MATCH_PARENT和WRAP_CONTENT以及指定具體寬高的情況下分別處理魄健,注意WRAP_CONTENT下這里是指定了最大寬/高為300px驹针,這與ImageView不同。代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (DEBUG) {
Log.d(TAG, "onMeasure widthMeasureSpec:" + widthSpecMode + "--" + widthSpecSize);
Log.d(TAG, "onMeasure heightMeasureSpec:" + heightSpecMode + "--" + heightSpecSize);
}
int width;
if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
width = Math.min(widthSpecSize, heightSpecSize);
} else {
width = Math.min(widthSpecSize, heightSpecSize);
width = Math.min(width, 300);
}
setMeasuredDimension(width, width);
}
總結
InsLoadingAnimation主要是由屬性動畫實現(xiàn)诀艰,也加深了對View的生命周期和事件傳遞等方法的理解柬甥。更進一步的,也練習了canvas上的繪圖其垄。最后我們就有了和Instagram和微博一樣炫酷的動畫效果~ 如果你覺得不錯苛蒲,趕快去https://github.com/qintong91/InsLoadingAnimation star/fork一下吧,歡迎交流和建議~