带手势交互的小程序半屏弹窗实现(Skyline)
Skyline
什么是SKyline?
小程序一直以来采用的都是 AppService 和 WebView 的双线程模型,基于 WebView 和原生控件混合渲染的方式,小程序优化扩展了 Web 的基础能力,保证了在移动端上有良好的性能和用户体验。Web 技术至今已有 30 多年历史,作为一款强大的渲染引擎,它有着良好的兼容性和丰富的特性。 尽管各大厂商在不断优化 Web 性能,但由于其繁重的历史包袱和复杂的渲染流程,使得 Web 在移动端的表现与原生应用仍有一定差距。
为了进一步优化小程序性能,提供更为接近原生的用户体验,在 WebView 渲染之外新增了一个渲染引擎 Skyline,其使用更精简高效的渲染管线,并带来诸多增强特性,让 Skyline 拥有更接近原生渲染的性能体验。
我们为什么要使用Skyline?
小程序采用多 WebView 架构,页面间跳转形式十分单一,仅能从右到左进行动画。而原生 App 的动画形式则多种多样,如从底部弹起,下一个为半屏页面,前一个页面下沉等。
Skyline 渲染引擎下,页面有两种渲染模式: WebView 和 Skyline,它们通过页面配置中的 renderer字段进行区分。在连续页面间跳转时,可实现自定义路由效果,路由动画的曲线、时长均可交由开发者控制。
在上周,我们已经实现了半屏弹窗效果:,这是这周使用Skyline改进后的:
那么,这两个页面有什么区别呢?
第一个页面的半屏弹窗,只是一个div,点击按钮弹出后,只有点击白色div外的部分,才会回到主页,交互效果较弱。
第二页页面的半屏弹窗,基于Skyline的 worklet 动画,通过手势协商机制,实现在半屏内列表往下拉到顶之后,无缝切换到半屏下拉的效果。
通俗来说,第一个半屏没有动画,只能点击弹回。而第二个半屏有交互,更加的“跟手”。
视频效果如下:
通过上述三个例子可以看出,通过新出的Skyline引擎,可以做到几乎媲美原生APP的动画、交互、路由效果!
代码实现
未改进版
这一版没有对半屏效果的实现代码进行封装,会对原来页面的js代码产生很严重的污染,在接下来的改进版本我将对其完整封装成组件的形式,并实现与主页面之间的通信
<view class="open-comment" bind:tap="onTapOpenComment">
<view class="open-comment-wording">查看链</view>
<view class="safe-area-inset-bottom"></view>
</view>
<view class="comment-container">
<!-- 顶部不参与手势协商,单独控制 -->
<pan-gesture-handler worklet:ongesture="handlePan" style="flex-shrink: 0;">
<view class="comment-header">
<view class="close-comment" bind:tap="onTapCloseComment">∨</view>
我的链
</view>
</pan-gesture-handler>
<!-- 滚动区要与 pan 手势协商 -->
<pan-gesture-handler id="pan" worklet:should-response-on-move="shouldPanResponse" simultaneous-handlers="{{['scroll']}}" worklet:ongesture="handlePan">
<vertical-drag-gesture-handler id="scroll" native-view="scroll-view" worklet:should-response-on-move="shouldScrollViewResponse" simultaneous-handlers="{{['pan']}}">
<scroll-view class="comment-list" scroll-y worklet:adjust-deceleration-velocity="adjustDecelerationVelocity" bindscroll="handleScroll" type="list" show-scrollbar="{{false}}">
</scroll-view>
</vertical-drag-gesture-handler>
</pan-gesture-handler>
</view>
const { shared, timing } = wx.worklet
const GestureState = {
POSSIBLE: 0, // 0 此时手势未识别,如 panDown等
BEGIN: 1, // 1 手势已识别
ACTIVE: 2, // 2 连续手势活跃状态
END: 3, // 3 手势终止
CANCELLED: 4, // 4 手势取消,
}
Component({
data: {
},
lifetimes: {
created() {
this.transY = shared(1000)
this.scrollTop = shared(0)
this.startPan = shared(true)
this.commentHeight = shared(1000)
},
ready() {
const query = this.createSelectorQuery()
// ready 生命周期里才能获取到首屏的布局信息
query.select('.comment-container').boundingClientRect()
query.exec((res) => {
this.transY.value = this.commentHeight.value = res[0].height
})
// 通过 transY 一个 SharedValue 控制半屏的位置
this.applyAnimatedStyle('.comment-container', () => {
'worklet'
return { transform: `translateY(${this.transY.value}px)` }
})
},
},
methods: {
onTapOpenComment() {
this.openComment(300)
},
openComment(duration) {
'worklet'
this.transY.value = timing(0, { duration })
},
onTapCloseComment() {
this.closeComment()
},
closeComment() {
'worklet'
this.transY.value = timing(this.commentHeight.value, { duration: 200 })
},
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
'worklet'
return this.startPan.value
},
shouldScrollViewResponse(pointerEvent) {
'worklet'
// transY > 0 说明 pan 手势在移动半屏,此时滚动不应生效
if (this.transY.value > 0) return false
const scrollTop = this.scrollTop.value
const { deltaY } = pointerEvent
// deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,滚动不生效
const result = scrollTop <= 0 && deltaY > 0
this.startPan.value = result
return !result
},
handlePan(gestureEvent) {
'worklet'
if (gestureEvent.state === GestureState.ACTIVE) {
const curPosition = this.transY.value
const destination = Math.max(0, curPosition + gestureEvent.deltaY)
if (curPosition === destination) return
this.transY.value = destination
}
if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) {
if (gestureEvent.velocityY > 500 && this.transY.value > 50) {
this.closeComment()
} else if (this.transY.value > this.commentHeight.value / 2) {
this.closeComment()
} else {
this.openComment(100)
}
}
},
adjustDecelerationVelocity(velocity) {
'worklet'
const scrollTop = this.scrollTop.value
return scrollTop <= 0 ? 0 : velocity
},
handleScroll(evt) {
'worklet'
this.scrollTop.value = evt.detail.scrollTop
},
},
})
{
"usingComponents": {},
"disableScroll": true,
"navigationStyle": "custom",
"componentFramework": "glass-easel",
"renderer": "skyline"
}
page {
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
width: 100vw;
height: 100vh;
color: #1A191E;
}
page, view {
box-sizing: border-box;
}
pan-gesture-handler, vertical-drag-gesture-handler {
display: flex;
flex-direction: column;
overflow: hidden;
}
.container {
flex: 1;
width: 100vw;
min-height: auto;
overflow: hidden;
}
.container image {
width: 100vw;
}
.open-comment {
display: flex;
flex-direction: column;
flex-shrink: 0;
width: 100%;
background-color: white;
}
.open-comment-wording {
height: 66px;
display: flex;
justify-content: center;
align-items: center;
}
.safe-area-inset-bottom {
height: env(safe-area-inset-bottom);
}
.comment-container {
width: 100vw;
height: 70vh;
display: flex;
flex-direction: column;
position: absolute;
bottom: 0;
z-index: 999;
background-color: white;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
transform: translateY(100%);
}
.comment-header {
width: 100%;
font-size: 16px;
text-align: center;
padding: 15px 0;
}
.close-comment {
position: absolute;
left: 20px;
font-size: 10px;
font-weight: bold;
background-color: #F8F8F8;
border-radius: 100%;
width: 20px;
height: 20px;
line-height: 20px;
z-index: 1;
}
.comment-list {
flex: 1;
overflow: hidden;
}
.comment-item {
padding: 0 20px 20px;
font-size: 13px;
line-height: 1.4;
}
.main-comment, .sub-comment {
display: flex;
flex-direction: row;
}
.sub-comment {
padding: 10px 22px 0;
}
.user-head-img {
width: 33px;
height: 33px;
border-radius: 50%;
margin-top: 5px;
}
.others {
flex: 1;
margin-left: 10px;
}
.user-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.content {
margin-top: 2px;
}
改进版(组件)
在JS中写了Page与Component,Page里的函数怎么也运行不了,于是查找微信小程序component和page的区别?
功能区别:组件是一个可复用的部件,可以在不同的页面之间共享;页面是小程序中最基本的单位,提供了用户与应用的交互界面。
文件结构区别:组件是单独的一个文件夹,包含了自己的 wxml、js 和 wxss 文件;页面也是单独的一个文件夹,但是它需要使用组件来组成页面的内容。
生命周期函数区别:组件有自己独特的生命周期函数,例如 attached、detached 等;页面也有自己的生命周期函数,例如 onLoad、onShow 等。
数据绑定区别:组件的数据绑定使用的是 properties 和 data;页面的数据绑定使用的是 data。
两者不能在一个JS中共存,所以要求作者对半屏组件封装,将page与compoent分离。
跟上面的未改进版没什么太大区别,详情请下载代码包查看
{/tabs-item} {tabs-item 主页面代码}- 感谢你赐予我前进的力量