flutter_for_openharmony家庭相册app实战+收藏实现
Flutter收藏页面设计与实现 本文介绍了Flutter收藏页面的开发过程,主要特点包括: 使用StatelessWidget和Consumer实现数据响应式UI 三列网格布局展示收藏照片,支持点击查看详情 完善的空状态处理,包含操作引导 通过Provider管理状态,实现跨页面收藏状态同步 性能优化考虑,包括按需构建和分页加载 细致的用户体验设计,如图标颜色、文字提示等 实现逻辑简单但注重细节

家庭照片那么多,总有几张特别喜欢的。
收藏功能让用户能快速找到这些照片,今天来实现这个页面。
页面定位
收藏页面是一个纯展示页面,显示所有被标记为收藏的照片。
逻辑简单,但细节处理好了用户体验会很不错。
收藏功能是相册App的标配,用户在浏览照片时看到喜欢的,点一下爱心就能收藏。收藏页面把这些照片集中展示,方便用户随时查看。页面设计上要简洁明了,不需要复杂的交互,重点是照片的展示效果。空状态的处理也很重要,要引导新用户知道怎么收藏照片。整个页面的实现虽然简单,但每个细节都要考虑到用户体验。
基础结构
收藏页面没有复杂的状态管理,用StatelessWidget就够了:
class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('我的收藏'),
),
页面标题"我的收藏",简单直接。
没有额外的操作按钮,保持界面清爽。
StatelessWidget配合Consumer就能实现数据响应。
StatelessWidget是无状态组件,适合这种纯展示的页面。虽然页面本身没有状态,但通过Consumer监听Provider的数据变化,当用户在其他页面收藏或取消收藏照片时,这个页面会自动刷新。AppBar只有标题,没有其他按钮,保持界面简洁。这种设计让用户的注意力集中在照片上,不会被其他元素干扰。
数据监听
用Consumer监听收藏数据的变化:
body: Consumer<AlbumProvider>(
builder: (context, provider, _) {
final favorites = provider.favoritePhotos;
favoritePhotos是AlbumProvider里的一个getter,返回所有收藏的照片。当用户在其他页面取消收藏时,这个列表会自动更新。
Consumer会在数据变化时重新构建UI。
Consumer是Provider的核心组件,它监听AlbumProvider的变化。favoritePhotos是一个计算属性,每次访问都会过滤出isFavorite为true的照片。这种设计的好处是数据总是最新的,不需要手动刷新。当用户在照片详情页点击收藏按钮时,Provider会调用notifyListeners通知所有监听者,Consumer收到通知后会重新执行builder函数,UI就更新了。这个过程是自动的,开发者不需要写任何刷新逻辑。
空状态处理
没有收藏时要给用户友好的提示:
if (favorites.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey[300]),
SizedBox(height: 16.h),
Text(
'暂无收藏的照片',
style: TextStyle(color: Colors.grey, fontSize: 16.sp),
),
空心爱心图标暗示"还没有收藏"。
主提示文字告诉用户当前状态。
图标用浅灰色,不会太突兀。
空状态设计是提升用户体验的关键。favorite_border是空心爱心图标,和收藏的实心爱心形成对比,视觉上很直观。图标尺寸64像素,足够大,用户一眼就能看到。颜色用grey[300],是个很浅的灰色,既能引起注意又不会太抢眼。主提示文字"暂无收藏的照片"说明当前状态,让用户知道这不是出错了,只是还没有收藏内容。这种友好的提示比空白页面好得多。
SizedBox(height: 8.h),
Text(
'点击照片上的❤️可以收藏',
style: TextStyle(color: Colors.grey[400], fontSize: 14.sp),
),
],
),
);
}
副提示告诉用户怎么收藏照片,降低学习成本。
用emoji爱心让文字更生动。
字体颜色比主提示更浅,形成层次感。
副提示是引导性文字,告诉用户下一步该做什么。"点击照片上的❤️可以收藏"这句话很直白,新用户看了就知道怎么操作。用emoji爱心代替文字描述,更直观也更生动。字体颜色用grey[400],比主提示的grey更浅,形成视觉层次。这种分层的提示设计,让信息的重要程度一目了然,用户能快速抓住重点。
网格布局
有收藏时用网格展示照片:
return GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 4.w,
crossAxisSpacing: 4.w,
),
itemCount: favorites.length,
三列网格布局,和搜索结果页保持一致。
间距4,照片之间有细小的缝隙。
GridView.builder按需构建,性能更好。
GridView.builder是懒加载的网格组件,只构建可见区域的item,滚动时动态创建和销毁,内存占用可控。crossAxisCount设为3,在手机屏幕上显示3列照片,既不会太小也不会太大。mainAxisSpacing和crossAxisSpacing都设为4,照片之间有4像素的间距,既能区分不同照片,又不会浪费太多空间。padding设为16.w给网格四周留出呼吸空间,避免照片紧贴屏幕边缘。这种布局和搜索结果页保持一致,用户在不同页面看到相同的布局,学习成本更低。
照片卡片
每张照片用Stack叠加收藏标记:
itemBuilder: (context, index) {
final photo = favorites[index];
return InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
),
child: Stack(
fit: StackFit.expand,
children: [
InkWell包裹让整个卡片可点击。点击跳转到照片详情页。
Stack用来叠加照片和收藏图标。
InkWell提供Material Design的水波纹点击效果,让交互更有质感。onTap回调处理点击事件,跳转到PhotoDetailScreen查看照片详情。Stack是层叠布局组件,fit设为expand让子组件填满整个空间。底层是Container显示照片(这里用颜色块模拟),上层用Positioned定位收藏图标。这种层叠设计让收藏标记浮在照片上方,视觉效果更好。实际项目中,Container里应该用Image.network或Image.file加载真实图片,配合缓存机制提升加载速度。
Container(
color: _getColorForPhoto(photo.url),
child: Center(
child: Icon(Icons.photo, color: Colors.white.withOpacity(0.5)),
),
),
底层是照片占位,用颜色块模拟。
实际项目中换成
Image组件加载真实图片。半透明图标作为占位符。
Container的color属性设置背景色,_getColorForPhoto方法根据照片URL生成颜色。Center让图标居中显示,Icons.photo是照片图标,颜色用白色50%透明度,作为占位符。实际项目中这里应该这样写:Image.network(photo.url, fit: BoxFit.cover),fit设为cover让图片填满容器,超出部分裁剪。还可以加loading和error的处理,比如loadingBuilder显示加载动画,errorBuilder显示错误图标。配合cached_network_image这个库,能实现图片缓存,避免重复下载,提升用户体验。
Positioned(
top: 4.w,
right: 4.w,
child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
),
],
),
);
},
);
},
),
);
}
右上角叠加红色实心爱心,表示已收藏。
Positioned精确控制图标位置。图标大小20,不会太大遮挡照片。
Positioned是Stack的定位组件,top和right都设为4.w,让图标距离右上角4像素。Icons.favorite是实心爱心图标,颜色用红色,这是收藏的标准颜色,用户一看就懂。size设为20.sp,不会太大遮挡照片内容,也不会太小看不清。这个收藏标记是只读的,不能点击取消收藏,因为收藏页面的定位是展示,取消收藏的操作应该在照片详情页进行。这种设计让功能职责更清晰,避免用户误操作。
颜色生成方法
照片占位颜色的生成:
Color _getColorForPhoto(String url) {
final colors = [
Colors.pink[200]!,
Colors.purple[200]!,
Colors.blue[200]!,
Colors.teal[200]!,
];
return colors[url.hashCode % colors.length];
}
}
四种柔和的颜色轮换使用。
用URL的哈希值取模,同一张照片颜色固定。
200色阶比较淡,适合做背景。
_getColorForPhoto方法生成照片的占位颜色。colors数组包含四种颜色,都是200色阶,比较淡,作为背景不会太抢眼。url.hashCode获取字符串的哈希值,是个整数。对colors.length取模,结果在0到3之间,用作数组索引。这样同一个URL总是得到相同的颜色,不同URL的颜色分布比较均匀。哈希算法保证了颜色的随机性和一致性,用户每次打开页面看到的颜色都一样,不会闪烁变化。实际项目中用真实图片后,这个方法就不需要了,但在开发阶段用颜色块能快速验证布局效果。
Provider中的实现
AlbumProvider里的favoritePhotosgetter:
List<PhotoModel> get favoritePhotos {
return _photos.where((p) => p.isFavorite).toList();
}
过滤出所有
isFavorite为true的照片。每次访问都会重新计算,保证数据最新。
返回新列表,不会影响原数据。
favoritePhotos是一个getter,不是普通的属性。每次访问都会执行where方法,过滤出isFavorite为true的照片。where返回的是Iterable,调用toList转成List。这种实时计算的方式保证数据总是最新的,不需要维护一个单独的收藏列表。缺点是每次访问都要遍历整个_photos列表,数据量大时可能有性能问题。优化方案是在Provider里维护一个_favoritePhotos列表,当照片的收藏状态变化时同步更新这个列表,这样favoritePhotos直接返回_favoritePhotos,不需要每次都过滤。
收藏状态切换
在照片详情页切换收藏状态:
void toggleFavorite(String photoId) {
final index = _photos.indexWhere((p) => p.id == photoId);
if (index != -1) {
_photos[index] = _photos[index].copyWith(
isFavorite: !_photos[index].isFavorite,
);
notifyListeners();
}
}
找到对应照片,切换
isFavorite状态。
notifyListeners通知所有监听者,收藏页面会自动刷新。用
copyWith创建新对象,符合不可变数据原则。
toggleFavorite方法实现收藏状态的切换。indexWhere查找指定ID的照片,返回索引。如果找到了(index不等于-1),用copyWith创建新对象,isFavorite取反。copyWith是一个常见的模式,创建一个新对象,大部分属性复制原对象,只修改指定的属性。这符合不可变数据的原则,不直接修改原对象,而是创建新对象替换。最后调用notifyListeners通知所有监听者,Consumer收到通知后会重新构建UI。这个方法通常在照片详情页调用,用户点击收藏按钮时触发。
交互流程
用户的典型操作流程:
// 在照片详情页点击收藏按钮
IconButton(
icon: Icon(
currentPhoto.isFavorite ? Icons.favorite : Icons.favorite_border,
color: currentPhoto.isFavorite ? Colors.red : Colors.white,
),
onPressed: () => provider.toggleFavorite(photo.id),
);
收藏按钮根据状态显示不同图标和颜色。
点击后调用
toggleFavorite切换状态。收藏页面会实时更新,不需要手动刷新。
照片详情页的收藏按钮是个IconButton,图标根据currentPhoto.isFavorite动态选择。已收藏显示实心爱心Icons.favorite,颜色用红色;未收藏显示空心爱心Icons.favorite_border,颜色用白色(假设详情页背景是深色)。onPressed回调调用provider.toggleFavorite切换状态。这个操作会触发notifyListeners,所有监听AlbumProvider的Consumer都会重新构建,包括收藏页面。用户在详情页收藏照片后,返回收藏页面就能看到新收藏的照片,不需要下拉刷新或其他手动操作,体验很流畅。
性能优化
当收藏照片很多时,可以考虑分页加载:
return GridView.builder(
itemCount: favorites.length,
itemBuilder: (context, index) {
// 按需构建
},
);
GridView.builder只构建可见区域的item。滚动时动态创建和销毁,内存占用可控。
如果要加载真实图片,还需要配合图片缓存。
GridView.builder的性能优势在于懒加载。itemBuilder只在item进入可见区域时才被调用,创建对应的Widget。滚动时,离开可见区域的item会被销毁,释放内存。这种机制让GridView能处理成千上万的item,内存占用始终保持在合理范围。如果用GridView.count配合children数组,所有item都会被创建,数据量大时会导致内存溢出。加载真实图片时,还需要配合图片缓存库,比如cached_network_image。这个库会把下载的图片缓存到本地,下次加载时直接从缓存读取,不需要重复下载,大大提升加载速度和用户体验。
用户体验细节
几个提升体验的小点:
Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey[300])
空状态图标够大,用户一眼就能看到。
颜色够浅,不会让页面显得很空洞。
空状态设计的关键是让用户知道当前状态,并引导下一步操作。图标尺寸64像素,在手机屏幕上很醒目,用户打开页面第一眼就能看到。颜色用grey[300],是个很浅的灰色,既能引起注意又不会太突兀。如果用深色图标,空状态页面会显得很沉重,给人一种"出错了"的感觉。浅色图标配合友好的提示文字,让用户知道这是正常状态,只是还没有收藏内容,心理上更容易接受。
Text('点击照片上的❤️可以收藏')
引导文字告诉用户下一步该做什么。
新用户不会迷茫,知道怎么开始收藏。
批量管理功能
添加批量取消收藏的功能:
class FavoritesScreen extends StatefulWidget {
const FavoritesScreen({super.key});
State<FavoritesScreen> createState() => _FavoritesScreenState();
}
class _FavoritesScreenState extends State<FavoritesScreen> {
bool _isSelectionMode = false;
final Set<String> _selectedPhotos = {};
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_isSelectionMode ? '已选择 ${_selectedPhotos.length} 项' : '我的收藏'),
actions: [
if (_isSelectionMode) ...[
IconButton(
icon: const Icon(Icons.select_all),
onPressed: () {
final provider = context.read<AlbumProvider>();
setState(() {
if (_selectedPhotos.length == provider.favoritePhotos.length) {
_selectedPhotos.clear();
} else {
_selectedPhotos.addAll(
provider.favoritePhotos.map((p) => p.id),
);
}
});
},
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showBatchDeleteDialog(context),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
_isSelectionMode = false;
_selectedPhotos.clear();
});
},
),
] else
IconButton(
icon: const Icon(Icons.checklist),
onPressed: () {
setState(() {
_isSelectionMode = true;
});
},
),
],
),
批量管理模式用_isSelectionMode标记,_selectedPhotos存储选中的照片ID。进入选择模式后,AppBar标题显示选中数量,右侧显示全选、删除和取消按钮。全选按钮的逻辑是:如果已经全选了,点击后清空选择;如果没全选,点击后选中所有照片。这种切换逻辑很常见,用户体验好。取消按钮退出选择模式,清空选中状态。未进入选择模式时,显示一个清单图标,点击后进入选择模式。
body: Consumer<AlbumProvider>(
builder: (context, provider, _) {
final favorites = provider.favoritePhotos;
if (favorites.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, 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),
),
],
),
);
}
return GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 4.w,
crossAxisSpacing: 4.w,
),
itemCount: favorites.length,
itemBuilder: (context, index) {
final photo = favorites[index];
final isSelected = _selectedPhotos.contains(photo.id);
return _buildPhotoCard(context, photo, isSelected);
},
);
},
),
);
}
GridView的itemBuilder传入isSelected参数,表示当前照片是否被选中。这个状态会影响照片卡片的显示效果,选中的照片会有特殊的视觉反馈。
Widget _buildPhotoCard(BuildContext context, PhotoModel photo, bool isSelected) {
return InkWell(
onTap: () {
if (_isSelectionMode) {
setState(() {
if (isSelected) {
_selectedPhotos.remove(photo.id);
} else {
_selectedPhotos.add(photo.id);
}
});
} else {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => PhotoDetailScreen(photo: photo)),
);
}
},
onLongPress: () {
if (!_isSelectionMode) {
setState(() {
_isSelectionMode = true;
_selectedPhotos.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)),
),
),
if (_isSelectionMode)
Container(
color: isSelected
? const Color(0xFFE91E63).withOpacity(0.3)
: Colors.black.withOpacity(0.1),
),
if (_isSelectionMode)
Positioned(
top: 4.w,
right: 4.w,
child: Container(
width: 24.w,
height: 24.w,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFE91E63) : Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: isSelected ? const Color(0xFFE91E63) : Colors.grey,
width: 2,
),
),
child: isSelected
? const Icon(Icons.check, color: Colors.white, size: 16)
: null,
),
)
else
Positioned(
top: 4.w,
right: 4.w,
child: Icon(Icons.favorite, color: Colors.red, size: 20.sp),
),
],
),
);
}
照片卡片的交互逻辑比较复杂。在选择模式下,点击照片切换选中状态;非选择模式下,点击跳转到详情页。长按照片进入选择模式,并选中当前照片。选择模式下,照片上方叠加一层半透明遮罩,选中的照片用粉色遮罩,未选中的用黑色遮罩。右上角显示选择框,选中的照片显示粉色圆圈加白色对勾,未选中的显示白色圆圈。非选择模式下,右上角显示红色爱心,表示已收藏。这种设计让用户能清楚地看到当前状态和可执行的操作。
void _showBatchDeleteDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('批量取消收藏'),
content: Text('确定要取消收藏选中的 ${_selectedPhotos.length} 张照片吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
final provider = context.read<AlbumProvider>();
for (final photoId in _selectedPhotos) {
provider.toggleFavorite(photoId);
}
Navigator.pop(context);
setState(() {
_isSelectionMode = false;
_selectedPhotos.clear();
});
},
child: const Text('确定', style: TextStyle(color: Colors.red)),
),
],
),
);
}
Color _getColorForPhoto(String url) {
final colors = [
Colors.pink[200]!,
Colors.purple[200]!,
Colors.blue[200]!,
Colors.teal[200]!,
];
return colors[url.hashCode % colors.length];
}
}
批量删除对话框显示选中的照片数量,确认后遍历_selectedPhotos,对每张照片调用toggleFavorite取消收藏。操作完成后退出选择模式,清空选中状态。这种批量操作能显著提升效率,特别是需要清理大量收藏时。
收藏分类展示
按相册分类展示收藏的照片:
Widget _buildCategoryView(List<PhotoModel> favorites) {
final categoryMap = <String, List<PhotoModel>>{};
for (final photo in favorites) {
final category = photo.albumName ?? '未分类';
categoryMap.putIfAbsent(category, () => []).add(photo);
}
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: categoryMap.length,
itemBuilder: (context, index) {
final category = categoryMap.keys.elementAt(index);
final photos = categoryMap[category]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 12.h),
child: Row(
children: [
Container(
width: 4.w,
height: 16.h,
decoration: BoxDecoration(
color: const Color(0xFFE91E63),
borderRadius: BorderRadius.circular(2.r),
),
),
SizedBox(width: 8.w),
Text(
category,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
SizedBox(width: 8.w),
Text(
'${photos.length}张',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
],
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 4.w,
crossAxisSpacing: 4.w,
),
itemCount: photos.length,
itemBuilder: (context, photoIndex) {
final photo = photos[photoIndex];
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),
),
),
),
Positioned(
top: 4.w,
right: 4.w,
child: Icon(
Icons.favorite,
color: Colors.red,
size: 20.sp,
),
),
],
),
);
},
),
SizedBox(height: 24.h),
],
);
},
);
}
分类视图先把收藏的照片按相册分组,用Map存储。遍历favorites,根据albumName分组,没有相册名的归入"未分类"。然后用ListView展示每个分类,每个分类包含标题和照片网格。标题左边有个粉色竖条装饰,显示分类名称和照片数量。照片网格用shrinkWrap和NeverScrollableScrollPhysics,让它不可滚动,高度自适应。这样整个页面只有外层ListView可以滚动,用户体验更好。这种分类展示能让用户快速找到特定相册的收藏照片。
收藏统计信息
在页面顶部显示收藏统计:
Widget _buildStatistics(List<PhotoModel> favorites) {
final categoryCount = <String, int>{};
for (final photo in favorites) {
final category = photo.albumName ?? '未分类';
categoryCount[category] = (categoryCount[category] ?? 0) + 1;
}
return Container(
margin: EdgeInsets.all(16.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: const Color(0xFFE91E63).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem('收藏总数', favorites.length.toString(), Icons.favorite),
Container(width: 1, height: 40.h, color: Colors.grey[300]),
_buildStatItem('相册数', categoryCount.length.toString(), Icons.photo_album),
],
),
if (categoryCount.isNotEmpty) ...[
SizedBox(height: 16.h),
Divider(color: Colors.grey[300]),
SizedBox(height: 12.h),
Wrap(
spacing: 8.w,
runSpacing: 8.h,
children: categoryCount.entries.map((entry) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
),
child: Text(
'${entry.key} ${entry.value}',
style: TextStyle(fontSize: 12.sp),
),
);
}).toList(),
),
],
],
),
);
}
Widget _buildStatItem(String label, String value, IconData icon) {
return Column(
children: [
Icon(icon, color: const Color(0xFFE91E63), size: 32),
SizedBox(height: 8.h),
Text(
value,
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: const Color(0xFFE91E63),
),
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600],
),
),
],
);
}
统计信息卡片显示收藏总数和涉及的相册数。下方用Wrap展示每个相册的收藏数量,每个标签显示相册名称和数量。这种统计信息能让用户对收藏情况有整体的了解,知道哪些相册的照片收藏得多。卡片用浅粉色背景,和主题色呼应。两个统计项用竖线分隔,视觉上更清晰。
小结
收藏页面虽然简单,但空状态处理和视觉细节很重要。
用Consumer监听数据变化,Stack叠加收藏标记。
引导文字帮助新用户快速上手。
批量管理功能让用户能高效地管理收藏,长按进入选择模式,支持全选和批量取消收藏。分类展示让用户能按相册查看收藏的照片,统计信息提供整体概览。这些功能的组合让收藏页面不仅是简单的列表展示,而是一个功能完整的收藏管理工具。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)