自定义控件ViewAndroid 自定义view

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

2017-07-12  本文已影响829人  BaseCoder

先看效果

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
上一篇下一篇

猜你喜欢

热点阅读