【HarmonyOS 7 沉浸光感深度实战】 07 手势、动画与动态浮动面板怎么组合
文章目录
前言
悬浮面板和普通材质卡片相比,麻烦的地方在于它会同时处理好几种变化。
手指按下时,组件需要有反馈;拖动标题区域时,面板要跟着手势移动;点开以后,面板还要展开更多操作;底层内容开始滚动时,它又不能一直挡在阅读区域上方。代码写到这里,interactive、lightEffect、PanGesture、translate、opacity、scale 和 animateTo 往往会同时出现。
我自己在组合这类效果时,最容易出现的问题就是动画加得太多。材质已经提供了按压反馈,页面又补一层缩放;拖动位置已经跟着手指更新,外面再套一层显式动画。单独看每段代码都说得通,实际操作后却容易出现跟手发黏、缩放幅度过大,或者面板停下来以后还有动画继续追位置。
后来我更愿意先把这些变化分清楚。按压形变和触点光感交给沉浸材质;拖动位置交给手势;展开收起和滚动显隐由页面状态控制。这样每一种变化都有自己的来源,出了问题也更容易找到对应位置。
这次实验页会做一块可以拖动的动态浮动面板。收起时只显示标题和展开按钮,展开以后增加三个操作项;拖动标题区域可以改变位置;列表向上滚动时,面板会暂时移出页面,反向滚动后再回来。
目前我的测试设备还没有 HarmonyOS 7 实机测试权限,因此当前先使用模拟器检查手势、状态切换和基础动画。材质按压反馈、触点光感和最终操作手感仍建议在具备权限的真机上继续确认。

一、先给每一种动态效果分工
面板开始移动之前,先把谁负责什么确定下来。
ImmersiveMaterial 的 interactive 可以开启材质的交互形变,lightEffect 用于触摸时的动态光感反馈。这两项都属于材质本身的能力,从 API 26.0.0 开始提供。
当前面板准备两份材质:
private readonly thinMaterial: uiMaterial.Material =
new uiMaterial.ImmersiveMaterial({
style: uiMaterial.ImmersiveStyle.THIN,
interactive: true,
lightEffect: {
color: Color.White
}
});
private readonly regularMaterial: uiMaterial.Material =
new uiMaterial.ImmersiveMaterial({
style: uiMaterial.ImmersiveStyle.REGULAR,
interactive: true,
lightEffect: {
color: Color.White
}
});
收起状态使用 THIN,展开以后使用 REGULAR。
这里我不会再给按下事件补一套额外的 scale。系统材质已经负责按压阶段的视觉反馈,再增加一套自定义按压缩放,很容易让两种变化叠加。
自定义动画只负责面板自身的状态:
| 当前变化 | 页面负责的属性 |
|---|---|
| 手指拖动面板 | translate |
| 展开和收起 | 内容高度、opacity、translate |
| 页面滚动时显隐 | translate、opacity、scale |
| 材质状态 | THIN、REGULAR |
因此,这块面板实际可以拆成四条很直观的操作关系:
按下卡片
→ interactive + lightEffect
拖动标题区域
→ PanGesture + translate
点击展开或收起
→ 状态变化 + animateTo
页面上下滚动
→ onDidScroll + 显示状态
PanGesture 可以识别平移手势,并允许设置方向和触发距离;当前官方文档中,非手写笔输入源的默认触发距离为 5vp,因此示例也直接使用 5vp。
这一点和拖动手感关系很直接。识别距离设置得过大以后,手指已经移动了一段距离,面板才开始响应,操作就会显得迟。
另一方面,拖动过程本身是连续输入。我不希望每一次位置更新都重新启动一段动画,所以拖动阶段只计算坐标。

把这些职责确定以后,后面真正需要处理的就只剩三个动作:拖动、展开、滚动。
二、把拖动、展开和滚动串进同一块面板
页面底层是一张普通 List,悬浮面板放在 Stack 上层。
我比较喜欢这种结构,因为浮动面板和内容区彼此独立。列表照常滚动,面板只处理自己的位置和状态。
整个面板主要有三种状态:
收起
展开
滚动隐藏
拖动位置则单独保存,不会因为展开或滚动显隐而丢掉。
拖动时直接更新位置
当前示例使用 PanGesture。
手势开始时,先记住面板现在的位置:
private dragStartX: number = 0;
private dragStartY: number = 0;
@State private panelOffsetX: number = 0;
@State private panelOffsetY: number = 0;
随后再根据手势偏移计算新坐标:
PanGesture({
direction: PanDirection.All,
distance: 5
})
.onActionStart(() => {
this.dragStartX = this.panelOffsetX;
this.dragStartY = this.panelOffsetY;
})
.onActionUpdate((event: PanGestureEvent) => {
this.panelOffsetX = this.clamp(
this.dragStartX + event.offsetX,
-110,
110
);
this.panelOffsetY = this.clamp(
this.dragStartY + event.offsetY,
-260,
120
);
})
PanGesture 支持 All、Horizontal、Vertical 等方向配置。这里允许所有方向移动,再通过 clamp() 限制可拖动范围,避免把整个面板甩出屏幕。
这个地方我没有使用 animateTo。
拖动过程中,面板需要尽量贴着手指。每一帧收到新的坐标以后直接更新 translate,操作会更自然:
.translate({
x: this.panelOffsetX,
y: this.panelOffsetY + this.visibilityShiftY
})
还有一个细节我觉得比较重要。
拖动手势只绑定在面板标题区域,没有覆盖整个面板,更没有覆盖底层页面。List 自身已经包含滚动手势,再大范围加入自定义手势时,需要留意组件之间的手势响应关系。官方 List 文档也特别提醒,列表内部已经绑定了用于跟手滚动的手势,自定义手势需要注意冲突处理。
所以当前页面的操作会比较明确:
拖动面板标题
→ 移动悬浮面板
滑动列表区域
→ 滚动页面
点击面板按钮
→ 执行对应操作
大家不需要在同一个区域里猜测这次滑动究竟是拖面板还是滚列表。
展开和收起使用一次状态动画
拖动属于连续操作,展开则完全不同。
点击一次按钮以后,面板会从收起进入展开,或者从展开回到收起。这个变化有明确的起点和终点,所以更适合使用显式动画。
当前会同时修改几个状态:
面板高度
内容透明度
内容纵向位置
当前材质
代码如下:
private togglePanel(): void {
const nextExpanded: boolean =
!this.isExpanded;
this.materialMode =
nextExpanded ? 1 : 0;
this.getUIContext().animateTo({
duration: 260,
curve: Curve.EaseOut
}, () => {
this.isExpanded = nextExpanded;
this.panelHeight =
nextExpanded ? 216 : 96;
this.detailOpacity =
nextExpanded ? 1 : 0;
this.detailTranslateY =
nextExpanded ? 0 : 12;
});
}
收起时使用 THIN,展开以后选择 REGULAR。
这里的材质对象早就已经创建完成。点击按钮只是在两份现成材质之间选择,没有在动画回调中反复 new ImmersiveMaterial()。
我觉得这个区别值得保留。
材质配置本身变化频率很低,而动画过程中的尺寸、透明度和位移会持续变化。把材质对象跟高频状态放在一起更新,会让代码越来越难看清楚。
当前面板结构简单,展开时直接调整高度没有太大问题。如果真实项目中的面板内容很多,布局变化已经造成动画掉帧,可以把更多视觉变化移到 scale、translate、opacity 这些图形变换属性上。HarmonyOS 当前的性能检查规则也建议,在布局属性动画出现性能问题时优先使用图形变换属性。
滚动时只在方向改变后切换一次状态
面板能够拖,也能够展开以后,页面还有一个实际问题。
长列表阅读过程中,如果面板一直停在底部,它会持续挡住一部分内容。
所以示例加入一个很轻的滚动显隐逻辑:
页面内容向上移动
→ 面板退出底部
页面反向滚动
→ 面板重新出现
当前 List 使用 onDidScroll。官方文档中,onDidScroll 会返回当前帧的滚动偏移量和滚动状态;内容向上滚动时偏移为正,向下滚动时为负。旧的 onScroll 已经废弃,并推荐使用 onDidScroll。
因此方向判断可以写得很简单:
private handleListScroll(
scrollOffset: number,
scrollState: ScrollState
): void {
if (scrollState === ScrollState.Idle) {
return;
}
if (scrollOffset > 4) {
this.hidePanel();
return;
}
if (scrollOffset < -4) {
this.showPanel();
}
}
这里留了 4vp 的小阈值。
细微滚动和回弹过程中会产生一些很小的偏移,如果完全按照正负号切换,面板容易在边界附近频繁显隐。加入一点缓冲以后,操作会稳定一些。
隐藏面板时:
private hidePanel(): void {
if (!this.panelVisible) {
return;
}
this.panelVisible = false;
this.getUIContext().animateTo({
duration: 180,
curve: Curve.EaseOut
}, () => {
this.panelOpacity = 0;
this.visibilityShiftY = 260;
this.panelScale = 0.96;
});
}
重新显示:
private showPanel(): void {
if (this.panelVisible) {
return;
}
this.panelVisible = true;
this.getUIContext().animateTo({
duration: 220,
curve: Curve.EaseOut
}, () => {
this.panelOpacity = 1;
this.visibilityShiftY = 0;
this.panelScale = 1;
});
}
这里真正帮到我的,是 panelVisible 这个状态。
onDidScroll 会在滚动过程中持续回调。面板已经隐藏以后,后续同方向滚动会直接返回,不会每一帧再重新启动隐藏动画。官方文档确认 onDidScroll 会在列表滑动过程中持续提供当前帧偏移和滚动状态。
因此整个过程会变成:
第一次向上滚动
→ 执行隐藏动画
继续向上滚动
→ 保持隐藏
开始向下滚动
→ 执行显示动画
继续向下滚动
→ 保持显示
这个小状态判断比不断调整动画参数更有用。

完成这一部分以后,手势和动画已经能够一起运行。后面更值得关注的是,哪些状态适合持续更新,哪些状态只应该偶尔发生变化。
三、面板动得多以后,代码反而要收一点
动态页面很容易越做越热闹。
已经有拖动,可以再加惯性;已经有展开,可以再改变圆角;已经有滚动显隐,也可以顺便修改模糊、阴影和材质颜色。
我自己更愿意少加几项。
判断一个动画怎么写时,我通常先看这个状态变化得有多频繁。
拖动位置属于高频状态
拖动时,手势会不断提供新的位移。
这种情况下最直接的方式就是更新:
panelOffsetX
panelOffsetY
然后交给:
.translate({
x: this.panelOffsetX,
y: this.panelOffsetY
})
页面没有必要在拖动的每一步重新创建材质,也没有必要反复启动 animateTo。
HarmonyOS 的性能规则建议,在布局动画出现丢帧时优先使用图形变换属性,scale、translate 等方式通常比频繁改变宽高更适合作为动画手段。
所以当前页面把高频变化尽量集中在:
translate
opacity
scale
这几个属性上。
展开收起属于低频状态
展开按钮只有在用户点击时才会触发。
这种变化频率低,而且有明确的开始和结束,所以用 animateTo 处理更自然。显式动画本身就是用状态变化描述起点和终点,再由系统完成中间过渡。
当前 Demo 为了真正腾出内容空间,会改变 panelHeight。
页面只有几个按钮时,这样处理比较直观。换成复杂工具箱以后,如果高度动画已经影响流畅度,可以把固定容器保留下来,再主要使用内容的 opacity、translate 和 scale 表现展开状态。
这里没有统一答案,还是要看面板里面到底放了多少内容。
材质对象没有必要跟着动画更新
当前页面只需要:
THIN
REGULAR
两种材质。
所以直接提前创建:
private readonly thinMaterial =
new uiMaterial.ImmersiveMaterial(...);
private readonly regularMaterial =
new uiMaterial.ImmersiveMaterial(...);
拖动过程中不会创建新对象。
列表滚动时不会创建新对象。
展开动画的每一帧也不会创建新对象。
状态变化只决定当前使用哪一份材质。
我会把材质对象看成组件比较稳定的视觉配置,而面板位置、透明度和展开状态才属于动态数据。两者分开以后,后面再加按钮或业务逻辑也不会把材质代码搅进手势事件里。
interactive 开启以后,按压缩放可以先省掉
当前两份材质都打开了:
interactive: true
因此,面板本身已经具备系统材质的交互反馈。
页面中的 panelScale 只用于滚动隐藏和重新出现,没有拿它继续制作按压效果。
如果后面再增加:
按下
→ 自定义 scale
同时
→ interactive 系统形变
两种效果会同时作用在组件上。
对于这种面板,我更喜欢先看系统材质反馈够不够。真机上觉得反馈不足时,再决定是否增加自定义动画,这样调节范围会更清楚。
拖动区域尽量小一些
面板可以拖动,底层列表也需要滚动,这两个需求天然会争夺手势。
当前做法是只让标题区域响应 PanGesture。
操作 A、操作 B、操作 C 仍然是正常按钮;列表区域也继续保持正常滚动。
官方 PanGesture 支持配置触发方向和距离,而 List 自身已经绑定滚动手势,所以组件内增加自定义拖动时,手势区域和响应关系值得单独检查。(华为开发者)
放回业务页面以后,可以先按这个思路划区域:
| 页面区域 | 主要操作 |
|---|---|
| 面板标题栏 | 拖动 |
| 展开按钮 | 展开或收起 |
| 面板操作区 | 点击 |
| 面板外列表 | 滚动 |
这样比整块浮层都可以拖更容易控制。
这种面板更适合临时高频操作
动态浮动面板并不适合所有页面。
我觉得比较合适的场景有三类。
第一类是内容阅读。图片、文章或视频页面需要一两个随时可用的操作,又不希望工具条长期占据固定空间。
第二类是编辑和创作。图片编辑、文档批注和轻量画布中,工具面板可以拖到当前内容比较空的位置。
第三类是播放和控制。收起时显示核心状态,展开后提供三到五个临时操作。
内容继续增加以后,我会开始考虑其他结构。
| 当前需求 | 更合适的形式 |
|---|---|
| 一个高频操作 | 悬浮按钮 |
| 两到五个临时操作 | 动态浮动面板 |
| 面板会遮住主要内容 | 增加拖动 |
| 阅读滚动时容易挡内容 | 增加滚动显隐 |
| 已经形成较长操作流程 | Sheet 或独立页面 |
浮动面板的价值还是让操作更顺手。
拖动、展开、材质和光感都只是手段。操作数量已经多到需要不断扩张面板时,页面结构本身可能已经需要重新安排。
总结
这块动态浮动面板做完以后,我觉得最值得保留的习惯,是先把动态效果分工。
材质负责按压形变和触点光感。
PanGesture 负责手指拖到哪里,面板就移动到哪里。
展开、收起以及滚动显隐属于明确的状态变化,可以交给 animateTo。
拖动和滚动过程中频繁变化的部分,则尽量集中在 translate、opacity 和 scale 上。HarmonyOS 当前也建议在动画性能出现问题时优先考虑图形变换属性,减少布局属性动画带来的额外开销。
材质对象也没有跟着动画过程重复创建。THIN 和 REGULAR 提前准备好,页面只根据展开状态选择当前材质。
我更喜欢这样的代码,因为出了问题以后方向比较明确。拖动不跟手,就看 PanGesture 和位移计算;列表滚动时反复闪动,就看显隐状态判断;按压反馈不合适,再回到 interactive 和 lightEffect。
当前实验可以在 HarmonyOS 7 模拟器中检查拖动、展开收起、列表滚动和基础动画。材质的交互形变、光感反馈以及最终操作手感仍建议在具备权限的真机上继续确认。
完整代码
Main.ets
/**
* HarmonyOS 7 沉浸光感深度实战 07
*
* 验证环境:
* HarmonyOS SDK API 26
* HarmonyOS 7 模拟器
*/
import { uiMaterial } from '@kit.ArkUI';
@Entry
@Component
struct Main {
/**
* 页面中的演示数据。
*/
private readonly items: Array<number> = [
1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18
];
/**
* 面板是否展开。
*/
@State private isExpanded: boolean = false;
/**
* 收起状态 96vp。
* 展开状态 216vp。
*/
@State private panelHeight: number = 96;
/**
* 展开内容使用透明度和纵向位移过渡。
*/
@State private detailOpacity: number = 0;
@State private detailTranslateY: number = 12;
/**
* 用户拖动产生的位置偏移。
*/
@State private panelOffsetX: number = 0;
@State private panelOffsetY: number = 0;
/**
* 滚动显隐单独使用一组状态。
* 这样不会覆盖用户已经拖动出来的位置。
*/
@State private visibilityShiftY: number = 0;
@State private panelOpacity: number = 1;
@State private panelScale: number = 1;
@State private panelVisible: boolean = true;
/**
* 0 表示 THIN。
* 1 表示 REGULAR。
*/
@State private materialMode: number = 0;
/**
* 每轮 PanGesture 开始时保存当前位置。
*/
private dragStartX: number = 0;
private dragStartY: number = 0;
/**
* 材质对象提前创建。
* 拖动和动画过程不会反复创建新材质。
*/
private readonly thinMaterial: uiMaterial.Material =
new uiMaterial.ImmersiveMaterial({
style: uiMaterial.ImmersiveStyle.THIN,
interactive: true,
lightEffect: {
color: Color.White
}
});
private readonly regularMaterial: uiMaterial.Material =
new uiMaterial.ImmersiveMaterial({
style: uiMaterial.ImmersiveStyle.REGULAR,
interactive: true,
lightEffect: {
color: Color.White
}
});
/**
* 限制拖动范围,避免面板完全离开窗口。
*/
private clamp(
value: number,
min: number,
max: number
): number {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
/**
* 展开或收起面板。
*/
private togglePanel(): void {
const nextExpanded: boolean =
!this.isExpanded;
/**
* 状态变化时直接切换已经创建好的材质。
*/
this.materialMode =
nextExpanded ? 1 : 0;
this.getUIContext().animateTo({
duration: 260,
curve: Curve.EaseOut
}, () => {
this.isExpanded =
nextExpanded;
this.panelHeight =
nextExpanded ? 216 : 96;
this.detailOpacity =
nextExpanded ? 1 : 0;
this.detailTranslateY =
nextExpanded ? 0 : 12;
});
}
/**
* 向上滚动内容时隐藏面板。
*
* panelVisible 可以避免连续滚动期间
* 反复启动同一段隐藏动画。
*/
private hidePanel(): void {
if (!this.panelVisible) {
return;
}
this.panelVisible = false;
this.getUIContext().animateTo({
duration: 180,
curve: Curve.EaseOut
}, () => {
this.panelOpacity = 0;
this.visibilityShiftY = 260;
this.panelScale = 0.96;
});
}
/**
* 反向滚动时重新显示面板。
*/
private showPanel(): void {
if (this.panelVisible) {
return;
}
this.panelVisible = true;
this.getUIContext().animateTo({
duration: 220,
curve: Curve.EaseOut
}, () => {
this.panelOpacity = 1;
this.visibilityShiftY = 0;
this.panelScale = 1;
});
}
/**
* List 滚动时只判断方向。
* 4vp 阈值用于过滤较小的滚动变化。
*/
private handleListScroll(
scrollOffset: number,
scrollState: ScrollState
): void {
if (scrollState === ScrollState.Idle) {
return;
}
if (scrollOffset > 4) {
this.hidePanel();
return;
}
if (scrollOffset < -4) {
this.showPanel();
}
}
/**
* 把面板恢复到默认位置。
*/
private resetPanelPosition(): void {
this.getUIContext().animateTo({
duration: 240,
curve: Curve.EaseOut
}, () => {
this.panelOffsetX = 0;
this.panelOffsetY = 0;
});
}
/**
* 普通列表内容。
*/
@Builder
private contentCard(item: number) {
Column({ space: 8 }) {
Text(`内容区域 ${item}`)
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#17203A')
.width('100%')
Text(
'上下滚动页面,观察浮动面板的显示状态。'
)
.fontSize(13)
.fontColor('#68708A')
.lineHeight(20)
.width('100%')
}
.width('100%')
.height(112)
.padding(16)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Start)
.backgroundColor(Color.White)
.borderRadius(20)
}
/**
* 标题区域负责拖动。
* 面板内部其他按钮继续处理点击操作。
*/
@Builder
private draggableHeader() {
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('动态浮动面板')
.fontSize(17)
.fontWeight(FontWeight.Bold)
.fontColor('#17203A')
.width('100%')
Text(
this.materialMode === 0
? 'THIN · 拖动这里移动'
: 'REGULAR · 拖动这里移动'
)
.fontSize(11)
.fontColor('#68708A')
.width('100%')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.gesture(
PanGesture({
direction: PanDirection.All,
distance: 5
})
.onActionStart(() => {
this.dragStartX =
this.panelOffsetX;
this.dragStartY =
this.panelOffsetY;
})
.onActionUpdate(
(event: PanGestureEvent) => {
this.panelOffsetX =
this.clamp(
this.dragStartX
+ event.offsetX,
-110,
110
);
this.panelOffsetY =
this.clamp(
this.dragStartY
+ event.offsetY,
-260,
120
);
}
)
)
Button(
this.isExpanded
? '收起'
: '展开'
)
.height(34)
.fontSize(12)
.fontColor('#5065E8')
.backgroundColor('#EEF1FF')
.onClick(() => {
this.togglePanel();
})
}
.width('100%')
.height(52)
.alignItems(VerticalAlign.Center)
}
/**
* 展开内容始终保留在组件树中。
* 收起时通过透明度、位移和裁剪隐藏。
*/
@Builder
private panelDetails() {
Column({ space: 10 }) {
Divider()
.color('#22FFFFFF')
Row({ space: 10 }) {
Button('操作 A')
.layoutWeight(1)
.height(36)
.fontSize(12)
Button('操作 B')
.layoutWeight(1)
.height(36)
.fontSize(12)
Button('操作 C')
.layoutWeight(1)
.height(36)
.fontSize(12)
}
.width('100%')
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('当前位置')
.fontSize(11)
.fontColor('#68708A')
Text(
`x=${Math.round(this.panelOffsetX)} `
+ `y=${Math.round(this.panelOffsetY)}`
)
.fontSize(12)
.fontWeight(FontWeight.Medium)
.fontColor('#17203A')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Button('重置位置')
.height(34)
.fontSize(12)
.fontColor('#5065E8')
.backgroundColor('#EEF1FF')
.onClick(() => {
this.resetPanelPosition();
})
}
.width('100%')
Text(
'向上滚动时面板退出,反向滚动后重新出现。'
)
.fontSize(11)
.fontColor('#68708A')
.lineHeight(17)
.width('100%')
}
.width('100%')
.opacity(this.detailOpacity)
.translate({
y: this.detailTranslateY
})
}
/**
* 完整动态浮动面板。
*/
@Builder
private floatingPanel() {
Column({ space: 8 }) {
this.draggableHeader()
this.panelDetails()
}
.width(316)
.height(this.panelHeight)
.padding({
left: 16,
right: 16,
top: 10,
bottom: 14
})
.backgroundColor(Color.Transparent)
.borderRadius(28)
.clip(true)
.systemMaterial(
this.materialMode === 0
? this.thinMaterial
: this.regularMaterial
)
.translate({
x: this.panelOffsetX,
y: this.panelOffsetY
+ this.visibilityShiftY
})
.scale({
x: this.panelScale,
y: this.panelScale
})
.opacity(this.panelOpacity)
.margin({
bottom: 28
})
.zIndex(10)
}
build() {
Stack({
alignContent: Alignment.Bottom
}) {
Column() {
Column({ space: 6 }) {
Text('HarmonyOS 7 沉浸光感')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#11182C')
.width('100%')
Text('动态浮动控制面板')
.fontSize(16)
.fontColor('#68708A')
.width('100%')
}
.width('100%')
.padding({
left: 20,
right: 20,
top: 24,
bottom: 14
})
.backgroundColor('#F4F6FB')
List({
space: 12
}) {
ForEach(
this.items,
(item: number) => {
ListItem() {
this.contentCard(item)
}
},
(item: number) =>
item.toString()
)
}
.layoutWeight(1)
.width('100%')
.padding({
left: 20,
right: 20,
bottom: 120
})
.scrollBar(BarState.Off)
.onDidScroll(
(
scrollOffset: number,
scrollState: ScrollState
) => {
this.handleListScroll(
scrollOffset,
scrollState
);
}
)
}
.width('100%')
.height('100%')
.backgroundColor('#F4F6FB')
this.floatingPanel()
}
.width('100%')
.height('100%')
}
}
更多推荐




所有评论(0)