feat(player): 重做播放页信息层级与互动栏点击动效
This commit is contained in:
1 parent
aed9ee2b01
commit
fd2ba0bf43
15 files changed
+2074
-551
No files matched your search
@@ -1,15 +1,44 @@
|
||||
// Author: fruit.com.cn
|
||||
// 职责:播放互动栏的图标、计数与不影响触控区域的点击反馈。
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/formatting/count_format.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import 'interaction_rail_burst.dart';
|
||||
|
||||
const double _defaultButtonSize = 44;
|
||||
const double _defaultIconSize = 32;
|
||||
const double _defaultCountFont = 12;
|
||||
const double _defaultIconCountGap = 2;
|
||||
const double _defaultItemGap = 24;
|
||||
const _tapDuration = Duration(milliseconds: 360);
|
||||
const double _popScale = 1.34;
|
||||
const double _cancelScale = 0.68;
|
||||
const double _popWeight = 40;
|
||||
const double _settleWeight = 60;
|
||||
final _growTween = _tapTween(_popScale);
|
||||
final _shrinkTween = _tapTween(_cancelScale);
|
||||
|
||||
TweenSequence<double> _tapTween(double target) => TweenSequence<double>([
|
||||
TweenSequenceItem(
|
||||
tween: Tween(
|
||||
begin: 1.0,
|
||||
end: target,
|
||||
).chain(CurveTween(curve: Curves.easeOutCubic)),
|
||||
weight: _popWeight,
|
||||
),
|
||||
TweenSequenceItem(
|
||||
tween: Tween(
|
||||
begin: target,
|
||||
end: 1.0,
|
||||
).chain(CurveTween(curve: Curves.easeOutCubic)),
|
||||
weight: _settleWeight,
|
||||
),
|
||||
]);
|
||||
|
||||
/// 首页与完整播放器互动项之间的紧凑间距。
|
||||
const double interactionRailCompactGap = 6;
|
||||
|
||||
/// 互动栏单项:图标、计数与点击。
|
||||
class InteractionRailItem {
|
||||
@@ -24,6 +53,7 @@ class InteractionRailItem {
|
||||
this.selected = false,
|
||||
this.selectedColor = AppColors.blue,
|
||||
this.mirrorIcon = false,
|
||||
this.animateTap = false,
|
||||
}) : assert((asset == null) != (icon == null));
|
||||
|
||||
final Key key;
|
||||
@@ -38,6 +68,9 @@ class InteractionRailItem {
|
||||
|
||||
/// 水平镜像图标(分享弯箭头用 reply 镜像得到)。
|
||||
final bool mirrorIcon;
|
||||
|
||||
/// 选中放大、取消缩小并显示周围纹路,计数和触控范围不参与缩放。
|
||||
final bool animateTap;
|
||||
}
|
||||
|
||||
/// 播放页与首页 feed 共用的右侧互动栏:收藏/评论/喜欢/分享。
|
||||
@@ -79,7 +112,7 @@ class InteractionRail extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RailButton extends StatelessWidget {
|
||||
class _RailButton extends StatefulWidget {
|
||||
const _RailButton({
|
||||
required this.item,
|
||||
required this.buttonSize,
|
||||
@@ -94,50 +127,135 @@ class _RailButton extends StatelessWidget {
|
||||
final double countFontSize;
|
||||
final double iconCountGap;
|
||||
|
||||
@override
|
||||
State<_RailButton> createState() => _RailButtonState();
|
||||
}
|
||||
|
||||
class _RailButtonState extends State<_RailButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _motion;
|
||||
Color _burstColor = AppColors.onPrimary;
|
||||
bool _contracting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_motion = AnimationController(vsync: this, duration: _tapDuration);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (MediaQuery.disableAnimationsOf(context)) _motion.reset();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_motion.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tint = item.selected ? item.selectedColor : AppColors.onPrimary;
|
||||
return Column(
|
||||
key: item.key,
|
||||
key: widget.item.key,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox.square(
|
||||
dimension: buttonSize,
|
||||
child: IconButton(
|
||||
onPressed: item.onTap,
|
||||
padding: EdgeInsets.zero,
|
||||
icon: item.asset != null
|
||||
? Image.asset(
|
||||
item.asset!,
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
fit: BoxFit.contain,
|
||||
color: tint,
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
)
|
||||
: Transform.flip(
|
||||
flipX: item.mirrorIcon,
|
||||
child: Icon(item.icon, size: iconSize, color: tint),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: iconCountGap),
|
||||
Text(
|
||||
item.count == null ? item.label : formatInteractionCount(item.count!),
|
||||
key: item.countKey,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
height: 1,
|
||||
fontSize: countFontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: const [
|
||||
Shadow(blurRadius: 6, color: AppColors.mediaBlack),
|
||||
],
|
||||
),
|
||||
dimension: widget.buttonSize,
|
||||
child: widget.item.animateTap ? _animatedButton() : _button(_icon()),
|
||||
),
|
||||
SizedBox(height: widget.iconCountGap),
|
||||
_count(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap() {
|
||||
if (widget.item.animateTap && !MediaQuery.disableAnimationsOf(context)) {
|
||||
_contracting = widget.item.selected;
|
||||
_burstColor = _contracting
|
||||
? AppColors.onPrimary
|
||||
: widget.item.selectedColor;
|
||||
_motion.forward(from: 0);
|
||||
}
|
||||
widget.item.onTap();
|
||||
}
|
||||
|
||||
Widget _button(Widget icon) => IconButton(
|
||||
onPressed: _onTap,
|
||||
iconSize: widget.iconSize,
|
||||
padding: EdgeInsets.zero,
|
||||
style: IconButton.styleFrom(
|
||||
overlayColor: AppColors.transparent,
|
||||
shadowColor: AppColors.transparent,
|
||||
elevation: 0,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
),
|
||||
icon: icon,
|
||||
);
|
||||
|
||||
Widget _animatedButton() => AnimatedBuilder(
|
||||
animation: _motion,
|
||||
child: _icon(),
|
||||
builder: (context, child) => Stack(
|
||||
fit: StackFit.expand,
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_button(
|
||||
Transform.scale(
|
||||
scale: (_contracting ? _shrinkTween : _growTween).transform(
|
||||
_motion.value,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
if (_motion.isAnimating)
|
||||
Positioned.fill(
|
||||
child: RailTapBurst(
|
||||
key: const Key('rail-tap-burst'),
|
||||
progress: _motion.value,
|
||||
color: _burstColor,
|
||||
contracting: _contracting,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _icon() {
|
||||
final item = widget.item;
|
||||
final tint = item.selected ? item.selectedColor : AppColors.onPrimary;
|
||||
return item.asset != null
|
||||
? Image.asset(
|
||||
item.asset!,
|
||||
width: widget.iconSize,
|
||||
height: widget.iconSize,
|
||||
fit: BoxFit.contain,
|
||||
color: tint,
|
||||
colorBlendMode: BlendMode.srcIn,
|
||||
)
|
||||
: Transform.flip(
|
||||
flipX: item.mirrorIcon,
|
||||
child: Icon(item.icon, size: widget.iconSize, color: tint),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _count() {
|
||||
final item = widget.item;
|
||||
return Text(
|
||||
item.count == null ? item.label : formatInteractionCount(item.count!),
|
||||
key: item.countKey,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: AppColors.onPrimary,
|
||||
height: 1,
|
||||
fontSize: widget.countFontSize,
|
||||
fontWeight: FontWeight.w600,
|
||||
shadows: const [Shadow(blurRadius: 6, color: AppColors.mediaBlack)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user