熟悉我们购物比价应用的老朋友一定还记得,之前首页做得中规中矩:顶部搜索栏、轮播Banner、分类入口、限时抢购、推荐瀑布流……该有的都有了,但总觉得少了点“彩蛋”。用户每天打开应用,看到的都是一样的内容,久而久之审美疲劳。直到有一天产品经理提出一个想法:“能不能在首页加一个隐藏空间,用户下拉首页就能进入,里面放一些高佣商品、限时秒杀或者趣味互动?”

这个需求听起来很酷,但实现起来有几个难点:

  1. 首页本身就是一个可滚动的长列表,怎么区分“正常下拉刷新”和“下拉进入二楼”?

  2. 下拉过程中要给用户明确的视觉反馈,不能突然跳转。

  3. 二楼页面要有一整套独立的布局,不能和首页内容混在一起。

我们调研了华为官方文档的“首页下拉进入二楼”示例,结合自己的业务场景,最终实现了一套流畅的下拉二楼交互。这篇文章完整记录一下实现过程和踩坑经验。

功能设计

先说说预期效果。

用户在首页任意位置(不限于顶部)向下滑动,当滑动距离超过一定阈值时,页面顶部会缓缓展开一个“二楼”入口提示,继续下拉,二楼页面逐渐显露出来。松手后,如果下拉距离达到阈值,自动展开二楼;如果没达到,回弹到首页。整个过程伴随平滑的位移和透明度动画,就像在翻一本杂志的折页。

核心目标:

  1. 手势识别:区分正常滚动和下拉进入二楼的手势,互不干扰。

  2. 视觉反馈:下拉过程中,首页内容整体下移,露出二楼顶部,并显示“继续下拉进入二楼”的文字提示。

  3. 动画流畅:展开和回弹都有弹性动画,不能僵硬。

  4. 二楼独立:二楼是一个独立的页面容器,可以承载商品列表、活动页等。

核心API

API/组件

说明

.onTouch()

监听触摸事件,捕获Down、Move、Up

animateTo

显式动画,控制二楼展开和回弹

TouchType

触摸事件类型枚举

Scroller

滚动控制器,用于获取当前滚动位置

Stack

层叠布局,用于将二楼放在首页下方

实现过程

手势拦截与距离计算

第一步,我们需要在首页最外层容器上监听触摸事件,但不能影响内部列表的正常滚动。关键在于:只有当我们检测到用户在当前滚动位置为0(即列表已经处于最顶部)且手指向下滑动时,才进入二楼模式。

// pages/MainPage.ets
@State isPulling: boolean = false;
@State pullDistance: number = 0;
private startY: number = 0;
private scroller: Scroller = new Scroller();

handleTouchDown(event: TouchEvent) {
  this.startY = event.touches[0].y;
  this.isPulling = true;
}

handleTouchMove(event: TouchEvent) {
  if (!this.isPulling) return;
  
  const currentY = event.touches[0].y;
  const deltaY = currentY - this.startY;
  
  // 只有向下滑动且列表在顶部时才处理
  if (deltaY > 0 && this.scroller.currentOffset().yOffset <= 0) {
    // 应用阻尼,让下拉手感更柔和
    this.pullDistance = deltaY * 0.5;
    
    // 根据下拉距离更新二楼露出的高度
    this.updateSecondFloorPosition(this.pullDistance);
  }
}

handleTouchUp() {
  if (this.pullDistance > EXPAND_THRESHOLD) {
    // 展开二楼
    this.expandSecondFloor();
  } else {
    // 回弹
    this.resetSecondFloor();
  }
  this.isPulling = false;
}

这里有一个关键点:deltaY * 0.5是阻尼系数。如果不加阻尼,下拉一点点二楼就露出来了,体验很生硬。加了0.5的系数后,用户需要下拉更多的距离才能看到二楼,手感更像“拉开抽屉”。

二楼容器与动画

二楼页面我们放在首页内容的下面,通过translate偏移来实现下拉效果。首页内容整体向下平移,二楼从上方滑入。

// 布局结构
Stack() {
  // 二楼页面(位于底层)
  SecondFloorView()
    .translate({ y: this.secondFloorOffset - SCREEN_HEIGHT })
  
  // 首页内容(位于上层)
  Column() {
    // ... 首页的各种组件
  }
  .translate({ y: Math.max(0, this.pullDistance) })
}

当用户下拉时,pullDistance增大,首页内容下移,二楼页面也随之向下移动(从负值向0靠近)。当pullDistance超过阈值时,调用animateTosecondFloorOffset设置为SCREEN_HEIGHT,二楼完全占据屏幕。

expandSecondFloor() {
  animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
    this.secondFloorOffset = SCREEN_HEIGHT;
    this.pullDistance = SCREEN_HEIGHT;
  });
}

resetSecondFloor() {
  animateTo({ duration: 200, curve: Curve.EaseOut }, () => {
    this.secondFloorOffset = 0;
    this.pullDistance = 0;
  });
}

二楼顶部的“拉手”提示

为了让用户知道下拉可以进入二楼,我们在二楼顶部设计了一个“拉手”区域,包含箭头图标和文字提示。当下拉距离较小时,只显示“继续下拉”;接近阈值时,提示变为“松手进入二楼”。

@Builder
buildPullHint() {
  Column() {
    Image($r('app.media.ic_arrow_up'))
      .rotate({ angle: this.pullDistance > 30 ? 180 : 0 }) // 箭头翻转
      .animation({ duration: 150 })
    
    Text(this.pullDistance > EXPAND_THRESHOLD ? '松手进入二楼' : '继续下拉')
      .fontSize(14)
      .fontColor(Color.Gray)
  }
  .opacity(Math.min(1, this.pullDistance / 50))
  .position({ y: -60 }) // 固定在二楼顶部
}

遇到的问题与解决方案

问题1:首页列表滚动和下拉手势冲突

一开始我们在首页的Scroll上直接监听onTouch,但发现当列表内部有可滚动区域(比如商品分类的横向滑动)时,手势会被吞掉。解决方案:将触摸监听放到最外层的Stack容器上,并通过hitTestBehavior设置透传,让内部组件仍然可以正常接收事件。

Stack()
  .hitTestBehavior(HitTestMode.Transparent) // 透传触摸事件
  .onTouch((event) => {
    // 处理下拉手势
  })

问题2:下拉回弹时首页内容抖动

当用户下拉后松手但未达到阈值,首页内容回弹时偶尔会出现抖动。原因是animateToScroller的滚动动画产生了冲突。解决方案:在回弹动画开始时,先将Scroller锁定,禁止其滚动,动画结束后再解锁。

resetSecondFloor() {
  this.scroller.disableScroll(); // 锁定滚动
  animateTo({ duration: 200 }, () => {
    this.secondFloorOffset = 0;
    this.pullDistance = 0;
  });
  setTimeout(() => {
    this.scroller.enableScroll(); // 解锁
  }, 250);
}

问题3:二楼页面加载延迟

如果二楼页面内容较多(比如几十个商品),首次展开时会有明显的卡顿。解决方案:在首页加载时预先创建二楼页面实例,但设置visibility: Hidden,展开时改为Visible,这样内容已经提前渲染好了。

@State secondFloorVisible: boolean = false;

expandSecondFloor() {
  this.secondFloorVisible = true; // 提前渲染
  animateTo({ duration: 300 }, () => {
    this.secondFloorOffset = SCREEN_HEIGHT;
  });
}

总结

下拉进入二楼是一个小而美的交互创新,给用户带来“寻宝”般的惊喜感。核心实现要点总结如下:

要点

实现方式

手势识别

外层Stack监听onTouch,结合Scroller位置判断

阻尼效果

下拉距离乘以0.5系数,手感柔和

动画控制

animateTo控制二楼展开和回弹,曲线用EaseOut

预渲染

二楼页面提前创建,展开时只做位移动画

冲突处理

hitTestMode透传 + 滚动锁定

改完之后,我们把这个功能上线做了A/B测试,结果令人惊喜:进入二楼页面的用户次日留存比首页直接跳转的活动页高出15%。用户喜欢这种“不经意间发现宝藏”的感觉。如果你也在做购物比价类应用,不妨试试给首页加一个“隐藏的二楼”。

Logo

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

更多推荐