暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

112.[HarmonyOS NEXT 实战案例:文件管理器] 高级篇 - 高级布局技巧与组件封装

原创 若城 2025-06-11
141

[HarmonyOS NEXT 实战案例:文件管理器] 高级篇 - 高级布局技巧与组件封装

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

效果演示

引言

在前两篇教程中,我们学习了如何使用HarmonyOS NEXT的ColumnSplit组件构建文件管理器的基本布局,以及如何添加交互功能和状态管理。本篇教程将进一步深入,讲解文件管理器的高级布局技巧和组件封装,包括自适应布局、主题切换、组件封装、性能优化等高级特性,使文件管理器更加专业和易于维护。

自适应布局

在不同的设备和屏幕尺寸上,文件管理器应该能够自动调整布局,提供最佳的用户体验。HarmonyOS NEXT提供了多种方式来实现自适应布局。

1. 媒体查询

使用媒体查询可以根据屏幕宽度调整布局。

// 定义断点 const BREAKPOINT_SM = 320 const BREAKPOINT_MD = 600 const BREAKPOINT_LG = 840 // 状态变量 @State currentBreakpoint: string = 'sm' // 在build函数中使用媒体查询 build() { Column() { // 文件管理器内容 } .width('100%') .height('100%') .backgroundColor('#f5f5f5') .onAreaChange((oldArea: Area, newArea: Area) => { // 根据宽度更新断点 const newWidth = newArea.width as number if (newWidth < BREAKPOINT_MD) { this.currentBreakpoint = 'sm' } else if (newWidth < BREAKPOINT_LG) { this.currentBreakpoint = 'md' } else { this.currentBreakpoint = 'lg' } }) } // 根据断点调整布局 private getColumnSplitLayoutWeight(): number { // 在小屏幕上,侧边栏占比更小 switch (this.currentBreakpoint) { case 'sm': return 0.3 // 侧边栏占30% case 'md': return 0.25 // 侧边栏占25% case 'lg': return 0.2 // 侧边栏占20% default: return 0.25 } } // 在ColumnSplit中使用动态权重 ColumnSplit() { // 侧边栏 Column() { // 侧边栏内容 } .width('100%') .height('100%') .backgroundColor('#ffffff') // 主内容区 Column() { // 主内容区内容 } .width('100%') .height('100%') .backgroundColor('#ffffff') .padding(15) } .layoutWeight({ left: this.getColumnSplitLayoutWeight(), right: 1 - this.getColumnSplitLayoutWeight() })

在这段代码中,我们定义了三个断点:小屏幕(320px以下)、中屏幕(320px-840px)和大屏幕(840px以上)。我们使用onAreaChange事件监听容器尺寸的变化,并根据宽度更新当前断点。然后,我们定义了一个getColumnSplitLayoutWeight方法,根据当前断点返回不同的侧边栏权重。在ColumnSplit组件中,我们使用这个方法动态设置左右两侧的权重。

2. 百分比和弹性布局

使用百分比和弹性布局可以使组件自动适应容器尺寸。

// 使用百分比设置宽度和高度 Grid() { ForEach(this.files, (file: FileItem) => { GridItem() { // 文件项内容 } .width(this.currentBreakpoint === 'sm' ? '100%' : '50%') // 小屏幕一行一个,中大屏幕一行两个 .aspectRatio(1) // 保持宽高比为1:1 }) } .columnsTemplate(this.getGridColumnsTemplate()) // 动态设置列模板 .width('100%') // 根据断点返回不同的列模板 private getGridColumnsTemplate(): string { switch (this.currentBreakpoint) { case 'sm': return '1fr' // 一列 case 'md': return '1fr 1fr' // 两列 case 'lg': return '1fr 1fr 1fr 1fr' // 四列 default: return '1fr 1fr' } }

在这段代码中,我们使用百分比设置GridItem的宽度,根据当前断点决定一行显示一个还是两个文件项。我们还使用aspectRatio属性保持文件项的宽高比为1:1,使其始终保持正方形。我们定义了一个getGridColumnsTemplate方法,根据当前断点返回不同的列模板,在小屏幕上显示一列,中屏幕上显示两列,大屏幕上显示四列。

3. 栅格布局

使用栅格布局可以更精细地控制组件的位置和尺寸。

// 定义栅格配置 const GRID_COLUMNS = 12 // 总列数 // 根据断点返回不同的栅格跨度 private getSidebarSpan(): number { switch (this.currentBreakpoint) { case 'sm': return 4 // 小屏幕侧边栏占4列 case 'md': return 3 // 中屏幕侧边栏占3列 case 'lg': return 2 // 大屏幕侧边栏占2列 default: return 3 } } // 使用栅格布局 Row() { // 侧边栏 Column() { // 侧边栏内容 } .width(`${this.getSidebarSpan() / GRID_COLUMNS * 100}%`) // 根据栅格跨度计算宽度百分比 .height('100%') .backgroundColor('#ffffff') // 主内容区 Column() { // 主内容区内容 } .width(`${(GRID_COLUMNS - this.getSidebarSpan()) / GRID_COLUMNS * 100}%`) // 剩余宽度 .height('100%') .backgroundColor('#ffffff') .padding(15) } .width('100%') .height('100%')

在这段代码中,我们定义了一个12列的栅格系统。我们使用getSidebarSpan方法根据当前断点返回侧边栏应该占据的列数。然后,我们使用这个值计算侧边栏和主内容区的宽度百分比。

主题切换

文件管理器应该支持不同的主题,如亮色主题和暗色主题,以适应不同的使用环境和用户偏好。

1. 主题定义

首先,我们需要定义不同主题的颜色和样式。

// 主题类型 type ThemeType = 'light' | 'dark' | 'system' // 主题颜色 interface ThemeColors { backgroundColor: ResourceColor cardBackgroundColor: ResourceColor primaryTextColor: ResourceColor secondaryTextColor: ResourceColor borderColor: ResourceColor primaryColor: ResourceColor selectedBackgroundColor: ResourceColor hoverBackgroundColor: ResourceColor iconColor: ResourceColor errorColor: ResourceColor successColor: ResourceColor warningColor: ResourceColor } // 亮色主题颜色 const lightThemeColors: ThemeColors = { backgroundColor: '#f5f5f5', cardBackgroundColor: '#ffffff', primaryTextColor: '#333333', secondaryTextColor: '#999999', borderColor: '#f0f0f0', primaryColor: '#1890ff', selectedBackgroundColor: '#e6f7ff', hoverBackgroundColor: '#f5f5f5', iconColor: '#666666', errorColor: '#ff4d4f', successColor: '#52c41a', warningColor: '#faad14' } // 暗色主题颜色 const darkThemeColors: ThemeColors = { backgroundColor: '#141414', cardBackgroundColor: '#1f1f1f', primaryTextColor: '#ffffff', secondaryTextColor: '#999999', borderColor: '#303030', primaryColor: '#1890ff', selectedBackgroundColor: '#111d2c', hoverBackgroundColor: '#1f1f1f', iconColor: '#a6a6a6', errorColor: '#ff4d4f', successColor: '#52c41a', warningColor: '#faad14' }

在这段代码中,我们定义了一个ThemeType类型,表示主题类型,可以是亮色主题、暗色主题或跟随系统。我们还定义了一个ThemeColors接口,包含了主题的各种颜色,如背景色、文本色、边框色等。然后,我们定义了亮色主题和暗色主题的颜色。

2. 主题切换

接下来,我们需要实现主题切换功能。

// 状态变量 @State currentTheme: ThemeType = 'system' // 默认跟随系统 @State isDarkMode: boolean = false // 当前是否是暗色模式 // 在aboutToAppear生命周期函数中初始化主题 aboutToAppear() { // 获取系统主题 this.updateThemeBySystem() // 监听系统主题变化 window.on('windowSystemThemeChange', () => { if (this.currentTheme === 'system') { this.updateThemeBySystem() } }) } // 根据系统主题更新当前主题 private updateThemeBySystem() { const systemTheme = window.getWindowSystemTheme() this.isDarkMode = systemTheme === 'dark' } // 切换主题 private switchTheme(theme: ThemeType) { this.currentTheme = theme if (theme === 'system') { // 跟随系统主题 this.updateThemeBySystem() } else { // 使用指定主题 this.isDarkMode = theme === 'dark' } } // 获取当前主题颜色 private getThemeColors(): ThemeColors { return this.isDarkMode ? darkThemeColors : lightThemeColors } // 主题切换按钮 Button(this.isDarkMode ? '切换到亮色主题' : '切换到暗色主题') .fontSize(14) .height(32) .backgroundColor(this.getThemeColors().primaryColor) .fontColor('#ffffff') .borderRadius(5) .margin({ right: 10 }) .onClick(() => { this.switchTheme(this.isDarkMode ? 'light' : 'dark') }) // 在组件中使用主题颜色 Column() { // 文件管理器内容 } .width('100%') .height('100%') .backgroundColor(this.getThemeColors().backgroundColor)

在这段代码中,我们添加了两个状态变量:currentThemeisDarkModecurrentTheme表示当前选择的主题类型,默认为"system"(跟随系统);isDarkMode表示当前是否是暗色模式。

aboutToAppear生命周期函数中,我们初始化主题并监听系统主题变化。当系统主题变化时,如果当前选择的是跟随系统,则更新主题。

我们定义了两个方法:updateThemeBySystemswitchThemeupdateThemeBySystem方法用于根据系统主题更新当前主题;switchTheme方法用于切换主题,可以选择亮色主题、暗色主题或跟随系统。

我们还定义了一个getThemeColors方法,根据当前是否是暗色模式返回相应的主题颜色。

我们添加了一个主题切换按钮,点击时在亮色主题和暗色主题之间切换。

在组件中,我们使用getThemeColors方法获取当前主题的颜色,并应用到组件的样式中。

组件封装

随着文件管理器功能的增加,代码会变得越来越复杂。通过组件封装,我们可以将复杂的功能拆分成多个小组件,使代码更加模块化和易于维护。

1. 侧边栏组件

将侧边栏封装成一个独立的组件。

// 侧边栏组件 @Component struct Sidebar { @Link currentCategory: string // 当前选中的分类 @Prop categories: string[] // 分类列表 @Prop themeColors: ThemeColors // 主题颜色 // 分类项点击事件 private onCategoryClick: (category: string) => void = () => {} build() { Column() { // 应用标题 Text('文件管理器') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(this.themeColors.primaryTextColor) .margin({ top: 20, bottom: 30 }) // 分类列表 List() { ForEach(this.categories, (category: string) => { ListItem() { Row() { // 分类图标 Image(this.getCategoryIcon(category)) .width(24) .height(24) .margin({ right: 10 }) // 分类名称 Text(category) .fontSize(16) .fontColor(this.currentCategory === category ? this.themeColors.primaryColor : this.themeColors.primaryTextColor) } .width('100%') .padding(10) .borderRadius(5) .backgroundColor(this.currentCategory === category ? this.themeColors.selectedBackgroundColor : 'transparent') .onClick(() => { this.onCategoryClick(category) }) } .margin({ bottom: 5 }) }) } .width('100%') .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor(this.themeColors.cardBackgroundColor) .padding(15) } // 获取分类图标 private getCategoryIcon(category: string): Resource { switch (category) { case '全部文件': return $r('app.media.folder_all') case '图片': return $r('app.media.folder_image') case '视频': return $r('app.media.folder_video') case '音乐': return $r('app.media.folder_music') case '文档': return $r('app.media.folder_document') case '下载': return $r('app.media.folder_download') case '收藏': return $r('app.media.folder_favorite') case '回收站': return $r('app.media.folder_trash') default: return $r('app.media.folder') } } }

在这段代码中,我们定义了一个Sidebar组件,用于显示文件管理器的侧边栏。该组件接收三个参数:currentCategory(当前选中的分类)、categories(分类列表)和themeColors(主题颜色)。

build方法中,我们渲染应用标题和分类列表。分类列表使用List组件和ForEach循环渲染每个分类项。每个分类项显示分类图标和名称,并根据是否选中设置不同的背景色和文本颜色。

我们还定义了一个getCategoryIcon方法,根据分类名称返回相应的图标资源。

2. 文件列表组件

将文件列表封装成一个独立的组件。

// 文件列表组件 @Component struct FileList { @Link files: FileItem[] // 文件列表 @Link selectedFiles: FileItem[] // 选中的文件 @Link viewMode: 'grid' | 'list' // 视图模式 @Link isMultiSelectMode: boolean // 是否多选模式 @Prop themeColors: ThemeColors // 主题颜色 // 文件项点击事件 private onFileClick: (file: FileItem) => void = () => {} // 文件项选中事件 private onFileSelect: (file: FileItem, isSelected: boolean) => void = () => {} build() { Column() { // 工具栏 Row() { // 视图模式切换按钮 Button(this.viewMode === 'grid' ? '列表' : '网格') .fontSize(14) .height(32) .backgroundColor(this.themeColors.primaryColor) .fontColor('#ffffff') .borderRadius(5) .margin({ right: 10 }) .onClick(() => { this.viewMode = this.viewMode === 'grid' ? 'list' : 'grid' }) // 多选按钮 Button(this.isMultiSelectMode ? '取消' : '多选') .fontSize(14) .height(32) .backgroundColor(this.isMultiSelectMode ? this.themeColors.errorColor : this.themeColors.primaryColor) .fontColor('#ffffff') .borderRadius(5) .onClick(() => { this.isMultiSelectMode = !this.isMultiSelectMode if (!this.isMultiSelectMode) { this.selectedFiles = [] } }) if (this.isMultiSelectMode) { // 选中文件数量 Text(`已选择 ${this.selectedFiles.length} 项`) .fontSize(14) .fontColor(this.themeColors.primaryColor) .margin({ left: 10 }) } } .width('100%') .margin({ bottom: 15 }) // 文件视图 if (this.viewMode === 'grid') { // 网格视图 Grid() { ForEach(this.files, (file: FileItem) => { GridItem() { this.FileItem(file) } }) } .columnsTemplate('1fr 1fr 1fr 1fr') .columnsGap(10) .rowsGap(10) .width('100%') .layoutWeight(1) } else { // 列表视图 List() { ForEach(this.files, (file: FileItem) => { ListItem() { this.FileItem(file) } .margin({ bottom: 10 }) }) } .width('100%') .layoutWeight(1) } } .width('100%') .height('100%') .backgroundColor(this.themeColors.cardBackgroundColor) .padding(15) } // 文件项组件 @Builder FileItem(file: FileItem) { Column() { if (this.viewMode === 'grid') { // 网格视图文件项 Column() { if (this.isMultiSelectMode) { // 多选模式下显示复选框 Checkbox({ name: `file_${file.id}`, group: 'selectGroup' }) .select(this.isFileSelected(file)) .onChange((value: boolean) => { this.onFileSelect(file, value) }) .position({ x: 0, y: 0 }) .zIndex(1) } // 文件图标 Image(file.icon) .width(48) .height(48) .margin({ bottom: 10 }) // 文件名称 Text(file.name) .fontSize(14) .fontColor(this.themeColors.primaryTextColor) .maxLines(2) .textOverflow({ overflow: TextOverflow.Ellipsis }) .textAlign(TextAlign.Center) } .width('100%') .height('100%') .alignItems(HorizontalAlign.Center) .justifyContent(FlexAlign.Center) .padding(10) } else { // 列表视图文件项 Row() { if (this.isMultiSelectMode) { // 多选模式下显示复选框 Checkbox({ name: `file_${file.id}`, group: 'selectGroup' }) .select(this.isFileSelected(file)) .onChange((value: boolean) => { this.onFileSelect(file, value) }) .margin({ right: 10 }) } // 文件图标 Image(file.icon) .width(32) .height(32) .margin({ right: 10 }) // 文件信息 Column() { // 文件名称 Text(file.name) .fontSize(14) .fontColor(this.themeColors.primaryTextColor) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis }) // 文件详情 if (file.type === 'file') { Row() { if (file.size) { Text(file.size) .fontSize(12) .fontColor(this.themeColors.secondaryTextColor) .margin({ right: 10 }) } if (file.modifiedTime) { Text(file.modifiedTime) .fontSize(12) .fontColor(this.themeColors.secondaryTextColor) } } .margin({ top: 5 }) } } .layoutWeight(1) } .width('100%') .padding(10) } } .width('100%') .backgroundColor(this.isFileSelected(file) ? this.themeColors.selectedBackgroundColor : this.themeColors.cardBackgroundColor) .borderRadius(5) .border({ width: this.isFileSelected(file) ? 2 : 1, color: this.isFileSelected(file) ? this.themeColors.primaryColor : this.themeColors.borderColor }) .onClick(() => { if (this.isMultiSelectMode) { // 多选模式下点击文件项切换选中状态 this.onFileSelect(file, !this.isFileSelected(file)) } else { // 普通模式下点击文件项 this.onFileClick(file) } }) } // 检查文件是否被选中 private isFileSelected(file: FileItem): boolean { return this.selectedFiles.some(selectedFile => selectedFile.id === file.id) } }

在这段代码中,我们定义了一个FileList组件,用于显示文件列表。该组件接收五个参数:files(文件列表)、selectedFiles(选中的文件)、viewMode(视图模式)、isMultiSelectMode(是否多选模式)和themeColors(主题颜色)。

build方法中,我们渲染工具栏和文件视图。工具栏包含视图模式切换按钮和多选按钮,多选模式下还显示已选择的文件数量。文件视图根据当前的视图模式显示网格视图或列表视图。

我们使用@Builder装饰器定义了一个FileItem方法,用于渲染文件项。根据当前的视图模式和是否多选模式,显示不同的文件项内容。

我们还定义了一个isFileSelected方法,用于检查文件是否被选中。

3. 路径导航组件

将路径导航封装成一个独立的组件。

// 路径导航组件 @Component struct PathNavigation { @Link currentPath: string // 当前路径 @Link pathSegments: string[] // 路径段 @Prop themeColors: ThemeColors // 主题颜色 // 路径段点击事件 private onPathSegmentClick: (index: number) => void = () => {} // 返回上级目录事件 private onNavigateUp: () => void = () => {} build() { Row() { // 返回按钮 Button('返回') .fontSize(14) .height(32) .backgroundColor(this.themeColors.primaryColor) .fontColor('#ffffff') .borderRadius(5) .margin({ right: 10 }) .enabled(this.currentPath !== '/') .opacity(this.currentPath !== '/' ? 1 : 0.5) .onClick(() => { this.onNavigateUp() }) // 路径段 Row() { ForEach(this.pathSegments, (segment: string, index: number) => { Row() { if (index > 0) { Text('>') .fontSize(16) .fontColor(this.themeColors.secondaryTextColor) .margin({ left: 5, right: 5 }) } Text(segment === '/' ? '根目录' : segment) .fontSize(16) .fontColor(index === this.pathSegments.length - 1 ? this.themeColors.primaryTextColor : this.themeColors.primaryColor) .onClick(() => { if (index < this.pathSegments.length - 1) { this.onPathSegmentClick(index) } }) } }) } .layoutWeight(1) .clip(true) // 超出部分裁剪 } .width('100%') .height(40) .backgroundColor(this.themeColors.cardBackgroundColor) .padding(10) .margin({ bottom: 15 }) } }

在这段代码中,我们定义了一个PathNavigation组件,用于显示路径导航栏。该组件接收三个参数:currentPath(当前路径)、pathSegments(路径段)和themeColors(主题颜色)。

build方法中,我们渲染返回按钮和路径段。返回按钮只有在当前路径不是根目录时才可用。路径段使用ForEach循环渲染每个段,每个段之间使用>符号分隔。当前路径段使用主文本颜色显示,其他路径段使用主题颜色显示,并添加点击事件,点击时导航到该路径段。

4. 主组件集成

最后,我们将这些组件集成到主组件中。

// 主组件 @Entry @Component struct FileManager { // 状态变量 @State currentCategory: string = '全部文件' // 当前选中的分类 @State categories: string[] = ['全部文件', '图片', '视频', '音乐', '文档', '下载', '收藏', '回收站'] // 分类列表 @State currentPath: string = '/' // 当前路径 @State pathSegments: string[] = ['/'] // 路径段 @State files: FileItem[] = [] // 文件列表 @State selectedFiles: FileItem[] = [] // 选中的文件 @State viewMode: 'grid' | 'list' = 'grid' // 视图模式 @State isMultiSelectMode: boolean = false // 是否多选模式 @State currentTheme: ThemeType = 'system' // 当前主题 @State isDarkMode: boolean = false // 是否暗色模式 @State currentBreakpoint: string = 'md' // 当前断点 // 在aboutToAppear生命周期函数中初始化 aboutToAppear() { // 初始化主题 this.updateThemeBySystem() // 监听系统主题变化 window.on('windowSystemThemeChange', () => { if (this.currentTheme === 'system') { this.updateThemeBySystem() } }) // 加载文件列表 this.loadFiles() } build() { Column() { // 应用标题栏 Row() { Text('文件管理器') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(this.getThemeColors().primaryTextColor) Blank() // 主题切换按钮 Button(this.isDarkMode ? '亮色' : '暗色') .fontSize(14) .height(32) .backgroundColor(this.getThemeColors().primaryColor) .fontColor('#ffffff') .borderRadius(5) .onClick(() => { this.switchTheme(this.isDarkMode ? 'light' : 'dark') }) } .width('100%') .height(50) .padding({ left: 15, right: 15 }) .backgroundColor(this.getThemeColors().cardBackgroundColor) // 主内容区 ColumnSplit() { // 侧边栏 Sidebar({ currentCategory: $currentCategory, categories: this.categories, themeColors: this.getThemeColors(), onCategoryClick: (category: string) => { this.currentCategory = category this.currentPath = '/' this.updatePathSegments() this.loadFiles() } }) // 文件区域 Column() { // 路径导航 PathNavigation({ currentPath: $currentPath, pathSegments: $pathSegments, themeColors: this.getThemeColors(), onPathSegmentClick: (index: number) => { this.navigateToPathSegment(index) }, onNavigateUp: () => { this.navigateUp() } }) // 文件列表 FileList({ files: $files, selectedFiles: $selectedFiles, viewMode: $viewMode, isMultiSelectMode: $isMultiSelectMode, themeColors: this.getThemeColors(), onFileClick: (file: FileItem) => { if (file.type === 'folder') { this.navigateToFolder(file.name) } else { this.openFile(file) } }, onFileSelect: (file: FileItem, isSelected: boolean) => { this.toggleFileSelection(file, isSelected) } }) } .width('100%') .height('100%') .backgroundColor(this.getThemeColors().backgroundColor) .padding(15) } .layoutWeight({ left: this.getColumnSplitLayoutWeight(), right: 1 - this.getColumnSplitLayoutWeight() }) .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor(this.getThemeColors().backgroundColor) .onAreaChange((oldArea: Area, newArea: Area) => { // 根据宽度更新断点 const newWidth = newArea.width as number if (newWidth < BREAKPOINT_MD) { this.currentBreakpoint = 'sm' } else if (newWidth < BREAKPOINT_LG) { this.currentBreakpoint = 'md' } else { this.currentBreakpoint = 'lg' } }) } // 根据断点获取ColumnSplit布局权重 private getColumnSplitLayoutWeight(): number { switch (this.currentBreakpoint) { case 'sm': return 0.3 case 'md': return 0.25 case 'lg': return 0.2 default: return 0.25 } } // 根据系统主题更新当前主题 private updateThemeBySystem() { const systemTheme = window.getWindowSystemTheme() this.isDarkMode = systemTheme === 'dark' } // 切换主题 private switchTheme(theme: ThemeType) { this.currentTheme = theme if (theme === 'system') { this.updateThemeBySystem() } else { this.isDarkMode = theme === 'dark' } } // 获取当前主题颜色 private getThemeColors(): ThemeColors { return this.isDarkMode ? darkThemeColors : lightThemeColors } // 更新路径段 private updatePathSegments() { if (this.currentPath === '/') { this.pathSegments = ['/'] } else { const segments = this.currentPath.split('/').filter(segment => segment !== '') this.pathSegments = ['/', ...segments] } } // 导航到指定路径段 private navigateToPathSegment(index: number) { if (index === 0) { this.currentPath = '/' } else { const segments = this.pathSegments.slice(1, index + 1) this.currentPath = '/' + segments.join('/') } this.updatePathSegments() this.loadFiles() } // 导航到文件夹 private navigateToFolder(folderName: string) { this.currentPath = this.currentPath === '/' ? `/${folderName}` : `${this.currentPath}/${folderName}` this.updatePathSegments() this.loadFiles() } // 返回上级目录 private navigateUp() { if (this.currentPath === '/') { return } const lastSlashIndex = this.currentPath.lastIndexOf('/') if (lastSlashIndex === 0) { this.currentPath = '/' } else { this.currentPath = this.currentPath.substring(0, lastSlashIndex) } this.updatePathSegments() this.loadFiles() } // 加载文件列表 private loadFiles() { // 在实际应用中,这里应该从文件系统或数据库中加载文件 // 这里简化为生成一些示例文件 this.files = this.generateSampleFiles() } // 生成示例文件 private generateSampleFiles(): FileItem[] { // 根据当前路径和分类生成不同的文件列表 // 这里简化为生成一些固定的示例文件 const files: FileItem[] = [] // 在根目录下显示一些文件夹 if (this.currentPath === '/') { files.push( { 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') } ) } else if (this.currentPath === '/文档') { // 在文档文件夹下显示一些文档文件 files.push( { id: 6, name: '工作报告.docx', type: 'file', icon: $r('app.media.doc'), size: '2.5 MB', modifiedTime: '2023-05-15' }, { id: 7, name: '会议记录.txt', type: 'file', icon: $r('app.media.txt'), size: '0.1 MB', modifiedTime: '2023-05-18' }, { id: 8, name: '项目计划.xlsx', type: 'file', icon: $r('app.media.xls'), size: '1.8 MB', modifiedTime: '2023-05-10' }, { id: 9, name: '演示文稿.pptx', type: 'file', icon: $r('app.media.ppt'), size: '5.2 MB', modifiedTime: '2023-05-12' }, { id: 10, name: '研究报告.pdf', type: 'file', icon: $r('app.media.pdf'), size: '3.7 MB', modifiedTime: '2023-05-05' } ) } else if (this.currentPath === '/图片') { // 在图片文件夹下显示一些图片文件 files.push( { id: 11, name: '风景照片.jpg', type: 'file', icon: $r('app.media.jpg'), size: '4.2 MB', modifiedTime: '2023-05-20' }, { id: 12, name: '家庭合影.png', type: 'file', icon: $r('app.media.png'), size: '6.8 MB', modifiedTime: '2023-05-19' }, { id: 13, name: '产品设计.svg', type: 'file', icon: $r('app.media.svg'), size: '0.5 MB', modifiedTime: '2023-05-18' }, { id: 14, name: '截图.png', type: 'file', icon: $r('app.media.png'), size: '1.2 MB', modifiedTime: '2023-05-17' }, { id: 15, name: '头像.jpg', type: 'file', icon: $r('app.media.jpg'), size: '0.8 MB', modifiedTime: '2023-05-16' } ) } // 根据当前分类过滤文件 if (this.currentCategory !== '全部文件') { return files.filter(file => { switch (this.currentCategory) { case '图片': return file.type === 'folder' || file.name.endsWith('.jpg') || file.name.endsWith('.png') || file.name.endsWith('.svg') case '视频': return file.type === 'folder' || file.name.endsWith('.mp4') || file.name.endsWith('.avi') || file.name.endsWith('.mov') case '音乐': return file.type === 'folder' || file.name.endsWith('.mp3') || file.name.endsWith('.wav') || file.name.endsWith('.flac') case '文档': return file.type === 'folder' || file.name.endsWith('.docx') || file.name.endsWith('.txt') || file.name.endsWith('.xlsx') || file.name.endsWith('.pptx') || file.name.endsWith('.pdf') case '下载': return this.currentPath === '/下载' case '收藏': // 在实际应用中,这里应该返回收藏的文件 return false case '回收站': // 在实际应用中,这里应该返回回收站中的文件 return false default: return true } }) } return files } // 打开文件 private openFile(file: FileItem) { // 在实际应用中,这里应该根据文件类型打开相应的应用 console.info(`打开文件: ${file.name}`) } // 切换文件选中状态 private toggleFileSelection(file: FileItem, isSelected: boolean) { if (isSelected) { // 添加到选中列表 if (!this.selectedFiles.some(selectedFile => selectedFile.id === file.id)) { this.selectedFiles.push(file) } } else { // 从选中列表中移除 this.selectedFiles = this.selectedFiles.filter(selectedFile => selectedFile.id !== file.id) } } }

在这段代码中,我们定义了一个FileManager组件,作为应用的主组件。该组件包含多个状态变量,如当前选中的分类、当前路径、文件列表、选中的文件、视图模式、是否多选模式、当前主题、是否暗色模式和当前断点。

aboutToAppear生命周期函数中,我们初始化主题、监听系统主题变化并加载文件列表。

build方法中,我们渲染应用标题栏和主内容区。应用标题栏包含应用名称和主题切换按钮。主内容区使用ColumnSplit组件分为侧边栏和文件区域。侧边栏使用Sidebar组件显示分类列表。文件区域包含路径导航和文件列表,分别使用PathNavigationFileList组件显示。

我们定义了多个方法来实现各种功能,如获取ColumnSplit布局权重、更新主题、获取主题颜色、更新路径段、导航到指定路径段、导航到文件夹、返回上级目录、加载文件列表、生成示例文件、打开文件和切换文件选中状态。

性能优化

随着文件数量的增加,文件管理器的性能可能会下降。我们需要采取一些措施来优化性能。

1. 虚拟列表

使用虚拟列表可以只渲染可见区域的文件项,减少DOM节点数量,提高渲染性能。

// 在FileList组件中使用LazyForEach代替ForEach @Component struct FileList { // 其他代码省略 build() { Column() { // 工具栏省略 // 文件视图 if (this.viewMode === 'grid') { // 网格视图 Grid() { LazyForEach(new VirtualDataSource(this.files), (file: FileItem) => { GridItem() { this.FileItem(file) } }, (file: FileItem) => file.id.toString()) } .columnsTemplate('1fr 1fr 1fr 1fr') .columnsGap(10) .rowsGap(10) .width('100%') .layoutWeight(1) } else { // 列表视图 List() { LazyForEach(new VirtualDataSource(this.files), (file: FileItem) => { ListItem() { this.FileItem(file) } .margin({ bottom: 10 }) }, (file: FileItem) => file.id.toString()) } .width('100%') .layoutWeight(1) } } // 其他代码省略 } // 其他代码省略 } // 虚拟数据源 class VirtualDataSource implements IDataSource { private files: FileItem[] constructor(files: FileItem[]) { this.files = files } totalCount(): number { return this.files.length } getData(index: number): FileItem { return this.files[index] } registerDataChangeListener(listener: DataChangeListener): void { // 注册数据变化监听器 } unregisterDataChangeListener(listener: DataChangeListener): void { // 注销数据变化监听器 } }

在这段代码中,我们定义了一个VirtualDataSource类,实现了IDataSource接口,用于提供虚拟列表的数据源。在FileList组件中,我们使用LazyForEach代替ForEach,并传入VirtualDataSource实例作为数据源。这样,只有可见区域的文件项会被渲染,大大减少了DOM节点数量,提高了渲染性能。

2. 懒加载

使用懒加载可以分批加载文件,减少初始加载时间。

// 状态变量 @State isLoading: boolean = false @State hasMoreFiles: boolean = true @State pageSize: number = 20 @State currentPage: number = 1 // 加载文件列表 private loadFiles() { // 重置分页 this.currentPage = 1 this.hasMoreFiles = true // 加载第一页 this.loadFilesPage() } // 加载更多文件 private loadMoreFiles() { if (this.isLoading || !this.hasMoreFiles) { return } this.currentPage++ this.loadFilesPage() } // 加载文件页 private loadFilesPage() { this.isLoading = true // 在实际应用中,这里应该从文件系统或数据库中分页加载文件 // 这里简化为生成一些示例文件 const newFiles = this.generateSampleFiles() // 模拟网络延迟 setTimeout(() => { if (this.currentPage === 1) { // 第一页,替换文件列表 this.files = newFiles } else { // 其他页,追加到文件列表 this.files = [...this.files, ...newFiles] } // 判断是否还有更多文件 this.hasMoreFiles = newFiles.length === this.pageSize this.isLoading = false }, 500) } // 在文件列表底部添加加载更多按钮或加载中指示器 if (this.isLoading) { // 加载中指示器 Row() { LoadingProgress() .width(24) .height(24) .margin({ right: 10 }) Text('加载中...') .fontSize(14) .fontColor(this.themeColors.secondaryTextColor) } .width('100%') .justifyContent(FlexAlign.Center) .margin({ top: 10, bottom: 10 }) } else if (this.hasMoreFiles) { // 加载更多按钮 Button('加载更多') .fontSize(14) .width('100%') .height(40) .backgroundColor(this.themeColors.cardBackgroundColor) .fontColor(this.themeColors.primaryColor) .borderRadius(5) .margin({ top: 10 }) .onClick(() => { this.loadMoreFiles() }) }

在这段代码中,我们添加了四个状态变量:isLoadinghasMoreFilespageSizecurrentPage。这些变量用于控制文件的分页加载。

我们修改了loadFiles方法,将其拆分为loadFilesloadMoreFilesloadFilesPage三个方法。loadFiles方法用于重置分页并加载第一页;loadMoreFiles方法用于加载下一页;loadFilesPage方法用于加载指定页的文件。

在文件列表底部,我们根据当前状态显示不同的内容。如果正在加载,则显示加载中指示器;如果还有更多文件,则显示加载更多按钮。

3. 缓存

使用缓存可以避免重复加载相同的文件列表。

// 文件列表缓存 private fileListCache: Map<string, FileItem[]> = new Map() // 加载文件列表 private loadFiles() { // 生成缓存键 const cacheKey = `${this.currentCategory}_${this.currentPath}` // 检查缓存 if (this.fileListCache.has(cacheKey)) { // 使用缓存 this.files = this.fileListCache.get(cacheKey) || [] return } // 缓存未命中,加载文件 this.isLoading = true // 在实际应用中,这里应该从文件系统或数据库中加载文件 // 这里简化为生成一些示例文件 const newFiles = this.generateSampleFiles() // 模拟网络延迟 setTimeout(() => { this.files = newFiles // 更新缓存 this.fileListCache.set(cacheKey, newFiles) this.isLoading = false }, 500) } // 清除缓存 private clearCache() { this.fileListCache.clear() } // 在文件操作后清除相关缓存 private createFolder(name: string) { // 创建文件夹的代码省略 // 清除当前路径的缓存 const cacheKey = `${this.currentCategory}_${this.currentPath}` this.fileListCache.delete(cacheKey) }

在这段代码中,我们添加了一个fileListCache变量,用于缓存文件列表。我们修改了loadFiles方法,首先检查缓存,如果缓存命中,则直接使用缓存中的文件列表;否则,加载文件并更新缓存。

我们还添加了一个clearCache方法,用于清除所有缓存,以及在文件操作后清除相关缓存的代码。

小结

在本教程中,我们详细讲解了文件管理器的高级布局技巧和组件封装,包括自适应布局、主题切换、组件封装和性能优化等高级特性。通过这些技巧,我们可以打造出一个专业、易用且易于维护的文件管理器。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论