美文网首页Android开发部落Android DevAndroid 开发技术分享
Android 仿instagram和微博的头像点击加载动画

Android 仿instagram和微博的头像点击加载动画

作者: Uprising | 来源:发表于2017-05-25 07:41 被阅读1350次

    github:https://github.com/qintong91/InsLoadingAnimation
    前段时间发现instagram点击用户头像的加载小视频动画,效果如下:

    ins.gif
    对,就是转圈圈的这个,这么酷炫,我也要做一个!在整理代码和总结时候,神奇的事情发生了,在我日常刷微博的时候点开微博客户端时候突然发现:
    weibo.gif
    缘分啊,发现了微博Android客户端也上线了类似动画!等等,不是类似,这是特么是除了颜色和ins的一毛一样啊!
    既然这个动画效果这么火,那还不赶快把我实现分享出来
    如下就是我实现的效果:
    demo

    工程链接:https://github.com/qintong91/InsLoadingAnimation
    (你如果觉得不错就不要控制自己,点进去star一下~)
    下文,分别为整理介绍,使用,具体实现与总结。

    1.介绍

    InsLoadingView继承自ImageView,其对应的image显示为圆形。InsLoadingView有三种状态:LOADING/UNCLICKED/CLICKED,Loading时候轮廓有不断循环的动画,如上图(下文分析源码时候会详细阐明其过程)。UNCLICKED时外侧轮廓为静态的彩色圈,CLICKED外层为静态的灰色圈。此外,在其被点击时还有控件收缩的动画效果。注意:由于状态是与应用中的情况相关的,所以状态变化需要用户手动去设置。
    整体效果如下(感谢家里的喵主子~)


    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

    设置状态:

    您可以手动设置其状态,来对应在您应用中的当前状态。InsLoadingView的状态有:
    LOADING: 表示InsLoadingView被点击之后正在加载内容(未加载完毕之前),该状态下动画正在执行。
    UNCLICKED: 该InsLoadingView被点击之前的状态,此状态下动画停止。
    CLICKED: 表示InsLoadingView被点击和加载过,此状态下动画停止切圆圈的颜色为灰色。
    默认的状态是LOADING。

    可以通过一下代码设置状态:
    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。

    设置速度

    通过设置环绕动画的时间和整体旋转的时间来改变速度:

    xml:

      app:circle_duration="2000"
      app:rotate_duration="10000"
    

    java:

      mInsLoadingView.setCircleDuration(2000);
      mInsLoadingView.setRotateDuration(10000);
    

    默认的时间为2000ms和10000ms。

    2.实现

    完整的代码请见https://github.com/qintong91/InsLoadingAnimation
    下面就对代码进行分析。
    InsLoadingView继承自ImageView,动画效果主要通过重写onDraw()函数重新绘制。所以可以先看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()为实现显示圆形图片重新完成了绘制图片的过程。之后根据当前status绘制图片外的圈:status为LOADING时候绘制时是动画,其他两种情况绘制是静态的圆圈。

    (1) 动画绘制:

    LOADING时候的动画是项目中最核心的部分。从动画效果中可以看出,圆弧的两端都在运动:运动较慢的一端其实反应了外圈的整体旋转(连同颜色),较快一端的旋转还有两个过程:圆弧向外“伸展”一圈和向回“收缩”一圈的过程。
    degress和cricleWidth是实时变化的,他们的值由ValueAnimator设置,这两个值分别表示整个动画整体旋转的角度(也就是动画中转速较慢一端)和转速较快的圆弧的动画。两个变量的单位都是度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参数由ValueAnimator和触摸事件控制。在onTouchEvent()中我们要分析event,ACTION_DOWN时候按下mScale开始变小,从当前值向最向0.9变化(中间值由ValueAnimator生成),在ACTION_UP和ACTION_CANCEL时候手指抬起,mScale由当前值向1变化。
    这里值得注意的是,在重写onTouchEvent()时候,有两点要注意:1.要保证super.onTouchEvent(event)被调用,否则该View的OnClickListener和OnLongClickListener将不会响应(具体可见事件传递机制,OnClickListener/OnLongClickListener层级最低)。2.在处理ACTION_DOWN时候要保证返回值为True,否则同次动作的ACTION_UP等事件将不会再响应,这也是事件传递机制的内容。为保证这两点,此处代码如下:

        @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,绘制圆弧的过程中是减速的过程,所以用了减速插值器,其他两个过程用的都是线性插值器。此外,还需要判断当前是绘制圆弧向外伸展还是向内伸缩,所以用了个boolean值isFirstCircle进行判断,在动画Repeat时候对其值反转。代码如下:

        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主要是由属性动画实现,也加深了对View的生命周期和事件传递等方法的理解。更进一步的,也练习了canvas上的绘图。最后我们就有了和Instagram和微博一样炫酷的动画效果~ 如果你觉得不错,赶快去https://github.com/qintong91/InsLoadingAnimation star/fork一下吧,欢迎交流和建议~

    这么牛逼的项目,你不star一个么

    相关文章

      网友评论

      本文标题:Android 仿instagram和微博的头像点击加载动画

      本文链接:https://www.haomeiwen.com/subject/xedbxxtx.html