美文网首页自定义控件ViewAndroid 自定义view
柠檬跑步:跑步轨迹回放动画实现(与咕咚类似)

柠檬跑步:跑步轨迹回放动画实现(与咕咚类似)

作者: BaseCoder | 来源:发表于2017-07-12 19:11 被阅读829次

    先看效果

    7月-12-2017 18-53-39.gif

    一、要求
    1、轨迹动画流畅,慢-快-慢;
    2、渐变色尽量与地图渐变API的效果一致;
    3、拖动地图,动画消失,显示完整渐变轨迹。

    二、分析
    1、高德地图并没有提供相应效果的API,但是可以通过经纬度坐标,转换未屏幕坐标,因此可以自定义一个View来实现轨迹动画的效果。(注意:在自定义的View上画轨迹,一定是要在地图缩放完成后执行,有对应的回调方法,API可查)

    2、自定义控件这里有两种思路,可以继承自View,也可以继承SurfaceView。他们的区别相信大家都清楚,我的解决方案中使用了自定义的属性动画,所以我是通过View来实现的。欢迎大家提供SurfaceView的解决方案,共同学习。

    3、渐变肯定是用Shader来进行实现,但是这里有一个误区,不能对整个运动轨迹的path设置渐变,Shader的渐变不会跟着你的轨迹走,所以只能分段设置渐变色,相信高德也是这么搞的。

    4、动画效果的实现是用的属性动画,这里也有多种实现方式,最初我用了一种比较愚蠢的方案,对每一段path设置动画,通过AnimationSet进行队列展示,但是没考虑到界面渲染效率的问题,导致界面卡顿。
    View的渲染大家都清楚,每次invalidate都会导致界面重画,所以我的方案也很简单,通过动画进度,可以算出当前动画执行到的path,根据比例截取,进行绘制。

    5、至于相关的相应事件就没什么了,实现方案很多,个人认为最简单的就是在自定义View中通过onTouchEvent处理逻辑并进行回调。

    三、相关代码

    1、轨迹动画相关数据的工具类

    package com.lemon.running.utils;
    
    import android.graphics.LinearGradient;
    import android.graphics.Path;
    import android.graphics.PathMeasure;
    import android.graphics.Point;
    import android.graphics.Shader;
    
    import com.lemon.running.LemonApplication;
    
    import java.util.ArrayList;
    
    /**
     * Created by viva on 17/7/5.
     */
    public class RecordPathAnimUtil {
    
        private final long MAX_ANIM_DURATION = 5 * 1000;//动画最大执行时间
    
        private final long MIN_ANIM_DURATION = 2 * 1000;
    
        private final int SCREEN_WIDTH_DEBUG = 1080;//当前调试手机的屏幕宽度,作为计算动画执行时间的标准,无实际意义
    
        private int SCREEN_WIDTH_RELEASE;//用户使用手机屏幕的实际宽度
    
        private long ANIM_DURATION = MIN_ANIM_DURATION;//动画执行总时间
    
        private final float PATH_SCREEN_LENGTH_1_KM = 2000.0f;
    
        private ArrayList<RecordPathBean> recordPathList;
    
        private PathMeasure pathMeasure;
    
        private Path totalPath;
    
        public RecordPathAnimUtil(){
            recordPathList = new ArrayList<>();
            SCREEN_WIDTH_RELEASE = ScreenUtils.getScreenWidth(LemonApplication.getContext());
        }
    
        public long getANIM_DURATION() {
            return ANIM_DURATION;
        }
    
        public void setANIM_DURATION(long ANIM_DURATION) {
            this.ANIM_DURATION = ANIM_DURATION;
        }
    
        public ArrayList<RecordPathBean> getRecordPathList() {
            return recordPathList;
        }
    
        /**
         * 创建坐标点对应的path 渐变
         * @param start
         * @param end
         * @param startColor
         * @param endColor
         */
        public void addPath(Point start,Point end,int startColor,int endColor){
            if (totalPath == null){
                totalPath = new Path();
                totalPath.moveTo(start.x,start.y);
                totalPath.lineTo(end.x,end.y);
            }
            totalPath.lineTo(end.x,end.y);
            Path path = new Path();
            path.moveTo(start.x,start.y);
            path.lineTo(end.x,end.y);
            pathMeasure = new PathMeasure(path,false);
            Shader shader = new LinearGradient(start.x, start.y, end.x, end.y,new int[]{startColor,endColor},null, Shader.TileMode.CLAMP);
            RecordPathBean recordPathBean = new RecordPathBean(path,pathMeasure.getLength(),shader);
            recordPathBean.setEndPoint(end);
            recordPathBean.setEndColor(endColor);
            recordPathList.add(recordPathBean);
            recordPathBean.setIndex(recordPathList.size() - 1);
        }
    
        /**
         * 所有path的总长度
         * @return
         */
        public float getAllPathLength(){
            float pathLength = 0;
            if (recordPathList != null){
                for (int i = 0,count = recordPathList.size();i < count;i++){
                    pathLength += recordPathList.get(i).getPathLength();
                }
            }
            caculateAnimDuration(pathLength);
            return pathLength;
        }
    
        /**
         * 计算动画执行的总时长
         * @param pathLength
         */
        private void caculateAnimDuration(float pathLength){
            float pathScreenLength1KmRelease = SCREEN_WIDTH_RELEASE * PATH_SCREEN_LENGTH_1_KM / SCREEN_WIDTH_DEBUG;
            float durationScale = pathLength / pathScreenLength1KmRelease;
            if (durationScale <= 1)
                return;
            long durationRelease = (long) (durationScale * MIN_ANIM_DURATION);
            if (durationRelease >= MAX_ANIM_DURATION){
                setANIM_DURATION(MAX_ANIM_DURATION);
                return;
            }
            setANIM_DURATION(durationRelease);
        }
    
        public Path getTotalPath() {
            return totalPath;
        }
    
        public class RecordPathBean{
    
            private Path path;//路径
            private Shader shader;//画笔渐变
            private float pathLength;
            private int index;
            private Point endPoint;
            private int endColor;
    
            public RecordPathBean(Path path,float pathLength,Shader shader){
                this.path = path;
                this.pathLength = pathLength;
                this.shader = shader;
            }
    
            public Path getPath() {
                return path;
            }
    
            public Shader getShader() {
                return shader;
            }
    
            public float getPathLength() {
                return pathLength;
            }
    
            public int getIndex() {
                return index;
            }
    
            public void setIndex(int index) {
                this.index = index;
            }
    
            public Point getEndPoint() {
                return endPoint;
            }
    
            public void setEndPoint(Point endPoint) {
                this.endPoint = endPoint;
            }
    
            public int getEndColor() {
                return endColor;
            }
    
            public void setEndColor(int endColor) {
                this.endColor = endColor;
            }
        }
    }
    

    2、自定义控件

    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.Color;
    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, endIcon, middleIcon;
    
        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;
    
        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.setColor(Color.argb(0, 0, 0, 0));
            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);
            endIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.outside_run_record_stop_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 (animIndex > 0){
                for (int i = 0; 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);
                    canvas.drawPath(recordPathBean.getPath(), paint);
                    paint.setShader(null);
                    paint.setStrokeWidth(1);
                    paint.setStyle(Paint.Style.FILL_AND_STROKE);
                    canvas.drawCircle(recordPathBean.getEndPoint().x, recordPathBean.getEndPoint().y, 5, paint);
                }
            }
    
            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);
            if (value >= 1) {
                canvas.drawBitmap(endIcon, pathEndPoint[0] - endIcon.getWidth() / 2, pathEndPoint[1] - endIcon.getHeight() / 2, iconPaint);
            } else {
                canvas.drawBitmap(middleIcon, dstPathEndPoint[0] - middleIcon.getWidth() / 2, dstPathEndPoint[1] - middleIcon.getHeight() / 2, iconPaint);
            }
        }
    
        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;
        }
    }
    
    

    注:本文只是提供一种实现方案,其实针对于跑步路径过长的情况,这样处理还是会有跳帧的问题。毕竟代码是死的,人是活的,针对这种情况也有多种优化方案,需要与产品、设计的要求找到一个平衡。
    我们自己测试的情况,当小段path的数量大概到达2000的时候,就会跳帧。那么在渲染的时候就需要两种解决方案,可以从渐变上考虑,可以从绘制方式上考虑等。

    优化方案请看下一篇文章:
    http://www.jianshu.com/p/996f2cfeed29

    最后给我们产品做个广告吧:

    WechatIMG26.jpeg

    相关文章

      网友评论

      • f56d1558904a:楼主,您好,请问这个自定义View要在布局中使用吗?我直接这样使用
        RecordPathView view = new RecordPathView(this);
        view.setPath(util);
        并不走OnDraw()方法
        f56d1558904a:@BaseCoder 我通过view.setPath(util);util里面是已经拿到了所有的point,point里面对应的pathLength也是有值的。但是就是没有走onDraw(),所以根本绘制不出来
        f56d1558904a:@BaseCoder 能否看下你的调用这个自定义View的代码?
        BaseCoder:@赵赵赵_ 断点跟踪一下吧,可以在布局中使用
      • 众彳亍:咕咚也是这么实现的吗?
        小污公子:轨迹完成后,地图缩放以后,轨迹线不是需要重新绘制吗
        N1njaC:PATH_SCREEN_LENGTH_1_KM是什么意思?计算duration的方式不是很理解
        BaseCoder:@众彳亍 不知道咕咚怎么实现的,效果差不多
      • lzh_coder:有没有ios版的?
        BaseCoder:@BadMonkey 其实核心代码都贴出来了,集成demo还需要高德地图的sdk,比较麻烦 你可以动手试试,有问题可以共同交流
        GYLEE:可否集成一个demo
        BaseCoder:@happyliuzh 并没有。。

      本文标题:柠檬跑步:跑步轨迹回放动画实现(与咕咚类似)

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