看到一个很好玩的gif等待动画,记录一下制作过程。
先上图,展示一下这gif。

图中四个空心圆,一个实心园,依次作规则双星运动。
三个晚上,目前已经已经实现了。又学到了不少东西,这几天把博客写完。
放个视频看下效果
先说一下思路,目前想到三种,一是自定义viewgroup,然后把小圆圈写成自定义的view,用animator属性动画来控制小圆圈的移动;二是自定义view,用canvas不断重绘来实现动画效果。我选择了第一种,第二种有空选另一个动画来实现,应该也不难,加油吧。
一、CircleView—小圆圈的制作
在gif图中,有四个空心圆,一个实心圆,因为没有太多的东西,所以直接用canvas绘制即可。
CircleView有五个参数,Context,是否是空心的,空心里面的颜色(gif中的红色),边框的颜色(gif中的白色),边框的宽度(单位是px);
PS:这里可以把strokeSize和circleSize设置成一样的大小,效果就是所有的CircleView都是实心的了。
CircleView的大小在onDraw方法里获取,由viewGroup来确定,这一点在第二部分说。
1package org.out.naruto.view; 2 3import android.content.Context; 4import android.graphics.Canvas; 5import android.graphics.Paint; 6import android.view.View; 7 8import org.out.naruto.utils.MyPoint; 9 10/** 11 * Created by Hao_S on 2016/6/1. 12 */ 13 14public class CircleView extends View { 15 16 private static final String TAG = "CircleView"; 17 18 private boolean isHollow = true; // 是否是空心圆 19 private int circleColor; // 颜色 20 private int strokeColor; // 边框颜色 21 private int mSize = 0; // view大小 22 private int strokeSize; // 边框宽度,单位 px 23 24 25 public CircleView(Context context) { 26 super(context); 27 } 28 29 public CircleView(Context context, Boolean isHollow, int circleColor, int strokeColor, int strokeSize) { 30 super(context); 31 this.isHollow = isHollow; 32 this.circleColor = circleColor; 33 this.strokeColor = strokeColor; 34 this.strokeSize = strokeSize; 35 } 36 37 @Override 38 protected void onDraw(Canvas canvas) { 39 super.onDraw(canvas); 40 mSize = this.getHeight(); 41 Paint paint = new Paint(); // 画笔 42 paint.setAntiAlias(true); // 抗锯齿 43 paint.setColor(strokeColor); 44 canvas.drawCircle(mSize / 2, mSize / 2, mSize / 2, paint); // 四个参数,分别是x坐标 y坐标 半径?? 画笔 45 if (isHollow) { // 如果是空心的,在里面再绘制一个圆 46 paint.setColor(this.circleColor); 47 canvas.drawCircle(mSize / 2, mSize / 2, (mSize - mSize / (strokeSize * 2)) / 2, paint); 48 } 49 } 50 51 /** 52 * @param myPoint 包含xy坐标的对象 53 * 这就是具体让小圆圈动起来的函数 54 * view.animate()函数是Android 3.1 提供的,返回的是ViewPropertyAnimator,简单来说就是对animator的封装。 55 */ 56 57 public void setPoint(MyPoint myPoint) { 58 59 this.animate().y(myPoint.getY()).x(myPoint.getX()).setDuration(0); 60 61 } 62 63}
canvas里面的绘制函数我就不详细解释了,就是画个圆 = =
setPoint和后面的一起解释。
二、ViewGroup的制作
这里我选择继承了FrameLayout,原因很简单:感觉(认真脸)。PS,抽空去试试其他的ViewGroup,应该会存在效率和资源上的差距。
这里先列举一下要确定的属性:ViewGroup的大小、CircleView的大小、CircleView之间的间距、CircleView的边框颜色、CircleView的数量(未实现,因为数量不同动画规律也不同)。
1 private Context context; 2 3 private int viewHeight, viewWidth; 4 5 private int viewColor = Color.RED; // ViewGroup里面的背景色,也是空心CircleView里面的颜色,默认红色。 6 private int circleSize = 100; // CircleView的大小,默认100像素。 7 private int spacing = 50; // CircleView之间的间隔,默认50像素。 8 private int strokeColor = Color.WHITE; // CircleView的圆形边框颜色,默认白色。 9 private boolean autoStart = false; // 是否自动执行动画 10 11 private int circleNum = 5; // CircleView的数量,默认5个。 12 private CircleView[] circleViews; // 所有的CircleView 13 private MyPoint[] myPoints; // 所有的坐标点 14 private CircleView targetView; // 那个实心的CircleView
首先在values文件夹下创建attrs.xml,规定好自己的属性
1<?xml version="1.0" encoding="utf-8"?> 2<resources> 3 4 <declare-styleable name="WaitingView"> 5 <attr name="viewColor" format="color" /> 6 <attr name="strokeColor" format="color" /> 7 <attr name="viewSpacing" format="integer" /> 8 <attr name="circleNum" format="integer" /> 9 <attr name="circleSize" format="integer"/> 10 <attr name="AutoStart" format="boolean" /> 11 </declare-styleable> 12 13</resources>
然后在构造方法里获取这些值(算是初级自定义view要掌握的):
1 public WaitingView(Context context, AttributeSet attrs) { 2 this(context, attrs, 0); 3 } 4 5 public WaitingView(Context context, AttributeSet attrs, int defStyleAttr) { 6 super(context, attrs, defStyleAttr); 7 this.context = context; 8 9 TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaitingView, defStyleAttr, 0); // 搞清楚这些参数 10 11 int num = a.getIndexCount(); 12 13 for (int i = 0; i < num; i++) { 14 int attr = a.getIndex(i); 15 switch (attr) { 16 case R.styleable.WaitingView_viewColor: 17 this.viewColor = a.getColor(attr, viewColor); 18 break; 19 case R.styleable.WaitingView_strokeColor: 20 this.strokeColor = a.getColor(attr, strokeColor); 21 break; 22 case R.styleable.WaitingView_viewSpacing: 23 this.spacing = a.getInteger(attr, spacing); 24 break; 25 case R.styleable.WaitingView_circleNum: 26 this.circleNum = a.getInteger(attr, circleNum); 27 break; 28 case R.styleable.WaitingView_circleSize: 29 this.circleSize = a.getInt(attr, circleSize); 30 break; 31 case R.styleable.WaitingView_AutoStart: 32 this.autoStart = a.getBoolean(attr, autoStart); 33 if (autoStart) { 34 Log.i(TAG, "autoStart is true"); 35 } 36 break; 37 case R.styleable.WaitingView_strokeSize: 38 int tempInt = a.getInteger(attr, strokeSize); 39 if (tempInt * 2 <= circleSize) { 40 strokeSize = tempInt; 41 } 42 break; 43 } 44 45 } 46 47 a.recycle(); // 释放资源 48 49 circleViews = new CircleView[circleNum]; 50 myPoints = new MyPoint[circleNum]; 51 52 setWillNotDraw(false); // 声明要调用onDraw方法。 53 54 55 }
这里要特别提一下构造方法中最后一个方法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)用很复杂的方式判断了,算是投机取巧?
1 private boolean first = true; // 用于标识只添加一次CircleView 2 3 @Override 4 protected void onDraw(Canvas canvas) { 5 super.onDraw(canvas); 6 canvas.drawColor(viewColor); 7 if (first) { 8 viewHeight = this.getHeight(); 9 viewWidth = this.getWidth(); 10 creatCircle(); 11 first = false; 12 if (autoStart) 13 startAnim(); 14 } 15 }
三、小圆圈添加到ViewGroup
gif图中五个圆在一条水平线上,水平居中。
直接上代码:
1 private void creatCircle() { 2 int top = (viewHeight - circleSize) / 2; // view的上边界距父View上边界的距离,单位是px(下同)。ViewGroup的高与CircleView的高之差的一半。 3 int left = (int) (viewWidth / 2 - ((circleNum / 2f) * circleSize + (circleNum - 1) / 2f * spacing)); 4 // int left = view左边界距父view左边界的距离,这里先算出了最左边view的数值,看着这么长,实在不想看。 5 // 总之就是,ViewGroup的宽的一半,减去一半数量的CircleView的宽和一半数量的CircleView间距,能理解级理解,不能理解我也没办法了。 6 int increats = circleSize + spacing; // left的增加量,每次增加一个CircleView的宽度和一个间距。 7 8 for (int i = 0; i < circleNum; i++) { 9 CircleView circleView = new CircleView(context, i != 0, viewColor, strokeColor); // new出来,除了第一个是实心圆,其他都是空心的。 10 circleViews[i] = circleView; // 添加到数组中,动画执行的时候要用。 11 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(circleSize, circleSize); // 这里就是确定CircleView大小的地方。 12 int realLeft = left + i * increats; // 实际的left值 13 layoutParams.setMargins(realLeft, top, 0, 0); // 设置坐标 14 MyPoint myPoint = new MyPoint(realLeft, top); // 把该坐标保存起来,动画执行的时候会用到。 15 myPoints[i] = myPoint; 16 circleView.setLayoutParams(layoutParams); 17 addView(circleView); // 添加 18 } 19 20 this.targetView = circleViews[0]; // 那个白色的实心圆 21 22 }
2016/6/3 17:45 先写到这里,有时间继续更。
四、小圆圈的运动
大部分说明都写在注释里了 = = 这里就不再重复了
1 /** 2 * 先说一下动画规律吧,实心白色圆不断依次和剩下的空心圆做半个双星运动。 3 * 每次一轮运动结束后,最先在前面的空心圆到了最后,就像一个循环队列一样。 4 * 但是这里我没有使用队列来实现,而是使用了数组,利用模除运算来计算出运动规律,这一点可能是这动画的短板,改进之后估计会解决自适应CircleView数量问题。 5 * 2016/6/4 1:00 解决了动画自适应CircleView的数量问题,是我之前的写法有点死板。 6 */ 7 8 private int position = 0; // CircleView动画执行次数 9 private int duration = 500; // 一次动画的持续时间 10 private AnimatorSet animatorSet; // AnimatorSet,使动画同时进行 11 private ObjectAnimator targetAnim, otherAnim; // 两个位移属性动画 12 13 public void startAnim() { 14 15 animatorSet = new AnimatorSet(); 16 // 添加一个监听,一小段动画结束之后立即开启下一小段动画 17 // 这里 18 animatorSet.addListener(new AnimatorListenerAdapter() { 19 @Override 20 public void onAnimationEnd(Animator animation) { 21 super.onAnimationEnd(animation); 22 startAnim(); 23 } 24 }); 25 26 int targetPosition = position % circleNum; // 这是实心白色CircleView所在次序,变化规律 0..(circleNum-1) 27 28 int otherPosition = (position + 1) % circleNum; // 即将和实心白色CircleView作圆周运动的空心圆所在次序,变化规律 1..(circleNum-1)0 29 30 int tempInt = (position + 1) % (circleNum - 1); // 这是除掉实心白色圆之后,剩下空心圆的次序,变化规律 1..(circleNum-1) 31 32 CircleView circleView = circleViews[tempInt == 0 ? (circleNum - 1) : tempInt]; // 获取即将和实心白色圆作圆周运动的CircleView对象 33 34 MyPoint targetPoint = myPoints[targetPosition]; // 实心白色圆实际的坐标点 35 36 MyPoint otherPoint = myPoints[otherPosition]; // 将要执行动画的空心圆坐标点 37 38 PointEvaluator targetPointEvaluator, otherPointEvaluator; // 坐标计算对象 39 40 // 这里有三种情况,第一种就是实心圆运动到了最后,和第一个空心圆交换 41 // 第二种就是实心圆在上面,空心圆在下面的交换动画 42 // 第三种是实心圆在下面,空心圆在上面的交换动画,除了第一种之外,其他都是实心圆往右移动,空心圆往左移动。 43 if (targetPosition == circleNum - 1) { 44 targetPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down); 45 otherPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up); 46 } else if ((targetPosition % 2) == 0) { 47 targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up); 48 otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down); 49 } else { 50 targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Down); 51 otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Up); 52 } 53 54 // 创建ObjectAnimator对象 55 // 第一个参数就是要做运动的view 56 // 第二个是要调用的方法,可以看看CircleView里面会有一个setPoint方法,这里会根据你填入的参数去寻找同名的set方法。 57 // 第三个是自定义的数值计算器,会根据运动状态的程度计算相应的结果 58 // 第四个和第五个参数是运动初始坐标和运动结束坐标。 59 targetAnim = ObjectAnimator.ofObject(this.targetView, "Point", targetPointEvaluator, targetPoint, otherPoint); 60 61 otherAnim = ObjectAnimator.ofObject(circleView, "Point", otherPointEvaluator, targetPoint, otherPoint); 62 63 animatorSet.playTogether(targetAnim, otherAnim); // 动画同时运行 64 animatorSet.setDuration(duration); // 设置持续时间 65 animatorSet.start(); // 执行动画 66 position++; 67 }
明天更新详细说明自定义动画值计算对象的写法,先放代码,这里是高中圆周运动知识,具体动画坐标是由运动角度和正弦余弦计算得出。
1 /** 2 * 枚举型标识动画运动类型 3 */ 4 public enum MoveType { 5 Left, Right, Up, Down 6 } 7 8 /** 9 * 运动算法: 10 * 根据做双星运动的两个CircleView的坐标,首先求出两坐标的中心点作为运动圆心。 11 * 根据运动的角度,结合cos与sin分别算出x轴与y轴的数值变化,然后返回当前运动坐标。 12 * x = (运动中心x坐标 ± Cos(运动角度)X 运动半径); 13 * y = (运动中心y坐标 ± Sin(运动角度)X 运动半径); 14 */ 15 private class PointEvaluator implements TypeEvaluator { 16 private MoveType LeftOrRight, UpOrDown; 17 18 public PointEvaluator(MoveType LeftOrRight, MoveType UpOrDown) { 19 this.LeftOrRight = LeftOrRight; 20 this.UpOrDown = UpOrDown; 21 } 22 23 @Override 24 public Object evaluate(float fraction, Object startValue, Object endValue) { 25 26 MyPoint startPoint = (MyPoint) startValue; // 运动开始时的坐标 27 MyPoint endPoint = (MyPoint) endValue; // 运动结束时的坐标 28 int R = (int) (Math.abs(startPoint.getX() - endPoint.getX()) / 2); // 运动圆周的半径 29 double r = Math.PI * fraction; // 当前运动角度 30 int circleX = (int) ((startPoint.getX() + endPoint.getX()) / 2); // 运动圆心坐标X 31 int circleY = (int) endPoint.getY();// 运动圆心坐标Y 32 float x = 0, y = 0; // 当前运动坐标 33 34 switch (LeftOrRight) { 35 case Left: 36 x = (float) (circleX + Math.cos(r) * R); 37 break; 38 case Right: 39 x = (float) (circleX - Math.cos(r) * R); 40 break; 41 } 42 43 switch (UpOrDown) { 44 case Up: 45 y = (float) (circleY - Math.sin(r) * R); 46 break; 47 case Down: 48 y = (float) (circleY + Math.sin(r) * R); 49 break; 50 } 51 52 MyPoint myPoint = new MyPoint(x, y); 53 54 return myPoint; 55 } 56 }
辅助类MyPoint
1package org.out.naruto.utils; 2 3/** 4 * Created by Hao_S on 2016/6/2. 5 */ 6 7public class MyPoint { 8 private float x, y; 9 10 11 public MyPoint(float x, float y) { 12 this.x = x; 13 this.y = y; 14 } 15 16 17 public float getY() { 18 return y; 19 } 20 21 public float getX() { 22 return x; 23 } 24}
最后感谢GQ、ZSJ学长和我一起找bug,衷心祝毕业愉快。
参考博客:
http://blog.csdn.net/lmj623565791/article/details/24555655