技术概述
需求是实现一个包含三种状态,分别是底部、半屏、全屏的浮层,并且浮层支持用户随意拖动,同时还要防止用户过度拖动。技术难点在于对动画的控制,必须得有一定的过渡动画否则整体会很突兀。
技术详述
首先看一下预期的样子,图为ios自带高德地图的底部浮层。
最开始的动画我是用CSS的keyframe来写的,即利用关键帧来标记三个状态,将过渡时间设置为300毫秒,出现的问题就是在程序运行时控制动画只能通过动态增删类来实现,十分繁琐。这样可以实现简单的点击事件切换状态,但是无法让用户滑动。
这时发现了uni-app官方的动画插件uni-transition,阅读文档之后发现类似于关键帧,就是从一个状态到另一个状态的滑动,设置好动画和过渡时间,可以很方便地用js来控制。
<uni-transition custom-class="location-box" :show="showLocationBox" ref="locationBox">
</uni-transition>
上方代码为uni-transition的使用方式。custom-class用于绑定自己写的类,:show用于绑定浮层的显示,ref用户注册动画的引用信息,可以让我们用this来控制。
结合官方文档,来谈谈uni-transition的使用方式。
this.$refs.locationBox.init({
duration: 1000,
timingFunction: \'linear\',
transformOrigin: \'50% 50%\',
delay: 500
})
使用init方式可以给动画初始化,并覆盖掉原本的动画参数,这里我们用不到,但是必须要知道可以用init方法覆盖原本的动画。
先从状态切换开始实现,原理就是用uni-transition标签包裹浮层内容,使用fixed位置,动画的参数就是修改其height和top的内容,辅以uni-transition动画的300毫秒过渡时间,以达到平滑过渡的效果。
//step方式相当于设置一个关键帧
this.$refs.locationBox.step({
//设置要执行到的样式大小,当调用run方法之后,uni-transition就会从当前的height和top以300毫秒过渡变化到
//height=14vh,top=86vh的状态
height: \'14vh\',
top: \'86vh\'
}, {
duration: 300,
})
//run方法执行动画
this.$refs.locationBox.run(() => {
//这是run方法的回调函数,可以在这里执行动画完成之后的操作
})
简单来说就是将三个状态封装为三个函数,每次要状态变化时就调用对应函数,以此能实现基本的绑定事件进行动画过渡。
为什么不封装成一个函数通过参数改变内容?因为状态的变化同时包含了一些其他状态的改变需要在函数中一起动态变化,这里可以自主选择。
这样就可以实现绑定事件的效果,如下图,通过绑定浮层底部的横条的点击时间和input的聚焦事件,来控制动画的执行。
接下来就是要让用户可以利用黑色横条拖动整个框的移动,因为网上找到写可拖动底部浮层的教程都是写Android的,这里我并没有找到比较好的办法,只能用监听事件来实现。
利用将@touchend和@touchmove两个事件绑定到黑色横条上,监听用户对于黑色横条的移动。
@touchmove:移动时触发
@touchend:移动结束时触发
<view @click="showList" @touchmove="touchLocationBox" @touchend="locationBoxReset">
<!-- 这里是黑色横条的内容或样式 -->
</view>
然后再来看两个事件绑定函数
///监听触摸滑动事件
touchLocationBox(res) {
//利用监听事件获取当前用户点击屏幕的位置
//而后将其除于整个屏幕的高度获取top值
let top = res.changedTouches[0].pageY / this.windowHeight
//防止顶部过度拖拽
//当拖拽部分小于10%时禁止用户继续拖拽
if (top < 0.1) {top = 0.1}
//防止底部过度拖拽
//同理,禁止拖拽到85%以上
else if (top > 0.85) {top = 0.85}
//这里是动画执行的关键
//每次函数执行时就执行一个过渡时间为0的动画
//动画内容就是将当前的heiht值和top值过渡到用户手指拖拽到的值
this.$refs.locationBox.step({
top: top * 100 + \'vh\',
height: (1 - top) * 100 + \'vh\',
}, {
duration: 0,
})
this.$refs.locationBox.run(() => {
})
//维护一个当前的top值,动态改变
this.locationBoxTop = top
},
这里要修改前面的动态函数。
this.$refs.locationBox.step({
height: \'14vh\',
top: \'86vh\'
}, {
duration: 300,
})
this.$refs.locationBox.run(() => {
//动画执行完毕后更新当前的top值
this.locationBoxTop = 0.86
})
因为需要强制修改top值,所以uni-transition需要新增绑定top参数,并使用!important强制更新。
<uni-transition custom-class="location-box" :show="showLocationBox" ref="locationBox"
:styles="{top:locationBoxTop*100+\'vh !important\',height:(1-locationBoxTop)*100+\'vh !important\'}">
</uni-transition>
这里已经基本实现了用户拖拽的问题,但是我们需要的仅仅为3个状态,即底栏、半屏、全屏三个状态,到这里用户拖动后浮层会停在当前位置。这里就需要使用到@touchend事件,在用户移动结束时触发函数,我们在这里对浮层进行复位。
//拖动结束将地点框复位
locationBoxReset(res) {
//获取当前的top值
let top = this.locationBoxTop
//自主设定拖动阈值,
if (top < 0.4) {
//这里转化为全屏状态
} else if (top > 0.7) {
//这里转化为底栏状态
} else {
//这里转化为半屏状态
}
},
然后来看一下最终效果,用户可以点击横条实现动画,还可以通过横条拖动浮层,并且不会出现拖拽过渡的现象,当用户拖拽到一定阈值时松手,浮层会自动复位到附近的状态。
问题和解决
在动画实现的问题还是遇到了很多问题。先抛开实现的问题不谈,目前已经存在的问题就是拖拽时的帧率不够高,因为一直在执行动画所以效率不够高,我个人觉得在小程序上是够用的,虽然不能想高德地图那样流畅,但也并不会感到卡顿。这个问题我并没有解决,这仅仅是我个人的实现方式,如果看到博客的人有效率更高的动画方式欢迎在评论区指教。
然后说说实现上的问题。最开始的问题就是浮层在执行一次动画之后会将step方法内的参数直接加在整个浮层样式的尾部,而不是修改他的值,这是uni-transition插件源码的问题,如下图底部的height和top就是动画执行后添加的样式,而CSS的写在后面的样式会覆盖掉写在前面的样式,所以我们动态绑定的height和top就无法生效,解决的方式就是在同态绑定时加上!important,给绑定的样式最高的权重,使其不会被覆盖。
上面那个问题我真的找了好久才找到,最后是通过看wxml的代码变化才找到。所以这个可以提个醒,写这种动画可以看看wxml的代码变化,注意样式的覆盖。
为了函数调用起来更加方便,我将三个状态封装为一个change方法,在使用使只需要使用change(0)可以直接执行到底栏状态。
//更改浮层状态,0:底栏(默认);1:半屏;2:全屏;
change(status) {
if (status === 0) {
//执行动画和其他参数的改变
} else if (status === 1) {
//执行动画和其他参数的改变
} else if (status === 2) {
//执行动画和其他参数的改变
}
},
最后说一个与动画无关但很有可能会遇到的问题,如果在浮层内有搜索框,在动画移动时可能会出现input内的placeholder移动残影的问题,即input中的内容总是慢动画一步移动,极大地影响了用户体验。我解决的方式就是在动画执行时将input框禁用掉,即绑定其disabled属性,只要input是出于禁用的状态,就不会出现移动残影的问题。
总结
uni-transition插件由于其比较小众,用的人比较少,所以网上能找到的教程并不多,建议最优先摸清官方的文档。基于这种过渡动画的方式,可以做出很多复杂的动画。
请发表评论