运动APP视频轨迹回放分享实现

喜欢户外运动的朋友一般都应该使用过运动APP(keep, 咕咚,悦跑圈,国外的Strava等)的一项功能,就是运动轨迹视频分享,分享到朋友圈或是运动群的圈子里。笔者本身平常也是喜欢户外跑、骑行、爬山等户外运动,也跑过半马、全马,疫情原因之前报的杭州的全马也延期了好几次了。回归正题,本文笔者基于自己的思想实现运动轨迹回放的一套算法策略,实现本身是基于Mapbox地图的,但是其实可以套用在任何地图都可以实现,基本可以脱离地图SDK的API。Mapbox 10 版本之后的官方给出的Demo里已经有类似轨迹回放的Case了,但是深度地依赖地图SDK本身的API,倘若在高德上实现很难可以迁移的。

这里先看下gif动图的效果,这是我在奥森跑的10KM的一个轨迹:

轨迹视频回放_AdobeExpress .gif

整个的实现包含了轨迹的回放,视频的录制,然后视频的录制这块不再笔者这篇文章的介绍的范畴内。所以这里主要介绍轨迹的回放,这个回放过程其实也是包含了大概10多种动画在里面的,辅助信息距离的文字跳转动画;距离下面配速、运动时间等的flap in 及 out的动画;播放button,底部button的渐变Visibility; 地图的缩放以及视觉角度的变化等;以上的这些也不做讨论。主要介绍轨迹回放、整公里点的显示(起始、结束), 回放过程中窗口控制等,作为主要的讲解范畴。

首先介绍笔者最开始的一种实现,假如以上轨迹List 有一百个点,每相邻的两个点做Animation之后,在AnimationEnd的Listener里开起距离下一个点的Animation,直到所有点结束,这里有个问题每次的运动轨迹的点的数量不一样,所以开起Animation的次数也不一样,整个轨迹回放的时间等于所有的Animation执行的时间和,每次动画启动需要损耗20~30ms。倘若要分享到微信朋友圈,视频的时间是限制的,但之前的那种方式时间上显然不可控,每次动画启动的损耗累加导致视频播放不完。

紧接着换成AnimationSet, 将各个线段Animation的动画放入Set里,然后playSequentially执行,同样存在上面的问题。假如只执行一次动画,那么这次动画start的损耗在整个视频播放上时长上的占比就可以忽略不计了,那如何才能将整个List的回放在一个Animation下执行完呢?假如轨迹只是一个普通的 Path,那么我们就可以基于Path的 length一个属性动画了,当转化到地图运动轨迹上去时,又如何去实现呢?

基于Path Length的属性动画
  1. 计算List对应的Path
  2. 通过PathMeasure获取 Path 的 Length
  3. 对Path做 Length的属性动画

这里有两套Point体系,一个是View的Path对应的Points, 然后就是Map上的List对应的Points,运动轨迹原始数据是Map上的List 点,上面的第一步就是将Map上的Points 转成屏幕Pixel对应的点并生成Path; 第二部通过PathMeasure 计算Path的Length; 最后在Path Length上做属性动画,然而这里并非将属性动画中每次渐变的值(这里对应的是View的Point点)绘制成View对应的Path,而是将渐变中的点又通过Map的SDK转成地图Location点,绘制地图轨迹。这里一共做了两道转换,中间只是借助View的Path做了一个依仗Length属性做的一个动画。因为基本上每种地图SDK都有Pixel 跟Location Point点互相transform的API,所以这个可以直接迁移到其它地图上,例如高德地图等。

下面具体看下代码,先将Location 转成View的Point体系,这里保存了总的一个Path,以及List 中两两相邻点对应的分段Path的一个list.

  • 生成Path:

1.1 生成Path2.png

其中用到 Mapbox地图API Location 点转View的PointF 接口API toScreenLocation(LatLng latlng), 这里生成List, 然后计算得到Path.

  • 基于Length做属性动画:

1.3 Path length 属性动画.png

首先创建属性动画的 Instance:

ValueAnimator.ofObject(new DstPathEvaluator(), 0, mPathMeasure.getLength());

将每次渐变的值经过 calculateAnimPathData(value) 计算后存入到 以下的四个变量中,这里除了Length的渐变值,还附带有角度的一个二元组值。

dstPathEndPoint[0] = 0;//x坐标
dstPathEndPoint[1] = 0;//y坐标
dstPathTan[0] = 0;//角度值
dstPathTan[1] = 0;//角度值

然后将dstPathEndPoint 的值转成Mapbox的 Location的 Latlng 经纬度点,

PointF lastPoint = new PointF(dstPathEndPoint[0], dstPathEndPoint[1]);
LatLng lastLatLng = mapboxMap.getProjection().fromScreenLocation(lastPoint);
Point point = Point.fromLngLat(lastLatLng.getLongitude(), lastLatLng.getLatitude());

过滤掉一些动画过程中可能产生的异常点,最后加入到Mapbox的轨迹绘制的Layer中形成轨迹的一个渐变:

Location curLocation = mLocationList.get(animIndex);
float degrees = MapBoxPathUtil.getRotate(curLocation, point);
if (animIndex < 5 || Math.abs(degrees - curRotate) < 5) {//排除异常点
    setMarkerRecord(point);
}

setMarkerRecord(point) 方法调用加入到 Map 轨迹的绘制Layer中

1.4 加入到Map轨迹绘制.png

动画过程中,当加入到Path中的点超过一定占比时,做了一个窗口显示的动画,窗口List跟整个List的一个计算:

//这里可以取后半段的数据,滑动窗口,保持 moveCamera 的窗口值不变。
int moveSize = passedPointList.size();
List<LatLng> windowPassList = passedPointList.subList(moveSize - windowLength, moveSize);

接下来看整公里点的绘制,看之前先看下上面的calculateAnimPathData()方法的逻辑

1.5 Path渐变的计算.png

如上,length为当前Path走过的距离,假设轨迹一共100点,当前走到 49 ~ 50 点之间,那么calculateLength就是0到50这个点的Path的长度,它是大于length的,offsetLength = calculateLength - length; 记录的是 当前点到50号点的一个长度offsetLength,animIndex值当前值对应50,recordPathList为一开始提到的跟计算总Path时一个分段Path的List, 获取到49 ~ 50 这个Path对应的一个model.

RecordPathBean recordPathBean = recordPathList.get(animIndex);

获得Path(49 ~ 50) 的长度减去 当前点到 50的Path(cur ~ 50)的到 Path(49 ~ cur) 的长度

float stopD = (float) (pathMeasure.getLength() - offsetLengthCur);

然后最终通过PathMeasure的 getPosTan 获得dstPathEndPoint以及dstPathTan数据。

pathMeasure.getSegment(0, stopD, dstPath, false);
mDstPathMeasure = new PathMeasure(dstPath, false);
//这里有个参数 tan
mDstPathMeasure.getPosTan(mDstPathMeasure.getLength(), dstPathEndPoint, dstPathTan);

  • 整公里点的绘制

原始数据中的List的Location中存储了一个字段kilometer, 当某个Location是整公里点时该字段就有对应的值,每次Path属性渐变时,上面的逻辑里记录了lastAnimIndex, animIndex。当 animIndex > lastAnimIndex时, 上面的calculateAnimPathData() 方法里分析animIndex有可能还没走到,所以在animIndex > lastAnimIndex时lastAnimIndex肯定走到了。

1.6 整公里点动画.png

当lastAnimIndex对应的点是 整公里时,做一个响应的属性动画。

至此,运动轨迹回放的一个动画执行逻辑分析完了,如文章开始所说,整个过程中其实还包含了好多种其它的动画,处理它们播放的一个时序问题,如何编排实现等等也是一个难点。另外还就是轨迹播放时的一个Camera的一个视觉跟踪的效果没有实现,这个用地图本身的Camera 的API是一种实现,但是如何跟上面的这些结合到一块;然后就是自行通过计算角度偏移,累计到一定的旋转角度时,转移地图的指南针;以上是笔者想到的方案,以上有计算角度的,但需要找准那个累计的角度值,然后大量实际数据适配。

最后,有需要了解轨迹回放功能其它实现的,可留言或私信笔者进行一起探讨。

原文作者:cxy107750
本文转自 [https://juejin.cn/post/7183602475591548986]

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现车辆轨迹回放,可以使用uniapp的map组件。以下是步骤: 1.引入map组件: 在页面的vue文件里,先引入map组件。如下: ``` <template> <view> <map :show-location="true" :markers="markers"></map> </view> </template> <script> import {uniMap} from '@dcloudio/uni-ui'; export default { components: { uniMap }, data(){ return { markers: [] } } } </script> ``` 2.设置地图样式: 在页面的style里,设置map组件的高度和宽度 ``` <style> map { width: 100%; height: 100%; position: fixed; top: 0; left: 0; z-index: 999; } </style> ``` 3.加载地图: 在mounted钩子函数里调用uniMap的createMap方法,创建地图 ``` mounted(){ this.createMap(); }, methods: { createMap(){ let mapCtx = uniMap.createMapContext('myMap', { showLocation: true }) mapCtx.moveToLocation(); } } ``` 4.显示轨迹: 使用setMarkers方法,将要显示的轨迹点添加到地图上 ``` let markers = [ { id: 1, longitude: 113.324520, latitude: 23.10229, iconPath: '/static/img/car.png', width: 50, height: 50, callout: { content: '起点' } }, { id: 2, longitude: 113.35938, latitude: 23.09211, iconPath: '/static/img/car.png', width: 50, height: 50, callout: { content: '终点' } }, { id: 3, longitude: 113.331441, latitude: 23.117706, iconPath: '/static/img/car.png', width: 50, height: 50, callout: { content: '中间点' } } ] this.markers = markers; ``` 5.轨迹回放: 使用定时器和moveToLocation方法,按照轨迹点顺序将地图移动到对应的位置 ``` play(){ let mapCtx = uniMap.createMapContext('myMap', { showLocation: true }) let i = 0; let len = markers.length; let timer = setInterval(() => { if(i >= len - 1){ clearInterval(timer); return; } i++; mapCtx.moveToLocation({ longitude: markers[i].longitude, latitude: markers[i].latitude }) }, 1000) } ``` 以上就是实现车辆轨迹回放的步骤。需要注意的是,地图的显示需要设置高度和宽度,因为地图是一个fixed定位的元素。另外,回放轨迹时需要使用定时器来控制地图移动的速度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值