美文网首页自定义控件Android 自定义viewandroid
柠檬跑步:跑步轨迹回放动画优化

柠檬跑步:跑步轨迹回放动画优化

作者: BaseCoder | 来源:发表于2017-07-13 21:43 被阅读479次

    相关分析请看上一篇文章:
    http://www.jianshu.com/p/cedfe72be146

    上篇文章中最后说到,针对于轨迹过长的数据,解决方案在渲染方面还是比较乏力,存在卡顿的问题,本文就对此情况进行优化。

    还是先看效果:

    7月-13-2017 21-22-47.gif

    其实相较于之前的解决方案改动不大,大体原理就是创建一个Bitmap,每次刷新画布的时候,不用去遍历所有的数据绘制,将已经画过的path的状态保存在Bitmap,只需要绘制动画执行进度对应的path和之前的Bitmap即可。

    还是直接看代码会比较清晰
    (RecordPathAnimUtil工具类就不贴了,上篇文章中有,没有什么变动)

    package com.lemon.running.ui.view;
    
    import android.animation.Animator;
    import android.animation.TypeEvaluator;
    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Path;
    import android.graphics.PathMeasure;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.animation.AccelerateDecelerateInterpolator;
    
    import com.lemon.running.R;
    import com.lemon.running.utils.RecordPathAnimUtil;
    
    import java.util.ArrayList;
    
    /**
     * Created by viva on 17/6/20.
     */
    public class RecordPathView extends View {
    
        private Context context;
        private Paint paint, iconPaint;
        private Path dstPath, totalPath;
        private PathMeasure mPathMeasure, mDstPathMeasure;
    
        private boolean isDrawRecordPath = false;
    
        private float pathLength;
    
        private Bitmap startIcon, middleIcon;
    
        private Bitmap bitmap;//当前canvas生成的bitmap
        private Canvas bitmapCanvas;
    
        private float[] pathStartPoint = new float[2];
        private float[] pathEndPoint = new float[2];
        private float[] dstPathEndPoint = new float[2];
    
        private float value = 0;
    
        private long ANIM_DURATION;
    
        private ArrayList<RecordPathAnimUtil.RecordPathBean> recordPathList;
    
        private OnAnimEnd onAnimEnd;
    
        private int animIndex, lastAnimIndex;
    
        public RecordPathView(Context context) {
            super(context);
            this.context = context;
            init();
        }
    
        public RecordPathView(Context context, AttributeSet attrs) {
            super(context, attrs);
            this.context = context;
            init();
        }
    
        public RecordPathView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.context = context;
            init();
        }
    
        private void init() {
            paint = new Paint();
            paint.setAntiAlias(true);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeWidth(10);
    
            iconPaint = new Paint();
            iconPaint.setAntiAlias(true);
    
            dstPath = new Path();
    
            startIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.outside_run_record_start_point);
            middleIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.speed_view_point);
        }
    
        public void setPath(RecordPathAnimUtil recordPathAnimUtil) {
            if (recordPathAnimUtil == null)
                return;
            if (!isDrawRecordPath) {
                pathLength = recordPathAnimUtil.getAllPathLength();
                ANIM_DURATION = recordPathAnimUtil.getANIM_DURATION();
                recordPathList = recordPathAnimUtil.getRecordPathList();
                totalPath = recordPathAnimUtil.getTotalPath();
                mPathMeasure = new PathMeasure(totalPath, false);
                mPathMeasure.getPosTan(0, pathStartPoint, null);//轨迹的起点
                mPathMeasure.getPosTan(mPathMeasure.getLength(), pathEndPoint, null);//轨迹的终点
                if (recordPathList == null || recordPathList.size() == 0)
                    return;
                startPathAnim();
                isDrawRecordPath = true;
            }
        }
    
        public void setOnAnimEnd(OnAnimEnd onAnimEnd) {
            this.onAnimEnd = onAnimEnd;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            if (recordPathList == null || recordPathList.size() == 0)
                return;
            if (value >= 1)
                return;
            if (bitmap == null) {
                bitmap = Bitmap.createBitmap(this.getMeasuredWidth(), this.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
                bitmapCanvas = new Canvas(bitmap);
            }
    
            bitmapCanvas.save();
            if (animIndex > lastAnimIndex && lastAnimIndex > 0) {//动画跳到下一段或下几段path
                for (int i = lastAnimIndex - 1; i < animIndex; i++) {
                    RecordPathAnimUtil.RecordPathBean recordPathBean = recordPathList.get(i);
                    paint.setColor(recordPathBean.getEndColor());
                    paint.setShader(recordPathBean.getShader());
                    paint.setStrokeWidth(10);
                    paint.setStyle(Paint.Style.STROKE);
                    bitmapCanvas.drawPath(recordPathBean.getPath(), paint);
                    paint.setShader(null);
                    paint.setStrokeWidth(1);
                    paint.setStyle(Paint.Style.FILL_AND_STROKE);
                    bitmapCanvas.drawCircle(recordPathBean.getEndPoint().x, recordPathBean.getEndPoint().y, 5, paint);
                }
            }
            bitmapCanvas.restore();
            canvas.drawBitmap(bitmap, 0, 0, iconPaint);
    
            paint.setStyle(Paint.Style.STROKE);
            paint.setShader(recordPathList.get(animIndex).getShader());
            paint.setStrokeWidth(10);
            canvas.drawPath(dstPath, paint);
    
            canvas.drawBitmap(startIcon, pathStartPoint[0] - startIcon.getWidth() / 2, pathStartPoint[1] - startIcon.getHeight() / 2, iconPaint);
            canvas.drawBitmap(middleIcon, dstPathEndPoint[0] - middleIcon.getWidth() / 2, dstPathEndPoint[1] - middleIcon.getHeight() / 2, iconPaint);
    
            if (lastAnimIndex != animIndex)
                lastAnimIndex = animIndex;
        }
    
        /**
         * 当前动画执行进度对应的分段path index
         */
        private void caculateAnimPathData() {
            float length = value * pathLength;
            float caculateLength = 0;
            float offsetLength = 0;
            for (int i = 0, count = recordPathList.size(); i < count; i++) {
                caculateLength += recordPathList.get(i).getPathLength();
                if (caculateLength > length) {
                    animIndex = i;
                    offsetLength = caculateLength - length;
                    break;
                }
            }
            dstPath.reset();
            PathMeasure pathMeasure = new PathMeasure(recordPathList.get(animIndex).getPath(), false);
            pathMeasure.getSegment(0, recordPathList.get(animIndex).getPathLength() - offsetLength, dstPath, true);
            mDstPathMeasure = new PathMeasure(dstPath, false);
            mDstPathMeasure.getPosTan(mDstPathMeasure.getLength(), dstPathEndPoint, null);
        }
    
        private void startPathAnim() {
            ValueAnimator animator = ValueAnimator.ofObject(new DstPathEvaluator(), 0, mPathMeasure.getLength());
            animator.setDuration(ANIM_DURATION);
            animator.setInterpolator(new AccelerateDecelerateInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    value = (float) animation.getAnimatedValue();
                    caculateAnimPathData();
                    invalidate();
                }
            });
            animator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
    
                }
    
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (onAnimEnd != null)
                        onAnimEnd.animEndCallback();
                }
    
                @Override
                public void onAnimationCancel(Animator animation) {
    
                }
    
                @Override
                public void onAnimationRepeat(Animator animation) {
    
                }
            });
            animator.start();
        }
    
        class DstPathEvaluator implements TypeEvaluator {
    
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                return fraction;
            }
        }
    
        public interface OnAnimEnd {
            void animEndCallback();
        }
    
        float x, y;
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    x = event.getX();
                    y = event.getY();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_MOVE:
                case MotionEvent.ACTION_CANCEL:
                    if (Math.abs(event.getX() - x) > 0 || Math.abs(event.getY() - y) > 0) {
                        if (onAnimEnd != null)
                            onAnimEnd.animEndCallback();
                    }
                    break;
                default:
                    break;
            }
            return true;
        }
    }
    
    

    注:数据量太大导致前期加载时间太长,这可以根据需求加loading,至于渲染后与地图上的轨迹重叠,这也很好解决,不再赘述。

    继续广告

    5322402-764c9ac6a1721316.jpeg

    相关文章

      网友评论

      • O李应:请问你们如何实现小米手机后台持续定位的?我按照高德地图提供的方法开启了Locationservice,在其他几个测试机型上都可以实现后台定位,唯有小米手机上不行(除非关闭呻吟模式下的后台限制)。咕咚和柠檬都可以在小米任何机型上实现后台持续定位(包括小米),你们怎么做到的呢?
      • Android_YangKe:两个问题:
        1:你的轨迹动画执行结束轨迹就消失了
        2:将ontouch函数返回false地图可以滚动, 如果view不消失, 地图滚动了view却是死的,不会随着地图滚动, 难道我操作流程问题 ?
        BaseCoder:@Android_YangKe 已关注
        Android_YangKe:@BaseCoder 谢谢, 我在研究下!

        这是我的主页也许有你需要的资源: http://www.jianshu.com/u/eb77504b1d68
        BaseCoder:动画结束后在地图上画轨迹。动画执行过程中如果有手触事件,动画的view隐藏,直接显示地图的轨迹。行为的触发节点是可控的。
      • 小污公子:优化部分启发很大,缩短开发时间,谢谢分享:clap:
        BaseCoder:@小农人建建 谢谢鼓励
      • GYLEE:在做相同需求,想要获得相同效果,无奈全程懵逼中,求助求助:sob:
        BaseCoder:@BadMonkey 哦,做到什么程度了? 具体分析在第一篇文章中。 借助地图的api获取到点的屏幕位置,生成path,然后绘制。具体到细节,有什么问题可以互相交流。
      • 卧水莱藤:在绘制的过程中多个path线的连接处不是圆滑的怎么处理,按照文中绘制出来有跟多的折线和毛刺
        BaseCoder:@卧水莱藤 :+1:
        卧水莱藤:@MyReturn 测试了一下也可以得到同样的效果的
        BaseCoder:@卧水莱藤 多个path绘制,连接处的确不平滑,其实代码有也有处理,在ondraw里bitmapCanvas.drawCircle(recordPathBean.getEndPoint().x, recordPathBean.getEndPoint().y, 5, paint); 其实我是在path的最后一个点画一个实心小圆,色值和大小根据自己的画笔调整,这样应该能盖住断裂处
      • 蓅哖伊人为谁笑:PathMeasure pathMeasure = new PathMeasure(recordPathList.get(animIndex).getPath(),false);
        pathMeasure.getSegment(0, recordPathList.get(animIndex).getPathLength() - offsetLength, dstPath, true);
        mDstPathMeasure = new PathMeasure(dstPath, false);
        mDstPathMeasure.getPosTan(mDstPathMeasure.getLength(),dstPathEndPoint,null);

        这里为什么还要在new mDstPathMeasure .getPosTan()呢
        卧水莱藤:我想了一种方案是,相邻的两段可以放在同一个Path中,这样会多绘制一倍的路线,应该也能使线路平滑一点,先测试一下
        BaseCoder:是为了获取path的边界点的坐标dstPathEndPoint,动画过程中最后一个点绘制了一个半透明效果的图片
        canvas.drawBitmap(middleIcon, dstPathEndPoint[0] - middleIcon.getWidth() / 2, dstPathEndPoint[1] - middleIcon.getHeight() / 2, iconPaint);
        项目需求~

      本文标题:柠檬跑步:跑步轨迹回放动画优化

      本文链接:https://www.haomeiwen.com/subject/wqughxtx.html