Android动画:一个等待动画的制作过程

原创
2016/06/03 03:43
阅读数 4.5K

看到一个很好玩的gif等待动画,记录一下制作过程。

先上图,展示一下这gif。

图中四个空心圆,一个实心园,依次作规则双星运动。

三个晚上,目前已经已经实现了。又学到了不少东西,这几天把博客写完。

放个视频看下效果

 

先说一下思路,目前想到三种,一是自定义viewgroup,然后把小圆圈写成自定义的view,用animator属性动画来控制小圆圈的移动;二是自定义view,用canvas不断重绘来实现动画效果。我选择了第一种,第二种有空选另一个动画来实现,应该也不难,加油吧。

 

一、CircleView—小圆圈的制作

在gif图中,有四个空心圆,一个实心圆,因为没有太多的东西,所以直接用canvas绘制即可。

CircleView有五个参数,Context,是否是空心的,空心里面的颜色(gif中的红色),边框的颜色(gif中的白色),边框的宽度(单位是px);

PS:这里可以把strokeSize和circleSize设置成一样的大小,效果就是所有的CircleView都是实心的了。

CircleView的大小在onDraw方法里获取,由viewGroup来确定,这一点在第二部分说。

package org.out.naruto.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;

import org.out.naruto.utils.MyPoint;

/**
 * Created by Hao_S on 2016/6/1.
 */

public class CircleView extends View {

    private static final String TAG = "CircleView";

    private boolean isHollow = true; // 是否是空心圆
    private int circleColor; // 颜色
    private int strokeColor; // 边框颜色
    private int mSize = 0; // view大小
    private int strokeSize; // 边框宽度,单位 px


    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, Boolean isHollow, int circleColor, int strokeColor, int strokeSize) {
        super(context);
        this.isHollow = isHollow;
        this.circleColor = circleColor;
        this.strokeColor = strokeColor;
        this.strokeSize = strokeSize;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mSize = this.getHeight();
        Paint paint = new Paint(); // 画笔
        paint.setAntiAlias(true); // 抗锯齿
        paint.setColor(strokeColor);
        canvas.drawCircle(mSize / 2, mSize / 2, mSize / 2, paint); // 四个参数,分别是x坐标 y坐标 半径?? 画笔
        if (isHollow) { // 如果是空心的,在里面再绘制一个圆
            paint.setColor(this.circleColor);
            canvas.drawCircle(mSize / 2, mSize / 2, (mSize - mSize / (strokeSize * 2)) / 2, paint);
        }
    }

    /**
     * @param myPoint 包含xy坐标的对象
     *                这就是具体让小圆圈动起来的函数
     *                view.animate()函数是Android 3.1 提供的,返回的是ViewPropertyAnimator,简单来说就是对animator的封装。
     */

    public void setPoint(MyPoint myPoint) {

        this.animate().y(myPoint.getY()).x(myPoint.getX()).setDuration(0);

    }

}

canvas里面的绘制函数我就不详细解释了,就是画个圆 = = 

setPoint和后面的一起解释。

二、ViewGroup的制作

这里我选择继承了FrameLayout,原因很简单:感觉(认真脸)。PS,抽空去试试其他的ViewGroup,应该会存在效率和资源上的差距。

这里先列举一下要确定的属性:ViewGroup的大小、CircleView的大小、CircleView之间的间距、CircleView的边框颜色、CircleView的数量(未实现,因为数量不同动画规律也不同)。

    private Context context;

    private int viewHeight, viewWidth;

    private int viewColor = Color.RED; // ViewGroup里面的背景色,也是空心CircleView里面的颜色,默认红色。
    private int circleSize = 100; // CircleView的大小,默认100像素。
    private int spacing = 50; // CircleView之间的间隔,默认50像素。
    private int strokeColor = Color.WHITE; // CircleView的圆形边框颜色,默认白色。
    private boolean autoStart = false; // 是否自动执行动画

    private int circleNum = 5; // CircleView的数量,默认5个。
    private CircleView[] circleViews; // 所有的CircleView
    private MyPoint[] myPoints; // 所有的坐标点
    private CircleView targetView; // 那个实心的CircleView

首先在values文件夹下创建attrs.xml,规定好自己的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="WaitingView">
        <attr name="viewColor" format="color" />
        <attr name="strokeColor" format="color" />
        <attr name="viewSpacing" format="integer" />
        <attr name="circleNum" format="integer" />
        <attr name="circleSize" format="integer"/>
        <attr name="AutoStart" format="boolean" />
    </declare-styleable>

</resources>

然后在构造方法里获取这些值(算是初级自定义view要掌握的):

 public WaitingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaitingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaitingView, defStyleAttr, 0); // 搞清楚这些参数

        int num = a.getIndexCount();

        for (int i = 0; i < num; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.WaitingView_viewColor:
                    this.viewColor = a.getColor(attr, viewColor);
                    break;
                case R.styleable.WaitingView_strokeColor:
                    this.strokeColor = a.getColor(attr, strokeColor);
                    break;
                case R.styleable.WaitingView_viewSpacing:
                    this.spacing = a.getInteger(attr, spacing);
                    break;
                case R.styleable.WaitingView_circleNum:
                    this.circleNum = a.getInteger(attr, circleNum);
                    break;
                case R.styleable.WaitingView_circleSize:
                    this.circleSize = a.getInt(attr, circleSize);
                    break;
                case R.styleable.WaitingView_AutoStart:
                    this.autoStart = a.getBoolean(attr, autoStart);
                    if (autoStart) {
                        Log.i(TAG, "autoStart is true");
                    }
                    break;
                case R.styleable.WaitingView_strokeSize:
                    int tempInt = a.getInteger(attr, strokeSize);
                    if (tempInt * 2 <= circleSize) {
                        strokeSize = tempInt;
                    }
                    break;
            }

        }

        a.recycle(); // 释放资源

        circleViews = new CircleView[circleNum];
        myPoints = new MyPoint[circleNum];

        setWillNotDraw(false); // 声明要调用onDraw方法。


    }

这里要特别提一下构造方法中最后一个方法setWillNotDraw(),之前还在这里卡了一下,因为背景要绘制颜色,所以在onDraw里直接canvas.drawColor,结果发现不起作用(递归蒙蔽ing)。后来查资料发现,原来是因为这是个ViewGroup,如果不在xml文件里写android:background = "color"的话,系统是不会调用onDraw方法的,因为ViewGroup背景默认透明啊。所以就要把WillNotDraw设置为false。

自定义view属性还有一种方法,不用配置attrs.xml,无意中发现的,因为我没有使用这个方法,所以放个链接:

http://terryblog.blog.51cto.com/1764499/414884/

 

我是在onDraw方法里获取view的大小然后再添加CircleView,目前还不知道有什么弊端,但是这样就不用在之前的方法(执行顺序:onMesure onLayout onDraw)用很复杂的方式判断了,算是投机取巧?

 private boolean first = true; // 用于标识只添加一次CircleView

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(viewColor);
        if (first) {
            viewHeight = this.getHeight();
            viewWidth = this.getWidth();
            creatCircle();
            first = false;
            if (autoStart)
                startAnim();
        }
    }

 

三、小圆圈添加到ViewGroup

gif图中五个圆在一条水平线上,水平居中。

直接上代码:

  private void creatCircle() {
        int top = (viewHeight - circleSize) / 2; // view的上边界距父View上边界的距离,单位是px(下同)。ViewGroup的高与CircleView的高之差的一半。
        int left = (int) (viewWidth / 2 - ((circleNum / 2f) * circleSize + (circleNum - 1) / 2f * spacing));
        // int left = view左边界距父view左边界的距离,这里先算出了最左边view的数值,看着这么长,实在不想看。
        // 总之就是,ViewGroup的宽的一半,减去一半数量的CircleView的宽和一半数量的CircleView间距,能理解级理解,不能理解我也没办法了。
        int increats = circleSize + spacing; // left的增加量,每次增加一个CircleView的宽度和一个间距。

        for (int i = 0; i < circleNum; i++) {
            CircleView circleView = new CircleView(context, i != 0, viewColor, strokeColor); // new出来,除了第一个是实心圆,其他都是空心的。
            circleViews[i] = circleView; // 添加到数组中,动画执行的时候要用。
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(circleSize, circleSize); // 这里就是确定CircleView大小的地方。
            int realLeft = left + i * increats; // 实际的left值
            layoutParams.setMargins(realLeft, top, 0, 0); // 设置坐标
            MyPoint myPoint = new MyPoint(realLeft, top); // 把该坐标保存起来,动画执行的时候会用到。
            myPoints[i] = myPoint;
            circleView.setLayoutParams(layoutParams);
            addView(circleView); // 添加
        }

        this.targetView = circleViews[0]; // 那个白色的实心圆

    }

2016/6/3 17:45 先写到这里,有时间继续更。

四、小圆圈的运动

大部分说明都写在注释里了 = = 这里就不再重复了

    /**
     * 先说一下动画规律吧,实心白色圆不断依次和剩下的空心圆做半个双星运动。
     * 每次一轮运动结束后,最先在前面的空心圆到了最后,就像一个循环队列一样。
     * 但是这里我没有使用队列来实现,而是使用了数组,利用模除运算来计算出运动规律,这一点可能是这动画的短板,改进之后估计会解决自适应CircleView数量问题。
     * 2016/6/4 1:00 解决了动画自适应CircleView的数量问题,是我之前的写法有点死板。
     */

    private int position = 0; // CircleView动画执行次数
    private int duration = 500; // 一次动画的持续时间
    private AnimatorSet animatorSet; // AnimatorSet,使动画同时进行
    private ObjectAnimator targetAnim, otherAnim; // 两个位移属性动画

    public void startAnim() {

        animatorSet = new AnimatorSet();
        // 添加一个监听,一小段动画结束之后立即开启下一小段动画
        // 这里
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                startAnim();
            }
        });

        int targetPosition = position % circleNum; // 这是实心白色CircleView所在次序,变化规律 0..(circleNum-1)

        int otherPosition = (position + 1) % circleNum; // 即将和实心白色CircleView作圆周运动的空心圆所在次序,变化规律 1..(circleNum-1)0

        int tempInt = (position + 1) % (circleNum - 1); // 这是除掉实心白色圆之后,剩下空心圆的次序,变化规律 1..(circleNum-1)

        CircleView circleView = circleViews[tempInt == 0 ? (circleNum - 1) : tempInt]; // 获取即将和实心白色圆作圆周运动的CircleView对象

        MyPoint targetPoint = myPoints[targetPosition]; // 实心白色圆实际的坐标点

        MyPoint otherPoint = myPoints[otherPosition]; // 将要执行动画的空心圆坐标点

        PointEvaluator targetPointEvaluator, otherPointEvaluator; // 坐标计算对象

        // 这里有三种情况,第一种就是实心圆运动到了最后,和第一个空心圆交换
        // 第二种就是实心圆在上面,空心圆在下面的交换动画
        // 第三种是实心圆在下面,空心圆在上面的交换动画,除了第一种之外,其他都是实心圆往右移动,空心圆往左移动。
        if (targetPosition == circleNum - 1) {
            targetPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down);
            otherPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up);
        } else if ((targetPosition % 2) == 0) {
            targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up);
            otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down);
        } else {
            targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Down);
            otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Up);
        }

        // 创建ObjectAnimator对象
        // 第一个参数就是要做运动的view
        // 第二个是要调用的方法,可以看看CircleView里面会有一个setPoint方法,这里会根据你填入的参数去寻找同名的set方法。
        // 第三个是自定义的数值计算器,会根据运动状态的程度计算相应的结果
        // 第四个和第五个参数是运动初始坐标和运动结束坐标。
        targetAnim = ObjectAnimator.ofObject(this.targetView, "Point", targetPointEvaluator, targetPoint, otherPoint);

        otherAnim = ObjectAnimator.ofObject(circleView, "Point", otherPointEvaluator, targetPoint, otherPoint);

        animatorSet.playTogether(targetAnim, otherAnim); // 动画同时运行
        animatorSet.setDuration(duration); // 设置持续时间
        animatorSet.start(); // 执行动画
        position++;
    }

明天更新详细说明自定义动画值计算对象的写法,先放代码,这里是高中圆周运动知识,具体动画坐标是由运动角度和正弦余弦计算得出。

    /**
     * 枚举型标识动画运动类型
     */
    public enum MoveType {
        Left, Right, Up, Down
    }

    /**
     * 运动算法:
     * 根据做双星运动的两个CircleView的坐标,首先求出两坐标的中心点作为运动圆心。
     * 根据运动的角度,结合cos与sin分别算出x轴与y轴的数值变化,然后返回当前运动坐标。
     * x = (运动中心x坐标 ± Cos(运动角度)X 运动半径);
     * y = (运动中心y坐标 ± Sin(运动角度)X 运动半径);
     */
    private class PointEvaluator implements TypeEvaluator {
        private MoveType LeftOrRight, UpOrDown;

        public PointEvaluator(MoveType LeftOrRight, MoveType UpOrDown) {
            this.LeftOrRight = LeftOrRight;
            this.UpOrDown = UpOrDown;
        }

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {

            MyPoint startPoint = (MyPoint) startValue; // 运动开始时的坐标
            MyPoint endPoint = (MyPoint) endValue; // 运动结束时的坐标
            int R = (int) (Math.abs(startPoint.getX() - endPoint.getX()) / 2); // 运动圆周的半径
            double r = Math.PI * fraction; // 当前运动角度
            int circleX = (int) ((startPoint.getX() + endPoint.getX()) / 2); // 运动圆心坐标X
            int circleY = (int) endPoint.getY();// 运动圆心坐标Y
            float x = 0, y = 0; // 当前运动坐标

            switch (LeftOrRight) {
                case Left:
                    x = (float) (circleX + Math.cos(r) * R);
                    break;
                case Right:
                    x = (float) (circleX - Math.cos(r) * R);
                    break;
            }

            switch (UpOrDown) {
                case Up:
                    y = (float) (circleY - Math.sin(r) * R);
                    break;
                case Down:
                    y = (float) (circleY + Math.sin(r) * R);
                    break;
            }

            MyPoint myPoint = new MyPoint(x, y);

            return myPoint;
        }
    }

辅助类MyPoint

package org.out.naruto.utils;

/**
 * Created by Hao_S on 2016/6/2.
 */

public class MyPoint {
    private float x, y;


    public MyPoint(float x, float y) {
        this.x = x;
        this.y = y;
    }


    public float getY() {
        return y;
    }

    public float getX() {
        return x;
    }
}

 

最后感谢GQ、ZSJ学长和我一起找bug,衷心祝毕业愉快。

 

参考博客:

http://blog.csdn.net/lmj623565791/article/details/24555655

http://blog.csdn.net/guolin_blog/article/details/43816093

http://blog.csdn.net/leehong2005/article/details/7299471

展开阅读全文
打赏
0
21 收藏
分享
加载中
更多评论
打赏
0 评论
21 收藏
0
分享
在线直播报名
返回顶部
顶部