在这里插入图片描述

每日一句正能量

“只要热情、努力、乐观,我们终将成为自己的太阳,无需凭借谁的光。”
热情是燃料,努力是燃烧,乐观是持续燃烧的姿态。成为自己的太阳,意味着不再需要从他处借光取暖——你本身就在发光,哪怕微弱,也是自己的、不熄灭的光。
愿你温柔而坚定地,做自己的太阳,也做别人的一缕晨光。

摘要

在移动端应用开发中,吸顶效果(Sticky Header) 是提升长列表浏览体验的核心交互模式之一。当用户向上滚动页面时,分组头部或导航栏固定在视口顶部,既保证了上下文信息的持续可见,又避免了频繁回滚的繁琐操作。HarmonyOS ArkTS 提供了从原生声明式到自定义布局的多层次吸顶实现方案,覆盖从简单分组列表到复杂电商详情页的全场景需求。

本文基于 HarmonyOS 6(API 23),系统梳理三种吸顶实现路径:ListItemGroup 原生吸顶Scroll + Stack 自定义吸顶,以及 Tabs + nestedScroll 嵌套联动吸顶。同时,结合前序文章「自定义手势识别器」的技术积累,深入探讨手势事件与吸顶状态的双向联动机制,为开发者提供一套完整的、可落地的吸顶效果工程化方案。


一、吸顶效果的核心原理

吸顶效果的本质,是滚动容器与固定元素之间的位置博弈。当滚动偏移量(scroll offset)超过预设阈值时,目标元素脱离正常文档流,以绝对定位或固定定位的方式吸附在视口边界;当滚动回退时,元素恢复原始布局位置。

在这里插入图片描述

在 HarmonyOS ArkTS 中,吸顶效果的实现依赖于以下核心机制:

机制 作用 适用组件
sticky 属性 声明式开启分组头部/尾部的吸附能力 List
onWillScroll / onScroll 实时监听滚动偏移量,驱动自定义吸顶逻辑 ScrollListWaterFlow
nestedScroll 协调父子滚动容器的响应优先级 Scroll 嵌套 Tabs/WaterFlow
Scroller 控制器 程序化控制滚动位置,实现精准跳转 所有滚动容器

从 API 演进来看,HarmonyOS 在 API 20 之前仅支持 StickyStyle.HeaderStickyStyle.Footer 的或运算组合;从 API 20 起新增 StickyStyle.BOTH,可一键同时开启吸顶与吸底,进一步简化了分组列表的配置成本。


二、方案一:ListItemGroup 原生吸顶(极简方案)

对于标准的分组列表场景(如通讯录、课程表、订单分类),HarmonyOS 提供了零成本的原生吸顶能力。开发者只需三步即可完成:定义分组数据源 → 配置 ListItemGroupheader/footer → 为 List 设置 sticky 属性。

2.1 数据层:标准 IDataSource 实现

// 分组数据模型
export interface GroupModel {
  title: string;        // 分组头部标题
  items: string[];      // 分组子项数据
}

// 分组数据源(适配 LazyForEach 懒加载)
export class GroupDataSource implements IDataSource {
  private list: GroupModel[] = [];
  private listeners: DataChangeListener[] = [];

  constructor(list: GroupModel[]) {
    this.list = list;
  }

  totalCount(): number { return this.list.length; }
  getData(index: number): GroupModel { return this.list[index]; }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) this.listeners.splice(pos, 1);
  }
}

// 子项数据源
export class ItemDataSource implements IDataSource {
  private list: string[] = [];
  constructor(list: string[]) { this.list = list; }
  totalCount(): number { return this.list.length; }
  getData(index: number): string { return this.list[index]; }
  registerDataChangeListener() {}
  unregisterDataChangeListener() {}
}

2.2 UI 层:吸顶 + 吸底完整实现

@Entry
@Component
struct StickyListPage {
  @State groupData: GroupDataSource = new GroupDataSource([]);

  aboutToAppear(): void {
    const data: GroupModel[] = [
      { title: '今日课程', items: ['高等数学', '大学英语', '线性代数'] },
      { title: '明日课程', items: ['数据结构', '操作系统', '计算机网络'] },
      { title: '本周实验', items: ['算法实验', '网络实验'] },
      { title: '考试安排', items: ['期中考试', '期末考试'] }
    ];
    this.groupData = new GroupDataSource(data);
  }

  @Builder
  groupHeader(title: string) {
    Row() {
      Text(title)
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
      Blank()
      Text('展开')
        .fontSize(12)
        .fontColor('#FFFFFF')
        .opacity(0.8)
    }
    .width('100%')
    .height(48)
    .padding({ left: 16, right: 16 })
    .backgroundColor('#4A90D9')
  }

  @Builder
  groupFooter(count: number) {
    Row() {
      Text(`${count}`)
        .fontSize(12)
        .fontColor('#666666')
    }
    .width('100%')
    .height(36)
    .padding({ left: 16 })
    .backgroundColor('#F5F5F5')
  }

  build() {
    Column() {
      List({ space: 0 }) {
        LazyForEach(this.groupData, (group: GroupModel) => {
          ListItemGroup({
            header: this.groupHeader(group.title),
            footer: this.groupFooter(group.items.length)
          }) {
            LazyForEach(new ItemDataSource(group.items), (item: string) => {
              ListItem() {
                Row() {
                  Text(item)
                    .fontSize(16)
                    .fontColor('#333333')
                  Blank()
                  Image($r('app.media.ic_arrow_right'))
                    .width(16)
                    .height(16)
                    .fillColor('#CCCCCC')
                }
                .width('100%')
                .height(56)
                .padding({ left: 16, right: 16 })
                .backgroundColor('#FFFFFF')
              }
            }, item => item)
          }
          .divider({ strokeWidth: 0.5, color: '#E8E8E8' })
        })
      }
      .width('100%')
      .layoutWeight(1)
      .sticky(StickyStyle.Header | StickyStyle.Footer)  // 同时开启吸顶+吸底
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F0F0')
  }
}

2.3 方案一总结

特性 说明
代码量 极少(约 10 行核心配置)
性能 框架原生优化,无需手动计算偏移
适用场景 通讯录、课程表、商品分类等标准分组列表
局限性 仅适用于 List 容器,无法支持非列表组件的吸顶

三、方案二:Scroll + Stack 自定义吸顶(灵活方案)

当吸顶目标不是 ListItemGroup 的头部,而是页面中的任意组件(如搜索栏、筛选条件栏)时,需要借助 Scroll + Stack 的组合,通过手动监听滚动偏移量实现动态吸顶。

3.1 实现原理

采用 Stack 层叠布局:底层为 Scroll 滚动容器(包含占位组件 + 长内容),上层为吸顶目标组件。通过 onWillScroll 实时获取滚动偏移量 currYOffset,动态计算吸顶组件的 margin.top,使其在滚动超过阈值时「贴」到顶部。

@Entry
@Component
struct CustomStickyPage {
  // 布局参数
  private readonly bannerHeight: number = 180;   // Banner高度
  private readonly stickyHeight: number = 48;    // 吸顶组件高度
  private readonly contentHeight: number = 1200; // 模拟内容高度

  // 状态与控制器
  @State currYOffset: number = 0;
  private scroller: Scroller = new Scroller();

  aboutToAppear(): void {
    this.scroller.scrollTo({ xOffset: 0, yOffset: 0 });
  }

  build() {
    Column() {
      // 顶部固定标题栏(不参与滚动)
      Row() {
        Text('自定义吸顶示例')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .height(56)
      .padding({ left: 16 })
      .backgroundColor('#2C3E50')

      Stack({ alignContent: Alignment.Top }) {
        // 底层:Scroll 滚动容器
        Scroll(this.scroller) {
          Column() {
            // Banner 区域(滚动时会被推出视口)
            Column() {
              Text('Banner 广告位')
                .fontSize(20)
                .fontColor('#FFFFFF')
            }
            .width('100%')
            .height(this.bannerHeight)
            .backgroundColor('#3498DB')
            .justifyContent(FlexAlign.Center)

            // 吸顶组件占位区(关键!保持滚动高度一致)
            Column()
              .width('100%')
              .height(this.stickyHeight)

            // 长内容区域
            Column() {
              ForEach(Array.from({ length: 30 }, (_, i) => i), (index: number) => {
                Row() {
                  Text(`列表项 ${index + 1}`)
                    .fontSize(14)
                    .fontColor('#333333')
                  Blank()
                  Text('详情 >')
                    .fontSize(12)
                    .fontColor('#999999')
                }
                .width('100%')
                .height(50)
                .padding({ left: 16, right: 16 })
                .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#FAFAFA')
              })
            }
            .width('100%')
          }
          .width('100%')
        }
        .width('100%')
        .height('100%')
        .scrollable(ScrollDirection.Vertical)
        .edgeEffect(EdgeEffect.Spring)
        .onWillScroll((xOffset: number, yOffset: number) => {
          // 累加滚动偏移量(注意:yOffset 为单次增量)
          this.currYOffset += yOffset;
          // 边界保护:防止快速滑动导致偏移量越界
          this.currYOffset = Math.max(0, Math.min(this.currYOffset, 
            this.bannerHeight + this.contentHeight));
        })

        // 上层:吸顶筛选栏(动态 margin 实现吸顶)
        Column() {
          Row() {
            Text('综合排序')
              .fontSize(14)
              .fontColor('#333333')
            Image($r('app.media.ic_arrow_down'))
              .width(12)
              .height(12)
              .fillColor('#666666')
            Blank()
            Text('销量优先')
              .fontSize(14)
              .fontColor('#666666')
            Blank()
            Text('价格筛选')
              .fontSize(14)
              .fontColor('#666666')
          }
          .width('100%')
          .height(this.stickyHeight)
          .padding({ left: 16, right: 16 })
          .backgroundColor('#FFFFFF')
          .shadow({ radius: 2, color: '#000000', offsetY: 1, opacity: 0.1 })
        }
        .width('100%')
        .margin({
          top: this.bannerHeight - Math.min(this.currYOffset, this.bannerHeight)
        })
        // 吸顶时增加阴影,提升视觉层级
        .shadow(this.currYOffset >= this.bannerHeight ? 
          { radius: 4, color: '#000000', offsetY: 2, opacity: 0.15 } : 
          { radius: 0, color: '#000000', offsetY: 0, opacity: 0 })
      }
      .width('100%')
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F0F0F0')
  }
}

3.2 关键设计要点

  1. 占位组件不可省略Scroll 内部必须预留与吸顶组件等高的占位区域,否则滚动高度计算会出现偏差,导致内容跳动。
  2. 偏移量边界保护onWillScroll 返回的是单次滚动增量,需累加并限制在合理范围内,防止边界回弹时偏移量异常。
  3. 视觉反馈:吸顶瞬间增加阴影(shadow),可显著提升用户的感知度,这是许多开发者容易忽略的细节。

四、方案三:Tabs + nestedScroll 嵌套联动吸顶(电商标配)

在电商详情页、内容社区等复杂场景中,吸顶效果往往与 Tab 切换 深度绑定:页面顶部有 Banner 头图,下方是分类 Tab 栏,Tab 栏滚动到顶部时吸住,继续滚动则切换 Tab 内容。这种「外层 Scroll + 内层 Tabs + 子列表」的三层嵌套结构,是吸顶效果中最具工程挑战的方案。

在这里插入图片描述

4.1 四个关键属性(缺一不可)

关键属性 配置值 作用
① 外层 Scroll 高度 height('100%') 不截断子内容,由内容回推总高度,产生滚动空间
② Tabs 高度 calc(100% - avoidanceHeight) 精确填满剩余空间,避免内容溢出或留白
nestedScroll PARENT_FIRST + SELF_FIRST 协调父子滚动优先级,实现吸顶丝滑过渡
edgeEffect EdgeEffect.None, { alwaysEnabled: true } 禁用回弹 + 保持边缘感知,防止吸顶抖动

在这里插入图片描述

4.2 完整实现代码

import { window } from '@kit.ArkUI';

@Entry
@Component
struct TabsStickyPage {
  // 滚动控制器
  private outerScroller: Scroller = new Scroller();
  private waterFlowScroller: Scroller = new Scroller();

  // 状态
  @State selectedTabIndex: number = 0;
  @State statusBarHeight: number = 0;

  aboutToAppear(): void {
    // 获取状态栏高度(用于计算避让区域)
    window.getLastWindow(getContext()).then((win) => {
      this.statusBarHeight = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height;
    });
  }

  // 计算避让高度 = 状态栏 + 自定义标题栏
  private getAvoidanceHeight(): number {
    return this.statusBarHeight + 48; // 48vp 为标题栏高度
  }

  @Builder
  tabBarBuilder() {
    Column() {
      Row() {
        ForEach(['推荐', '热门', '最新', '关注'], (tab: string, index: number) => {
          Column() {
            Text(tab)
              .fontSize(15)
              .fontWeight(this.selectedTabIndex === index ? FontWeight.Bold : FontWeight.Normal)
              .fontColor(this.selectedTabIndex === index ? '#00CC66' : '#999999')
            Divider()
              .width(this.selectedTabIndex === index ? 20 : 0)
              .height(3)
              .backgroundColor('#00CC66')
              .margin({ top: 4 })
              .animation({ duration: 200, curve: Curve.EaseInOut })
          }
          .width(60)
          .onClick(() => {
            this.selectedTabIndex = index;
          })
        })
      }
      .width('100%')
      .height(44)
      .justifyContent(FlexAlign.SpaceEvenly)

      Divider()
        .width('100%')
        .height(1)
        .color('#E8E8E8')
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
  }

  @Builder
  waterFlowContent() {
    WaterFlow({ scroller: this.waterFlowScroller }) {
      LazyForEach(this.mockDataSource(), (item: number) => {
        FlowItem() {
          Column() {
            Text(`${item}`)
              .fontSize(16)
              .fontColor('#FFFFFF')
          }
          .width('100%')
          .height(80 + (item % 5) * 30)
          .backgroundColor(this.getItemColor(item))
          .borderRadius(8)
          .justifyContent(FlexAlign.Center)
        }
      }, (item: number) => item.toString())
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(12)
    .rowsGap(12)
    .padding(16)
    .scrollBar(BarState.Off)
    // 【关键③】nestedScroll:父子滚动联动
    .nestedScroll({
      scrollForward: NestedScrollMode.PARENT_FIRST,  // 上滑:列表先滚,到顶后外层接管
      scrollBackward: NestedScrollMode.SELF_FIRST     // 下滑:外层先滚回,TabBar 落位后列表再滚
    })
    // 【关键④】edgeEffect:禁用回弹 + 保持边缘感知
    .edgeEffect(EdgeEffect.None, { alwaysEnabled: true })
  }

  build() {
    NavDestination() {
      Stack({ alignContent: Alignment.Top }) {
        // 顶部标题栏(固定层级)
        Row() {
          Text('发现')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFFFFF')
        }
        .width('100%')
        .height(this.getAvoidanceHeight())
        .padding({ left: 16 })
        .backgroundColor('#2C3E50')
        .zIndex(2)

        // 【关键①】外层 Scroll:height='100%',由内容回推高度
        Scroll(this.outerScroller) {
          Column() {
            // Banner 头图区(上滑时滚出视口)
            Column() {
              Text('Banner 轮播图')
                .fontSize(20)
                .fontColor('#FFFFFF')
            }
            .width('100%')
            .height(200)
            .backgroundColor('#3498DB')
            .justifyContent(FlexAlign.Center)

            // Tabs 吸顶区域
            Tabs({ index: $$this.selectedTabIndex }) {
              TabContent() { this.waterFlowContent() }
                .tabBar(this.tabBarBuilder())
            }
            // 【关键②-a】Tabs 高度 = 剩余空间
            .height(`calc(100% - ${this.getAvoidanceHeight()}vp)`)
            // 【关键②-b】tabBar 高度由 Builder 内容撑开
            .barHeight('auto')
            .scrollable(false)              // 禁用 Tabs 内置滑动,由子列表承载
            .barPosition(BarPosition.Start) // TabBar 在内容上方
            .clip(true)
          }
          .width('100%')
        }
        .width('100%')
        .height('100%')   // ← 关键①
        .scrollBar(BarState.Off)
      }
      .width('100%')
      .height('100%')
    }
    .hideTitleBar(true)
  }

  // 模拟数据源
  private mockDataSource(): IDataSource {
    class MockData implements IDataSource {
      private data: number[] = Array.from({ length: 50 }, (_, i) => i + 1);
      totalCount(): number { return this.data.length; }
      getData(index: number): number { return this.data[index]; }
      registerDataChangeListener() {}
      unregisterDataChangeListener() {}
    }
    return new MockData();
  }

  private getItemColor(index: number): string {
    const colors = ['#E74C3C', '#3498DB', '#2ECC71', '#F39C12', '#9B59B6'];
    return colors[index % colors.length];
  }
}

4.3 常见踩坑指南

问题现象 根因 解决方案
TabBar 吸顶后抖动 edgeEffect 未设置 alwaysEnabled: true 必须显式配置 { alwaysEnabled: true }
列表到顶后外层不响应 nestedScroll 模式配置错误 scrollForward 应为 PARENT_FIRST
TabBar 被内容遮挡 zIndex 未设置或层级错误 标题栏 zIndex(2),确保在最上层
内容溢出屏幕底部 Tabs 高度未固定 使用 calc(100% - avoidanceHeight)

五、进阶:自定义手势识别器与吸顶效果联动

在前序文章中,我们深入探讨了 HarmonyOS 自定义手势识别器的实现(包括长按、滑动、捏合等手势的识别与分发)。将手势能力与吸顶效果结合,可以创造出更具沉浸感的交互体验。

在这里插入图片描述

5.1 场景设计:长按吸顶头部展开快捷菜单

// 手势识别器与吸顶控制器联动
@Entry
@Component
struct GestureStickyPage {
  @State isSticky: boolean = false;
  @State showQuickMenu: boolean = false;
  @State menuOffsetY: number = 0;

  private scroller: Scroller = new Scroller();
  private readonly stickyThreshold: number = 200;

  // 自定义手势识别器:长按检测
  private longPressGesture = GestureGroup(GestureMode.Sequence,
    LongPressGesture({ duration: 500 })
      .onAction(() => {
        // 长按吸顶头部时展开快捷菜单
        if (this.isSticky) {
          animateTo({ duration: 200, curve: Curve.EaseOut }, () => {
            this.showQuickMenu = true;
            this.menuOffsetY = 48; // 吸顶头部高度
          });
        }
      }),
    PanGesture({ direction: PanDirection.Down })
      .onActionUpdate((event: GestureEvent) => {
        // 下拉手势:收起菜单并触发刷新
        if (this.showQuickMenu && event.offsetY > 50) {
          this.showQuickMenu = false;
          this.triggerRefresh();
        }
      })
  );

  build() {
    Column() {
      // 吸顶头部(支持手势交互)
      Row() {
        Text('吸顶导航栏')
          .fontSize(16)
          .fontColor('#FFFFFF')
        Blank()
        Text('长按展开菜单')
          .fontSize(12)
          .fontColor('#FFFFFF')
          .opacity(0.7)
      }
      .width('100%')
      .height(48)
      .padding({ left: 16, right: 16 })
      .backgroundColor('#E74C3C')
      .gesture(this.longPressGesture)  // 绑定自定义手势识别器
      .shadow({ radius: this.isSticky ? 4 : 0, color: '#000000', offsetY: 2, opacity: 0.15 })

      // 快捷菜单(手势触发展开)
      if (this.showQuickMenu) {
        Column() {
          Row() {
            ForEach(['刷新', '分享', '设置'], (action: string) => {
              Button(action)
                .fontSize(12)
                .height(32)
                .backgroundColor('#FFFFFF')
                .fontColor('#333333')
                .margin({ right: 8 })
            })
          }
          .width('100%')
          .height(48)
          .padding({ left: 16 })
          .backgroundColor('#F8F9F9')
        }
        .width('100%')
        .transition(TransitionEffect.asymmetric(
          TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -20 })),
          TransitionEffect.OPACITY.combine(TransitionEffect.translate({ y: -20 }))
        ))
      }

      // 滚动内容
      Scroll(this.scroller) {
        Column() {
          ForEach(Array.from({ length: 50 }, (_, i) => i), (index: number) => {
            Row() {
              Text(`内容项 ${index + 1}`)
                .fontSize(14)
              Blank()
              Text('>')
                .fontColor('#CCCCCC')
            }
            .width('100%')
            .height(50)
            .padding({ left: 16, right: 16 })
            .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#FAFAFA')
          })
        }
        .width('100%')
      }
      .width('100%')
      .layoutWeight(1)
      .onWillScroll((_, yOffset) => {
        // 更新吸顶状态
        const newOffset = this.scroller.currentOffset().yOffset + yOffset;
        this.isSticky = newOffset >= this.stickyThreshold;
      })
    }
    .width('100%')
    .height('100%')
  }

  private triggerRefresh(): void {
    // 触发下拉刷新逻辑
    console.info('触发下拉刷新...');
  }
}

5.2 手势与吸顶联动的设计原则

  1. 状态同步:手势识别器通过 onAction 回调修改状态变量,吸顶组件通过 @State 响应式绑定自动刷新,避免手动操作 DOM。
  2. 动画衔接:使用 animateTo 包裹状态变更,确保手势触发后的 UI 反馈(如菜单展开、吸顶阴影变化)具有流畅的过渡效果。
  3. 冲突消解:当吸顶头部同时绑定点击(Tab 切换)和长按(快捷菜单)时,需通过 GestureMode.SequenceGesturePriority 明确手势优先级,防止事件竞争。

六、性能优化与最佳实践

6.1 滚动性能优化

优化手段 实现方式 收益
懒加载 LazyForEach + IDataSource 仅渲染可视区域,降低内存占用
组件复用 ListItem 设置 reuseId 减少组件创建销毁开销
离屏渲染 clip(true) 限制绘制区域 避免不可见区域的无效渲染
阴影优化 吸顶时动态开启阴影 非吸顶状态移除阴影计算

6.2 多设备适配

HarmonyOS 应用需同时适配手机、平板、折叠屏等多种形态。吸顶效果在不同设备上的行为应保持一致:

// 响应式吸顶高度计算
import { display } from '@kit.ArkUI';

private getAdaptiveStickyHeight(): number {
  const screenWidth = display.getDefaultDisplaySync().width;
  // 平板/折叠屏展开态:增加吸顶头部高度
  if (screenWidth > 840) {
    return 64; // vp
  }
  return 48; // vp
}

6.3 调试技巧

// 在 DevEco Studio 中开启布局边界调试
.onWillScroll((xOffset, yOffset) => {
  // 打印滚动偏移量,辅助调试吸顶阈值
  console.debug(`[StickyDebug] offset: ${this.currYOffset}, yDelta: ${yOffset}`);
})

七、总结

本文围绕 HarmonyOS 6(API 23)的吸顶效果,从原生声明式自定义布局再到手势联动,构建了完整的知识体系:

  • 方案一(ListItemGroup):适用于标准分组列表,代码极简,性能最优,是首选方案。
  • 方案二(Scroll + Stack):适用于任意组件吸顶,灵活可控,但需手动管理偏移量。
  • 方案三(Tabs + nestedScroll):适用于复杂电商/内容场景,四个关键属性缺一不可,是实现专业级吸顶的必备技能。
  • 手势联动:将自定义手势识别器与吸顶状态绑定,可拓展出长按菜单、下拉刷新、捏合缩放等高级交互。

吸顶效果看似简单,实则是滚动容器嵌套、偏移量计算、动画过渡、手势分发等多维度能力的综合考验。掌握本文所述的三种方案及其适用边界,开发者即可在 HarmonyOS 应用中游刃有余地实现各类吸顶需求。


转载自:https://blog.csdn.net/u014727709/article/details/163418865
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐