场景:
- 存在较多绘制内容的区域需要某些动画效果,
- 需要尽量少修改视图的绘制方法,做到动画与绘制分离。
看个简单例子:
我在一个视图上绘制了一行文字,先看一下绘制部分的代码:
public class MyLayout extends LinearLayout
{
private String mText = "show me the money";
//……
@Override
protected void dispatchDraw(Canvas canvas)
{
super.dispatchDraw(canvas);
doPaint(canvas, null);
}
public void doPaint(Canvas canvas, TextAnimationController.AnimationView aniView)
{
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setTextSize(80);
paint.setStyle(Paint.Style.FILL);
//切分字符串
String[] words = mText.split("\\s+");
//起始位置
int x_start = 100;
int y_start = 400;
for (String subText : words)
{
//绘制文本
canvas.drawText(subText, x_start, y_start, paint);
float width = paint.measureText(subText);
float height = paint.descent() - paint.ascent();
x_start += width;
//绘制空格
String blank = " ";
canvas.drawText(blank, x_start, y_start, paint);
x_start += paint.measureText(blank);
}
}
}
现在需要做删除最后一个单词的动作并为之添加动画。
一个实现方法:
将属性动画结合到自己的视图(View)中,通过数值发生器(ValueAnimator)和估值器(TypeEvaluator)不断产生出的位置信息,修改单词绘制时的位置和大小,进行重新绘制。
这种做法会产生两个问题:
- 动画和绘制结合得太紧密,绘制的逻辑和动画的逻辑搅和在一起,导致代码复杂且混乱
- 频繁的直接绘制会导致屏幕闪烁。(当然,这个可以用双缓冲解决)
解决思路:
在执行动画时,可以在视图上再盖一层PopupWindow。强制调用原视图的绘制方法,让其在PopupWindow视图的canvas上重新绘制一遍文本,来参与动画的显示(双缓冲):
对于动画的绘制思路可参考下图来理解:
说明:我们先把最终的状态的Bitmap对象绘制到动画视图(PopupWindow的视图)上,然后再把原始状态Bitmap上的动画字符区域经过缩放绘制到动画数值发生器给出的具体位置上
为了实现动画,我们需要一些数据的支持:
- 动画文本在默认绘制时的坐标位置及宽(原始位置)
- 通过数值发生器(ValueAnimator)和估值器(TypeEvaluator)不断产生出的动画文本的位置信息
首先,我们需要修改原始视图的绘制方法,添加文本位置的搜集器:
public void doPaint(Canvas canvas, TextAnimationController.AnimationView aniView)
{
……
for (String subText : words)
{
//绘制文本
canvas.drawText(subText, x_start, y_start, paint);
float width = paint.measureText(subText);
float height = paint.descent() - paint.ascent();
//==============================================================================================
if (aniView != null)
{
TextAnimationController.TextObject textObj = new TextAnimationController.TextObject();
textObj.rcRangeSrc = new RectF(x_start, y_start + paint.descent() - height, x_start + width,
y_start + paint.descent());
aniView.addTextObject(textObj);
}
//==============================================================================================
x_start += width;
//绘制空格
String blank = " ";
canvas.drawText(blank, x_start, y_start, paint);
x_start += paint.measureText(blank);
}
}
为了方便控制,我们创建一个动画控制器类:TextAnimationController,所有的数据导入、动画生成、弹层、控制都在这个类里。
public class TextAnimationController
{
private MyLayout mOriginalView = null; //PopupWindow的视图
private AnimationView mAnimationView = null;
private AnimationWindow mAnimationWindow = null;
public TextAnimationController(MyLayout view)
{
mOriginalView = view;
}
//开始动画
public void startAnimation(Point target)
{
//创建动画视图层
mAnimationView = new AnimationView(mOriginalView.getContext(), target);
mAnimationWindow = new AnimationWindow(mAnimationView, mOriginalView.getWidth(), mOriginalView.getHeight());
mAnimationWindow.setFocusable(true);
//加载动画视图层
int[] location = new int[2];
mOriginalView.getLocationOnScreen(location);
mAnimationWindow.showAtLocation(mOriginalView, Gravity.TOP | Gravity.LEFT, location[0], location[1]);
}
public void endAnimation()
{
//关闭动画层
if (mAnimationWindow != null && mAnimationWindow.isShowing())
{
mAnimationWindow.dismiss();
mAnimationWindow = null;
}
}
static class TextObject
{
//原始尺寸
RectF rcRangeSrc;
//目标尺寸
RectF rcRangeDes;
//中间步骤尺寸
RectF rcRangeStep;
}
//PopupWindow弹层
public class AnimationWindow extends PopupWindow
{
public AnimationWindow(View contentView, int width, int height)
{
super(contentView, width, height);
setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
}
//动画视图(PopupWindow的视图)
class AnimationView extends View
{
private LinkedList<TextObject> objectList = new LinkedList<>();
private Paint paint = new Paint();
private Rect rectSrc = new Rect();
private Bitmap bitmapSrc = Bitmap
.createBitmap(mOriginalView.getWidth(), mOriginalView.getHeight(), Bitmap.Config.ARGB_8888);
private Bitmap bitmapFinal = Bitmap
.createBitmap(mOriginalView.getWidth(), mOriginalView.getHeight(), Bitmap.Config.ARGB_8888);
public AnimationView(Context context, final Point target)
{
super(context);
Canvas bmpCanvas = new Canvas(bitmapSrc);
mOriginalView.showStartText();
mOriginalView.doPaint(bmpCanvas, this);
bmpCanvas = new Canvas(bitmapFinal);
mOriginalView.showEndText();
mOriginalView.doPaint(bmpCanvas, null);
post(new Runnable()
{
@Override
public void run()
{
startAnimation(target.x, target.y);
}
});
}
public void addTextObject(TextObject object)
{
objectList.add(object);
}
@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
//绘制底图
canvas.drawBitmap(bitmapFinal, 0, 0, paint);
//绘制动画文本图
int lastIndex = objectList.size() - 1;
RectF rcSrc = objectList.get(lastIndex).rcRangeSrc;
rectSrc.set((int) rcSrc.left, (int) rcSrc.top, (int) rcSrc.right, (int) rcSrc.bottom);
RectF rcStep = objectList.get(lastIndex).rcRangeStep;
if (rcStep != null)
{
canvas.drawBitmap(bitmapSrc, rectSrc, rcStep, paint);
}
}
private void startAnimation(int desX, int desY)
{
for (int i = 0; i < objectList.size(); i++)
{
RectF src = objectList.get(i).rcRangeSrc;
objectList.get(i).rcRangeDes = new RectF(desX, desY, desX + src.width(), desY + src.height());
objectList.get(i).rcRangeStep = new RectF(src);
}
//==========================================================================================================
ArrayList<Animator> animators = new ArrayList<>();
for (int i = 0; i < objectList.size(); i++)
{
final RectF src = objectList.get(i).rcRangeSrc;
RectF des = objectList.get(i).rcRangeDes;
AnimatorSet aniDisappear = new AnimatorSet();
final int index = i;
//贝塞尔曲线动画
final int pointX = (int) (src.left + 200);
final int pointY = (int) (src.top + (des.top - src.top) / 2);
Point controlPoint = new Point(pointX, pointY); //控制点
BezierEvaluator bezierEvaluator = new BezierEvaluator(controlPoint);
ValueAnimator animBezier = ValueAnimator.ofObject(bezierEvaluator,
new Point((int)src.left, (int)src.top),
new Point((int)des.left, (int)des.top));
animBezier.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
Point point = (Point) animation.getAnimatedValue();
RectF step = new RectF(objectList.get(index).rcRangeStep);
step.set(point.x, point.y, point.x + src.width(), point.y + src.height());
objectList.get(index).rcRangeStep.set(step);
invalidate();
}
});
//缩放动画
ValueAnimator animScale = ValueAnimator.ofFloat(1.0f, 0.1f);
animScale.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
float scaleSize = (Float) animation.getAnimatedValue();
RectF step = new RectF(objectList.get(index).rcRangeStep);
step.set(step.left, step.top, step.left + step.width() * scaleSize,
step.top + step.height() * scaleSize);
objectList.get(index).rcRangeStep.set(step);
}
});
aniDisappear.setDuration(1200);
aniDisappear.setInterpolator(new AccelerateInterpolator());
aniDisappear.play(animBezier).with(animScale);
animators.add(aniDisappear);
}
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new Animator.AnimatorListener()
{
@Override
public void onAnimationStart(Animator animation)
{
}
@Override
public void onAnimationEnd(Animator animation)
{
endAnimation();
}
@Override
public void onAnimationCancel(Animator animation)
{
endAnimation();
}
@Override
public void onAnimationRepeat(Animator animation)
{
}
});
animatorSet.playTogether(animators);
animatorSet.start();
}
}
public class BezierEvaluator implements TypeEvaluator<Point>
{
private Point controlPoint;
public BezierEvaluator(Point controlPoint)
{
this.controlPoint = controlPoint;
}
@Override
public Point evaluate(float t, Point startValue, Point endValue)
{
int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controlPoint.x + t * t * endValue.x);
int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controlPoint.y + t * t * endValue.y);
return new Point(x, y);
}
}
}
ps:其中动画文本的运动轨迹使用了贝塞尔曲线,关于贝塞尔曲线的TypeEvaluator,参考了
https://www.jianshu.com/p/d9a3ae9e806d