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('番剧暂无相关视频');
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
void initState() {
super.initState();
@@ -396,45 +399,69 @@ class _BangumiInfoState extends State<BangumiInfo>
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
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(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap:
handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false,
semanticsLabel: '投币',
text: !widget.loadingStatus
? Utils.numFormat(
widget.bangumiDetail!.stat!['coins']!)
: Utils.numFormat(bangumiItem!.stat!['coins']!)),
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(bangumiIntroController.actionLikeVideo),
onLongPress: bangumiIntroController.actionOneThree,
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: false,
semanticsLabel: '点赞',
text: !widget.loadingStatus
? Utils.numFormat(widget.bangumiDetail!.stat!['likes']!)
: Utils.numFormat(
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(
() => ActionItem(
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']!)),
key: _coinKey,
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(bangumiIntroController.actionCoinVideo),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: false,
semanticsLabel: '投币',
text: !widget.loadingStatus
? Utils.numFormat(widget.bangumiDetail!.stat!['coins']!)
: 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(
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
void initState() {
super.initState();
@@ -572,16 +575,30 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
onLongPress: handleState(videoIntroController.actionOneThree),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
semanticsLabel: '点赞',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.like!)
: '-'),
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: handleState(videoIntroController.actionLikeVideo),
onLongPress: handleState(videoIntroController.actionOneThree),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
semanticsLabel: '点赞',
text: !loadingStatus
? 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(
() => ActionItem(
@@ -601,28 +618,34 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
semanticsLabel: '投币',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.coin!)
: '-'),
key: _coinKey,
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: handleState(videoIntroController.actionCoinVideo),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
semanticsLabel: '投币',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.coin!)
: '-',
needAnim: true,
),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
semanticsLabel: '收藏',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.favorite!)
: '-'),
key: _favKey,
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
semanticsLabel: '收藏',
text: !loadingStatus
? Utils.numFormat(widget.videoDetail!.stat!.favorite!)
: '-',
needAnim: true,
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),

View File

@@ -1,7 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:PiliPalaX/utils/feed_back.dart';
class ActionItem extends StatelessWidget {
class ActionItem extends StatefulWidget {
final Icon? icon;
final Icon? selectIcon;
final Function? onTap;
@@ -10,6 +12,9 @@ class ActionItem extends StatelessWidget {
final String? text;
final bool selectStatus;
final String semanticsLabel;
final bool needAnim;
final bool hasOneThree;
final Function? callBack;
const ActionItem({
Key? key,
@@ -20,62 +25,188 @@ class ActionItem extends StatelessWidget {
this.loadingStatus,
this.text,
this.selectStatus = false,
this.needAnim = false,
this.hasOneThree = false,
this.callBack,
required this.semanticsLabel,
}) : 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
Widget build(BuildContext context) {
return Expanded(
child: Semantics(
label: (text ?? "") + (selectStatus ? "" : "") + semanticsLabel,
child: InkWell(
borderRadius: BorderRadius.circular(6),
onTap: () => {
feedBack(),
onTap!(),
},
onLongPress: () => {
if (onLongPress != null) {onLongPress!()}
},
// borderRadius: StyleString.mdRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
child: Semantics(
label: (widget.text ?? "") +
(widget.selectStatus ? "" : "") +
widget.semanticsLabel,
child: InkWell(
borderRadius: BorderRadius.circular(6),
onTap: _isThumbUp
? null
: () {
feedBack();
widget.onTap!();
},
onLongPress: _isThumbUp
? 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: [
// 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(
selectStatus ? selectIcon!.icon! : icon!.icon!,
widget.selectStatus
? widget.selectIcon!.icon!
: widget.icon!.icon!,
size: 18,
color: selectStatus
color: widget.selectStatus
? Theme.of(context).colorScheme.primary
: 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;
}
}