Android 仿instagram和微博的頭像點擊加載動畫

github:https://github.com/qintong91/InsLoadingAnimation
前段時間發(fā)現(xiàn)instagram點擊用戶頭像的加載小視頻動畫畴栖,效果如下:

ins.gif

對哈打,就是轉(zhuǎn)圈圈的這個碌廓,這么酷炫,我也要做一個唧瘾!在整理代碼和總結時候,神奇的事情發(fā)生了,在我日常刷微博的時候點開微博客戶端時候突然發(fā)現(xiàn):
weibo.gif

緣分啊岸裙,發(fā)現(xiàn)了微博Android客戶端也上線了類似動畫!等等速缆,不是類似降允,這是特么是除了顏色和ins的一毛一樣啊艺糜!
既然這個動畫效果這么火剧董,那還不趕快把我實現(xiàn)分享出來
如下就是我實現(xiàn)的效果:
demo

工程鏈接: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一下吧,歡迎交流和建議~

這么牛逼的項目绿满,你不star一個么
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臂外,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子喇颁,更是在濱河造成了極大的恐慌漏健,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橘霎,死亡現(xiàn)場離奇詭異蔫浆,居然都是意外死亡,警方通過查閱死者的電腦和手機姐叁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門瓦盛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人外潜,你說我怎么就攤上這事原环。” “怎么了处窥?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵嘱吗,是天一觀的道長。 經(jīng)常有香客問我滔驾,道長谒麦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任嵌灰,我火速辦了婚禮弄匕,結果婚禮上颅悉,老公的妹妹穿的比我還像新娘沽瞭。我一直安慰自己,他們只是感情好剩瓶,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布驹溃。 她就那樣靜靜地躺著城丧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豌鹤。 梳的紋絲不亂的頭發(fā)上亡哄,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音布疙,去河邊找鬼蚊惯。 笑死,一個胖子當著我的面吹牛灵临,可吹牛的內(nèi)容都是我干的截型。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼儒溉,長吁一口氣:“原來是場噩夢啊……” “哼宦焦!你這毒婦竟也來了?” 一聲冷哼從身側響起顿涣,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤波闹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后涛碑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體精堕,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年蒲障,在試婚紗的時候發(fā)現(xiàn)自己被綠了锄码。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡晌涕,死狀恐怖滋捶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情余黎,我是刑警寧澤重窟,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站惧财,受9級特大地震影響巡扇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜垮衷,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一厅翔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧搀突,春花似錦刀闷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顽分。三九已至,卻和暖如春施蜜,著一層夾襖步出監(jiān)牢的瞬間卒蘸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工翻默, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缸沃,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓修械,卻偏偏與公主長得像和泌,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子祠肥,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,846評論 25 707
  • 發(fā)現(xiàn) 關注 消息 iOS 第三方庫武氓、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,066評論 4 62
  • 當我們獨自背上行囊仇箱,開啟一場遠行的時候县恕,便注定要一個人承受旅途里的所有孤獨。 不知道找誰來拍照剂桥,記錄旅途中美好的自...
    田錦鈞閱讀 348評論 0 5
  • 知小酌忠烛,又名懶小拖,年過3旬权逗,自畫像...還在學習中...美尸,一直對手繪很感興趣,不過人如其名斟薇,懶嘛师坎,又拖、拖堪滨、拖胯陋,...
    知小酌閱讀 230評論 2 1