android 自定义view仿支付宝写五褔及播放

本文记录一下实现仿支付宝写五褔及回放的过程。

先看效果如下,没有找到相关的背景图,只能以田字格当作背景。
写五褔
整个过程分为两部分,一部分是写字,一部份是回放。 该过程主要使用了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、源码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值