> 技术文档 > 110.[HarmonyOS NEXT 实战案例:文件管理器] 基础篇 - 垂直分割布局构建文件管理界面

110.[HarmonyOS NEXT 实战案例:文件管理器] 基础篇 - 垂直分割布局构建文件管理界面


文章目录

  • [HarmonyOS NEXT 实战案例:文件管理器] 基础篇 - 垂直分割布局构建文件管理界面
    • 效果演示
    • 引言
    • 组件概述
    • 数据模型
      • 数据类型
      • 状态变量
    • 布局结构分析
    • 代码详解
      • 组件定义与状态声明
      • 整体布局结构
      • 左侧侧边栏
      • 右侧主内容区
      • 辅助方法
    • 布局技巧
      • 1. 比例设置
      • 2. 网格布局
      • 3. 文本溢出处理
      • 4. 条件渲染
      • 5. 布局权重
    • 交互实现
      • 1. 分类切换
      • 2. 文件夹导航
      • 3. 操作按钮
    • 小结

[HarmonyOS NEXT 实战案例:文件管理器] 基础篇 - 垂直分割布局构建文件管理界面

项目已开源,开源地址: https://gitcode.com/nutpi/HarmonyosNextCaseStudyTutorial , 欢迎fork & star

效果演示

110.[HarmonyOS NEXT 实战案例:文件管理器] 基础篇 - 垂直分割布局构建文件管理界面

引言

文件管理器是操作系统中不可或缺的应用,它允许用户浏览、组织和管理文件系统中的文件和文件夹。一个设计良好的文件管理器应该提供清晰的导航结构和直观的文件操作界面。本教程将详细讲解如何使用HarmonyOS NEXT的ColumnSplit组件构建一个文件管理器界面,通过垂直分割布局将界面分为侧边栏和主内容区两个主要部分。

组件概述

在本案例中,我们将使用以下HarmonyOS NEXT组件:

组件名称 功能描述 ColumnSplit 垂直分割布局容器,将界面分为左右两部分 Column 垂直布局容器,用于垂直排列子组件 Row 水平布局容器,用于水平排列子组件 List 列表容器,用于显示侧边栏的导航项 ListItem 列表项组件,用于显示单个导航项 Grid 网格容器,用于显示文件和文件夹 GridItem 网格项组件,用于显示单个文件或文件夹 Text 文本组件,用于显示文件名、导航项名称等 Image 图片组件,用于显示文件和文件夹图标 Button 按钮组件,用于执行文件操作 ForEach 循环渲染组件,用于渲染文件列表和导航列表

数据模型

在这个文件管理器案例中,我们定义了一个数据类型和三个状态变量:

数据类型

// 定义文件数据类型interface FileItem { id: number name: string type: \'file\' | \'folder\' icon: Resource size?: string modifiedTime?: string}

这个数据类型用于表示一个文件或文件夹,包含以下字段:

  • id:文件或文件夹的唯一标识符
  • name:文件或文件夹的名称
  • type:类型,可以是文件(‘file’)或文件夹(‘folder’)
  • icon:文件或文件夹的图标
  • size:文件大小(可选,仅文件有此属性)
  • modifiedTime:最后修改时间(可选)

状态变量

@State currentPath: string = \'/\'@State selectedCategory: string = \'所有文件\'@State files: FileItem[] = [ { id: 1, name: \'文档\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 2, name: \'图片\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 3, name: \'视频\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 4, name: \'音乐\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 5, name: \'下载\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 6, name: \'项目报告.docx\', type: \'file\', icon: $r(\'app.media.doc\'), size: \'2.5 MB\', modifiedTime: \'2023-05-15\' }, { id: 7, name: \'财务表格.xlsx\', type: \'file\', icon: $r(\'app.media.xls\'), size: \'1.8 MB\', modifiedTime: \'2023-05-10\' }, { id: 8, name: \'产品介绍.pptx\', type: \'file\', icon: $r(\'app.media.ppt\'), size: \'5.2 MB\', modifiedTime: \'2023-05-08\' }, { id: 9, name: \'会议记录.txt\', type: \'file\', icon: $r(\'app.media.txt\'), size: \'0.1 MB\', modifiedTime: \'2023-05-05\' }, { id: 10, name: \'项目计划.pdf\', type: \'file\', icon: $r(\'app.media.pdf\'), size: \'3.7 MB\', modifiedTime: \'2023-05-01\' }]

这些状态变量用于:

  • currentPath:存储当前浏览的路径
  • selectedCategory:存储当前选中的分类
  • files:存储当前路径下的文件和文件夹列表

布局结构分析

我们的文件管理器布局采用了垂直分割的方式,将界面分为左右两个部分:

  1. 左侧:侧边栏区域,占总宽度的25%,包含分类导航和存储信息
  2. 右侧:主内容区,占总宽度的75%,包含路径导航栏、操作按钮和文件网格

整体布局结构如下:

Column (整体容器)└── Text (标题)└── ColumnSplit (垂直分割布局) ├── Column (左侧 - 侧边栏) │ ├── List (分类导航) │ │ └── ForEach (循环渲染分类) │ │ └── ListItem (分类项) │ │  └── Row (分类信息) │ │  ├── Image (分类图标) │ │  └── Text (分类名称) │ └── Column (存储信息) │ ├── Text (存储空间标题) │ ├── Row (存储空间进度条) │ │ └── Column (进度条) │ └── Text (存储空间信息) └── Column (右侧 - 主内容区) ├── Row (路径导航栏) │ ├── Text (当前路径) │ └── Row (操作按钮) │ ├── Button (新建按钮) │ ├── Button (上传按钮) │ └── Button (更多按钮) └── Grid (文件网格) └── ForEach (循环渲染文件) └── GridItem (文件项)  └── Column (文件信息) ├── Image (文件图标) ├── Text (文件名称) └── Text (文件信息)

代码详解

组件定义与状态声明

@Componentexport struct FileManagerExample { @State currentPath: string = \'/\' @State selectedCategory: string = \'所有文件\' @State files: FileItem[] = [ // 文件数据 ] build() { // 组件内容 }}

我们使用@Component装饰器定义了一个名为FileManagerExample的组件,并使用@State装饰器声明了三个状态变量,用于管理文件管理器的数据。

整体布局结构

Column() { Text(\'文件管理器布局\') .fontSize(20) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) ColumnSplit() { // 左侧侧边栏 // 右侧主内容区 } .height(600)}.padding(15)

整体布局使用一个Column组件作为容器,包含一个标题文本和一个ColumnSplit组件。ColumnSplit组件的高度设置为600像素,整个Column容器设置了15像素的内边距。

左侧侧边栏

Column() { // 分类导航 List() { ForEach([\'所有文件\', \'文档\', \'图片\', \'视频\', \'音乐\', \'下载\', \'收藏\', \'最近\'], (category: string) => { ListItem() { Row() {  Image(this.getCategoryIcon(category)) .width(24) .height(24) .margin({ right: 10 })  Text(category) .fontSize(16) } .width(\'100%\') .padding(10) .backgroundColor(this.selectedCategory === category ? \'#e6f7ff\' : \'#ffffff\') .borderRadius(5) } .onClick(() => { this.selectedCategory = category this.filterFilesByCategory(category) }) }) } .width(\'100%\') .layoutWeight(1) // 存储信息 Column() { Text(\'存储空间\') .fontSize(16) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) Row() { Column() .width(\'70%\') .height(10) .backgroundColor(\'#e6f7ff\') .borderRadius(5) Column() .width(\'30%\') .height(10) .backgroundColor(\'#ffffff\') .borderRadius(5) } .width(\'100%\') .backgroundColor(\'#f0f0f0\') .borderRadius(5) Text(\'已使用 70% - 剩余 30GB\') .fontSize(14) .margin({ top: 10 }) } .width(\'100%\') .padding(10) .backgroundColor(\'#fafafa\') .borderRadius(10) .margin({ top: 20 })}.width(\'25%\').padding(10).backgroundColor(\'#ffffff\')

左侧侧边栏区域使用一个Column组件,宽度设置为总宽度的25%,内边距为10像素,背景色为白色。包含以下内容:

  1. 分类导航:使用List组件,宽度为100%,布局权重为1(占据剩余空间)。使用ForEach组件循环渲染分类列表,每个分类使用一个ListItem组件显示。

  2. 分类项:每个分类项使用一个Row组件水平排列分类信息,包括:

    • 分类图标:使用Image组件,宽度和高度为24像素,右边距为10像素。
    • 分类名称:使用Text组件,字体大小为16像素。
      整个分类项设置了宽度、内边距、背景色和圆角。根据当前选中的分类设置不同的背景色,选中的分类背景色为浅蓝色,未选中的分类背景色为白色。
  3. 点击事件:为每个分类项添加点击事件,点击时更新当前选中的分类,并根据分类过滤文件列表。

  4. 存储信息:使用Column组件,宽度为100%,内边距为10像素,背景色为浅灰色,圆角为10像素,上边距为20像素。包含以下内容:

    • 存储空间标题:使用Text组件,字体大小为16像素,字体粗细为粗体,下边距为10像素。
    • 存储空间进度条:使用Row组件包裹两个Column组件,表示已使用和剩余的存储空间。
    • 存储空间信息:使用Text组件,字体大小为14像素,上边距为10像素。

右侧主内容区

Column() { // 路径导航栏 Row() { Text(this.currentPath) .fontSize(16) .layoutWeight(1) Row() { Button(\'新建\') .fontSize(14) .height(32) .backgroundColor(\'#e6f7ff\') .fontColor(\'#1890ff\') .borderRadius(5) .margin({ right: 10 }) Button(\'上传\') .fontSize(14) .height(32) .backgroundColor(\'#e6f7ff\') .fontColor(\'#1890ff\') .borderRadius(5) .margin({ right: 10 }) Button(\'更多\') .fontSize(14) .height(32) .backgroundColor(\'#e6f7ff\') .fontColor(\'#1890ff\') .borderRadius(5) } } .width(\'100%\') .padding({ left: 10, right: 10, top: 5, bottom: 5 }) .backgroundColor(\'#f5f5f5\') .borderRadius(5) .margin({ bottom: 10 }) // 文件网格 Grid() { ForEach(this.files, (file: FileItem) => { GridItem() { Column() {  Image(file.icon) .width(48) .height(48) .margin({ bottom: 5 })  Text(file.name) .fontSize(14) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .textAlign(TextAlign.Center) .margin({ bottom: 5 })  if (file.type === \'file\' && file.size) { Text(file.size) .fontSize(12) .fontColor(\'#999999\')  } } .width(\'100%\') .height(\'100%\') .alignItems(HorizontalAlign.Center) .padding(10) .backgroundColor(\'#ffffff\') .borderRadius(5) } .onClick(() => { if (file.type === \'folder\') {  this.navigateToFolder(file.name) } else {  this.openFile(file) } }) }) } .columnsTemplate(\'1fr 1fr 1fr 1fr\') .rowsTemplate(\'1fr 1fr 1fr\') .columnsGap(10) .rowsGap(10) .layoutWeight(1)}.backgroundColor(\'#fafafa\').padding(10)

右侧主内容区使用一个Column组件,背景色为浅灰色,内边距为10像素。包含以下内容:

  1. 路径导航栏:使用Row组件,宽度为100%,内边距设置为左右10像素、上下5像素,背景色为浅灰色,圆角为5像素,下边距为10像素。包含以下内容:

    • 当前路径:使用Text组件,字体大小为16像素,布局权重为1(占据剩余空间)。
    • 操作按钮:使用Row组件包裹三个Button组件,分别表示新建、上传和更多操作。每个按钮设置了字体大小、高度、背景色、字体颜色、圆角和右边距。
  2. 文件网格:使用Grid组件,设置了列模板、行模板、列间距、行间距和布局权重。使用ForEach组件循环渲染文件列表,每个文件使用一个GridItem组件显示。

  3. 文件项:每个文件项使用一个Column组件垂直排列文件信息,包括:

    • 文件图标:使用Image组件,宽度和高度为48像素,下边距为5像素。
    • 文件名称:使用Text组件,字体大小为14像素,最大行数为2,文本溢出时显示省略号,文本对齐方式为居中,下边距为5像素。
    • 文件大小:如果是文件且有大小信息,使用Text组件显示文件大小,字体大小为12像素,字体颜色为灰色。
      整个文件项设置了宽度、高度、对齐方式、内边距、背景色和圆角。
  4. 点击事件:为每个文件项添加点击事件,点击时根据文件类型执行不同的操作。如果是文件夹,则导航到该文件夹;如果是文件,则打开该文件。

辅助方法

// 获取分类图标private getCategoryIcon(category: string): Resource { switch (category) { case \'所有文件\': return $r(\'app.media.all\') case \'文档\': return $r(\'app.media.doc\') case \'图片\': return $r(\'app.media.img\') case \'视频\': return $r(\'app.media.video\') case \'音乐\': return $r(\'app.media.music\') case \'下载\': return $r(\'app.media.download\') case \'收藏\': return $r(\'app.media.favorite\') case \'最近\': return $r(\'app.media.recent\') default: return $r(\'app.media.all\') }}// 根据分类过滤文件private filterFilesByCategory(category: string) { // 在实际应用中,这里应该根据分类从数据库或文件系统中加载文件 // 这里使用模拟数据 if (category === \'所有文件\') { this.files = [ { id: 1, name: \'文档\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 2, name: \'图片\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 3, name: \'视频\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 4, name: \'音乐\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 5, name: \'下载\', type: \'folder\', icon: $r(\'app.media.folder\') }, { id: 6, name: \'项目报告.docx\', type: \'file\', icon: $r(\'app.media.doc\'), size: \'2.5 MB\', modifiedTime: \'2023-05-15\' }, { id: 7, name: \'财务表格.xlsx\', type: \'file\', icon: $r(\'app.media.xls\'), size: \'1.8 MB\', modifiedTime: \'2023-05-10\' }, { id: 8, name: \'产品介绍.pptx\', type: \'file\', icon: $r(\'app.media.ppt\'), size: \'5.2 MB\', modifiedTime: \'2023-05-08\' }, { id: 9, name: \'会议记录.txt\', type: \'file\', icon: $r(\'app.media.txt\'), size: \'0.1 MB\', modifiedTime: \'2023-05-05\' }, { id: 10, name: \'项目计划.pdf\', type: \'file\', icon: $r(\'app.media.pdf\'), size: \'3.7 MB\', modifiedTime: \'2023-05-01\' } ] } else if (category === \'文档\') { this.files = [ { id: 6, name: \'项目报告.docx\', type: \'file\', icon: $r(\'app.media.doc\'), size: \'2.5 MB\', modifiedTime: \'2023-05-15\' }, { id: 7, name: \'财务表格.xlsx\', type: \'file\', icon: $r(\'app.media.xls\'), size: \'1.8 MB\', modifiedTime: \'2023-05-10\' }, { id: 8, name: \'产品介绍.pptx\', type: \'file\', icon: $r(\'app.media.ppt\'), size: \'5.2 MB\', modifiedTime: \'2023-05-08\' }, { id: 9, name: \'会议记录.txt\', type: \'file\', icon: $r(\'app.media.txt\'), size: \'0.1 MB\', modifiedTime: \'2023-05-05\' }, { id: 10, name: \'项目计划.pdf\', type: \'file\', icon: $r(\'app.media.pdf\'), size: \'3.7 MB\', modifiedTime: \'2023-05-01\' } ] } else if (category === \'图片\') { this.files = [ { id: 11, name: \'产品照片.jpg\', type: \'file\', icon: $r(\'app.media.img\'), size: \'3.2 MB\', modifiedTime: \'2023-05-12\' }, { id: 12, name: \'团队合影.png\', type: \'file\', icon: $r(\'app.media.img\'), size: \'4.5 MB\', modifiedTime: \'2023-05-09\' }, { id: 13, name: \'设计稿.png\', type: \'file\', icon: $r(\'app.media.img\'), size: \'2.8 MB\', modifiedTime: \'2023-05-07\' } ] } else { // 其他分类使用空数组 this.files = [] }}// 导航到文件夹private navigateToFolder(folderName: string) { this.currentPath = this.currentPath === \'/\' ? `/${folderName}` : `${this.currentPath}/${folderName}` // 在实际应用中,这里应该根据路径从数据库或文件系统中加载文件 // 这里使用模拟数据 if (folderName === \'文档\') { this.files = [ { id: 6, name: \'项目报告.docx\', type: \'file\', icon: $r(\'app.media.doc\'), size: \'2.5 MB\', modifiedTime: \'2023-05-15\' }, { id: 7, name: \'财务表格.xlsx\', type: \'file\', icon: $r(\'app.media.xls\'), size: \'1.8 MB\', modifiedTime: \'2023-05-10\' }, { id: 8, name: \'产品介绍.pptx\', type: \'file\', icon: $r(\'app.media.ppt\'), size: \'5.2 MB\', modifiedTime: \'2023-05-08\' }, { id: 9, name: \'会议记录.txt\', type: \'file\', icon: $r(\'app.media.txt\'), size: \'0.1 MB\', modifiedTime: \'2023-05-05\' }, { id: 10, name: \'项目计划.pdf\', type: \'file\', icon: $r(\'app.media.pdf\'), size: \'3.7 MB\', modifiedTime: \'2023-05-01\' } ] } else if (folderName === \'图片\') { this.files = [ { id: 11, name: \'产品照片.jpg\', type: \'file\', icon: $r(\'app.media.img\'), size: \'3.2 MB\', modifiedTime: \'2023-05-12\' }, { id: 12, name: \'团队合影.png\', type: \'file\', icon: $r(\'app.media.img\'), size: \'4.5 MB\', modifiedTime: \'2023-05-09\' }, { id: 13, name: \'设计稿.png\', type: \'file\', icon: $r(\'app.media.img\'), size: \'2.8 MB\', modifiedTime: \'2023-05-07\' } ] } else { // 其他文件夹使用空数组 this.files = [] }}// 打开文件private openFile(file: FileItem) { // 在实际应用中,这里应该根据文件类型打开相应的应用 // 这里只是一个示例 console.info(`打开文件:${file.name}`)}

我们定义了四个辅助方法:

  1. getCategoryIcon:根据分类名称返回对应的图标资源。

  2. filterFilesByCategory:根据分类过滤文件列表。在实际应用中,这个方法应该从数据库或文件系统中加载文件,这里我们使用模拟数据进行演示。

  3. navigateToFolder:导航到指定的文件夹。更新当前路径,并加载该文件夹下的文件列表。

  4. openFile:打开指定的文件。在实际应用中,这个方法应该根据文件类型打开相应的应用,这里我们只是打印一条日志信息。

布局技巧

1. 比例设置

在本案例中,我们使用百分比设置左侧侧边栏区域的宽度:

.width(\'25%\')

这样可以确保在不同屏幕尺寸下,左侧区域始终占据总宽度的25%,右侧区域占据剩余的75%。

2. 网格布局

我们使用Grid组件创建文件网格,并设置了列模板、行模板、列间距和行间距:

.columnsTemplate(\'1fr 1fr 1fr 1fr\').rowsTemplate(\'1fr 1fr 1fr\').columnsGap(10).rowsGap(10)

这样可以创建一个4列3行的网格,每个网格项之间有10像素的间距,使文件排列整齐美观。

3. 文本溢出处理

对于可能过长的文件名,我们使用maxLinestextOverflow属性处理文本溢出:

.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })

这样可以确保文件名不会超出预定的区域,而是最多显示2行,超出部分以省略号结尾,保持界面的整洁。

4. 条件渲染

我们使用条件渲染显示或隐藏某些组件,如文件大小:

if (file.type === \'file\' && file.size) { Text(file.size) .fontSize(12) .fontColor(\'#999999\')}

这样可以只为文件显示大小信息,而不为文件夹显示,使界面更加清晰。

5. 布局权重

我们使用layoutWeight属性使某些组件占据剩余空间:

.layoutWeight(1)

这样可以确保这些组件能够自适应地占据剩余空间,使布局更加灵活。

交互实现

1. 分类切换

.onClick(() => { this.selectedCategory = category this.filterFilesByCategory(category)})

我们为每个分类项添加点击事件,点击时更新当前选中的分类,并根据分类过滤文件列表。通过改变背景色,用户可以清楚地看到当前选中的分类。

2. 文件夹导航

.onClick(() => { if (file.type === \'folder\') { this.navigateToFolder(file.name) } else { this.openFile(file) }})

我们为每个文件项添加点击事件,点击时根据文件类型执行不同的操作。如果是文件夹,则导航到该文件夹;如果是文件,则打开该文件。

3. 操作按钮

Button(\'新建\') .fontSize(14) .height(32) .backgroundColor(\'#e6f7ff\') .fontColor(\'#1890ff\') .borderRadius(5) .margin({ right: 10 })

我们在路径导航栏中添加了三个操作按钮:新建、上传和更多。这些按钮使用了相同的样式,包括字体大小、高度、背景色、字体颜色、圆角和右边距。在实际应用中,这些按钮应该添加点击事件,执行相应的操作。

小结

在本教程中,我们详细讲解了如何使用HarmonyOS NEXT的ColumnSplit组件构建一个文件管理器界面。通过垂直分割布局,我们将界面分为侧边栏和主内容区两个主要部分,使用户能够清晰地看到文件分类和当前目录下的文件。