本文记录一下实现仿支付宝写五褔及回放的过程。
先看效果如下,没有找到相关的背景图,只能以田字格当作背景。
整个过程分为两部分,一部分是写字,一部份是回放。 该过程主要使用了path和pathmeasure类,在网上有很多写的非常好博文可以参考。
自定义view的源码见文末链接,此处只摘取部分代码记录,以便后续参考和温故。
首先,实现写的过程。
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent: action down" + MotionEvent.ACTION_DOWN);
mPath.moveTo(event.getX(), event.getY());
lastX = event.getX();
lastY = event.getY();
long[] time = new long[2];
time[0] = System.currentTimeMillis();
mPathStartEndTime.put(mPathIndex, time);
break;
case MotionEvent.ACTION_MOVE:
float tx = (lastX + event.getX()) / 2;
float ty = (lastY + event.getY()) / 2;
mPath.quadTo(lastX, lastY, tx, ty);
lastX = event.getX();
lastY = event.getY();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
Log.i(TAG, "onTouchEvent: 当前path count is " + getPathCount(mPath));
long[] t = mPathStartEndTime.get(mPathIndex);
if (t != null && t.length == 2) {
t[1] = System.currentTimeMillis();
} else {
Log.i(TAG, "onTouchEvent: data error!!!");
}
mPathIndex++;
Log.i(TAG, "onTouchEvent: 当前path count is " + mPathIndex);
break;
}
invalidate();
return true;
}
通过触摸事件,将手势轨迹记录到path中。 在移动的过程中,使用的是path二阶贝塞尔曲线,防止轨迹过于生硬的问题。 然后通过invalidate触发绘制,将path绘制到画布上。
其次,实现写字过程的回放。
由于要模拟每个笔画的时长和停顿,在上面的触摸事件中记录了每个path轮廓的开始和结束时间,用于构建回放动画。
在这里使用的数据结构是 SparseArray。它是专为android设备设计的数据结构,可以节省内存,与hashmap有点类似,但它只能存储以int为key的键值对。
在这里定义了一个成员变量 mPathIndex 用来标识每个path轮廓,并以此用作key来记录轮廓的开始和结束时间。
显示回放时,先根据path轮廓时长信息构建相应的动画,如下:
/**
* 获取每个笔画的动画
* @return
*/
private List<Animator> getAnimatorList() {
long[] duration = new long[mPathIndex];
long[] startOffset = new long[mPathIndex];
// 计算每个轮廓的书写时长
for (int i = 0; i < mPathIndex; i++) {
long[] t = mPathStartEndTime.get(i);
duration[i] = t[1] - t[0];
if (i > 0) {
long[] prev = mPathStartEndTime.get(i - 1);
// 计算每个轮廓与前一个轮廓的间隔时间
startOffset[i] = t[0] - prev[1];
}
}
List<Animator> animatorList = new ArrayList<>();
for (int i = 0; i < mPathIndex; i++) {
animatorList.add(createAnimator(duration[i], startOffset[i]));
}
return animatorList;
}
其中,创建动画的过程每个轮廓都类似,故单独写一个方法如下
/**
* 根据每个笔画时间和偏移来构建动画
* @param duration
* @param startOffset 与上一个动画的偏移
* @return
*/
private Animator createAnimator(long duration, long startOffset) {
ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.0f);
va.setDuration(duration).setStartDelay(startOffset);
va.addUpdateListener(animation -> {
Log.i(TAG, "showResultPath: " + animation.getAnimatedFraction());
float curVal = (float) animation.getAnimatedValue();
mPathMeasure.getSegment(0, curVal * mPathMeasure.getLength(), mDstPath, true);
invalidate();
});
va.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
// 每次结束后跳到下一个轮廓上
mPathMeasure.nextContour();
}
});
return va;
}
需要注意的是,每个动画完成时需要将动画跳转到下一个轮廓上。
得到了所有的动画后,就可以开始播放了。这里使用的是AnimatorSet,由于每个动画的 setStartDelay 时间都是相对上一个动画的。
故需要使用 AnimatorSet 的 playSequentially 方法。 具体代码如下:
/**
* 显示字迹动画
*/
public void showResultPath() {
// 如果path有变动,那么 mPathMeasure 需要重新调用setPath绑定一下。
mPathMeasure.setPath(mPath, false);
mShowResult = true;
mDstPath.reset();
List<Animator> animatorList = getAnimatorList();
AnimatorSet set = new AnimatorSet();
// 依次播放。
set.playSequentially(animatorList);
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animation) {
super.onAnimationRepeat(animation);
}
});
set.start();
}
调用showResultPath就可以显示书写的轨迹了。 具体绘制的代码比较简单,如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mShowResult) {
canvas.drawPath(mPath, mPaint);
} else {
canvas.drawPath(mDstPath, mPaint);
}
}
其中,使用一个 mShowResult 变量来控制显示书写的回放。
注意事项:
1、 Path 可以由多条曲线构成,但不论是 getLength()、 getSegment()还是其他函数, 都只会针对其中第一条线段进行计算。 而 nextContour()就是用于跳转到下一条曲线的函数。 如果跳转成功, 则返回 true;如果跳转失败, 则返回 false。
2、pathmeausre 被关联的Path必须是已经创建好的,如果关联之后Path内容进行了更改,则需要使用setPath方法重新关联。
3、在默认情况下,path会生成连贯的路径。但调用moveTo() 与addXXX类函数除外。
4、如果需要计算path的轮廓数,可以通过如下代码实现。
/**
* 获取path中的轮廓数
* @param path
* @return
*/
private int getPathCount(Path path) {
// Path copy = new Path(path);
PathMeasure m = new PathMeasure(path, false);
int count = 0;
while (m.nextContour()) {
count++;
}
return count;
}
在使用这种方法计算的时候在注意,nextContour会将path移动到最后一个轮廓上。 如果继续使用path,可能会造成绘制的问题。 最好是copy一个path来计算。
5、直接调用 mPathMeasure.nextContour ()会跳到第一条线段,但如果操作了mPathMeasure,如getLength,它会自动跳到第一条线段。
参考及源码:
1、pathmeasure的用法。
2、源码。