最近在做Libgdx项目开发时,遇到一个问题,那就是如果在Libgdx中使用ValueAnimator。平时我们在Android项目中,做动画时会经常会使用ValueAnimator.ofInt(0, 100)或者ValueAnimator.ofFloat设置一个duration,就可以在指定的时间内完成从Start到End的过程转化了。
样例:
ValueAnimator alphaAction = ValueAnimator.ofFloat(0f, 1f);
alphaAction.setDuration(1000);
alphaAction.addUpdateListener(animation -> {
if (getMainGame().isAppFinishing()) {
return;
}
float animatedFraction = (float) animation.getAnimatedValue();
if (mAccumulateState.getColor() != null) {
mAccumulateState.getColor().a = animatedFraction;
}
if (mAccumulateBgImg.getColor() != null) {
mAccumulateBgImg.getColor().a = animatedFraction;
}
});
alphaAction.start();
但是ValueAnimator只能在Android的UI线程中执行,所以还需要通过Handler进行线程切换。不过这里需要说明一点的是:Libgdx是线程不安全的,所以在执行过程中需要考虑到多线程的问题。
那既然Libgdx是一个动画库,那自然会有相应的方式,这就让我想到了动作(Action)。
动作(Action) 是附加在演员身上的在指定时间内随着时间推移而被执行的一些任务逻辑。演员都是静态不会动的,给演员附加上动作可以让演员自己“动”起来,相当于 Android 中给 View 添加的动画(Animation)。
LibGDX 中所有的动作均继承自 Action 抽象类,所有的动作实现均在 com.badlogic.gdx.scenes.scene2d.actions 包中。常用的动作可分为 特效类动作 和 控制类动作 两种。
(1)特效类动作
特效类动作会改变演员的状态(位置,尺寸,旋转角度,缩放比,透明度等),还可细分为 绝对动作 和 相对动作。
绝对动作(从 当前状态 过渡到 指定状态 的动作,结果位置确定 并与原位置无关):
- MoveToAction: 将演员从当前位置移动到指定的位置
- RotateToAction: 将演员从当前角度旋转到指定的角度
- ScaleToAction: 将演员从当前的缩放值过渡到指定的缩放值
- SizeToAction: 将演员从当前尺寸(宽高)过渡到指定尺寸(宽高)
- AlphaAction: 将演员的透明度从当前 alpha 值过渡到指定的 alpha 值
相对动作(在 当前状态 基础上给 某一状态的值 增加一个增量,结果位置相对于原位置):
- MoveByAction: 演员在当前位置基础上移动指定的距离
- RotateByAction: 演员在当前角度值的基础上旋转指定的角度
- ScaleByAction: 演员在当前缩放值的基础上进行再次缩放
- SizeByAction: 演员在当前尺寸基础上增加指定的尺寸
(1)控制类动作
控制类动作本身一般不会改变演员的状态,只是用来控制动作的执行(对动作的包装,本身也是一个动作),是控制动作的动作。
- SequenceAction: 顺序动作,包含一个或多个动作,这些动作按顺序依次执行。
- ParallelAction: 并行动作,包含一个或多个动作,这些动作一起同时执行。
- RepeatAction: 重复动作,包含一个动作,重复执行这个被包含的动作。
- DelayAction: 延时动作,一般添加到顺序动作的动作序列中使用。例如按顺序执行完某一动作后,执行一个延时动作(延时指定时间),然后再继续执行下一个动作。
- AfterAction: 包含一个动作,该动作和其他动作一起添加到演员身上,等到演员身上的其他所有动作都执行完后,执行该 AfterAction 所包含的动作。
- RunnableAction: 包含一个 Runnable 实例,可以在指定时机执行自己的代码。例如将一个 RunnableAction 添加到顺序动作的末尾,则可在顺序动作执行完成时执行自己的 Runnable 中 run() 方法内的代码。
Libgdx已经为我们提供了这么多的Action,基本上已经满足了我们日常开发中使用的效果了。但是,当我们想把Label放大缩小时,以上的这些Action就不能满足了。因为以上的Action只是对Actor进行基本上的操作,
我们就以ScaleToAction代码为例:
private float startX, startY;
private float endX, endY;
protected void begin () {
startX = target.getScaleX();
startY = target.getScaleY();
}
protected void update (float percent) {
float x, y;
if (percent == 0) {
x = startX;
y = startY;
} else if (percent == 1) {
x = endX;
y = endY;
} else {
x = startX + (endX - startX) * percent;
y = startY + (endY - startY) * percent;
}
//调用这个方法设置大小,故无法设置Label大小
target.setScale(x, y);
}
而Label的缩放是需要通过setFontScale,这也是为什么我会使用ValueAnimator了。
那既然没有那我们就自动动手,丰衣足食呗。
首先大家需要明白一点的是,所有的动画计算都是放在 Action.act(float) 中的。逐帧更新Actor的行为,通常在此方法上进行逻辑代码的更新,默认的空实现是 **Action.act(float) **。既然明白了,那我们就自动动手造轮子。
第一步:
创建一个名为FloatAction 的Action并继承Action抽象类,不过我们可以通过分析现有的Action可知,它们继承类另一个TemporalAction,而这个类的父类其实也是继承了Action抽象类。那我们来看一下TemporalAction代码结构:
abstract public class TemporalAction extends Action {
private float duration, time;
private Interpolation interpolation;
private boolean reverse, began, complete;
....
---------关键代码点-------------
/**
* 演员的逻辑处理
*
* @param delta
* 表示从渲染上一帧开始到现在渲染当前帧的时间间隔, 或者称为渲染的 时间步 / 时间差。单位: 秒
*/
public boolean act (float delta) {
if (complete) return true;
Pool pool = getPool();
setPool(null); // Ensure this action can't be returned to the pool while executing.
try {
if (!began) {
begin();
began = true;
}
time += delta;
//判断是否已经执行完成
complete = time >= duration;
//当前进度的百分比
float percent = complete ? 1 : time / duration;
if (interpolation != null) percent = interpolation.apply(percent);
//更新,并判断是否需要反转
update(reverse ? 1 - percent : percent);
if (complete) end();
return complete;
} finally {
setPool(pool);
}
}
...
}
从代码中,我们可以看出,其实它已经帮我们计算好了时长和执行的百分比,我们直接继承它即可。
第二步:
这一步我们就开始边界具体的代码逻辑了,代码如下:
public class FloatAction extends TemporalAction {
private AnimatorUpdateListener mAnimatorUpdateListener;
private float start, end;
private float value;
....
protected void update(float percent) {
if (percent == 0)
value = start;
else if (percent == 1)
value = end;
else
value = start + (end - start) * percent;
if (mAnimatorUpdateListener != null) {
mAnimatorUpdateListener.onAnimationUpdate(value);
}
}
public void setAnimatorUpdateListener(AnimatorUpdateListener mAnimatorUpdateListener) {
this.mAnimatorUpdateListener = mAnimatorUpdateListener;
}
....
public static interface AnimatorUpdateListener {
//动画执行回调
void onAnimationUpdate(float animation);
}
}
是不是看起来特别简单。😁😁😁
第三步:
那就让我们在实际的项目中使用这个吧。
FloatAction scaleAction = new FloatAction(startScale, endScale, duration);
scaleAction.setAnimatorUpdateListener(animation -> {
mAccumulateState.setFontScale(animation);
mAccumulateState.setX((getWidth() - getAccumulateStateWidth()) / 2);
mAccumulateBgImg.setScale(mAccumulateState.getFontScaleX(), mAccumulateState.getFontScaleY());
mAccumulateBgImg.setX(mAccumulateState.getX() - (mAccumulateBgImg.getWidth() - getAccumulateStateWidth()) / 2);
});
FloatAction alphaAction = new FloatAction(0f, alpha, duration);
alphaAction.setAnimatorUpdateListener(animation -> {
if (mAccumulateState.getColor() != null) {
mAccumulateState.getColor().a = animation;
}
if (mAccumulateBgImg.getColor() != null) {
mAccumulateBgImg.getColor().a = animation;
}
});
scaleAction.setInterpolation(Interpolation.fade);
alphaAction.setInterpolation(Interpolation.fade);
ParallelAction parallelAction = Actions.parallel(scaleAction, alphaAction);
SequenceAction sequenceAction = Actions.sequence(parallelAction, new RunnableAction() {
@Override
public void run() {
runSecondStageAction();
}
});
mAccumulateBgImg.addAction(sequenceAction);
在这里大家可能会一个有疑问点,那就是这个Action应该add到那个Actor。其实这个Action添加到那个Actor都无所谓,应该核心科技掌握在我们手上,所以不用担心老M的打压。😂😂😂
至此 这个记录就到此完结了