mod: anim for sanlian

This commit is contained in:
bggRGjQaUbCoE
2024-10-01 19:58:19 +08:00
parent 49b856be86
commit fb1ae9df15
4 changed files with 311 additions and 107 deletions

View File

@@ -417,4 +417,27 @@ class BangumiIntroController extends CommonController {
SmartDialog.showToast('番剧暂无相关视频'); SmartDialog.showToast('番剧暂无相关视频');
return false; return false;
} }
// 一键三连
Future actionOneThree() async {
feedBack();
if (userInfo == null) {
SmartDialog.showToast('账号未登录');
return;
}
if (hasLike.value && hasCoin.value && hasFav.value) {
// 已点赞、投币、收藏
SmartDialog.showToast('已三连');
return false;
}
var result = await VideoHttp.oneThree(bvid: bvid);
if (result['status']) {
hasLike.value = result["data"]["like"];
hasCoin.value = result["data"]["coin"];
hasFav.value = result["data"]["fav"];
SmartDialog.showToast('三连成功');
} else {
SmartDialog.showToast(result['msg']);
}
}
} }

View File

@@ -135,6 +135,9 @@ class _BangumiInfoState extends State<BangumiInfo>
}; };
} }
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -396,45 +399,69 @@ class _BangumiInfoState extends State<BangumiInfo>
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
Obx(() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap:
handleState(bangumiIntroController.actionLikeVideo),
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false,
semanticsLabel: '点赞',
text: !widget.loadingStatus
? Utils.numFormat(
widget.bangumiDetail!.stat!['likes']!)
: Utils.numFormat(bangumiItem!.stat!['likes']!),
)),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.b), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: onTap: handleState(bangumiIntroController.actionLikeVideo),
handleState(bangumiIntroController.actionCoinVideo), onLongPress: bangumiIntroController.actionOneThree,
selectStatus: bangumiIntroController.hasCoin.value, selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false, loadingStatus: false,
semanticsLabel: '投币', semanticsLabel: '点赞',
text: !widget.loadingStatus text: !widget.loadingStatus
? Utils.numFormat( ? Utils.numFormat(widget.bangumiDetail!.stat!['likes']!)
widget.bangumiDetail!.stat!['coins']!) : Utils.numFormat(
: Utils.numFormat(bangumiItem!.stat!['coins']!)), bangumiItem!.stat!['likes']!,
),
needAnim: true,
hasOneThree: bangumiIntroController.hasLike.value &&
bangumiIntroController.hasCoin.value &&
bangumiIntroController.hasFav.value,
callBack: (start) {
if (start) {
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
}
},
),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.star), key: _coinKey,
selectIcon: const Icon(FontAwesomeIcons.solidStar), icon: const Icon(FontAwesomeIcons.b),
onTap: () => showFavBottomSheet(), selectIcon: const Icon(FontAwesomeIcons.b),
selectStatus: bangumiIntroController.hasFav.value, onTap: handleState(bangumiIntroController.actionCoinVideo),
loadingStatus: false, selectStatus: bangumiIntroController.hasCoin.value,
semanticsLabel: '收藏', loadingStatus: false,
text: !widget.loadingStatus semanticsLabel: '投币',
? Utils.numFormat( text: !widget.loadingStatus
widget.bangumiDetail!.stat!['favorite']!) ? Utils.numFormat(widget.bangumiDetail!.stat!['coins']!)
: Utils.numFormat(bangumiItem!.stat!['favorite']!)), : Utils.numFormat(
bangumiItem!.stat!['coins']!,
),
needAnim: true,
),
),
Obx(
() => ActionItem(
key: _favKey,
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: false,
semanticsLabel: '收藏',
text: !widget.loadingStatus
? Utils.numFormat(
widget.bangumiDetail!.stat!['favorite']!)
: Utils.numFormat(
bangumiItem!.stat!['favorite']!,
),
needAnim: true,
),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.comment),

View File

@@ -152,6 +152,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
}; };
} }
late final _coinKey = GlobalKey<ActionItemState>();
late final _favKey = GlobalKey<ActionItemState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -572,16 +575,30 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
children: <Widget>[ children: <Widget>[
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp), icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo), onTap: handleState(videoIntroController.actionLikeVideo),
onLongPress: handleState(videoIntroController.actionOneThree), onLongPress: handleState(videoIntroController.actionOneThree),
selectStatus: videoIntroController.hasLike.value, selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
semanticsLabel: '点赞', semanticsLabel: '点赞',
text: !loadingStatus text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.like!) ? Utils.numFormat(widget.videoDetail!.stat!.like!)
: '-'), : '-',
needAnim: true,
hasOneThree: videoIntroController.hasLike.value &&
videoIntroController.hasCoin.value &&
videoIntroController.hasFav.value,
callBack: (start) {
if (start) {
_coinKey.currentState?.controller?.forward();
_favKey.currentState?.controller?.forward();
} else {
_coinKey.currentState?.controller?.reverse();
_favKey.currentState?.controller?.reverse();
}
},
),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
@@ -601,28 +618,34 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// text: '稍后再看'), // text: '稍后再看'),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), key: _coinKey,
selectIcon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo), selectIcon: const Icon(FontAwesomeIcons.b),
selectStatus: videoIntroController.hasCoin.value, onTap: handleState(videoIntroController.actionCoinVideo),
loadingStatus: loadingStatus, selectStatus: videoIntroController.hasCoin.value,
semanticsLabel: '投币', loadingStatus: loadingStatus,
text: !loadingStatus semanticsLabel: '投币',
? Utils.numFormat(widget.videoDetail!.stat!.coin!) text: !loadingStatus
: '-'), ? Utils.numFormat(widget.videoDetail!.stat!.coin!)
: '-',
needAnim: true,
),
), ),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.star), key: _favKey,
selectIcon: const Icon(FontAwesomeIcons.solidStar), icon: const Icon(FontAwesomeIcons.star),
onTap: () => showFavBottomSheet(), selectIcon: const Icon(FontAwesomeIcons.solidStar),
onLongPress: () => showFavBottomSheet(type: 'longPress'), onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value, onLongPress: () => showFavBottomSheet(type: 'longPress'),
loadingStatus: loadingStatus, selectStatus: videoIntroController.hasFav.value,
semanticsLabel: '收藏', loadingStatus: loadingStatus,
text: !loadingStatus semanticsLabel: '收藏',
? Utils.numFormat(widget.videoDetail!.stat!.favorite!) text: !loadingStatus
: '-'), ? Utils.numFormat(widget.videoDetail!.stat!.favorite!)
: '-',
needAnim: true,
),
), ),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.comment), icon: const Icon(FontAwesomeIcons.comment),

View File

@@ -1,7 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/feed_back.dart';
class ActionItem extends StatelessWidget { class ActionItem extends StatefulWidget {
final Icon? icon; final Icon? icon;
final Icon? selectIcon; final Icon? selectIcon;
final Function? onTap; final Function? onTap;
@@ -10,6 +12,9 @@ class ActionItem extends StatelessWidget {
final String? text; final String? text;
final bool selectStatus; final bool selectStatus;
final String semanticsLabel; final String semanticsLabel;
final bool needAnim;
final bool hasOneThree;
final Function? callBack;
const ActionItem({ const ActionItem({
Key? key, Key? key,
@@ -20,62 +25,188 @@ class ActionItem extends StatelessWidget {
this.loadingStatus, this.loadingStatus,
this.text, this.text,
this.selectStatus = false, this.selectStatus = false,
this.needAnim = false,
this.hasOneThree = false,
this.callBack,
required this.semanticsLabel, required this.semanticsLabel,
}) : super(key: key); }) : super(key: key);
@override
State<ActionItem> createState() => ActionItemState();
}
class ActionItemState extends State<ActionItem>
with SingleTickerProviderStateMixin {
late AnimationController? controller;
late Animation<double>? _animation;
bool get _isThumbUp => widget.semanticsLabel == '点赞';
late int _lastTime;
bool _hideCircle = false;
void _startLongPress() {
_lastTime = DateTime.now().millisecondsSinceEpoch;
if (!widget.hasOneThree) {
controller?.forward();
widget.callBack!(true);
}
}
void _cancelLongPress() {
int duration = DateTime.now().millisecondsSinceEpoch - _lastTime;
if (duration < 1500) {
controller?.reverse();
widget.callBack!(false);
}
if (duration <= 500) {
feedBack();
widget.onTap!();
}
}
@override
void initState() {
super.initState();
if (widget.needAnim) {
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1500),
reverseDuration: const Duration(milliseconds: 500),
);
_animation = Tween<double>(begin: 0, end: -2 * pi).animate(controller!)
..addListener(() {
setState(() {
_hideCircle = _animation?.value == -2 * pi;
if (_hideCircle) {
controller?.reset();
if (_isThumbUp) {
widget.onLongPress!();
}
}
});
});
}
}
@override
void dispose() {
_animation?.removeListener(() {});
controller?.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
child: Semantics( child: Semantics(
label: (text ?? "") + (selectStatus ? "" : "") + semanticsLabel, label: (widget.text ?? "") +
child: InkWell( (widget.selectStatus ? "" : "") +
borderRadius: BorderRadius.circular(6), widget.semanticsLabel,
onTap: () => { child: InkWell(
feedBack(), borderRadius: BorderRadius.circular(6),
onTap!(), onTap: _isThumbUp
}, ? null
onLongPress: () => { : () {
if (onLongPress != null) {onLongPress!()} feedBack();
}, widget.onTap!();
// borderRadius: StyleString.mdRadius, },
child: Column( onLongPress: _isThumbUp
mainAxisAlignment: MainAxisAlignment.center, ? null
: () {
if (widget.onLongPress != null) {
widget.onLongPress!();
}
},
onTapDown: (details) => _isThumbUp ? _startLongPress() : null,
onTapUp: (details) => _isThumbUp ? _cancelLongPress() : null,
onTapCancel: () => _isThumbUp ? _cancelLongPress() : null,
// borderRadius: StyleString.mdRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// const SizedBox(height: 2),
Stack(
alignment: Alignment.center,
children: [ children: [
// const SizedBox(height: 2), if (widget.needAnim && !_hideCircle)
CustomPaint(
size: const Size(28, 28),
painter: _ArcPainter(
color: Theme.of(context).colorScheme.primary,
sweepAngle: _animation!.value,
),
),
Icon( Icon(
selectStatus ? selectIcon!.icon! : icon!.icon!, widget.selectStatus
? widget.selectIcon!.icon!
: widget.icon!.icon!,
size: 18, size: 18,
color: selectStatus color: widget.selectStatus
? Theme.of(context).colorScheme.primary ? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline, : Theme.of(context).colorScheme.outline,
), ),
const SizedBox(height: 3),
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
text ?? '',
key: ValueKey<String>(text ?? ''),
style: TextStyle(
color: selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
semanticsLabel: "",
),
),
),
], ],
), ),
))); const SizedBox(height: 3),
AnimatedOpacity(
opacity: widget.loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
widget.text ?? '',
key: ValueKey<String>(widget.text ?? ''),
style: TextStyle(
color: widget.selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize),
semanticsLabel: "",
),
),
),
],
),
),
),
);
}
}
class _ArcPainter extends CustomPainter {
const _ArcPainter({
required this.color,
required this.sweepAngle,
});
final Color color;
final double sweepAngle;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = 2
..style = PaintingStyle.stroke;
final rect = Rect.fromCircle(
center: Offset(size.width / 2, size.height / 2),
radius: size.width / 2,
);
const startAngle = -pi / 2;
// const sweepAngle = -2 * pi;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
} }
} }