仿PC端12306的刷新loading动画的自定义view

想法由来

大家肯定都在PC端的12306网站上买过票吧,最近的这两天又去买票的时候留意到这个刷新loading,觉得还是挺好看的,所以就想动手实现下,顺便把自定义view和动画的知识稍微再熟悉一遍。

原12306的刷新图:
原12306的刷新图
实现的效果图如下:
实现后的效果图

自定义view的代码如下(暂且就叫PassView吧)
package com.example.yanxu.loading.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;

import com.example.yanxu.loading.R;


/**
 * Created by chen on 2018/12/12.
 * 仿PC端12306的刷新loading动画的自定义view
 */

public class PassView extends View {

    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mWidth, mHeight;
    private float mRadius = 0f;
    private ValueAnimator animator;
    private int theCircle = -1;//当前哪个圆球变化

    public PassView(Context context) {
        this(context, null);
    }

    public PassView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public PassView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    //初始化
    private void init(AttributeSet attrs) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PassView);
        if (typedArray != null) {
            mColor = typedArray.getColor(R.styleable.PassView_pass_color, Color.RED);
            mRadius = typedArray.getDimension(R.styleable.PassView_pass_radius, 0);
            typedArray.recycle();
        }
        mWidth = (int) (2 * mRadius);
        mHeight = (int) (2 * mRadius);
    }

    /**
     * 重写onMeasure方法,解决wrap_content问题,因为:
     * 直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent
     * 这里的view默认宽高为了方便直接指定为小圆的直径
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

    /**
     * 重写onDraw方法实现自己的想要的效果,同时处理padding问题,因为:
     * 绘制的时候需要考虑到View四周的空白,即padding,否则计算会有偏差
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int pLeft = getPaddingLeft();
        int pRight = getPaddingRight();
        int pTop = getPaddingTop();
        int pBottom = getPaddingBottom();
        int width = getWidth() - pLeft - pRight;
        int height = getHeight() - pTop - pBottom;
        int mHLength = 150;//大圆的半径
        mPaint.setColor(mColor);
        mPaint.setTextSize(50);
        //1、动画开启前,theCircle的初始值为-1,所以初始化时只走canvas.drawCircle()方法,即所有位置均为蓝色
        //2、动画开启后,theCircle=0时不改变颜色,即所有位置均为蓝色
        //3、继续,theCircle=1、i=2时,第一个位置依旧为蓝色,第二个及剩余其他位置为灰色
        //4、继续,theCircle=2,i=3时,第一、二个位置依旧为蓝色,第三个及剩余其他位置为灰色
        //5、......
        //6、theCircle=11,i=12时,第一到十一个位置全部为蓝色,剩余的第十二个为灰色
        //7、直到,theCircle=0,循环继续
        //注:思路重点为,Paint使用的是同一个,所以在重新setColor()前的颜色是一样的,setColor()后的颜色是另一样的
        for (int i = 1; i <= 12; i++) {
            if (theCircle + 1 == i && theCircle != 0) {
                mPaint.setColor(getResources().getColor(R.color.colorPassGray));
            }
            canvas.drawCircle(width / 2 + (float) Math.sin(Math.PI * (i - 1) / 6) * mHLength,
                    height / 2 - (float) Math.cos(Math.PI * (i - 1) / 6) * mHLength, mRadius, mPaint);
            //Math.PI即为π,等于180度
        }
    }

    //开启动画
    public void start() {
        if (animator != null)
            animator.cancel();
        animator = ValueAnimator.ofInt(0, 12);
        animator.setDuration(2000);//动画时长
        animator.setRepeatMode(ValueAnimator.RESTART);//动画的重复次数
        animator.setRepeatCount(ValueAnimator.INFINITE);//动画的重复模式
        animator.setInterpolator(new LinearInterpolator());//线性插值器:匀速动画
        //监听动画过程
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取当前动画的进度值,此处的theCircle值为(0、1、2...11、0)
                theCircle = ((int) animation.getAnimatedValue() % 12);
                invalidate();
            }
        });
        animator.start();
    }

}
在说具体的实现思路前,这里先回顾下自定义view的流程:

对于继承自View的自定义view而言,首先我们肯定是要重写onDraw()方法通过代码逻辑来实现具体的效果,如果想要支持wrap_content,那么还需要重写onMeasure()方法,既然是自定义view,那么我们免不了需要添加自定义的属性,概括下来具体流程如下:

  1. 第一步,在values目录下创建自定义属性的XML(如attrs.xml),并声明自定义属性集合,本例为“PassView”
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="PassView">
        <attr name="pass_color" format="color" />
        <attr name="pass_radius" format="dimension" />
    </declare-styleable>

</resources>
  1. 第二步,在view的构造方法中去解析自定义属性的值
    //初始化
    private void init(AttributeSet attrs) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.PassView);
        if (typedArray != null) {
            mColor = typedArray.getColor(R.styleable.PassView_pass_color, Color.RED);
            mRadius = typedArray.getDimension(R.styleable.PassView_pass_radius, 0);
            typedArray.recycle();
        }
        mWidth = (int) (2 * mRadius);
        mHeight = (int) (2 * mRadius);
    }
  1. 第三步,在布局文件中使用自定义的view
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.MainActivity">

    <com.example.yanxu.loading.view.PassView
        android:id="@+id/pv_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"
        app:pass_color="@color/colorPassLight"
        app:pass_radius="5dp" />

</LinearLayout>
说完了自定义view的流程,紧接着再简单说下我的思路,主要的涉及到核心代码的地方都加了注释,本身逻辑也不是特别复杂,相信大家应该都能看的明白。
  1. 首先是先画出十二个小圆球;
  2. 实现让当前小圆球从灰色变成蓝色的状态;
  3. 依次循环绘画,实现动画效果。
1、首先是先画出十二个小圆球

画的有点丑,凑合看下吧。。
如图,十二个小圆球均匀分布在大圆的周边,我们定义大圆的中心点为该控件的中心点(width / 2 , height / 2),则图中那个小圆球的坐标为如图手写所示,其他几个小圆球的坐标只要改变为对应的角度即可画出所有的小圆球:

for (int i = 1; i <= 12; i++) {
            canvas.drawCircle(width / 2 + (float) Math.sin(Math.PI * (i - 1) / 6) * mHLength,
                    height / 2 - (float) Math.cos(Math.PI * (i - 1) / 6) * mHLength, mRadius, mPaint);
            //Math.PI即为π,等于180度
        }
2、实现让当前小圆球从灰色变成蓝色的状态

这里我们可以使用属性动画,让ValueAnimator对象的变化范围为ValueAnimator.ofInt(0, 12),匀速循环播放

        animator = ValueAnimator.ofInt(0, 12);
        animator.setDuration(2000);//动画时长
        animator.setRepeatMode(ValueAnimator.RESTART);//动画的重复次数
        animator.setRepeatCount(ValueAnimator.INFINITE);//动画的重复模式
        animator.setInterpolator(new LinearInterpolator());//线性插值器:匀速动画
3、依次循环绘画,实现动画效果

这里通过监听动画过程,获取当前动画的进度值,即绘制到哪一个小圆球,通过监听动画过程,我们可得到此处的theCircle值的变化为(0、1、2…11、0)

        //监听动画过程
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取当前动画的进度值,此处的theCircle值为(0、1、2...11、0)
                theCircle = ((int) animation.getAnimatedValue() % 12);
                invalidate();
            }
        });

然后我们进行依次绘制的过程,具体如下:

  1. 动画开启前,theCircle的初始值为-1,所以初始化时只走canvas.drawCircle()方法,即所有位置均为蓝色
  2. 动画开启后,theCircle=0时不改变颜色,即所有位置均为蓝色
  3. 继续,theCircle=1、i=2时,第一个位置为蓝色,第二个及剩余其他位置为灰色
  4. 继续,theCircle=2,i=3时,第一、二个位置为蓝色,第三个及剩余其他位置为灰色
  5. theCircle=11,i=12时,第一到十一个位置全部为蓝色,剩余的第十二个为灰色
  6. 直到,theCircle=0,循环继续

注:思路重点为,Paint使用的是同一个,所以在重新setColor()前的颜色是一样的,setColor()后的颜色是另一样的

        mPaint.setColor(mColor);
        mPaint.setTextSize(50);
        for (int i = 1; i <= 12; i++) {
            if (theCircle + 1 == i && theCircle != 0) {
                mPaint.setColor(getResources().getColor(R.color.colorPassGray));
            }
            canvas.drawCircle(width / 2 + (float) Math.sin(Math.PI * (i - 1) / 6) * mHLength,
                    height / 2 - (float) Math.cos(Math.PI * (i - 1) / 6) * mHLength, mRadius, mPaint);
            //Math.PI即为π,等于180度
        }

总结:以上就是我的具体实现方案以及代码逻辑,整个过程涉及到自定义view以及属性动画的使用,应该还有更好的实现方案,还望指正。个人觉得有些效果真的很有必要去亲自代码逻辑实现下,开始看到这个效果的时候觉得很简单,但是真正实现起来有些细节部分还是需要仔细想想的,而且在实现的过程中也是一个知识巩固的过程~

demo已经上传至github,需要的小伙伴可以看一下–>传送门

介绍: DEMO,一般的玩家会以为是游戏开始前介绍剧情的动画。但我们今天讲的DEMO是一些团体为参加国际性DEMO比赛而制作的DEMO,展现出许多高难度的图形,带给欣赏者不少的赞叹。 “DEMO是demonstration的缩写,在电脑上的DEMO简单的说就是展示电脑图形与音乐的程式,所以游戏开始的动画战士也是DEMO的一种。在电脑公司,可以看到电脑上展示介绍电脑软硬件的程式,这些属于商业性质的DEMO;这些DEMO是凭借图形与音乐来吸引顾客,达到寻穿的目的。 但如果知识一般DEMO那就没有什么好看的了。这里主要介绍的DEMO并非指的商业性的DEMO,而是在国际比赛,有个参赛团体专门为DEMO比赛而制作的DEMO。这些DEMO主要目的是:带给欣赏者趣味并且发挥电脑在秽土与音乐上的亲历。也就是说DEMO结合另人看到目瞪口呆的CG与音乐,在加上DEMO制作者的编程技巧与功力,展现出许多高难度的表演。有人说DEMO就是:“亲爱的,我把PC变成SGI了。”得奖的DEMO在设计时一般进行程序最优化,充分发挥PC的硬件潜力,产生惊人的效果,包括:多变的音乐,即时运算产生的RENER图形,FRACTRL,透明,PLASMA,3D VECTOR SPACE,VIRTUAL REALITY,MORPH等。 为了达到这些效果,这些DEMO通常有下面四个特性: 1。使用汇编语言,要产生一个简单的DEMO,用高级语言可以很轻松的写出来,但因为一些限制速度很不理想。运用汇编语言最优化,可以充分发挥与控制软硬件饿威力。 2。多声道的音乐。 3。突破传统的绘图能力:在PC上标准VGA在320X200的解析度只能显示256色,很少有记忆页,造成很多限制。而DEMO往往使用特殊的模式,通常称做X MODE,在这些模式下能达到320X200 256色多记忆页。 4。即时运算:在这些DEMO里大多有3D向量空间,虚拟真实的部分,或是有许多的电脑上色效果,还有变形等。由于即时运算的关系,尽管一个DEMO不大,也可以播10-20分钟。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值