fix(flutter): 首页 feed 顶栏固定悬浮
This commit is contained in:
1 parent
1cb4c56261
commit
9175a7f253
6 files changed
+341
-345
No files matched your search
@@ -0,0 +1,275 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
|
||||
const double _topIconSize = 36;
|
||||
const double _topIconGlyph = 22;
|
||||
const double _tabGap = 22;
|
||||
const double _tabFontSize = 15;
|
||||
const double _underlineWidth = 18;
|
||||
const double _underlineHeight = 2;
|
||||
|
||||
/// 首页 feed 固定顶栏:刷新 / 漫剧·真人剧·推荐 / 搜索。
|
||||
class HomeFeedHeader extends StatefulWidget {
|
||||
const HomeFeedHeader({
|
||||
super.key,
|
||||
required this.l10n,
|
||||
required this.onRefresh,
|
||||
required this.onSearch,
|
||||
required this.onMore,
|
||||
});
|
||||
|
||||
final AppLocalizations l10n;
|
||||
final Future<bool> Function() onRefresh;
|
||||
final VoidCallback onSearch;
|
||||
final VoidCallback onMore;
|
||||
|
||||
@override
|
||||
State<HomeFeedHeader> createState() => _HomeFeedHeaderState();
|
||||
}
|
||||
|
||||
class _HomeFeedHeaderState extends State<HomeFeedHeader> {
|
||||
_FeedTab _tab = _FeedTab.recommend;
|
||||
bool _refreshing = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_RefreshButton(
|
||||
tooltip: widget.l10n.retry,
|
||||
dimension: _topIconSize,
|
||||
glyphSize: _topIconGlyph,
|
||||
refreshing: _refreshing,
|
||||
onTap: _refresh,
|
||||
onLongPress: widget.onMore,
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (var index = 0; index < _FeedTab.values.length; index++)
|
||||
_tabSlot(_FeedTab.values[index], index),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_TopIconButton(
|
||||
key: const Key('home-feed-search'),
|
||||
tooltip: widget.l10n.search,
|
||||
icon: Icons.search_rounded,
|
||||
dimension: _topIconSize,
|
||||
glyphSize: _topIconGlyph,
|
||||
onPressed: widget.onSearch,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabSlot(_FeedTab tab, int index) {
|
||||
final item = _FeedTabItem(
|
||||
tab: tab,
|
||||
label: tab.label(widget.l10n),
|
||||
selected: _tab == tab,
|
||||
fontSize: _tabFontSize,
|
||||
underlineWidth: _underlineWidth,
|
||||
underlineHeight: _underlineHeight,
|
||||
onTap: () => _selectTab(tab),
|
||||
);
|
||||
if (index == 0) return item;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: _tabGap),
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
|
||||
void _selectTab(_FeedTab tab) {
|
||||
if (tab.comingSoon) {
|
||||
_showComingSoon();
|
||||
return;
|
||||
}
|
||||
setState(() => _tab = tab);
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
if (_refreshing) return;
|
||||
setState(() => _refreshing = true);
|
||||
await widget.onRefresh();
|
||||
if (mounted) setState(() => _refreshing = false);
|
||||
}
|
||||
|
||||
void _showComingSoon() {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(widget.l10n.comingSoon)));
|
||||
}
|
||||
}
|
||||
|
||||
/// 顶部频道 tab:漫剧点击提示即将上线。
|
||||
enum _FeedTab {
|
||||
anime,
|
||||
real,
|
||||
recommend;
|
||||
|
||||
bool get comingSoon => this == _FeedTab.anime;
|
||||
|
||||
String get keyName => switch (this) {
|
||||
_FeedTab.anime => 'anime',
|
||||
_FeedTab.real => 'real',
|
||||
_FeedTab.recommend => 'recommend',
|
||||
};
|
||||
|
||||
String label(AppLocalizations l10n) => switch (this) {
|
||||
_FeedTab.anime => l10n.feedTabAnime,
|
||||
_FeedTab.real => l10n.feedTabReal,
|
||||
_FeedTab.recommend => l10n.feedTabForYou,
|
||||
};
|
||||
}
|
||||
|
||||
class _TopIconButton extends StatelessWidget {
|
||||
const _TopIconButton({
|
||||
super.key,
|
||||
required this.tooltip,
|
||||
required this.icon,
|
||||
required this.dimension,
|
||||
required this.glyphSize,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
final String tooltip;
|
||||
final IconData icon;
|
||||
final double dimension;
|
||||
final double glyphSize;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: dimension,
|
||||
child: IconButton(
|
||||
tooltip: tooltip,
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(
|
||||
icon,
|
||||
size: glyphSize,
|
||||
color: Colors.white,
|
||||
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新入口:点击重新加载 feed,长按打开更多(清屏等)。
|
||||
class _RefreshButton extends StatelessWidget {
|
||||
const _RefreshButton({
|
||||
required this.tooltip,
|
||||
required this.dimension,
|
||||
required this.glyphSize,
|
||||
required this.refreshing,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
|
||||
final String tooltip;
|
||||
final double dimension;
|
||||
final double glyphSize;
|
||||
final bool refreshing;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: GestureDetector(
|
||||
key: const Key('home-feed-refresh'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: SizedBox.square(
|
||||
dimension: dimension,
|
||||
child: refreshing
|
||||
? const Center(
|
||||
child: SizedBox.square(
|
||||
dimension: 18,
|
||||
child: CircularProgressIndicator(
|
||||
key: Key('home-feed-refreshing'),
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.refresh_rounded,
|
||||
size: glyphSize,
|
||||
color: Colors.white,
|
||||
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeedTabItem extends StatelessWidget {
|
||||
const _FeedTabItem({
|
||||
required this.tab,
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.fontSize,
|
||||
required this.underlineWidth,
|
||||
required this.underlineHeight,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final _FeedTab tab;
|
||||
final String label;
|
||||
final bool selected;
|
||||
final double fontSize;
|
||||
final double underlineWidth;
|
||||
final double underlineHeight;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
key: Key('home-feed-tab-${tab.keyName}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: selected ? Colors.white : Colors.white70,
|
||||
fontSize: fontSize,
|
||||
fontWeight: selected ? FontWeight.w800 : FontWeight.w600,
|
||||
shadows: const [Shadow(blurRadius: 6, color: Colors.black)],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
width: underlineWidth,
|
||||
height: underlineHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user