看到一个很好玩的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