之前学了安卓版的吸顶嵌套可左右翻页上下滑动的rv:

安卓demo-CeilingNestedScrollView-CSDN博客之后,下一个需求就是做鸿蒙版的这个需求,然后鸿蒙也没有这种组件,凭借俺对安卓的理解,也很快promot出了鸿蒙版,哈哈哈哈,真是机智如俺,不过这个没有tabbar吸顶效果哈。之前很少用mvvm,结果现在项目全是mvvm。嗯,用mvvm重构一下吧,再封装成单个组件(人模狗样版)

组件封装

1.背景

NestedScrollTabContainer 是一个可复用的"嵌套滑动 Tab 容器",解决 Scroll + Swiper + List 三层嵌套下的滑动分发问题。 典型场景:顶部 Header 区域 + 多 Tab 内列表区,手指上滑先滚走 Header 再滚列表;手指下滑先把内层列表滚到顶,再让外层 Scroll 把 Header 显示出来。

2.划分原则

共性、稳定的布局进容器;业务差异大的留外面。

封装时问自己三句话:

  1. 不同页面这段代码会一样吗?一样 → 进容器;不一样 → 留外面。

  2. 这段代码跟业务数据有关吗?无关 → 进容器;有关系 → 留外面。

  3. 这段代码要不要被外部主动触发?要(如 scrollTo / changeIndex)→ 不放在容器方法里,改用控制器由外部持有。

3.进容器(共性)

以下内容由容器内部 build() 承担,调用方无需关心:

维度 内容
外层滚动 Scroll(this.parentScroller) { Column() { ... } }
Swiper 通用配置 indicator(false) / loop(false) / width('100%') / height('100%')
内层 Swiper 与控制器绑定 Swiper(this.swiperController)
滑动分发方向 nestedScroll({ scrollForward: SELF_ONLY, scrollBackward: SELF_ONLY })
多层嵌套手势分发 ScrollForward 先消费外层 → 再内层;ScrollBackward 先内层 → 再外层
第一Tab右滑退出手势监听 onGestureSwipe:第一个Tab且向右滑动时触发回调
防重复退出标记 private isPopping,节流 onFirstTabSwipeOut
背景样式 支持 bgColor 纯色 或 useLinearGradient 渐变(二选一)
4.滑动冲突解决方式

容器内部存在两层嵌套滚动:外层 Scroll(包裹 Header + Swiper)与内层 List(每个 Tab 的列表)。 关键手法是用 ArkUI 的 nestedScroll 配置 + 外层 Scroll 采用 SELF_ONLY,把滑动事件链路拆解成可预期的优先级:

手指上滑(scrollForward,内容向上滚): 外层 Scroll 先消费 → Header 被滚走 → 然后内层 List 接管继续滚 内层 List 配置:nestedScroll({ scrollForward: PARENT_FIRST, scrollBackward: SELF_FIRST })

手指下滑(scrollBackward,内容向下滚): 内层 List 先消费 → 列表滚到顶 → 然后外层 Scroll 接管 → Header 显示出来

外层 Scroll 用 SELF_ONLY 的用意: 确保外层与内层的 nestedScroll 链路独立可控,避免外层"无脑"拦截事件导致内层 List 在 Header 还没滚走时就已经开始滚动。

Tab 间横向切换: 由 Swiper 自身处理,不参与纵向滑动分发;onGestureSwipe 监听第一个 Tab 的右滑手势,触发 onFirstTabSwipeOut 回调(通常调 viewModel.popPage)。

5.留外面(业务差异大)

以下内容由调用方自行实现,通过 builder / callback 注入:

维度 注入方式 说明
Header 布局 @BuilderParam headerBuilder 每个页面 Header 结构差异大,不强行参数化,调用方写 @Builder 直接嵌入
Tab 内容(每个Tab的List结构) @BuilderParam contentBuilder 每页 Item 渲染逻辑完全不同,调用方根据 currentTabIndex 渲染
Tab 切换的业务回调 onTabChange?: (index) => void 例如切换Tab后刷新对应列表、切换tabType 维度等
当前Tab索引来源 @Prop currentTabIndex 由调用方 @State 维护,并通过 onTabChange 同步
第一Tab右滑退出动作 onFirstTabSwipeOut?: () => void 通常调用 viewModel.popPage()
内层 List 的 Scroller 数组 Page 自己持有 Scroller[] Page 在 tabContentBuilder 里按 tab.index 选用对应 Scroller 绑定到 List
6.控制器传递约定

ArkTS 中 @Component 不能被外部 new 实例化,外部无法直接持有容器引用来调方法。 因此改为 "外部持有控制器,传给容器使用" 的反向依赖:

控制器 声明方 用途
SwiperController 外部 Page 外部点击 Tab 时调 changeIndex(index) 切换;容器内部用同一实例绑定 Swiper
Scroller(外层) 外部 Page 外部 scrollToTop() 等场景调用 scrollTo(...);容器内部用同一实例绑定 Scroll
Scroller(每个Tab) 外部 Page 外部维护一个 Scroller[],按当前 tabIndex 选对应实例调用

俺写大需求终于是先做架构设计再写代码了,而不是直接一上来就代码的暴发户土狗了。

架构简述

1.分层结构
  nestedscroll/
  ├── pages/
  │   └── NestedScrollDemoPage.ets     ← View 层(ArkTS struct @Component)
  ├── viewmodel/
  │   ├── INestedScrollViewModel.ets   ← ViewModel 接口 + Callback 接口
  │   └── NestedScrollViewModelImpl.ets ← ViewModel 实现(@ObservedV2 + @Trace)
  ├── model/
  │   ├── INestedScrollModel.ets       ← Model 接口
  │   ├── NestedScrollModelImpl.ets    ← Model 实现(mock Tab 数据)
  │   └── NestedScrollTabConfig.ets    ← DTO
  └── components/
      └── NestedScrollTabContainer.ets ← 业务专用 UI 组件
2.通信方式

单向数据流 + 双向控制流


  ┌────────────────┐       setCallback(callback)        ┌──────────────────┐
  │                │ ─────────────────────────────────► │                  │
  │   View (Page)  │                                     │   ViewModel      │
  │                │ ◄───────────────────────────────── │  (@ObservedV2)   │
  └────────────────┘    @Trace 字段变化触发 UI 重渲染      └──────────────────┘
          │                       ▲                              │
          │ 用户事件               │ Promise resolve/reject       │ 调用
          │ onTabClick/onSwiper    │                              ▼
          │   TabChange            │                       ┌──────────────────┐
          └────────────────────────┼──────────────────────►│   Model          │
                                   │                       │  (mock 数据)     │
                                   │                       └──────────────────┘
                                   │
                      callback.popPage / showLoading / ...
3.三个方向的通信

1. View → ViewModel(控制流,下行)

Page 通过持有 viewModel 实例直接调接口方法:

  this.viewModel.pageOnReady();           // 页面就绪时拉数据
  this.viewModel.onTabClick(tabIndex);    // 点击 Tab
  this.viewModel.onSwiperTabChange(idx);  // Swiper 滑动

2. ViewModel → View(状态流,上行)

ViewModel 用 @ObservedV2 + @Trace 暴露状态,Page 在 build() 里直接读这些字段。字段变化时 ArkUI 自动触发重渲染:

  // ViewModelImpl
  @Trace tabs: NestedScrollTabConfig[] = [];
  @Trace currentTabIndex: number = 0;
  @Trace pageLoading: boolean = false;

  // Page 的 build()
  Tabs({ index: this.viewModel.currentTabIndex }) { ... }

3. ViewModel → View(事件流,异步回调)

Page 在 aboutToAppear 注入 callback、aboutToDisappear 反注册。ViewModel 通过 callback 通知 Page 执行"非状态型"操作(弹 Toast、跳转页面等): 

// Page 注入
  private nestedScrollViewModelCallback: NestedScrollViewModelCallback = {
    popPage: (): void => { this.pathStack.pop(); }
  };
  aboutToAppear() {
    this.viewModel.setCallback(this.nestedScrollViewModelCallback);
  }
  aboutToDisappear() {
    this.viewModel.setCallback(undefined);  // 反注册,防内存泄漏
  }

  // ViewModel 触发
  private callback?.popPage?.();

4. ViewModel → Model(数据流)

ViewModel 内部 new ModelImpl,调 Model 的方法拿 Promise:

  this.nestedScrollModel.queryTabs().then(tabs => {
    this.tabs = tabs;  // @Trace 字段赋值,自动触发 UI 刷新
  }).catch(...);
4.关键约束
约束 做法
ViewModel 不持有 View 通过 callback 接口反向调用,不 import Page
Model 不持有 ViewModel 只返回 Promise,由 ViewModel .then 处理
状态可观察 @ObservedV2 + @Trace,UI 自动响应
生命周期对称 aboutToAppear 注入 / aboutToDisappear 反注册
闭包不泄漏 callback 是 Page 成员,反注册后 ViewModel 断引用

整体是标准的单向数据流 MVVM,《数据下行、事件上行、状态自动同步》。

安卓vs鸿蒙差异

安卓端结构(NestedScrollLayout.java)

NestedScrollLayout (NestedScrollingParent)
└── 外层 RecyclerView
├── HeaderVH (header)
└── FooterVH
└── ViewPager (左右切换Tab)
└── Fragment → 内层 RecyclerView (列表)
安卓冲突解决方式(onNestedPreScroll):
* isPinnedMode():当 Footer 完全可见(Header 滚走了),进入 pinned 模式
* pinned 模式:滚动优先给内层 child RV
* 非 pinned 模式:滚动给外层 parent RV(滚走 Header)

鸿蒙对应结构:

外层 Scroll (parentScroller)          ← 对应安卓外层 RecyclerView
├── Header (headerBuilder)           ← 对应安卓 HeaderVH
├── Tab (tabBarBuilder)              ← 对应安卓 HeaderVH 中的 Tab
└── Swiper (swiperController)        ← 对应安卓 FooterVH 中的 ViewPager
└── List (nestedScroll)         ← 对应安卓内层 RecyclerView

鸿蒙冲突解决方式(nestedScroll):
* 内层 List:
    * scrollForward: PARENT_FIRST — 手指上滑时,外层 Scroll 先消费(滚走 Header),然后内层 List 再滚
    * scrollBackward: SELF_FIRST — 手指下滑时,内层 List 先消费(滚到顶),然后外层 Scroll 再滚(显示 Header)
* 外层 Scroll:SELF_ONLY — 自己处理自己的滚动

效果视频

无,参考安卓的效果吧。效果图可以来一张

源码纯享版

核心组件

负责当组件核心容器,响应滑动事件,tab改变

/**
 * 嵌套滑动Tab容器 (Demo)
 *
 * 结构:外层 Scroll 包裹 Header + Swiper,Swiper 内每个 Tab 是 List。
 *
 * 滑动分发:
 *   - 手指上滑(scrollForward):外层 Scroll 先消费(滚走 Header),然后内层 List 再滚 => PARENT_FIRST
 *   - 手指下滑(scrollBackward):内层 List 先消费(滚到顶),然后外层 Scroll 再滚(显示 Header) => SELF_FIRST
 *
 * 注意:外层 Scroll 使用 SELF_ONLY,确保外层与内层 nestedScroll 链路独立可控。
 */

@Component
export struct NestedScrollTabContainer {
  /** 当前 Tab 索引 */
  @Prop currentTabIndex: number = 0;
  @Prop bgColor: string = '#FFFFFF';
  @Prop useLinearGradient: boolean = false;
  @Prop gradientAngle: number = 180;
  @Prop gradientColors: Array<[string, number]> = [];
  /** 内层 Swiper 控制器(由外部传入,便于外部主动 changeIndex) */
  swiperController: SwiperController = new SwiperController();
  /** 外层 Scroll 控制器(由外部传入,便于外部 scrollTo) */
  parentScroller: Scroller = new Scroller();
  /** 防止重复退出 */
  private isPopping: boolean = false;
  /** Tab 切换回调 */
  onTabChange?: (index: number) => void;
  /** 第一个 Tab 右滑退出页面回调 */
  onFirstTabSwipeOut?: () => void;
  /** Header 区域内容 */
  @BuilderParam headerBuilder: () => void = this.defaultHeader;
  /** Swiper 中的每个 Tab 的内容(由外部按 currentTabIndex 渲染) */
  @BuilderParam contentBuilder: () => void = this.defaultContent;

  @Builder
  private defaultHeader() {
  }

  @Builder
  private defaultContent() {
  }

  build() {
    Stack() {
      Scroll(this.parentScroller) {
        Column() {
          this.headerBuilder()
          Swiper(this.swiperController) {
            this.contentBuilder()
          }
          .index(this.currentTabIndex)
          .indicator(false)
          .loop(false)
          .width('100%')
          .height('100%')
          .onChange((index: number) => {
            if (this.currentTabIndex !== index) {
              this.onTabChange?.(index);
            }
          })
          .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
            // 第一个 Tab 且向右滑出时,触发外部退出逻辑
            if (index === 0 && extraInfo.currentOffset > 0 && !this.isPopping) {
              this.isPopping = true;
              this.onFirstTabSwipeOut?.();
            }
          })
        }
        .width('100%')
        .alignItems(HorizontalAlign.Center)
      }
      .width('100%')
      .height('100%')
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.None)
      .linearGradient(this.useLinearGradient ? {
        angle: this.gradientAngle,
        colors: this.gradientColors
      } : {
        angle: 0,
        colors: []
      })
      .nestedScroll({
        scrollForward: NestedScrollMode.SELF_ONLY,
        scrollBackward: NestedScrollMode.SELF_ONLY
      })
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.useLinearGradient ? Color.Transparent : this.bgColor)
  }
}

model抽象层,代码少不用这一个抽象层,只要model就好了,代码太少model可能都不用,这里只是意思一下,因为俺在学mvvm。标准开发一般没这么少接口

/**
 * 嵌套滑动 Demo Model 接口
 */
import { NestedScrollTabConfig } from "./NestedScrollTabConfig";

export interface INestedScrollModel {
  /**
   * 查询 Tab 列表数据
   */
  queryTabs(): Promise<NestedScrollTabConfig[]>;
}

model数据源

/**
 * 嵌套滑动 Demo Model 实现
 */
import { INestedScrollModel } from './INestedScrollModel';
import { NestedScrollTabConfig, NestedScrollListItem } from './NestedScrollTabConfig';

const TAB_COUNT = 4;
const ITEM_COUNT_PER_TAB = 30;

const TAB_NAMES: string[] = ['推荐', '关注', '热门', '最新'];
const TAB_THEME_COLORS: string[] = ['#33B62E', '#1E88E5', '#E53935', '#8E24AA'];
const ITEM_COLORS: string[] = ['#FFFFFF', '#F5F5F5', '#FFF8E1', '#E8F5E9'];

export class NestedScrollModelImpl implements INestedScrollModel {
  queryTabs(): Promise<NestedScrollTabConfig[]> {
    return new Promise<NestedScrollTabConfig[]>((resolve, reject) => {
      try {
        const tabs = this.buildMockTabs();
        resolve(tabs);
      } catch (e) {
        reject(e);
      }
    });
  }

  private buildMockTabs(): NestedScrollTabConfig[] {
    const tabs: NestedScrollTabConfig[] = [];
    for (let i = 0; i < TAB_COUNT; i++) {
      tabs.push(new NestedScrollTabConfig(i, TAB_NAMES[i], TAB_THEME_COLORS[i], this.buildMockItems(i)));
    }
    return tabs;
  }

  private buildMockItems(tabIndex: number): NestedScrollListItem[] {
    const items: NestedScrollListItem[] = [];
    for (let i = 0; i < ITEM_COUNT_PER_TAB; i++) {
      const id = tabIndex * ITEM_COUNT_PER_TAB + i + 1;
      items.push(new NestedScrollListItem(
        id,
        `${TAB_NAMES[tabIndex]} - 第${i + 1}条`,
        `这是 ${TAB_NAMES[tabIndex]} Tab 下的第 ${i + 1} 条 mock 数据`,
        ITEM_COLORS[i % ITEM_COLORS.length]
      ));
    }
    return items;
  }
}

model实体类

/**
 * 嵌套滑动 Demo 的 Tab 配置数据模型
 */

/** 列表项数据 */
export class NestedScrollListItem {
  id: number = 0;
  title: string = '';
  subtitle: string = '';
  color: string = '#FFFFFF';

  constructor(id: number, title: string, subtitle: string, color: string) {
    this.id = id;
    this.title = title;
    this.subtitle = subtitle;
    this.color = color;
  }
}

/** Tab 配置 */
export class NestedScrollTabConfig {
  index: number = 0;
  name: string = '';
  themeColor: string = '';
  listData: NestedScrollListItem[] = [];

  constructor(index: number, name: string, themeColor: string, listData: NestedScrollListItem[]) {
    this.index = index;
    this.name = name;
    this.themeColor = themeColor;
    this.listData = listData;
  }
}

viewmodel 交互抽象层,事件响应

/**
 * 嵌套滑动 Demo ViewModel 接口
 */
export interface INestedScrollViewModel {
  /** 页面就绪:拉取 Tab 数据 */
  pageOnReady(): void;

  /** 点击 Tab;返回是否真的发生了切换 */
  onTabClick(tabIndex: number): boolean;

  /** Swiper 滑动切换 Tab */
  onSwiperTabChange(index: number): void;

  /** 第一个 Tab 继续向右滑(退出页面) */
  onFirstTabSwipeOut(): void;
}

/**
 * View 回调能力(由 Page 在 aboutToAppear 时注入)
 */
export interface NestedScrollViewModelCallback {
  popPage?: () => void;
}

viewmodel 交互实现层。处理具体事件,数据状态管理

import { NestedScrollModelImpl } from '../model/NestedScrollModelImpl';
import { NestedScrollTabConfig } from '../model/NestedScrollTabConfig';
import { INestedScrollViewModel, NestedScrollViewModelCallback } from './INestedScrollViewModel';

const VM_TAG = 'NestedScrollVM';

/**
 * 嵌套滑动 Demo ViewModel 实现
 * 通过 @ObservedV2 + @Trace 让 View 层感知状态变化
 */
@ObservedV2
export class NestedScrollViewModelImpl implements INestedScrollViewModel {
  private nestedScrollModel: NestedScrollModelImpl;
  private callback?: NestedScrollViewModelCallback;
  @Trace tabs: NestedScrollTabConfig[] = [];
  @Trace currentTabIndex: number = 0;
  @Trace pageLoading: boolean = false;

  constructor() {
    this.nestedScrollModel = new NestedScrollModelImpl();
  }

  aboutToAppear(): void {
  }

  aboutToDisappear(): void {
  }

  setCallback(callback: NestedScrollViewModelCallback | undefined): void {
    this.callback = callback;
  }

  pageOnReady(): void {
    this.showPageLoading();
    this.queryTabs();
  }

  private queryTabs(): void {
    this.nestedScrollModel.queryTabs()
      .then((tabs: NestedScrollTabConfig[]) => {
        this.tabs = tabs;
        this.hidePageLoading();
      })
      .catch((e: Error) => {
        console.error(`${VM_TAG} queryTabs error: ${e?.message ?? e}`);
        this.hidePageLoading();
        //TODO::异常处理
      });
  }

  onTabClick(tabIndex: number): boolean {
    if (this.currentTabIndex === tabIndex) {
      return false;
    }
    this.currentTabIndex = tabIndex;
    return true;
  }

  onSwiperTabChange(index: number): void {
    if (this.currentTabIndex !== index) {
      this.currentTabIndex = index;
    }
  }

  onFirstTabSwipeOut(): void {
    this.callback?.popPage?.();
  }

  private showPageLoading(): void {
    this.pageLoading = true;
  }

  private hidePageLoading(): void {
    this.pageLoading = false;
  }
}

view page层。view组件组装/展示、数据渲染

/**
 * View 层:嵌套滑动 Demo 页
 */
import { NestedScrollTabContainer } from '../components/NestedScrollTabContainer';
import { NestedScrollViewModelImpl } from '../viewmodel/NestedScrollViewModelImpl';
import { NestedScrollViewModelCallback } from '../viewmodel/INestedScrollViewModel';
import { NestedScrollTabConfig, NestedScrollListItem } from '../model/NestedScrollTabConfig';

@Builder
export function NestedScrollDemoPageBuilder(name: string, param: Object) {
  NestedScrollDemoPage()
}

@Entry
@Component
struct NestedScrollDemoPage {
  private viewModel: NestedScrollViewModelImpl = new NestedScrollViewModelImpl();
  private parentScroller: Scroller = new Scroller();
  private swiperController: SwiperController = new SwiperController();
  private childScrollers: Scroller[] = [
    new Scroller(), new Scroller(), new Scroller(), new Scroller()
  ];
  private pathStack: NavPathStack = new NavPathStack();
  private nestedScrollViewModelCallback: NestedScrollViewModelCallback = {
    popPage: (): void => {
      this.pathStack.pop();
    }
  };

  build() {
    NavDestination() {
      Stack() {
        NestedScrollTabContainer({
          currentTabIndex: this.viewModel.currentTabIndex,
          useLinearGradient: true,
          gradientAngle: 180,
          gradientColors: [
            ['#33B62E', 0.0],
            ['#00B365', 1.0]
          ],
          swiperController: this.swiperController,
          parentScroller: this.parentScroller,
          onTabChange: (index: number) => {
            this.viewModel.onSwiperTabChange(index);
          },
          onFirstTabSwipeOut: () => {
            this.viewModel.onFirstTabSwipeOut();
          },
          headerBuilder: () => {
            this.headerBuilder()
          },
          contentBuilder: () => {
            this.swiperContentBuilder()
          }
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#00B365')
    .hideTitleBar(true)
    .hideBackButton(true)
    .hideToolBar(true)
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack;
      this.viewModel.pageOnReady();
    })
  }

  aboutToAppear() {
    this.viewModel.setCallback(this.nestedScrollViewModelCallback);
    this.viewModel.aboutToAppear();
  }

  aboutToDisappear() {
    this.viewModel.setCallback(undefined);
    this.viewModel.aboutToDisappear();
  }

  @Builder
  headerBuilder() {
    Column() {
      Column() {
        Text('嵌套滑动 Demo')
          .fontSize(24)
          .fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold)
          .margin({ top: 48, bottom: 8 })

        Text('下滑内层列表到顶后,外层 Header 才会下移出现')
          .fontSize(12)
          .fontColor('#E0FFFFFF')
          .margin({ bottom: 16 })
      }
      .width('100%')
      .alignItems(HorizontalAlign.Center)

      Row() {
        ForEach(this.viewModel.tabs, (tab: NestedScrollTabConfig) => {
          Text(tab.name)
            .fontSize(this.viewModel.currentTabIndex === tab.index ? 16 : 14)
            .fontColor(this.viewModel.currentTabIndex === tab.index ? '#FFFFFF' : '#B3FFFFFF')
            .fontWeight(this.viewModel.currentTabIndex === tab.index ? FontWeight.Bold : FontWeight.Normal)
            .padding({
              left: 12,
              right: 12,
              top: 8,
              bottom: 8
            })
            .borderRadius(16)
            .backgroundColor(this.viewModel.currentTabIndex === tab.index ? '#33FFFFFF' : Color.Transparent)
            .onClick(() => {
              const changed = this.viewModel.onTabClick(tab.index);
              if (changed) {
                this.swiperController.changeIndex(tab.index);
              }
            })
        })
      }
      .width('100%')
      .height(48)
      .justifyContent(FlexAlign.Center)
      .margin({ bottom: 8 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Center)
  }

  @Builder
  swiperContentBuilder() {
    ForEach(this.viewModel.tabs, (tab: NestedScrollTabConfig) => {
      this.tabContentBuilder(tab)
    })
  }

  @Builder
  tabContentBuilder(tab: NestedScrollTabConfig) {
    List({ scroller: this.childScrollers[tab.index], space: 8 }) {
      ForEach(tab.listData, (item: NestedScrollListItem) => {
        ListItem() {
          Column() {
            Text(item.title)
              .fontSize(16)
              .fontColor('#333333')
              .fontWeight(FontWeight.Medium)
            Text(item.subtitle)
              .fontSize(12)
              .fontColor('#666666')
              .margin({ top: 4 })
            Text(`#${item.id}`)
              .fontSize(11)
              .fontColor('#999999')
              .margin({ top: 4 })
          }
          .width('100%')
          .padding(16)
          .backgroundColor(item.color)
          .borderRadius(12)
          .alignItems(HorizontalAlign.Start)
        }
        .padding({ left: 12, right: 12 })
      })

      ListItem() {
        Text('— 已经到底啦 —')
          .width('100%')
          .textAlign(TextAlign.Center)
          .fontSize(12)
          .fontColor('#FFFFFF')
          .padding({ top: 16, bottom: 32 })
      }
    }
    .width('100%')
    .height('100%')
    .listDirection(Axis.Vertical)
    .scrollBar(BarState.Off)
    .edgeEffect(EdgeEffect.None)
    .nestedScroll({
      scrollForward: NestedScrollMode.PARENT_FIRST,
      scrollBackward: NestedScrollMode.SELF_FIRST
    })
  }
}

撒花撒花~

后续优化

这个是基础组件,如果项目使用,还需要根据项目进行拓展。比如:

1.增加footer,footer需要放在list里面。

2.懒加载+分页加载,懒加载是为了不让大列表卡顿,懒加载是lazyforeach,分页加载是因为服务端数据太大一次性下发不完

3.错误页/空白页处理,这个放在tabContentBuilder里面比较好。一般进页面是查第一个tab数据,如果失败该tab展示异常或者空白,切到其他tab的时候才懒加载其他tab,其他tab仍然可以继续请求数据正常展示,然后切回原来异常tab的时候继续请求数据,直到数据请求成功

4.tab动态化。这个根据业务规则来,有的强诉求tab动态化,即端上代码不消费具体的tab类型,只做容器和兼容处理。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐