通过网盘分享的文件:flutter1.zip
链接: https://pan.baidu.com/s/1jkLZ9mZXjNm0LgP6FTVRzw 提取码: 2t97
标签筛选是内容类App的常见功能,让用户快速切换不同类型的内容。微动漫App的发现页面用标签筛选热门、当季、即将上映、随机推荐等不同类型的动漫。
这篇文章会实现标签筛选功能,讲解 FilterChip 组件的使用、横向滚动列表、筛选状态管理,以及如何让筛选和内容加载联动。

标签筛选的设计思路
标签筛选通常放在页面顶部,横向排列,可以滚动。点击标签切换选中状态,同时加载对应的内容。
视觉设计:选中的标签要有明显的区分,通常用颜色或背景。
交互设计:点击即切换,不需要确认按钮。
状态管理:记录当前选中的标签,切换时重新加载数据。
页面状态定义
dart
import 'package:flutter/material.dart';
import '../services/api_service.dart';
import '../models/anime.dart';
import '../widgets/anime_card.dart';
import '../widgets/shimmer_loading.dart';
class ExploreScreen extends StatefulWidget {
const ExploreScreen({super.key});
@override
State<ExploreScreen> createState() => _ExploreScreenState();
}
class _ExploreScreenState extends State<ExploreScreen> {
int _selectedFilter = 0;
List<Anime> _animes = [];
bool _isLoading = true;
int _currentPage = 1;
final ScrollController _scrollController = ScrollController();
bool _showBackToTop = false;
final List<String> _filters = [
'热门',
'当季',
'即将',
'随机',
];
_selectedFilter 记录当前选中的标签索引,默认选中第一个。
_filters 是标签列表,定义了所有可选的筛选项。
_animes 存储当前筛选条件下的动漫列表。
_isLoading 标记加载状态。
标签列表布局
dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('发现')),
body: Column(
children: [
SizedBox(
height: 50,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8),
itemCount: _filters.length,
itemBuilder: (_, i) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: FilterChip(
label: Text(_filters[i]),
selected: _selectedFilter == i,
onSelected: (selected) {
setState(() {
_selectedFilter = i;
_currentPage = 1;
});
_loadAnimes();
},
),
),
),
),
Expanded(
child: // 内容区域
),
],
),
);
}
Column 垂直排列标签栏和内容区域。
SizedBox 固定标签栏高度为 50。
ListView.builder 横向滚动,scrollDirection: Axis.horizontal。
Expanded 让内容区域占满剩余空间。
FilterChip 组件详解
dart
FilterChip(
label: Text(_filters[i]),
selected: _selectedFilter == i,
onSelected: (selected) {
setState(() {
_selectedFilter = i;
_currentPage = 1;
});
_loadAnimes();
},
)
FilterChip 是 Material Design 的筛选标签组件。
label 是标签文本。
selected 控制是否选中,选中时会有不同的样式。
onSelected 在点击时触发,参数 selected 是新的选中状态。
FilterChip vs ChoiceChip vs Chip
Flutter 提供了几种 Chip 组件:
Chip:基础标签,可以有删除按钮。
FilterChip:筛选标签,有选中/未选中状态,可以多选。
ChoiceChip:选择标签,有选中/未选中状态,通常单选。
ActionChip:动作标签,点击触发动作,没有选中状态。
InputChip:输入标签,可以有头像和删除按钮。
对于单选筛选,FilterChip 和 ChoiceChip 都可以,视觉效果略有不同。
自定义 FilterChip 样式
dart
FilterChip(
label: Text(_filters[i]),
selected: _selectedFilter == i,
selectedColor: Theme.of(context).primaryColor.withOpacity(0.2),
checkmarkColor: Theme.of(context).primaryColor,
labelStyle: TextStyle(
color: _selectedFilter == i
? Theme.of(context).primaryColor
: Colors.grey[700],
),
onSelected: (selected) {
// 处理选中
},
)
selectedColor 是选中时的背景色。
checkmarkColor 是选中时勾选图标的颜色。
labelStyle 可以根据选中状态设置不同的文字样式。
隐藏勾选图标
FilterChip 默认选中时显示勾选图标,可以隐藏:
dart
FilterChip(
label: Text(_filters[i]),
selected: _selectedFilter == i,
showCheckmark: false,
onSelected: (selected) {
// 处理选中
},
)
showCheckmark: false 隐藏勾选图标,只用颜色区分选中状态。
数据加载逻辑
dart
Future<void> _loadAnimes() async {
setState(() => _isLoading = true);
try {
List<Anime> animes = [];
switch (_selectedFilter) {
case 0:
animes = await ApiService.getTopAnime(page: _currentPage);
break;
case 1:
animes = await ApiService.getSeasonalAnime(page: _currentPage);
break;
case 2:
animes = await ApiService.getUpcomingAnime(page: _currentPage);
break;
case 3:
final random = await ApiService.getRandomAnime();
animes = random != null ? [random] : [];
break;
}
setState(() {
_animes = animes;
_isLoading = false;
});
} catch (e) {
setState(() => _isLoading = false);
}
}
根据 _selectedFilter 调用不同的 API。
switch 语句处理不同的筛选条件。
加载前设置 _isLoading = true,加载完成后设置为 false。
内容区域
dart
Expanded(
child: _isLoading
? const ShimmerLoading(itemCount: 8, isGrid: true)
: GridView.builder(
controller: _scrollController,
padding: const EdgeInsets.all(12),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _animes.length,
itemBuilder: (_, i) => AnimeCard(anime: _animes[i]),
),
),
加载中显示骨架屏,加载完成显示网格。
GridView.builder 展示动漫卡片,2 列布局。
回到顶部按钮
切换筛选后内容会变,可能需要回到顶部:
dart
@override
void initState() {
super.initState();
_loadAnimes();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.offset > 300 && !_showBackToTop) {
setState(() => _showBackToTop = true);
} else if (_scrollController.offset <= 300 && _showBackToTop) {
setState(() => _showBackToTop = false);
}
}
void _scrollToTop() {
_scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
监听滚动位置,超过 300 像素时显示回到顶部按钮。
animateTo 平滑滚动到顶部。
dart
floatingActionButton: _showBackToTop
? FloatingActionButton(
mini: true,
onPressed: _scrollToTop,
child: const Icon(Icons.arrow_upward),
)
: null,
mini: true 使用小尺寸的 FAB。
多选筛选
如果需要多选,可以用 Set 存储选中的标签:
dart
Set<int> _selectedFilters = {0};
FilterChip(
label: Text(_filters[i]),
selected: _selectedFilters.contains(i),
onSelected: (selected) {
setState(() {
if (selected) {
_selectedFilters.add(i);
} else {
_selectedFilters.remove(i);
}
});
_loadAnimes();
},
)
Set 自动去重,适合存储多选状态。
contains 检查是否选中,add 和 remove 切换状态。
标签带图标
可以给标签加图标:
dart
FilterChip(
avatar: Icon(
_getFilterIcon(i),
size: 18,
),
label: Text(_filters[i]),
selected: _selectedFilter == i,
onSelected: (selected) {
// 处理选中
},
)
IconData _getFilterIcon(int index) {
switch (index) {
case 0:
return Icons.trending_up;
case 1:
return Icons.calendar_today;
case 2:
return Icons.upcoming;
case 3:
return Icons.shuffle;
default:
return Icons.filter_list;
}
}
avatar 属性可以放图标或图片,显示在标签文本前面。
标签带数量
可以显示每个筛选条件的数量:
dart
FilterChip(
label: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(_filters[i]),
if (_filterCounts[i] > 0) ...[
const SizedBox(width: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(10),
),
child: Text(
'${_filterCounts[i]}',
style: const TextStyle(fontSize: 10),
),
),
],
],
),
selected: _selectedFilter == i,
onSelected: (selected) {
// 处理选中
},
)
在标签文本后面加一个小圆角矩形显示数量。
动画效果
切换筛选时可以加动画:
dart
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _isLoading
? const ShimmerLoading(key: ValueKey('loading'))
: GridView.builder(
key: ValueKey(_selectedFilter),
// 其他配置
),
)
AnimatedSwitcher 在子组件切换时添加淡入淡出动画。
key 必须不同,AnimatedSwitcher 才知道是不同的组件。
深色模式适配
FilterChip 会自动适配深色模式,但可以进一步定制:
dart
FilterChip(
label: Text(_filters[i]),
selected: _selectedFilter == i,
backgroundColor: Theme.of(context).brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[200],
selectedColor: Theme.of(context).primaryColor.withOpacity(0.3),
onSelected: (selected) {
// 处理选中
},
)
根据主题设置不同的背景色。
小结
标签筛选功能涉及的技术点:FilterChip 组件 、横向 ListView 、状态管理 、switch 条件分支 、ScrollController 滚动监听 、FloatingActionButton 回到顶部。
FilterChip 是实现筛选标签的标准组件,提供了选中/未选中状态、自定义样式、图标支持等功能。
筛选和内容加载的联动是关键:切换筛选 → 更新状态 → 重新加载数据 → 更新 UI。
好的筛选设计能帮助用户快速找到想要的内容,是内容类 App 的重要功能。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net