android 自定义园动画,Android动画:一个等待动画的制作过程

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

先上图,展示一下这gif。

78ED1F4CD8D72DF1F65DF8299C5FD41A.jpg

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

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

放个视频看下效果

先说一下思路,目前想到三种,一是自定义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,规定好自己的属性

然后在构造方法里获取这些值(算是初级自定义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,无意中发现的,因为我没有使用这个方法,所以放个链接:

我是在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,衷心祝毕业愉快。

参考博客:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值