feat(home): 顶部导航随翻页联动并把推荐更名为精选
This commit is contained in:
1 parent
2f4593a46e
commit
335e5dc286
27 files changed
+1095
-214
No files matched your search
@@ -122,7 +122,7 @@ class HomePosterCard extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/data/models/content_category.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/widgets/animated_nav_label.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import 'search_gift_glyphs.dart';
|
||||
|
||||
@@ -16,7 +17,7 @@ const double _giftIconSize = 26;
|
||||
const double _underlineWidth = 26;
|
||||
const double _underlineHeight = 3;
|
||||
|
||||
/// 推荐页(礼花 tab)顶部频道导航:频道全部来自分类接口,右侧搜索/礼物。
|
||||
/// 推荐页顶部频道导航:频道全部来自分类接口,右侧搜索/礼物。
|
||||
class RecommendChannelNav extends StatefulWidget {
|
||||
const RecommendChannelNav({
|
||||
super.key,
|
||||
@@ -49,8 +50,7 @@ class _RecommendChannelNavState extends State<RecommendChannelNav> {
|
||||
final GlobalKey _scrollViewKey = GlobalKey();
|
||||
final Map<String, GlobalKey> _tabKeys = {};
|
||||
|
||||
GlobalKey _keyFor(String id) =>
|
||||
_tabKeys.putIfAbsent(id, () => GlobalKey());
|
||||
GlobalKey _keyFor(String id) => _tabKeys.putIfAbsent(id, () => GlobalKey());
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant RecommendChannelNav oldWidget) {
|
||||
@@ -82,9 +82,10 @@ class _RecommendChannelNavState extends State<RecommendChannelNav> {
|
||||
final tabLeft = tabBox.localToGlobal(Offset.zero).dx;
|
||||
final relative = tabLeft - viewportLeft;
|
||||
final position = _scrollController.position;
|
||||
final target =
|
||||
(position.pixels + relative - _revealLeftInset)
|
||||
.clamp(0.0, position.maxScrollExtent);
|
||||
final target = (position.pixels + relative - _revealLeftInset).clamp(
|
||||
0.0,
|
||||
position.maxScrollExtent,
|
||||
);
|
||||
if ((target - position.pixels).abs() < 0.5) return;
|
||||
_scrollController.animateTo(
|
||||
target,
|
||||
@@ -147,7 +148,10 @@ class _RecommendChannelNavState extends State<RecommendChannelNav> {
|
||||
),
|
||||
iconSize: _searchIconSize,
|
||||
color: AppColors.textPrimary,
|
||||
icon: SearchGlyph(size: _searchIconSize, color: AppColors.textPrimary),
|
||||
icon: SearchGlyph(
|
||||
size: _searchIconSize,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.onGift != null)
|
||||
@@ -188,18 +192,16 @@ class _RecommendChannelNavState extends State<RecommendChannelNav> {
|
||||
children: [
|
||||
// 补偿文字下方的 4pt 间距 + 3pt 下划线,让文字视觉中心与右侧图标对齐。
|
||||
const SizedBox(height: 4 + _underlineHeight),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
AnimatedNavLabel(
|
||||
label: label,
|
||||
selected: selected,
|
||||
style: const TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _tabFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0,
|
||||
height: 1.0,
|
||||
shadows: const [
|
||||
Shadow(color: AppColors.mediaBlack, blurRadius: 6),
|
||||
],
|
||||
shadows: [Shadow(color: AppColors.mediaBlack, blurRadius: 6)],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
@@ -4,13 +4,14 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../core/widgets/animated_nav_label.dart';
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../recommend_types.dart';
|
||||
import 'search_gift_glyphs.dart';
|
||||
|
||||
const double _navHeight = 56;
|
||||
const double _tabGap = 20;
|
||||
const double _tabFont = 17;
|
||||
const double _tabFont = 18;
|
||||
const double _maxNavigationTextScale = 1.3;
|
||||
const double _searchIconSize = 26;
|
||||
const double _giftIconSize = 26;
|
||||
@@ -25,6 +26,7 @@ class RecommendTopNav extends StatelessWidget {
|
||||
required this.onSearch,
|
||||
this.onVideo = false,
|
||||
this.onGift,
|
||||
this.pagePosition,
|
||||
});
|
||||
|
||||
static const double height = _navHeight;
|
||||
@@ -34,6 +36,9 @@ class RecommendTopNav extends StatelessWidget {
|
||||
final VoidCallback onSearch;
|
||||
final bool onVideo;
|
||||
|
||||
/// 首页横向页面的连续位置,用于文字和下划线跟手过渡。
|
||||
final double? pagePosition;
|
||||
|
||||
/// 右上角金色礼物盒(钱包入口);为空时不显示。
|
||||
final VoidCallback? onGift;
|
||||
|
||||
@@ -65,14 +70,7 @@ class RecommendTopNav extends StatelessWidget {
|
||||
children: [
|
||||
for (var index = 0; index < items.length; index++) ...[
|
||||
if (index > 0) const SizedBox(width: _tabGap),
|
||||
_tab(
|
||||
key: Key('recommend-tab-${items[index].$3}'),
|
||||
code: items[index].$3,
|
||||
label: items[index].$2,
|
||||
selected: selected == items[index].$1,
|
||||
shadows: shadows,
|
||||
onTap: () => onSelected(items[index].$1),
|
||||
),
|
||||
_tab(items[index], shadows, context),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -120,47 +118,79 @@ class RecommendTopNav extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tab({
|
||||
required Key key,
|
||||
required String code,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required List<Shadow>? shadows,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
Widget _tab(
|
||||
(RecommendInnerTab, String, String) item,
|
||||
List<Shadow>? shadows,
|
||||
BuildContext context,
|
||||
) {
|
||||
final (tab, label, code) = item;
|
||||
final isSelected = selected == tab;
|
||||
final position = pagePosition;
|
||||
final progress = position == null
|
||||
? null
|
||||
: (1 - (position - tab.index).abs()).clamp(0.0, 1.0);
|
||||
return GestureDetector(
|
||||
key: key,
|
||||
key: Key('recommend-tab-$code'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
onTap: () => onSelected(tab),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 补偿文字下方的 4pt 间距 + 3pt 下划线,让文字视觉中心与右侧图标对齐。
|
||||
const SizedBox(height: 4 + _underlineHeight),
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
AnimatedNavLabel(
|
||||
label: label,
|
||||
selected: isSelected,
|
||||
selectionProgress: progress,
|
||||
inactiveColor: AppColors.homeTabInactive,
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: _tabFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w400,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 0,
|
||||
height: 1.0,
|
||||
shadows: shadows,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
key: Key('recommend-tab-$code-underline'),
|
||||
width: _underlineWidth,
|
||||
height: _underlineHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? AppColors.brandRed : AppColors.transparent,
|
||||
borderRadius: BorderRadius.circular(_underlineHeight / 2),
|
||||
),
|
||||
_underline(
|
||||
code,
|
||||
progress,
|
||||
isSelected,
|
||||
MediaQuery.disableAnimationsOf(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _underline(
|
||||
String code,
|
||||
double? progress,
|
||||
bool isSelected,
|
||||
bool reduceMotion,
|
||||
) {
|
||||
if (progress != null) return _line(code, progress);
|
||||
final target = isSelected ? 1.0 : 0.0;
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: target, end: target),
|
||||
duration: reduceMotion
|
||||
? Duration.zero
|
||||
: AppDimens.homePrimaryTabAnimationDuration,
|
||||
curve: Curves.easeOut,
|
||||
builder: (_, value, _) => _line(code, value),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _line(String code, double progress) => Container(
|
||||
key: Key('recommend-tab-$code-underline'),
|
||||
width: _underlineWidth,
|
||||
height: _underlineHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: progress == 0
|
||||
? AppColors.transparent
|
||||
: AppColors.brandRed.withValues(alpha: progress),
|
||||
borderRadius: BorderRadius.circular(_underlineHeight / 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user