在这里插入图片描述

相册多了之后,按分类查看会方便很多。

旅行的放一起,生日的放一起,找起来快多了。

今天来实现分类浏览页面。

页面设计

分类浏览页面用可展开的卡片列表。

每个分类是一张卡片,点击展开显示该分类下的所有相册。

这种设计既能看到全局,又能深入某个分类。

基础结构

页面比较简单,用StatelessWidget

class CategoryScreen extends StatelessWidget {
  const CategoryScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('分类浏览'),
      ),

标题"分类浏览",清晰表达页面功能。

没有复杂的本地状态,StatelessWidget足够用。

数据获取

Consumer监听分类和相册数据:

      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final categories = provider.categories.where((c) => c != '全部').toList();

provider.categories获取所有分类。

过滤掉"全部"这个选项,它不是真正的分类。

转成列表方便后续遍历。

分类列表

ListView.builder构建分类卡片列表:

          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: categories.length,
            itemBuilder: (context, index) {
              final category = categories[index];
              final albumsInCategory = provider.albums.where((a) => a.category == category).toList();
              return _buildCategoryCard(context, category, albumsInCategory);
            },
          );
        },
      ),
    );
  }

遍历每个分类,筛选出该分类下的相册。

把分类名和相册列表传给卡片构建方法。

四周加16的内边距,不贴着屏幕边缘。

分类卡片

ExpansionTile实现可展开的卡片:

  Widget _buildCategoryCard(BuildContext context, String category, List<AlbumModel> albums) {
    return Card(
      margin: EdgeInsets.only(bottom: 16.h),
      child: ExpansionTile(
        leading: Container(
          width: 48.w,
          height: 48.w,
          decoration: BoxDecoration(
            color: _getColorForCategory(category).withOpacity(0.1),
            borderRadius: BorderRadius.circular(8.r),
          ),
          child: Icon(
            _getIconForCategory(category),
            color: _getColorForCategory(category),
          ),
        ),

Card包裹让卡片有阴影效果。

ExpansionTile自带展开收起的动画和箭头图标。

leading放分类图标,背景色是分类颜色的10%透明度。

        title: Text(
          category,
          style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
        ),
        subtitle: Text('${albums.length}个相册'),

标题是分类名,字体稍微加粗。

副标题显示该分类下有多少个相册。

用户不用展开就能知道每个分类的规模。

展开内容

展开后显示该分类下的相册列表:

        children: albums.map((album) => ListTile(
          contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
          title: Text(album.name),
          subtitle: Text('${album.photoCount}张照片'),
          trailing: const Icon(Icons.chevron_right),
          onTap: () => Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => AlbumDetailScreen(album: album)),
          ),
        )).toList(),
      ),
    );
  }

children是展开后显示的内容列表。

每个相册用ListTile展示,包含名称和照片数。

右侧箭头提示可以点击进入。

contentPadding加大左边距,和父级形成层次感。

分类颜色映射

不同分类用不同颜色区分:

  Color _getColorForCategory(String category) {
    switch (category) {
      case '旅行':
        return Colors.blue;
      case '生日':
        return Colors.orange;
      case '节日':
        return Colors.red;
      case '日常':
        return Colors.green;
      case '纪念日':
        return Colors.pink;
      default:
        return Colors.grey;
    }
  }

旅行用蓝色,让人联想到天空和大海。

生日用橙色,温暖喜庆的感觉。

节日用红色,中国传统节日的主色调。

日常用绿色,平和自然。

纪念日用粉色,浪漫温馨。

分类图标映射

每个分类配一个直观的图标:

  IconData _getIconForCategory(String category) {
    switch (category) {
      case '旅行':
        return Icons.flight;
      case '生日':
        return Icons.cake;
      case '节日':
        return Icons.celebration;
      case '日常':
        return Icons.home;
      case '纪念日':
        return Icons.favorite;
      default:
        return Icons.folder;
    }
  }
}

旅行用飞机图标,一目了然。

生日用蛋糕,过生日必备元素。

节日用庆祝图标,喜庆的感觉。

日常用房子,代表家庭日常生活。

纪念日用爱心,表达珍贵的回忆。

ExpansionTile的优势

为什么选择ExpansionTile

ExpansionTile(
  title: Text(category),
  children: [...],
)

自带展开收起动画,不用自己写。

自带箭头图标,会随展开状态旋转。

点击标题区域就能触发,交互区域大。

同时只能展开一个的话,可以用ExpansionPanelList

数据流转

从Provider到页面的数据流:

final albumsInCategory = provider.albums.where((a) => a.category == category).toList();

provider.albums是所有相册的列表。

where过滤出指定分类的相册。

每次构建都会重新计算,保证数据最新。

页面跳转

点击相册跳转到详情页:

onTap: () => Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => AlbumDetailScreen(album: album)),
),

把相册对象传给详情页。

用户可以继续查看相册里的照片。

返回时回到分类浏览页,展开状态会保持。

视觉层次

通过缩进和颜色建立层次感:

contentPadding: EdgeInsets.symmetric(horizontal: 24.w),

子项比父项多8的左边距。

视觉上形成父子关系。

用户能清楚看出哪些相册属于哪个分类。

空分类处理

如果某个分类下没有相册:

final albumsInCategory = provider.albums.where((a) => a.category == category).toList();

过滤结果可能是空列表。

ExpansionTilechildren为空时,展开后什么都不显示。

可以加个判断,显示"暂无相册"的提示。

小结

分类浏览页面用ExpansionTile实现可展开的分类卡片。

颜色和图标让每个分类都有辨识度。

层次分明的布局让用户快速找到想要的相册。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

空分类优化处理

当某个分类下没有相册时,应该给用户友好的提示:

Widget _buildCategoryCard(BuildContext context, String category, List<AlbumModel> albums) {
  return Card(
    margin: EdgeInsets.only(bottom: 16.h),
    child: ExpansionTile(
      leading: Container(
        width: 48.w,
        height: 48.w,
        decoration: BoxDecoration(
          color: _getColorForCategory(category).withOpacity(0.1),
          borderRadius: BorderRadius.circular(8.r),
        ),
        child: Icon(
          _getIconForCategory(category),
          color: _getColorForCategory(category),
        ),
      ),
      title: Text(
        category,
        style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
      ),
      subtitle: Text(
        albums.isEmpty ? '暂无相册' : '${albums.length}个相册',
        style: TextStyle(
          color: albums.isEmpty ? Colors.grey : null,
        ),
      ),
      children: albums.isEmpty
          ? [
              Padding(
                padding: EdgeInsets.all(16.w),
                child: Column(
                  children: [
                    Icon(Icons.photo_album_outlined, 
                        size: 48.sp, color: Colors.grey.shade300),
                    SizedBox(height: 8.h),
                    Text('该分类下还没有相册',
                        style: TextStyle(color: Colors.grey.shade600)),
                    SizedBox(height: 8.h),
                    TextButton(
                      onPressed: () {
                        // 跳转到创建相册页面,并预选该分类
                        Navigator.push(
                          context,
                          MaterialPageRoute(
                            builder: (_) => CreateAlbumScreen(
                              preselectedCategory: category,
                            ),
                          ),
                        );
                      },
                      child: const Text('创建相册'),
                    ),
                  ],
                ),
              ),
            ]
          : albums.map((album) => ListTile(
              contentPadding: EdgeInsets.symmetric(horizontal: 24.w),
              title: Text(album.name),
              subtitle: Text('${album.photoCount}张照片'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => Navigator.push(
                context,
                MaterialPageRoute(builder: (_) => AlbumDetailScreen(album: album)),
              ),
            )).toList(),
    ),
  );
}

空分类显示灰色图标和提示文字,并提供创建相册的快捷入口。这种设计既告知用户当前状态,又引导用户采取行动。副标题文字也会变成灰色,与有内容的分类形成对比。点击"创建相册"按钮会跳转到创建页面,并自动选中当前分类,减少用户操作步骤。

分类统计信息

在分类卡片上显示更丰富的统计信息:

Widget _buildCategoryCard(BuildContext context, String category, List<AlbumModel> albums) {
  final totalPhotos = albums.fold<int>(0, (sum, album) => sum + album.photoCount);
  final latestAlbum = albums.isNotEmpty 
      ? albums.reduce((a, b) => a.createTime.isAfter(b.createTime) ? a : b)
      : null;
  
  return Card(
    margin: EdgeInsets.only(bottom: 16.h),
    child: ExpansionTile(
      leading: Container(
        width: 48.w,
        height: 48.w,
        decoration: BoxDecoration(
          color: _getColorForCategory(category).withOpacity(0.1),
          borderRadius: BorderRadius.circular(8.r),
        ),
        child: Icon(
          _getIconForCategory(category),
          color: _getColorForCategory(category),
        ),
      ),
      title: Text(
        category,
        style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
      ),
      subtitle: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('${albums.length}个相册 · $totalPhotos张照片'),
          if (latestAlbum != null)
            Text(
              '最新: ${latestAlbum.name}',
              style: TextStyle(fontSize: 12.sp, color: Colors.grey.shade600),
            ),
        ],
      ),
      // ... children
    ),
  );
}

统计信息包括相册数量、总照片数和最新相册名称。totalPhotos通过fold方法累加所有相册的照片数。latestAlbum使用reduce找出创建时间最晚的相册。这些信息让用户对每个分类有更全面的了解,不用展开就能看到概况。

分类排序功能

支持按不同方式排序分类:

enum CategorySortType {
  name,        // 按名称排序
  albumCount,  // 按相册数量排序
  photoCount,  // 按照片数量排序
  latest,      // 按最新更新排序
}

class CategoryScreen extends StatefulWidget {
  const CategoryScreen({super.key});

  
  State<CategoryScreen> createState() => _CategoryScreenState();
}

class _CategoryScreenState extends State<CategoryScreen> {
  CategorySortType _sortType = CategorySortType.name;

  List<String> _sortCategories(List<String> categories, AlbumProvider provider) {
    final categoriesWithData = categories.map((category) {
      final albums = provider.albums.where((a) => a.category == category).toList();
      final totalPhotos = albums.fold<int>(0, (sum, album) => sum + album.photoCount);
      final latestTime = albums.isEmpty 
          ? DateTime(2000)
          : albums.map((a) => a.createTime).reduce((a, b) => a.isAfter(b) ? a : b);
      
      return {
        'name': category,
        'albumCount': albums.length,
        'photoCount': totalPhotos,
        'latestTime': latestTime,
      };
    }).toList();

    switch (_sortType) {
      case CategorySortType.name:
        categoriesWithData.sort((a, b) => (a['name'] as String).compareTo(b['name'] as String));
        break;
      case CategorySortType.albumCount:
        categoriesWithData.sort((a, b) => (b['albumCount'] as int).compareTo(a['albumCount'] as int));
        break;
      case CategorySortType.photoCount:
        categoriesWithData.sort((a, b) => (b['photoCount'] as int).compareTo(a['photoCount'] as int));
        break;
      case CategorySortType.latest:
        categoriesWithData.sort((a, b) => (b['latestTime'] as DateTime).compareTo(a['latestTime'] as DateTime));
        break;
    }

    return categoriesWithData.map((data) => data['name'] as String).toList();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('分类浏览'),
        actions: [
          PopupMenuButton<CategorySortType>(
            icon: const Icon(Icons.sort),
            onSelected: (type) => setState(() => _sortType = type),
            itemBuilder: (context) => [
              const PopupMenuItem(
                value: CategorySortType.name,
                child: Text('按名称排序'),
              ),
              const PopupMenuItem(
                value: CategorySortType.albumCount,
                child: Text('按相册数量'),
              ),
              const PopupMenuItem(
                value: CategorySortType.photoCount,
                child: Text('按照片数量'),
              ),
              const PopupMenuItem(
                value: CategorySortType.latest,
                child: Text('按最新更新'),
              ),
            ],
          ),
        ],
      ),
      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final categories = provider.categories.where((c) => c != '全部').toList();
          final sortedCategories = _sortCategories(categories, provider);
          
          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: sortedCategories.length,
            itemBuilder: (context, index) {
              final category = sortedCategories[index];
              final albumsInCategory = provider.albums
                  .where((a) => a.category == category)
                  .toList();
              return _buildCategoryCard(context, category, albumsInCategory);
            },
          );
        },
      ),
    );
  }
}

排序功能让用户可以按不同维度查看分类。按名称排序适合查找特定分类,按数量排序可以看出哪些分类内容最多,按最新更新排序能快速找到最近活跃的分类。AppBar右上角的排序按钮使用PopupMenuButton实现下拉菜单。

分类搜索功能

添加搜索框快速定位分类:

class _CategoryScreenState extends State<CategoryScreen> {
  CategorySortType _sortType = CategorySortType.name;
  String _searchQuery = '';

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('分类浏览'),
        actions: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => _showSearchDialog(),
          ),
          PopupMenuButton<CategorySortType>(
            icon: const Icon(Icons.sort),
            onSelected: (type) => setState(() => _sortType = type),
            itemBuilder: (context) => [
              // ... 排序选项
            ],
          ),
        ],
      ),
      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          var categories = provider.categories.where((c) => c != '全部').toList();
          
          // 应用搜索过滤
          if (_searchQuery.isNotEmpty) {
            categories = categories.where((c) => 
              c.toLowerCase().contains(_searchQuery.toLowerCase())
            ).toList();
          }
          
          final sortedCategories = _sortCategories(categories, provider);
          
          if (sortedCategories.isEmpty) {
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.search_off, size: 64.sp, color: Colors.grey.shade300),
                  SizedBox(height: 16.h),
                  Text('未找到匹配的分类', style: TextStyle(color: Colors.grey.shade600)),
                ],
              ),
            );
          }
          
          return ListView.builder(
            padding: EdgeInsets.all(16.w),
            itemCount: sortedCategories.length,
            itemBuilder: (context, index) {
              final category = sortedCategories[index];
              final albumsInCategory = provider.albums
                  .where((a) => a.category == category)
                  .toList();
              return _buildCategoryCard(context, category, albumsInCategory);
            },
          );
        },
      ),
    );
  }

  void _showSearchDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('搜索分类'),
        content: TextField(
          autofocus: true,
          decoration: const InputDecoration(
            hintText: '输入分类名称',
            prefixIcon: Icon(Icons.search),
          ),
          onChanged: (value) {
            setState(() => _searchQuery = value);
            Navigator.pop(context);
          },
        ),
        actions: [
          TextButton(
            onPressed: () {
              setState(() => _searchQuery = '');
              Navigator.pop(context);
            },
            child: const Text('清除'),
          ),
        ],
      ),
    );
  }
}

搜索功能支持模糊匹配分类名称。点击搜索图标弹出对话框,输入关键词实时过滤分类列表。如果没有匹配结果,显示友好的空状态提示。清除按钮可以快速重置搜索条件。

分类编辑功能

长按分类卡片可以编辑分类信息:

Widget _buildCategoryCard(BuildContext context, String category, List<AlbumModel> albums) {
  return GestureDetector(
    onLongPress: () => _showCategoryOptions(context, category, albums),
    child: Card(
      margin: EdgeInsets.only(bottom: 16.h),
      child: ExpansionTile(
        // ... 卡片内容
      ),
    ),
  );
}

void _showCategoryOptions(BuildContext context, String category, List<AlbumModel> albums) {
  showModalBottomSheet(
    context: context,
    builder: (context) => SafeArea(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.edit),
            title: const Text('重命名分类'),
            onTap: () {
              Navigator.pop(context);
              _showRenameCategoryDialog(context, category);
            },
          ),
          ListTile(
            leading: const Icon(Icons.palette),
            title: const Text('更改颜色'),
            onTap: () {
              Navigator.pop(context);
              _showColorPicker(context, category);
            },
          ),
          if (albums.isEmpty)
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text('删除分类', style: TextStyle(color: Colors.red)),
              onTap: () {
                Navigator.pop(context);
                _confirmDeleteCategory(context, category);
              },
            ),
        ],
      ),
    ),
  );
}

void _showRenameCategoryDialog(BuildContext context, String oldName) {
  final controller = TextEditingController(text: oldName);
  
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('重命名分类'),
      content: TextField(
        controller: controller,
        autofocus: true,
        decoration: const InputDecoration(
          labelText: '分类名称',
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('取消'),
        ),
        ElevatedButton(
          onPressed: () {
            final newName = controller.text.trim();
            if (newName.isNotEmpty && newName != oldName) {
              context.read<AlbumProvider>().renameCategory(oldName, newName);
              Navigator.pop(context);
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('已将"$oldName"重命名为"$newName"')),
              );
            }
          },
          child: const Text('确定'),
        ),
      ],
    ),
  );
}

长按分类卡片弹出操作菜单,提供重命名、更改颜色、删除等选项。只有空分类才能删除,避免误操作导致数据丢失。重命名功能会更新所有相册的分类字段,保持数据一致性。

分类统计图表

添加可视化图表展示分类分布:

Widget _buildCategoryStats(AlbumProvider provider) {
  final categories = provider.categories.where((c) => c != '全部').toList();
  final categoryData = categories.map((category) {
    final albums = provider.albums.where((a) => a.category == category).toList();
    final photoCount = albums.fold<int>(0, (sum, album) => sum + album.photoCount);
    return {
      'category': category,
      'count': photoCount,
      'color': _getColorForCategory(category),
    };
  }).toList();
  
  final total = categoryData.fold<int>(0, (sum, data) => sum + (data['count'] as int));
  
  return Card(
    margin: EdgeInsets.all(16.w),
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('分类统计', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
          SizedBox(height: 16.h),
          Row(
            children: categoryData.map((data) {
              final percentage = total > 0 ? (data['count'] as int) / total : 0;
              return Expanded(
                flex: (percentage * 100).toInt(),
                child: Container(
                  height: 8.h,
                  color: data['color'] as Color,
                ),
              );
            }).toList(),
          ),
          SizedBox(height: 16.h),
          ...categoryData.map((data) => Padding(
            padding: EdgeInsets.symmetric(vertical: 4.h),
            child: Row(
              children: [
                Container(
                  width: 12.w,
                  height: 12.w,
                  decoration: BoxDecoration(
                    color: data['color'] as Color,
                    shape: BoxShape.circle,
                  ),
                ),
                SizedBox(width: 8.w),
                Text(data['category'] as String),
                const Spacer(),
                Text('${data['count']}张 (${((data['count'] as int) / total * 100).toStringAsFixed(1)}%)'),
              ],
            ),
          )),
        ],
      ),
    ),
  );
}

统计图表使用条形图展示各分类的照片数量占比。每个分类用对应的颜色表示,视觉上一目了然。下方列出详细数据,包括照片数量和百分比。这个组件可以放在列表顶部,让用户对整体分布有直观认识。

性能优化

对于大量分类和相册的情况,需要优化性能:

class _CategoryScreenState extends State<CategoryScreen> {
  final Map<String, List<AlbumModel>> _categoryCache = {};
  
  List<AlbumModel> _getAlbumsForCategory(String category, AlbumProvider provider) {
    if (_categoryCache.containsKey(category)) {
      return _categoryCache[category]!;
    }
    
    final albums = provider.albums.where((a) => a.category == category).toList();
    _categoryCache[category] = albums;
    return albums;
  }
  
  
  void didChangeDependencies() {
    super.didChangeDependencies();
    // 清除缓存,确保数据最新
    _categoryCache.clear();
  }
}

使用缓存避免重复过滤相册列表。每次构建时先检查缓存,如果存在直接返回。当数据变化时清除缓存,确保显示最新数据。这种优化对于有几十个分类和上百个相册的场景效果明显。

总结与技术要点

分类浏览页面通过ExpansionTile实现可展开的分类卡片,配合颜色和图标让每个分类都有辨识度。排序、搜索、统计等功能让用户可以从不同角度查看和管理分类。空状态处理和编辑功能提升了用户体验。性能优化确保在大量数据时依然流畅。

这个页面是家庭相册应用的重要导航入口,帮助用户快速找到想要的相册。通过合理的信息架构和交互设计,我们创建了一个既美观又实用的分类浏览系统。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐