【共创季稿事节】鸿蒙原生 ArkTS 布局实战:Swiper + displayCount 多卡片轮播
·



## 目录
1. [引言:一屏多卡的意义](#1-引言一屏多卡的意义)
2. [displayCount 属性详解](#2-displaycount-属性详解)
- [2.1 基本概念](#21-基本概念)
- [2.2 宽度计算公式](#22-宽度计算公式)
- [2.3 不同数值的视觉效果](#23-不同数值的视觉效果)
3. [itemSpace:卡片间距控制](#3-itemspace卡片间距控制)
4. [实战:8 张电影推荐卡片轮播](#4-实战8-张电影推荐卡片轮播)
5. [代码逐段解析](#5-代码逐段解析)
- [5.1 数据模型:8 种配色方案](#51-数据模型8-种配色方案)
- [5.2 CardView:渐变色卡片的叠层构建](#52-cardview渐变色卡片的叠层构建)
- [5.3 build():displayCount + itemSpace 核心配置](#53-builddisplaycount--itemspace-核心配置)
6. [displayCount 与常规 Swiper 的对比](#6-displaycount-与常规-swiper-的对比)
7. [常见问题与解决方案](#7-常见问题与解决方案)
8. [本系列七篇全览](#8-本系列七篇全览)
9. [总结](#9-总结)
---
## 1. 引言:一屏多卡的意义
在移动应用中,"横向滚动卡片"是一种极为常见的内容展示形式:
| 应用 | 横向滚动场景 | displayCount 典型值 |
|------|------------|-------------------|
| **Apple Music** | "朋友正在听"推荐列表 | 2.5 |
| **Netflix** | 影片横向分类行 | 3.0~3.5 |
| **App Store** | 今日推荐卡片 | 2.0~2.5 |
| **淘宝** | 商品横向推荐 | 2.5~3.0 |
| **Instagram** | 快拍/故事列表 | 3.5~4.0 |
这些场景的共同特征是:**不希望用户一次只看一张卡片**。一屏展示多张卡片有三大好处:
1. **空间效率**:同样的屏幕空间展示更多内容,信息密度更高
2. **视觉暗示**:右侧露出的半张卡片告诉用户"还可以滑"
3. **对比浏览**:用户可以同时看到相邻的几条内容,快速决定是否滑动
HarmonyOS NEXT 的 Swiper 组件通过 `displayCount` 属性完美支持这种"一屏多卡"布局。
---
## 2. displayCount 属性详解
### 2.1 基本概念
`displayCount` 控制 Swiper 一屏(一页)显示多少个子项:
```ets
Swiper() { ... }
.displayCount(2.5) // 一屏显示 2.5 张卡片
```
**整数部分** —— 完整可见的卡片数量
**小数部分** —— 右侧露出的下一张卡片的宽度比例
- `displayCount(1)` = 每次显示 1 张(默认,标准轮播)
- `displayCount(2)` = 每次显示 2 张完整卡片
- `displayCount(2.5)` = 每次显示 2 张完整卡片 + 右半张预览
- `displayCount(3)` = 每次显示 3 张完整卡片
### 2.2 宽度计算公式
```text
卡片宽度 = (Swiper宽度 - (displayCount - 1) × itemSpace) / displayCount
```
**实际计算**(假设 Swiper 宽度 = 360vp):
| displayCount | itemSpace | 卡片宽度 | 说明 |
|-------------|-----------|---------|------|
| 1.0 | 0 | 360 | 标准轮播,一屏一张 |
| 2.0 | 12 | 174 | 两卡并排,中间 12px 间距 |
| **2.5** | **12** | **~136.8** | **两卡完整 + 右半张预览** |
| 3.0 | 8 | ~114.7 | 三卡并排 |
| 3.5 | 8 | ~98.3 | 三卡 + 半张预览 |
**以 displayCount=2.5, itemSpace=12 为例**:
```text
卡1 (完整) | 12px | 卡2 (完整) | 12px | 卡3 (半张可见)
<───── 136.8 ─────> <───── 136.8 ─────> <── 68.4 ──>
<────────────────── 总宽 360 ─────────────────────>
```
### 2.3 不同数值的视觉效果
| displayCount | 视觉感受 | 适用场景 |
|-------------|---------|---------|
| **1.0** | 全屏轮播,焦点突出 | 首页大 Banner |
| **1.5** | 右侧露出半张,有"下一页"暗示 | 引导页 |
| **2.0** | 两卡并排,信息均衡 | 对比展示 |
| **2.5** | 两卡 + 半张预览,"继续滑"暗示强烈 | **推荐列表(最常用)** |
| **3.0** | 三卡并排,信息密度高 | 视频分类 / 图标网格 |
| **3.5+** | 密集展示,适合小卡片 | 表情 / Emoji 选择器 |
**2.5 是使用最广泛的配置**。它兼顾了内容可见性(2 张完整卡片)和可发现性(半张预览),是 Apple Music、Netflix 等主流应用的首选值。
---
## 3. itemSpace:卡片间距控制
`itemSpace` 设置相邻卡片之间的间距:
```ets
Swiper() { ... }
.displayCount(2.5)
.itemSpace(12) // 卡片间距 12vp
```
**itemSpace 的作用**:
| itemSpace | 视觉效果 | 适用场景 |
|-----------|---------|---------|
| 0 | 卡片紧贴,无间隙 | 图片无缝拼接 |
| 8 | 紧凑,略有间隔 | 小卡片网格 |
| **12** | **舒适,明显分隔** | **通用卡片列表** |
| 16 | 宽松,呼吸感强 | 高端品牌展示 |
| 24 | 非常宽松 | 大卡片精选推荐 |
**itemSpace 在 displayCount 计算中的角色**:
itemSpace 不占用卡片的宽度,而是消耗 Swiper 总宽度的一部分。itemSpace 越大,每张卡片的可用宽度越小。
---
## 4. 实战:8 张电影推荐卡片轮播
### 4.1 设计目标
创建一个类似"Netflix 推荐行"的横向滑动卡片列表,一屏显示 2.5 张卡片,8 张不同主题的电影卡片自动轮播。
### 4.2 8 张卡片配色
| # | 电影 | 配色 | 色值 |
|---|------|------|------|
| 1 | 🎬 星际穿越 | 深蓝→蓝 | `#0D47A1` → `#1976D2` |
| 2 | 🎭 悲剧之王 | 深紫→紫 | `#4A148C` → `#7B1FA2` |
| 3 | 🎪 马戏迷城 | 橙→金黄 | `#E65100` → `#FF8F00` |
| 4 | 🎨 梵高之眼 | 深绿→绿 | `#2E7D32` → `#43A047` |
| 5 | 🎵 波西米亚 | 深红→红 | `#B71C1C` → `#E53935` |
| 6 | 🧙 中土传奇 | 深棕→棕 | `#3E2723` → `#6D4C41` |
| 7 | 🤖 机械纪元 | 深灰→灰 | `#37474F` → `#607D8B` |
| 8 | 🌊 深海探秘 | 墨绿→青 | `#004D40` → `#00897B` |
8 种配色覆盖蓝、紫、橙、绿、红、棕、灰、青色系,滑动时色彩变化丰富。
### 4.3 布局预览
```
┌──────────────────────────────────────┐
│ 🎪 热门推荐 │
│ displayCount(2.5) 一屏多卡 │
│ │
│ ┌──────┐ ┌──────┐ ┌──── ┐ │
│ │ 🎬 │ │ 🎭 │ │ 🎪 │ │
│ │星际穿│ │悲剧之│ │马戏迷│ ← 半张 │
│ │越 │ │王 │ │城 │ ← 预览 │
│ │科幻· │ │剧情· │ │奇幻· │ │
│ │冒险 │ │励志 │ │家庭 │ │
│ └──────┘ └──────┘ └──── ┘ │
│ ← 12px → ← 12px → │
│ ○ ● ○ ○ ○ ○ ○ ○ │
│ │
│ 📐 Swiper + displayCount 多卡片布局 │
│ ● displayCount(2.5) │
│ ● itemSpace(12) │
│ ● 右半张预览 → 继续滑动 │
│ ● autoPlay 自动轮播 │
└──────────────────────────────────────┘
```
---
## 5. 代码逐段解析
### 5.1 数据模型:8 种配色方案
```ets
interface CardInfo {
emoji: string; // 电影图标
title: string; // 标题
category: string; // 分类 + 标签
gradientStart: string; // 渐变起点色
gradientEnd: string; // 渐变终点色
}
private readonly cards: CardInfo[] = [
{ emoji: '🎬', title: '星际穿越', category: '科幻 · 冒险', gradientStart: '#0D47A1', gradientEnd: '#1976D2' },
{ emoji: '🎭', title: '悲剧之王', category: '剧情 · 励志', gradientStart: '#4A148C', gradientEnd: '#7B1FA2' },
// ... 共 8 张
];
```
**配色设计思路**:
每张卡片使用 `linearGradient` 从深色到亮色的垂直渐变,模拟"电影海报"的视觉质感。8 组配色覆盖了完整的色环,确保滑动时色彩跳跃明显:
```
蓝 → 紫 → 橙 → 绿 → 红 → 棕 → 灰 → 青 → 蓝(循环)
```
### 5.2 CardView:渐变色卡片的叠层构建
```ets
@Builder
private CardView(card: CardInfo) {
Stack() {
// 底层:渐变背景
Column()
.width('100%').height('100%').borderRadius(16)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [[card.gradientStart, 0], [card.gradientEnd, 1]],
})
// 中层:底部半透明遮罩(增强文字可读性)
Column()
.width('100%').height(80)
.linearGradient({
direction: GradientDirection.Bottom,
colors: [['#00000000', 0], ['#00000066', 1]],
})
.borderRadius({ bottomLeft: 16, bottomRight: 16 })
.position({ y: 140 })
// 上层:图标
Text(card.emoji).fontSize(48).position({ x: 0, y: 20 }).width('100%')
// 上层:标题 + 分类(底部对齐)
Column() {
Text(card.title).fontSize(17).fontWeight(FontWeight.Bold).fontColor('#FFFFFF')
Text(card.category).fontSize(12).fontColor('#FFFFFF').opacity(0.8)
}
.position({ x: 14, y: 164 })
}
.width('100%').height('100%').clip(true)
}
```
**叠层结构**:
```
┌──────────────────────┐
│ 🎬 │ ← 图标 (position y:20)
│ │
│ │
│ │
│ ┌─────────────┐ │ ← 渐变遮罩 (position y:140, height:80)
│ │ 星际穿越 │ │
│ │ 科幻 · 冒险 │ │ ← 文字 (position y:164)
│ └─────────────┘ │
└──────────────────────┘
↑ 渐变背景 (铺满)
```
**底部遮罩的作用**:
渐变背景上的白色文字,在浅色背景下可能不易阅读。底部半透明黑色遮罩(从完全透明到 40% 黑色)确保了文字区域始终有足够的对比度,同时不破坏渐变的美感。
### 5.3 build():displayCount + itemSpace 核心配置
```ets
build() {
Scroll() {
Column() {
// 标题
Text('🎪 热门推荐').fontSize(22).fontWeight(FontWeight.Bold)
Swiper() {
ForEach(this.cards, (card: CardInfo, idx: number) => {
Stack() { this.CardView(card) }
.width('100%').height('100%')
}, (card: CardInfo, idx: number): string => idx.toString())
}
.width('100%').height(220)
.index(this.currentIndex)
.displayCount(2.5) // ← 核心属性:一屏显示 2.5 张
.itemSpace(12) // ← 核心属性:卡片间距 12vp
.autoPlay(true).interval(3000).loop(true).duration(500)
.indicator(false)
.onChange((index) => { this.currentIndex = index; })
// 指示器 + 说明卡片
Stack() { this.DotIndicator() }.margin({ top: 4 })
Stack() { this.FeatureCard() }.width('90%').margin({ top: 14 })
}
}
.backgroundColor('#F2F3F8')
}
```
**displayCount 与卡片子元素的 width 关系**:
注意子元素使用了 `.width('100%').height('100%')`——它们的宽度由 Swiper 根据 displayCount 自动计算分配,不需要也不应该手动设置固定宽度。
**Swiper 高度 220vp 的选择依据**:
相比标准轮播(通常 300vp 以上),多卡片布局的卡片更小,220vp 高度在手机竖屏下刚好显示 2.5 张卡片 + 底部信息,无需用户滚动页面。
---
## 6. displayCount 与常规 Swiper 的对比
| 对比维度 | 常规 Swiper(displayCount=1) | 多卡片布局(displayCount=2.5) |
|---------|-----------------------------|------------------------------|
| **一屏卡片数** | 1 张 | 2.5 张 |
| **信息密度** | 低(一张大图) | 高(多张卡片) |
| **右侧预览** | 无(看不到下一张) | 有(半张预览暗示可滑) |
| **焦点** | 单卡片焦点集中 | 多卡片对比浏览 |
| **滑动步长** | 1 张卡片 | 1 张卡片(还是在同一个分页体系下) |
| **适合场景** | 首页大 Banner | 推荐列表 / 商品展示 |
| **卡片宽度** | 100% Swiper 宽度 | 由 displayCount 公式计算 |
**滑动行为的一致性**:
无论 `displayCount` 设置为多少,Swiper 的滑动步长始终是"一张卡片"。也就是说,从第 0 张滑到第 1 张,内容会移动一个卡片的宽度(不是半个 Swiper 宽度)。
这使得 `displayCount` 更像是一个"显示"属性而非"行为"属性——它只改变"一屏能看到多少",不改变"一次能滑多少"。
---
## 7. 常见问题与解决方案
### 7.1 卡片显示不全
**现象**:displayCount 设置后,卡片右侧被裁剪。
**原因**:卡片子元素设置了固定宽度,覆盖了 Swiper 的自动分配。
**解决**:子元素使用 `.width('100%')`,不要设置固定宽度。
```ets
// ✅ 正确:宽度由 Swiper 自动分配
Stack() { this.CardView(card) }
.width('100%').height('100%')
// ❌ 错误:固定宽度会覆盖 Swiper 的计算
Stack() { this.CardView(card) }
.width(150).height(220)
```
### 7.2 displayCount 取整问题
**现象**:设置 `displayCount(2.5)` 但最终显示的不是精确的 2.5 张。
**原因**:Swiper 宽度可能不是 itemSpace 和 displayCount 的整数倍,Swiper 会在内部做适应性调整,最终结果可能与理论值有 ±0.1 的偏差。
**解决方案**:这不是 bug,是正常的舍入行为。只要视觉上接近 2.5 即可。
### 7.3 卡片间距不均匀
**现象**:相邻卡片间距不一致。
**原因**:`itemSpace` 是在 Swiper 侧设置的,但 `padding` 是在卡片侧设置的,两者叠加导致间距翻倍。
**解决**:不要在卡片子元素上设置左右 padding/margin,间距统一由 `itemSpace` 控制。
### 7.4 displayCount 与 loop 配合
**现象**:`displayCount(2.5)` + `loop(true)` 时,循环边界出现空白。
**原因**:loop 模式下的虚拟列表增加了额外的页面副本,displayCount 需要在这些副本之间保持一致的布局。
**解决方案**:大多数情况下可以正常工作。如果出现异常,确认 ForEach 的 keyGenerator 返回了稳定的唯一值。
### 7.5 displayCount 在小屏幕上的表现
**现象**:在屏幕宽度较小的设备上,displayCount 2.5 导致卡片过窄。
**解决方案**:使用条件语句适配不同屏幕:
```ets
// 根据屏幕宽度动态调整
aboutToAppear(): void {
let w = this.getUIContext()?.getWindowWidth() ?? 360;
if (w < 360) {
this.cardCount = 2.0; // 小屏 2 张
} else {
this.cardCount = 2.5; // 正常 2.5 张
}
}
```
---
## 8. 本系列七篇全览
本文是"鸿蒙原生 ArkTS 布局实战"系列的第六篇(不含项目回顾篇)。以下是全系列概览:
| # | 标题 | 组件 | 核心属性 | 核心知识点 |
|---|------|------|---------|-----------|
| 1 | Tabs + animateTo 切换动画 | Tabs | `animateTo` | 显式动画编排 |
| 2 | Swiper 轮播图 | Swiper | `autoPlay`, `interval` | 基础轮播配置 |
| 3 | Tabs + vertical 侧边栏 | Tabs | `vertical`, `barPosition.Start` | 左侧导航布局 |
| 4 | Swiper + loop 无限循环 | Swiper | `loop(true)` | 虚拟列表原理 |
| 5 | Swiper + Curve 缓动曲线 | Swiper | `.curve(Curve.xxx)` | 动画节奏控制 |
| **6** | **Swiper + displayCount** | **Swiper** | **`displayCount`, `itemSpace`** | **多卡片并排** |
| — | 项目回顾 | — | — | 24 个编译错误总结 |
**三篇 Swiper 文章的递进关系**:
1. **第 2 篇**(基础轮播):学会 Swiper 的基本使用
2. **第 4 篇**(loop):理解循环原理
3. **第 5 篇**(Curve):掌控动画节奏
4. **第 6 篇(本篇)**(displayCount):掌握多卡片布局
---
## 9. 总结
### 9.1 核心知识点
| 知识点 | 掌握程度 |
|--------|---------|
| displayCount 属性 | ✅ 控制一屏显示的卡片数量 |
| 宽度计算公式 | ✅ `(总宽 - (count-1) × itemSpace) / count` |
| itemSpace 卡片间距 | ✅ 统一间距控制 |
| 2.5 的典型配置 | ✅ 两卡完整 + 半张预览 |
| 卡片子元素 width | ✅ 使用 100%,不设固定值 |
| displayCount vs. 滑动步长 | ✅ 显示 ≠ 行为 |
### 9.2 displayCount 的最佳实践
| 卡片类型 | 推荐 displayCount | 推荐 itemSpace |
|---------|------------------|---------------|
| 大图海报 | 1.5~2.0 | 12~16 |
| 中等卡片 | **2.5** | **12** |
| 小卡片/头像 | 3.5~4.5 | 6~8 |
| Emoji/图标 | 5.0~6.0 | 4~6 |
### 9.3 扩展方向
1. **displayCount 动态切换**:根据屏幕宽度或用户偏好动态调整 count
2. **嵌套 displayCount**:外层 Swiper(全屏轮播)内嵌 displayCount Swiper(分类行)
3. **3D 堆叠效果**:通过自定义动画,让多张卡片在滑动时产生"3D 层叠"效果
4. **drag 手势**:关闭 autoPlay,让用户完全通过拖拽浏览,displayCount 提供预览
### 9.4 推荐阅读
- [HarmonyOS NEXT 开发文档 — Swiper 组件](https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-container-swiper-V5)
- [本系列第 2 篇 — Swiper 轮播图基础](./HarmonyOS_Swiper_Blog.md)
- [本系列第 4 篇 — Swiper + loop 无限循环](./HarmonyOS_Swiper_Loop_Blog.md)
---
> **本文配套完整代码**:`entry/src/main/ets/pages/Index.ets`,209 行,已通过编译验证。
> **编译命令**:`hvigorw assembleApp --no-daemon`
> **运行方式**:在 DevEco Studio 中打开项目,连接鸿蒙 NEXT 模拟器或真机运行。
更多推荐



所有评论(0)