手势系统原理
通过给元素添加一个手势组件的父级,通过手势组件的触发事件来进行对应操作
pan-gesture-handler
拖动(横向/纵向)时触发的组件
vertical-drag-gesture-handler
纵向时触发的组件
worklet:ongesture: 手势识别成功的回调
**worklet:should-response-on-move:**手指移动过程中手势是否响应
**simultaneous-handlers:**需要协同的手势,即,如果某个手势组件包含在另一个手势组件里面,那么将会手势冲突。此时需要协商手势,谁在外面谁在里面。设置协商手势后在子级触发组件 那么两者都会触发
native-view :native-view
支持的枚举值有 scroll-view
和 swiper
。滚动容器纵向滚动时,使用 <vertical-drag-gesture-handler>
手势组件代理内部手势,横向滚动时,则使用 <horizontal-drag-gesture-handler>
。
怎么使用skyline
diff
Skyline 具体支持版本如下:
- 微信安卓客户端 8.0.33 或以上版本(对应基础库为 2.30.4 或以上版本)
- 微信 iOS 客户端 8.0.34 或以上版本(对应基础库为 2.31.1 或以上版本)
- 开发者工具 Stable 1.06.2307260 或以上版本(建议使用 Nightly 最新版)
初始项目配置
- 在 app.json 或者所需 page.json 中配上
renderer: skyline
- 确保右上角 > 详情 > 本地设置里的
开启 Skyline 渲染调试
选项被勾选上 - 使用
worklet
动画特性时,确保右上角 > 详情 > 本地设置里的编译 worklet 代码
选项被勾选上 (代码包体积会少量增加) - 调试基础库切到 3.0.0 或以上版本
-
- Skyline 依赖 按需注入 特性,需在 app.json 配上
"lazyCodeLoading": "requiredComponents"
- Skyline 依赖 按需注入 特性,需在 app.json 配上
- 在全局或页面配置中声明使用新版 glass-easel 组件框架,即
{ "componentFramework": "glass-easel" }
- 在全局配置中声明默认
block
布局,即app.json
配上"rendererOptions": { "skyline": { "defaultDisplayBlock": true } }
- 在全局配置中声明默认
content-box
布局,即app.json
配上"rendererOptions": { "skyline": { "defaultContentBox": true } }
注意事项
- Skyline 不支持页面全局滚动,需在页面配置加上
"disableScroll": true
(使之与 WebView 保持兼容),在需要滚动的区域使用 scroll-view 实现 - Skyline 不支持原生导航栏,需在页面配置加上
"navigationStyle": "custom"
(使之与 WebView 保持兼容),并自行实现自定义导航栏 - 所有的
worklet
代码的函数顶部需要加上"worklet;"
编写Popup效果
WXML
xml
<view class="skyline-popup comment-container" style="height: {{height}}px;">
<pan-gesture-handler worklet:ongesture="handlePan" style="flex-shrink: 0;">
<view class="comment-header" bind:touchend="handleTouchEnd">
<view class="comment-handler"></view>
Popup滑动标题
</view>
</pan-gesture-handler>
<!-- 滚动区要与 pan 手势协商 -->
<pan-gesture-handler id="pan" worklet:should-response-on-move="shouldPanResponse" simultaneousHandlers="{{['scroll']}}" worklet:ongesture="handlePan">
<vertical-drag-gesture-handler id="scroll" native-view="scroll-view" worklet:should-response-on-move="shouldScrollViewResponse" simultaneousHandlers="{{['pan']}}">
<scroll-view class="comment-list" scroll-y worklet:adjust-deceleration-velocity="adjustDecelerationVelocity" worklet:onscrollupdate="handleScroll" type="list" show-scrollbar="{{false}}">
<view>1</view>
</scroll-view>
</vertical-drag-gesture-handler>
</pan-gesture-handler>
</view>
这个地方参考官方的留言Popup布局,有header
显示标题,有scroll-view
对内容进行滚动。header
和scroll-view
各自添加了手势组件并且同样添加了拖动事件handlePan
,进行Popup
的拖动效果实现。而scroll-view
还添加了vertical-drag-gesture-handler
此处用native-view
进行scroll-view
组件的代理。
JS
kotlin
// shared:用于跨线程共享数据和驱动动画。
// timing:基于时间的动画。
const { shared, timing } = wx.worklet;
// 获取系统信息
const { screenHeight, statusBarHeight, safeArea } = wx.getSystemInfoSync();
enum GestureState {
POSSIBLE = 0, // 0 此时手势未识别,如 panDown等
BEGIN = 1, // 1 手势已识别
ACTIVE = 2, // 2 连续手势活跃状态
END = 3, // 3 手势终止
CANCELLED = 4, // 4 手势取消,
}
Component({
data: {
height: 600,
},
lifetimes: {
// 生命周期对象 新语法
created() {
// 初始化对象
this.transY = shared(1000); // 留言半屏的位置
this.scrollTop = shared(0); // 留言列表的滚动位置
this.startPan = shared(true); // 是否开始滑动
this.initTransY = shared(0); // 留言半屏的初始位置
this.upward = shared(false); // 是否向上滑动
},
attached() {
// 组件挂载时
// 屏幕高度减去状态栏高度 设置最高高度
this.setData({
height: screenHeight - statusBarHeight,
});
},
ready() {
// 在组件在视图层布局完成后执行
const query = this.createSelectorQuery();
console.log(safeArea.bottom, "bottom");
// ready 生命周期里才能获取到首屏的布局信息
query.select(".comment-header").boundingClientRect();
query.exec((res) => {
// 设置transY的值和初始transY的值为屏幕高度减去留言header的高度 再减去安全区域(即可以使用的区域)
// 因为初始只显示popup的header
this.transY.value = this.initTransY.value =
screenHeight - res[0].height - (screenHeight - safeArea.bottom);
});
// 通过 transY 一个 SharedValue 控制半屏的位置 设置动画
this.applyAnimatedStyle(".comment-container", () => {
"worklet";
return { transform: `translateY(${this.transY.value}px)` };
});
},
},
methods: {
handlePan(gestureEvent) {
"worklet";
// 如果手势正在滑动
if (gestureEvent.state === GestureState.ACTIVE) {
// 更新是否向上和transY的值
this.upward.value = getPopupUpward(gestureEvent.deltaY);
this.transY.value = getUpdatePopupTransY(
gestureEvent.deltaY,
this.transY.value
);
}else if (
gestureEvent.state === GestureState.END ||
gestureEvent.state === GestureState.CANCELLED
) {
// 当前位置 <= 屏幕高度的一半 即在屏幕上方
if (this.transY.value <= screenHeight / 2) {
// 在上面的位置
if (this.upward.value) {
this.scrollTo(statusBarHeight);
} else {
this.scrollTo(screenHeight / 2);
}
// 当前位置 > 屏幕高度的一半 即在屏幕下方
} else if (
this.transY.value > screenHeight / 2 &&
this.transY.value <= this.initTransY.value
) {
// 在中间位置的时候
if (this.upward.value) {
this.scrollTo(screenHeight / 2);
} else {
this.scrollTo(this.initTransY.value);
}
} else {
// 在最下面的位置
this.scrollTo(this.initTransY.value);
}
},
},
});
function clamp(val, min, max) {
"worklet";
return Math.min(Math.max(val, min), max);
}
const getPopupUpward = (deltaY: number) => {
"worklet";
// deltaY < 0,往上滑动
return deltaY < 0;
};
const getUpdatePopupTransY = (deltaY: number, transY: number) => {
"worklet";
// deltaY < 0,往上滑动
// 当前半屏位置
const curPosition = transY;
// 只能在 [statusBarHeight, screenHeight] 之间移动
const destination = clamp(
curPosition + deltaY,
statusBarHeight,
screenHeight
);
if (curPosition === destination) return curPosition;
// 改变 transY,来改变半屏的位置
return destination;
};
这里通过生命周期
进行初始化数据,通过handlePan
进行Popup的拖动
WXSS
详见最下方
到这里就可以实现拖动效果了,但在这里会出现一个问题,那就是在拖动scroll-view
的时候会出现里面滚动内容也在滚动的问题。此时就需要worklet:should-response-on-move
来设置手势操作时候是否响应了
设置scroll-view的手势响应
JS
kotlin
Component({
methods: {
// 编写scroll-view的手势协商
// shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商
shouldPanResponse() {
"worklet";
return this.startPan.value;
},
// 进来这里证明是在拖动scroll-view
shouldScrollViewResponse(pointerEvent) {
"worklet";
// transY(popup当前位置) > 状态栏高度 popup在状态栏下面。代表还没到达最上面一层
// 因为这里滚动条件为到达最上面一层才开始滚动 到达的条件为 transY === 状态栏高度
if (this.transY.value > statusBarHeight) 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;
},
// 设置滚动位置
handleScroll(evt) {
"worklet";
this.scrollTop.value = evt.detail.scrollTop;
},
}
})
在这个地方主要用了worklet:should-response-on-move
来进行手势组件的响应,因为vertical-drag-gesture-handler
组件使用native-view
代理了scroll-view
组件,所以它可以控制scroll-view
的行为。在这里主要是在scroll-view
触发手势时进行判断,**必须要popup高度到达顶部栏,并且滚动位置不为0,deltaY>0(代表手势为向下滑动)才可以滚动。**详见shouldScrollViewResponse
方法。