feat(flutter): 重设计剧场与首页 Hero
This commit is contained in:
1 parent
678b976ce1
commit
73aef4e44e
19 files changed
+339
-312
No files matched your search
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user