在这里插入图片描述

点击相册卡片后,需要进入相册详情页查看里面的照片。

今天来实现这个页面,主要是展示照片网格和相册信息。

页面接收参数

相册详情页需要接收一个相册对象:

class AlbumDetailScreen extends StatelessWidget {
  final AlbumModel album;

  const AlbumDetailScreen({super.key, required this.album});

通过构造函数接收AlbumModel对象。

这个对象包含相册的名称、分类、描述等信息。

从上一个页面跳转时传过来。

AlbumDetailScreen是个StatelessWidget,因为页面本身不需要管理状态。album参数用required标记,表示必须传入,不能为null。从相册列表页跳转时,会把点击的相册对象传过来。这个对象包含id、name、category、description、photoCount等字段,详情页会用这些信息展示相册的基本信息和照片列表。

页面基础结构

搭建页面框架:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(album.name),
        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => EditAlbumScreen(album: album)),
            ),
          ),

标题直接用相册名称,用户一眼就知道在看哪个相册。

右上角放编辑按钮,点击跳转到编辑页面。

编辑页面可以修改相册名称、分类、描述等。

AppBar的title用Text(album.name),动态显示相册名称。这样用户打开页面就知道当前在查看哪个相册,不需要额外的提示。actions数组包含两个按钮,第一个是编辑按钮,用edit图标。点击后跳转到EditAlbumScreen,把album对象传过去,编辑页面可以预填充当前的信息,用户修改后保存。

          PopupMenuButton<String>(
            onSelected: (value) {
              if (value == 'delete') {
                _showDeleteDialog(context);
              }
            },
            itemBuilder: (context) => [
              const PopupMenuItem(value: 'delete', child: Text('删除相册')),
            ],
          ),
        ],
      ),

还有一个更多菜单,用PopupMenuButton实现。

目前只有删除功能,点击后弹出确认对话框。

后续可以在这里加更多功能,比如分享、导出等。

PopupMenuButton是Material Design的弹出菜单组件,点击后显示一个下拉菜单。onSelected回调接收选中的值,这里判断如果是’delete’,调用_showDeleteDialog显示删除确认对话框。itemBuilder返回菜单项列表,目前只有一个"删除相册"选项。后续可以添加更多选项,比如"分享相册"、“导出照片”、"设为封面"等。这种设计把不常用的操作收起来,保持界面简洁。

主体内容区域

主体用CustomScrollView实现:

      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final photos = provider.getPhotosByAlbum(album.id);
          return CustomScrollView(
            slivers: [
              SliverToBoxAdapter(
                child: _buildAlbumInfo(),
              ),

Consumer监听数据变化,照片增删后会自动刷新。

通过getPhotosByAlbum方法获取当前相册的照片列表。

第一个Sliver放相册信息区域。

Consumer监听AlbumProvider的变化,当照片被添加、删除或修改时,页面会自动刷新。provider.getPhotosByAlbum(album.id)查询当前相册的所有照片,返回List。CustomScrollView是一个高级的滚动组件,可以包含多个Sliver子组件,每个Sliver可以有不同的滚动行为。SliverToBoxAdapter把普通Widget包装成Sliver,这里用来显示相册信息区域。

照片网格展示

根据照片数量显示不同内容:

              SliverPadding(
                padding: EdgeInsets.all(16.w),
                sliver: photos.isEmpty
                    ? SliverToBoxAdapter(
                        child: Center(
                          child: Column(
                            children: [
                              SizedBox(height: 50.h),
                              Icon(Icons.photo_library, size: 64.sp, color: Colors.grey[300]),
                              SizedBox(height: 16.h),
                              Text('暂无照片', style: TextStyle(color: Colors.grey, fontSize: 16.sp)),
                            ],
                          ),
                        ),
                      )

如果没有照片,显示空状态提示。

用一个大图标加文字,比空白页面友好。

提示用户这个相册还没有照片。

SliverPadding给Sliver添加内边距,这里设为16像素。如果photos列表为空,显示空状态。photo_library图标表示"照片库",尺寸64像素,颜色用浅灰色。下方显示"暂无照片"文字,也用灰色。这种空状态设计友好,让用户知道不是出错了,只是相册还没有照片。可以加个副标题,比如"点击右下角+号添加照片",引导用户下一步操作。

                    : SliverGrid(
                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 3,
                          mainAxisSpacing: 4.w,
                          crossAxisSpacing: 4.w,
                        ),
                        delegate: SliverChildBuilderDelegate(
                          (context, index) => _buildPhotoItem(context, photos[index]),
                          childCount: photos.length,
                        ),
                      ),
              ),
            ],
          );
        },
      ),
    );
  }

有照片时用三列网格展示。

间距设成4,照片之间紧凑一些,像相册的感觉。

SliverChildBuilderDelegate按需构建,性能更好。

SliverGrid是网格布局的Sliver版本,gridDelegate设置网格参数。crossAxisCount设为3,每行显示3张照片。mainAxisSpacing和crossAxisSpacing都设为4像素,照片之间有细小的间距,既能区分不同照片,又保持紧凑的布局,像真实相册的感觉。delegate用SliverChildBuilderDelegate,这是懒加载的构建方式,只构建可见区域的照片,性能好。childCount是照片数量,builder函数调用_buildPhotoItem构建每张照片。

相册信息区域

显示相册的分类和描述:

  Widget _buildAlbumInfo() {
    return Container(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
                decoration: BoxDecoration(
                  color: const Color(0xFFE91E63).withOpacity(0.1),
                  borderRadius: BorderRadius.circular(16.r),
                ),
                child: Text(
                  album.category,
                  style: TextStyle(
                    color: const Color(0xFFE91E63),
                    fontSize: 12.sp,
                  ),
                ),
              ),

分类用标签的形式显示,粉色背景加粉色文字。

圆角16,看起来像一个小标签。

比如"旅行"、"生日"这样的分类。

_buildAlbumInfo构建相册信息区域。Container用16像素的padding,Column垂直排列分类和描述。分类标签用Container包装Text,padding设为水平12像素、垂直4像素,让标签有一定的大小。decoration设置背景色和圆角,背景色用粉色10%透明度,圆角16像素,看起来像一个圆润的小标签。文字用粉色,字号12像素。这种标签设计在很多App中都能看到,视觉上很清晰。

              SizedBox(width: 8.w),
              Text(
                '${album.photoCount}张照片',
                style: TextStyle(
                  color: Colors.grey,
                  fontSize: 12.sp,
                ),
              ),
            ],
          ),

分类旁边显示照片数量。

用灰色小字,作为辅助信息。

分类标签右边用SizedBox添加8像素间距,然后显示照片数量。格式是"X张照片",字号12像素,颜色用灰色,作为辅助信息。这个数量是实时的,如果用户添加或删除照片,这里会自动更新。

          if (album.description.isNotEmpty) ...[
            SizedBox(height: 12.h),
            Text(
              album.description,
              style: TextStyle(
                fontSize: 14.sp,
                color: Colors.grey[700],
              ),
            ),
          ],
        ],
      ),
    );
  }

如果有描述就显示,没有就不显示。

if加展开运算符...[]来实现条件渲染。

描述用深灰色,比标签颜色深一点。

如果album.description不为空,显示描述文字。用if表达式加展开运算符…[],这是Dart的语法糖,可以在列表中条件性地添加元素。如果条件为true,[]里的元素会被展开到外层列表;如果为false,什么都不添加。这里如果有描述,添加SizedBox和Text两个Widget;如果没有描述,什么都不添加。描述文字用14像素字号,颜色用grey[700],比标签的灰色深一点,让描述更醒目。

照片项组件

每张照片的展示:

  Widget _buildPhotoItem(BuildContext context, PhotoModel photo) {
    return InkWell(
      onTap: () => Navigator.push(
        context,
        MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
      ),
      child: Stack(
        fit: StackFit.expand,
        children: [
          Container(
            color: _getColorForPhoto(photo.url),
            child: Center(
              child: Icon(Icons.photo, color: Colors.white.withOpacity(0.5), size: 32.sp),
            ),
          ),

点击照片跳转到照片详情页。

Stack实现层叠布局,底层是照片,上层是收藏标记。

这里用颜色块代替真实图片,加载更快。

_buildPhotoItem构建单张照片的Widget。InkWell提供点击效果,onTap跳转到PhotoDetailScreen查看照片详情。Stack实现层叠布局,fit设为expand让子组件填满整个空间。底层是Container,用颜色块模拟照片。_getColorForPhoto根据照片URL生成颜色,保证同一张照片的颜色固定。Center让图标居中显示,Icons.photo是照片图标,颜色用白色50%透明度,尺寸32像素,作为占位符。实际项目中应该用Image.network或Image.file加载真实图片。

          if (photo.isFavorite)
            Positioned(
              top: 4.w,
              right: 4.w,
              child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
            ),
        ],
      ),
    );
  }

如果照片被收藏了,右上角显示红色爱心。

Positioned定位到右上角。

这样用户一眼就能看出哪些是收藏的照片。

如果photo.isFavorite为true,在Stack上层添加收藏标记。Positioned定位到右上角,top和right都设为4像素。Icons.favorite是实心爱心图标,颜色用红色,尺寸20像素。这个标记是只读的,不能点击取消收藏,因为详情页的定位是展示,取消收藏的操作应该在照片详情页进行。这种设计让功能职责更清晰,避免用户误操作。

颜色生成方法

根据照片URL生成颜色:

  Color _getColorForPhoto(String url) {
    final colors = [
      Colors.pink[200]!,
      Colors.purple[200]!,
      Colors.blue[200]!,
      Colors.teal[200]!,
      Colors.green[200]!,
      Colors.orange[200]!,
    ];
    return colors[url.hashCode % colors.length];
  }

准备了6种浅色,用哈希值取模选择。

每张照片颜色不同,但同一张照片每次打开颜色固定。

浅色系看起来比较柔和。

_getColorForPhoto方法生成照片的占位颜色。colors数组包含6种颜色,都是200色阶,比较淡,作为背景不会太抢眼。url.hashCode获取字符串的哈希值,是个整数。对colors.length取模,结果在0到5之间,用作数组索引。这样同一个URL总是得到相同的颜色,不同URL的颜色分布比较均匀。哈希算法保证了颜色的随机性和一致性,用户每次打开页面看到的颜色都一样,不会闪烁变化。实际项目中用真实图片后,这个方法就不需要了,但在开发阶段用颜色块能快速验证布局效果。

删除确认对话框

删除相册前需要确认:

  void _showDeleteDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('删除相册'),
        content: const Text('确定要删除这个相册吗?相册中的所有照片也会被删除。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),

AlertDialog弹出确认框。

提示用户删除相册会同时删除里面的照片。

取消按钮关闭对话框。

_showDeleteDialog显示删除确认对话框。showDialog是Flutter的对话框API,builder返回AlertDialog。title显示"删除相册",content显示详细说明,特别强调"相册中的所有照片也会被删除",让用户明白这是个危险操作。actions数组包含两个按钮,第一个是取消按钮,点击后调用Navigator.pop关闭对话框,不执行删除操作。

          TextButton(
            onPressed: () {
              context.read<AlbumProvider>().deleteAlbum(album.id);
              Navigator.pop(context);
              Navigator.pop(context);
            },
            child: const Text('删除', style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

确认删除后调用Provider的deleteAlbum方法。

然后关闭对话框,再返回上一页。

删除按钮用红色,提醒用户这是危险操作。

第二个按钮是删除按钮,点击后执行删除操作。context.read()获取Provider实例,调用deleteAlbum方法删除相册。这个方法会从数据库中删除相册记录,同时删除相册中的所有照片。删除完成后,第一个Navigator.pop关闭对话框,第二个Navigator.pop返回上一页(相册列表页)。删除按钮的文字用红色,这是Material Design的惯例,红色表示危险操作,提醒用户谨慎点击。

添加照片功能

在页面底部添加悬浮按钮:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(album.name),
        actions: [
          IconButton(
            icon: const Icon(Icons.edit),
            onPressed: () => Navigator.push(
              context,
              MaterialPageRoute(builder: (_) => EditAlbumScreen(album: album)),
            ),
          ),
          PopupMenuButton<String>(
            onSelected: (value) {
              if (value == 'delete') {
                _showDeleteDialog(context);
              } else if (value == 'share') {
                _shareAlbum(context);
              }
            },
            itemBuilder: (context) => [
              const PopupMenuItem(value: 'share', child: Text('分享相册')),
              const PopupMenuItem(value: 'delete', child: Text('删除相册')),
            ],
          ),
        ],
      ),
      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final photos = provider.getPhotosByAlbum(album.id);
          return CustomScrollView(
            slivers: [
              SliverToBoxAdapter(
                child: _buildAlbumInfo(),
              ),
              SliverPadding(
                padding: EdgeInsets.all(16.w),
                sliver: photos.isEmpty
                    ? SliverToBoxAdapter(
                        child: Center(
                          child: Column(
                            children: [
                              SizedBox(height: 50.h),
                              Icon(Icons.photo_library, size: 64.sp, color: Colors.grey[300]),
                              SizedBox(height: 16.h),
                              Text('暂无照片', style: TextStyle(color: Colors.grey, fontSize: 16.sp)),
                              SizedBox(height: 8.h),
                              Text(
                                '点击右下角+号添加照片',
                                style: TextStyle(color: Colors.grey[400], fontSize: 14.sp),
                              ),
                            ],
                          ),
                        ),
                      )
                    : SliverGrid(
                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 3,
                          mainAxisSpacing: 4.w,
                          crossAxisSpacing: 4.w,
                        ),
                        delegate: SliverChildBuilderDelegate(
                          (context, index) => _buildPhotoItem(context, photos[index]),
                          childCount: photos.length,
                        ),
                      ),
              ),
            ],
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(
          context,
          MaterialPageRoute(
            builder: (_) => AddPhotoScreen(albumId: album.id),
          ),
        ),
        child: const Icon(Icons.add),
      ),
    );
  }

  void _shareAlbum(BuildContext context) {
    // 实现分享功能
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('分享功能开发中...')),
    );
  }
}

FloatingActionButton是悬浮操作按钮,点击后跳转到AddPhotoScreen添加照片。albumId参数传入当前相册的ID,这样新添加的照片会自动归入这个相册。空状态提示也更新了,加了"点击右下角+号添加照片"的引导文字,帮助用户快速上手。PopupMenuButton也增加了"分享相册"选项,虽然功能还没实现,但UI框架已经搭好了。_shareAlbum方法目前只是显示一个SnackBar提示,实际项目中可以调用系统分享API,让用户把相册分享给其他人。

照片长按选择功能

支持长按照片进入多选模式:

class _AlbumDetailScreenState extends State<AlbumDetailScreen> {
  bool _isSelectionMode = false;
  final Set<String> _selectedPhotoIds = {};

  Widget _buildPhotoItem(BuildContext context, PhotoModel photo) {
    final isSelected = _selectedPhotoIds.contains(photo.id);
    
    return InkWell(
      onTap: () {
        if (_isSelectionMode) {
          setState(() {
            if (isSelected) {
              _selectedPhotoIds.remove(photo.id);
              if (_selectedPhotoIds.isEmpty) {
                _isSelectionMode = false;
              }
            } else {
              _selectedPhotoIds.add(photo.id);
            }
          });
        } else {
          Navigator.push(
            context,
            MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
          );
        }
      },
      onLongPress: () {
        setState(() {
          _isSelectionMode = true;
          _selectedPhotoIds.add(photo.id);
        });
      },
      child: Stack(
        fit: StackFit.expand,
        children: [
          Container(
            color: _getColorForPhoto(photo.url),
            child: Center(
              child: Icon(Icons.photo, color: Colors.white.withOpacity(0.5), size: 32.sp),
            ),
          ),
          if (photo.isFavorite)
            Positioned(
              top: 4.w,
              right: 4.w,
              child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
            ),
          if (_isSelectionMode)
            Positioned.fill(
              child: Container(
                color: isSelected ? Colors.blue.withOpacity(0.3) : Colors.transparent,
                child: isSelected
                    ? const Align(
                        alignment: Alignment.topRight,
                        child: Padding(
                          padding: EdgeInsets.all(8.0),
                          child: Icon(Icons.check_circle, color: Colors.blue, size: 24),
                        ),
                      )
                    : null,
              ),
            ),
        ],
      ),
    );
  }

长按照片进入选择模式,再次点击可以选中或取消选中照片。选中的照片会显示蓝色半透明遮罩和勾选图标。_isSelectionMode标记当前是否处于选择模式,_selectedPhotoIds存储已选中的照片ID。

在选择模式下,点击照片不再跳转详情页,而是切换选中状态。如果取消所有选中,自动退出选择模式。这种交互方式在相册类App中很常见,用户体验好。

批量操作工具栏

选择模式下显示底部工具栏:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _isSelectionMode
          ? AppBar(
              leading: IconButton(
                icon: const Icon(Icons.close),
                onPressed: () {
                  setState(() {
                    _isSelectionMode = false;
                    _selectedPhotoIds.clear();
                  });
                },
              ),
              title: Text('已选择${_selectedPhotoIds.length}张'),
              actions: [
                TextButton(
                  onPressed: () {
                    setState(() {
                      final provider = context.read<AlbumProvider>();
                      final photos = provider.getPhotosByAlbum(widget.album.id);
                      _selectedPhotoIds.addAll(photos.map((p) => p.id));
                    });
                  },
                  child: const Text('全选', style: TextStyle(color: Colors.white)),
                ),
              ],
            )
          : AppBar(
              title: Text(widget.album.name),
              actions: [
                IconButton(
                  icon: const Icon(Icons.edit),
                  onPressed: () => Navigator.push(
                    context,
                    MaterialPageRoute(builder: (_) => EditAlbumScreen(album: widget.album)),
                  ),
                ),
                PopupMenuButton<String>(
                  onSelected: (value) {
                    if (value == 'delete') {
                      _showDeleteDialog(context);
                    } else if (value == 'share') {
                      _shareAlbum(context);
                    }
                  },
                  itemBuilder: (context) => [
                    const PopupMenuItem(value: 'share', child: Text('分享相册')),
                    const PopupMenuItem(value: 'delete', child: Text('删除相册')),
                  ],
                ),
              ],
            ),
      body: Consumer<AlbumProvider>(
        builder: (context, provider, _) {
          final photos = provider.getPhotosByAlbum(widget.album.id);
          return CustomScrollView(
            slivers: [
              if (!_isSelectionMode)
                SliverToBoxAdapter(
                  child: _buildAlbumInfo(),
                ),
              SliverPadding(
                padding: EdgeInsets.all(16.w),
                sliver: photos.isEmpty
                    ? SliverToBoxAdapter(
                        child: Center(
                          child: Column(
                            children: [
                              SizedBox(height: 50.h),
                              Icon(Icons.photo_library, size: 64.sp, color: Colors.grey[300]),
                              SizedBox(height: 16.h),
                              Text('暂无照片', style: TextStyle(color: Colors.grey, fontSize: 16.sp)),
                              SizedBox(height: 8.h),
                              Text(
                                '点击右下角+号添加照片',
                                style: TextStyle(color: Colors.grey[400], fontSize: 14.sp),
                              ),
                            ],
                          ),
                        ),
                      )
                    : SliverGrid(
                        gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 3,
                          mainAxisSpacing: 4.w,
                          crossAxisSpacing: 4.w,
                        ),
                        delegate: SliverChildBuilderDelegate(
                          (context, index) => _buildPhotoItem(context, photos[index]),
                          childCount: photos.length,
                        ),
                      ),
              ),
            ],
          );
        },
      ),
      bottomNavigationBar: _isSelectionMode
          ? BottomAppBar(
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                children: [
                  IconButton(
                    icon: const Icon(Icons.favorite),
                    onPressed: _selectedPhotoIds.isEmpty ? null : _batchAddToFavorites,
                    tooltip: '添加到收藏',
                  ),
                  IconButton(
                    icon: const Icon(Icons.drive_file_move),
                    onPressed: _selectedPhotoIds.isEmpty ? null : _batchMovePhotos,
                    tooltip: '移动到其他相册',
                  ),
                  IconButton(
                    icon: const Icon(Icons.share),
                    onPressed: _selectedPhotoIds.isEmpty ? null : _batchSharePhotos,
                    tooltip: '分享',
                  ),
                  IconButton(
                    icon: const Icon(Icons.delete),
                    onPressed: _selectedPhotoIds.isEmpty ? null : _batchDeletePhotos,
                    tooltip: '删除',
                  ),
                ],
              ),
            )
          : null,
      floatingActionButton: _isSelectionMode
          ? null
          : FloatingActionButton(
              onPressed: () => Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => AddPhotoScreen(albumId: widget.album.id),
                ),
              ),
              child: const Icon(Icons.add),
            ),
    );
  }

选择模式下,AppBar显示已选数量和全选按钮,左边的关闭按钮退出选择模式。底部显示工具栏,提供收藏、移动、分享、删除四个批量操作。

如果没有选中任何照片,工具栏按钮会禁用(onPressed设为null)。选择模式下隐藏悬浮按钮,避免界面混乱。这种设计让批量操作变得简单高效。

批量操作实现

实现各种批量操作方法:

  void _batchAddToFavorites() {
    final provider = context.read<AlbumProvider>();
    for (final photoId in _selectedPhotoIds) {
      provider.togglePhotoFavorite(photoId);
    }
    setState(() {
      _isSelectionMode = false;
      _selectedPhotoIds.clear();
    });
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已添加到收藏')),
    );
  }

  void _batchMovePhotos() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('移动到'),
        content: SizedBox(
          width: double.maxFinite,
          child: Consumer<AlbumProvider>(
            builder: (context, provider, _) {
              final albums = provider.albums.where((a) => a.id != widget.album.id).toList();
              return ListView.builder(
                shrinkWrap: true,
                itemCount: albums.length,
                itemBuilder: (context, index) {
                  final album = albums[index];
                  return ListTile(
                    title: Text(album.name),
                    subtitle: Text(album.category),
                    onTap: () {
                      provider.movePhotos(_selectedPhotoIds.toList(), album.id);
                      Navigator.pop(context);
                      setState(() {
                        _isSelectionMode = false;
                        _selectedPhotoIds.clear();
                      });
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('已移动到${album.name}')),
                      );
                    },
                  );
                },
              );
            },
          ),
        ),
      ),
    );
  }

  void _batchSharePhotos() {
    // 实现分享功能
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('分享${_selectedPhotoIds.length}张照片')),
    );
    setState(() {
      _isSelectionMode = false;
      _selectedPhotoIds.clear();
    });
  }

  void _batchDeletePhotos() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('删除照片'),
        content: Text('确定要删除选中的${_selectedPhotoIds.length}张照片吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          TextButton(
            onPressed: () {
              final provider = context.read<AlbumProvider>();
              for (final photoId in _selectedPhotoIds) {
                provider.deletePhoto(photoId);
              }
              Navigator.pop(context);
              setState(() {
                _isSelectionMode = false;
                _selectedPhotoIds.clear();
              });
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('已删除')),
              );
            },
            child: const Text('删除', style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

批量收藏遍历选中的照片ID,调用togglePhotoFavorite切换收藏状态。批量移动弹出对话框,列出其他相册,用户选择目标相册后调用movePhotos方法。

批量分享目前只是显示提示,实际项目中可以调用系统分享API。批量删除弹出确认对话框,显示要删除的数量,确认后遍历删除。所有操作完成后都退出选择模式并清空选中列表。

照片排序功能

支持按不同方式排序照片:

enum PhotoSortType {
  dateDesc,
  dateAsc,
  nameAsc,
  nameDesc,
}

class _AlbumDetailScreenState extends State<AlbumDetailScreen> {
  PhotoSortType _sortType = PhotoSortType.dateDesc;

  List<PhotoModel> _sortPhotos(List<PhotoModel> photos) {
    final sorted = List<PhotoModel>.from(photos);
    switch (_sortType) {
      case PhotoSortType.dateDesc:
        sorted.sort((a, b) => b.createdAt.compareTo(a.createdAt));
        break;
      case PhotoSortType.dateAsc:
        sorted.sort((a, b) => a.createdAt.compareTo(b.createdAt));
        break;
      case PhotoSortType.nameAsc:
        sorted.sort((a, b) => a.name.compareTo(b.name));
        break;
      case PhotoSortType.nameDesc:
        sorted.sort((a, b) => b.name.compareTo(a.name));
        break;
    }
    return sorted;
  }

  void _showSortDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('排序方式'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            RadioListTile<PhotoSortType>(
              title: const Text('最新优先'),
              value: PhotoSortType.dateDesc,
              groupValue: _sortType,
              onChanged: (value) {
                setState(() => _sortType = value!);
                Navigator.pop(context);
              },
            ),
            RadioListTile<PhotoSortType>(
              title: const Text('最早优先'),
              value: PhotoSortType.dateAsc,
              groupValue: _sortType,
              onChanged: (value) {
                setState(() => _sortType = value!);
                Navigator.pop(context);
              },
            ),
            RadioListTile<PhotoSortType>(
              title: const Text('名称升序'),
              value: PhotoSortType.nameAsc,
              groupValue: _sortType,
              onChanged: (value) {
                setState(() => _sortType = value!);
                Navigator.pop(context);
              },
            ),
            RadioListTile<PhotoSortType>(
              title: const Text('名称降序'),
              value: PhotoSortType.nameDesc,
              groupValue: _sortType,
              onChanged: (value) {
                setState(() => _sortType = value!);
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
    );
  }
}

定义PhotoSortType枚举,包含四种排序方式:日期降序、日期升序、名称升序、名称降序。_sortPhotos方法根据当前排序类型对照片列表排序。

_showSortDialog弹出排序选择对话框,用RadioListTile显示单选项。选择后立即应用排序并关闭对话框。在AppBar的actions中添加排序按钮,调用这个方法。

小结

相册详情页主要是展示照片网格和相册信息。

用三列网格展示照片,收藏的照片右上角有爱心标记。

支持编辑和删除相册,删除前有确认对话框,提醒用户这是危险操作。

悬浮按钮提供添加照片的快捷入口,空状态有友好的引导文字。

PopupMenuButton收纳了不常用的操作,保持界面简洁。长按照片进入选择模式,支持批量收藏、移动、分享、删除等操作。

选择模式下AppBar显示已选数量和全选按钮,底部显示操作工具栏。支持按日期或名称排序照片,满足不同查看需求。整个页面的交互流畅,功能完整,是相册App的核心页面之一。


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

Logo

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

更多推荐