看到一个很好玩的gif等待动画,记录一下制做过程。php
先上图,展现一下这gif。java
图中四个空心圆,一个实心园,依次做规则双星运动。android
三个晚上,目前已经已经实现了。又学到了很多东西,这几天把博客写完。算法
放个视频看下效果canvas
先说一下思路,目前想到三种,一是自定义viewgroup,而后把小圆圈写成自定义的view,用animator属性动画来控制小圆圈的移动;二是自定义view,用canvas不断重绘来实现动画效果。我选择了第一种,第二种有空选另外一个动画来实现,应该也不难,加油吧。数组
1、CircleView—小圆圈的制做app
在gif图中,有四个空心圆,一个实心圆,由于没有太多的东西,因此直接用canvas绘制便可。ide
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和后面的一块儿解释。
2、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(); } }
3、小圆圈添加到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 先写到这里,有时间继续更。
4、小圆圈的运动
大部分说明都写在注释里了 = = 这里就再也不重复了
/** * 先说一下动画规律吧,实心白色圆不断依次和剩下的空心圆作半个双星运动。 * 每次一轮运动结束后,最早在前面的空心圆到了最后,就像一个循环队列同样。 * 可是这里我没有使用队列来实现,而是使用了数组,利用模除运算来计算出运动规律,这一点多是这动画的短板,改进以后估计会解决自适应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