feat(flutter): 重设计剧场与首页 Hero

This commit is contained in:
Zhou Weidong committed 2026-10-20 10:24:19 +08:00
1 parent 678b976ce1
commit 73aef4e44e
19 files changed
+339 -312

No files matched your search

+31 -85
View File
@@ -3,37 +3,48 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
import 'discover_search_field.dart';
/// 剧场页页头:品牌、搜索与个人页入口。
/// 剧场页页头:搜索与头像入口。
class DiscoverHeader extends StatelessWidget {
const DiscoverHeader({
super.key,
required this.onSearch,
required this.controller,
required this.focusNode,
required this.onChanged,
required this.loading,
required this.onProfile,
});
final VoidCallback onSearch;
final TextEditingController controller;
final FocusNode focusNode;
final ValueChanged<String> onChanged;
final bool loading;
final VoidCallback onProfile;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SizedBox(
height: 46,
height: AppDimens.contentHeaderHeight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.pageHorizontal,
),
child: Row(
children: [
_brand(l10n),
const Spacer(),
_HeaderIcon(
key: const Key('discover-search-button'),
icon: Icons.search_rounded,
tooltip: l10n.search,
onTap: onSearch,
Expanded(
child: DiscoverSearchField(
controller: controller,
focusNode: focusNode,
onChanged: onChanged,
loading: loading,
horizontalPadding: 0,
),
),
const SizedBox(width: 8),
const SizedBox(width: AppDimens.spaceSm),
_profile(l10n),
],
),
@@ -41,37 +52,6 @@ class DiscoverHeader extends StatelessWidget {
);
}
Widget _brand(AppLocalizations l10n) {
return Row(
children: [
Container(
width: 24,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
color: AppColors.blue,
borderRadius: BorderRadius.circular(7),
),
child: const Icon(
Icons.play_arrow_rounded,
color: AppColors.onPrimary,
size: 16,
),
),
const SizedBox(width: 8),
Text(
l10n.discoverTab,
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: 15,
fontWeight: FontWeight.w800,
),
),
],
);
}
Widget _profile(AppLocalizations l10n) {
return Semantics(
button: true,
@@ -81,54 +61,20 @@ class DiscoverHeader extends StatelessWidget {
customBorder: const CircleBorder(),
onTap: onProfile,
child: Container(
width: 36,
height: 36,
decoration: const BoxDecoration(
width: AppDimens.tapTarget,
height: AppDimens.tapTarget,
decoration: BoxDecoration(
color: AppColors.card,
shape: BoxShape.circle,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.secondary, AppColors.blue],
),
border: Border.all(color: AppColors.border),
),
child: const Icon(
Icons.person_rounded,
color: AppColors.onPrimary,
size: 18,
color: AppColors.onSurface,
size: AppDimens.accountIcon,
),
),
),
);
}
}
class _HeaderIcon extends StatelessWidget {
const _HeaderIcon({
super.key,
required this.icon,
required this.tooltip,
required this.onTap,
});
final IconData icon;
final String tooltip;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 36,
height: 36,
child: IconButton(
tooltip: tooltip,
onPressed: onTap,
padding: EdgeInsets.zero,
style: IconButton.styleFrom(
backgroundColor: AppColors.raised,
foregroundColor: AppColors.onSurface,
),
icon: Icon(icon, size: 18),
),
);
}
}
@@ -3,6 +3,7 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../l10n/app_localizations.dart';
/// 剧场顶部搜索框。
@@ -13,19 +14,21 @@ class DiscoverSearchField extends StatelessWidget {
required this.focusNode,
required this.onChanged,
required this.loading,
this.horizontalPadding = 14,
});
final TextEditingController controller;
final FocusNode focusNode;
final ValueChanged<String> onChanged;
final bool loading;
final double horizontalPadding;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14),
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: SizedBox(
height: 36,
height: AppDimens.searchHeight,
child: TextField(
key: const Key('discover-search'),
controller: controller,
@@ -33,13 +36,13 @@ class DiscoverSearchField extends StatelessWidget {
onChanged: onChanged,
onTapOutside: (_) => focusNode.unfocus(),
textInputAction: TextInputAction.search,
style: const TextStyle(color: AppColors.onSurface, fontSize: 13),
style: const TextStyle(color: AppColors.onSurface, fontSize: 14),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: AppColors.input,
hintText: AppLocalizations.of(context)!.searchDiscover,
hintStyle: const TextStyle(color: AppColors.cream, fontSize: 11),
hintStyle: const TextStyle(color: AppColors.subtle, fontSize: 12),
prefixIcon: const Icon(
Icons.search_rounded,
color: AppColors.subtle,
@@ -53,11 +56,11 @@ class DiscoverSearchField extends StatelessWidget {
: null,
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppDimens.pillRadius),
borderSide: const BorderSide(color: AppColors.blue, width: 1),
),
),
@@ -39,9 +39,7 @@ class FeedBannerCarousel extends StatefulWidget {
}
class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
final PageController _controller = PageController(
viewportFraction: _viewportFraction,
);
late final PageController _controller;
Timer? _timer;
int _index = 0;
bool _pressed = false;
@@ -50,6 +48,9 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
@override
void initState() {
super.initState();
_controller = PageController(
viewportFraction: widget.dramaFor == null ? _viewportFraction : 1,
);
WidgetsBinding.instance.addPostFrameCallback((_) => _syncTimer());
}
@@ -116,28 +117,43 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
@override
Widget build(BuildContext context) {
if (widget.banners.isEmpty) return const SizedBox.shrink();
final height = widget.dramaFor == null
? _carouselHeight
: AppDimens.heroBannerHeight;
final hero = widget.dramaFor != null;
final height = hero ? AppDimens.heroBannerHeight : _carouselHeight;
final pages = PageView.builder(
controller: _controller,
itemCount: widget.banners.length,
onPageChanged: (value) => setState(() => _index = value),
itemBuilder: (context, index) => _bannerCard(index),
);
return Listener(
key: const Key('feed-banner-carousel'),
onPointerDown: (_) => _pause(),
onPointerUp: (_) => _resume(),
onPointerCancel: (_) => _resume(),
child: Column(
children: [
SizedBox(
height: height,
child: PageView.builder(
controller: _controller,
itemCount: widget.banners.length,
onPageChanged: (value) => setState(() => _index = value),
itemBuilder: (context, index) => _bannerCard(index),
child: hero
? SizedBox(
height: height,
child: Stack(
children: [
Positioned.fill(child: pages),
Positioned(
left: 0,
right: 0,
bottom: AppDimens.spaceXs,
child: SizedBox(
height: _indicatorHeight,
child: _indicator(context),
),
),
],
),
)
: Column(
children: [
SizedBox(height: height, child: pages),
SizedBox(height: _indicatorHeight, child: _indicator(context)),
],
),
),
SizedBox(height: _indicatorHeight, child: _indicator(context)),
],
),
);
}
@@ -146,7 +162,9 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
final dramaId = banner.dramaId;
final drama = widget.dramaFor?.call(banner);
final card = ClipRRect(
borderRadius: BorderRadius.circular(AppDimens.authCardRadius),
borderRadius: BorderRadius.circular(
drama == null ? AppDimens.authCardRadius : AppDimens.heroBannerRadius,
),
child: Stack(
fit: StackFit.expand,
children: [
@@ -158,7 +176,7 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
end: Alignment.bottomCenter,
colors: drama == null
? const [Color(0x00000000), Color(0x8A0B0C10)]
: const [Color(0x120B0C10), Color(0xE60B0C10)],
: const [Color(0x00000000), Color(0xE6000000)],
),
),
),
@@ -166,7 +184,7 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
Positioned(
left: AppDimens.pageHorizontal,
right: _copyRightGap,
bottom: AppDimens.cardPadding,
bottom: AppDimens.spaceXl + AppDimens.spaceSm,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@@ -179,7 +197,7 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
style: const TextStyle(
fontFamily: 'Outfit',
color: AppColors.onSurface,
fontSize: AppDimens.pageTitle,
fontSize: AppDimens.heroTitle,
fontWeight: FontWeight.w800,
),
),
@@ -190,24 +208,28 @@ class _FeedBannerCarouselState extends State<FeedBannerCarousel> {
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.muted,
fontSize: AppDimens.caption,
fontSize: AppDimens.heroSubtitle,
),
),
],
),
),
if (dramaId != null)
if (dramaId != null && drama == null)
const Positioned(right: 14, bottom: 14, child: _PlayMark()),
],
),
);
final page = Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
padding: EdgeInsets.symmetric(horizontal: drama == null ? 5 : 0),
child: dramaId == null
? card
: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(AppDimens.authCardRadius),
borderRadius: BorderRadius.circular(
drama == null
? AppDimens.authCardRadius
: AppDimens.heroBannerRadius,
),
clipBehavior: Clip.antiAlias,
child: InkWell(
key: Key('feed-banner-$index'),