diff --git a/lib/common/widgets/cropped_image.dart b/lib/common/widgets/cropped_image.dart new file mode 100644 index 000000000..6cb61ad75 --- /dev/null +++ b/lib/common/widgets/cropped_image.dart @@ -0,0 +1,163 @@ +/* + * This file is part of PiliPlus + * + * PiliPlus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PiliPlus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PiliPlus. If not, see . + */ + +import 'dart:ui' as ui; + +import 'package:flutter/widgets.dart'; + +class CroppedImage extends LeafRenderObjectWidget { + const CroppedImage({ + super.key, + required this.size, + required this.image, + required this.srcRect, + required this.dstRect, + required this.rrect, + required this.imgPaint, + required this.borderPaint, + }); + + final Size size; + final ui.Image image; + final Rect srcRect; + final Rect dstRect; + final RRect rrect; + final Paint imgPaint; + final Paint borderPaint; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderCroppedImage( + preferredSize: size, + image: image, + srcRect: srcRect, + dstRect: dstRect, + rrect: rrect, + imgPaint: imgPaint, + borderPaint: borderPaint, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderCroppedImage renderObject, + ) { + renderObject + ..preferredSize = size + ..image = image + ..srcRect = srcRect + ..dstRect = dstRect + ..rrect = rrect + ..imgPaint = imgPaint + ..borderPaint = borderPaint; + } +} + +class RenderCroppedImage extends RenderBox { + RenderCroppedImage({ + required Size preferredSize, + required ui.Image image, + required Rect srcRect, + required Rect dstRect, + required RRect rrect, + required Paint imgPaint, + required Paint borderPaint, + }) : _preferredSize = preferredSize, + _image = image, + _srcRect = srcRect, + _dstRect = dstRect, + _rrect = rrect, + _imgPaint = imgPaint, + _borderPaint = borderPaint; + + Size _preferredSize; + Size get preferredSize => _preferredSize; + set preferredSize(Size value) { + if (_preferredSize == value) return; + _preferredSize = value; + markNeedsLayout(); + } + + ui.Image _image; + ui.Image get image => _image; + set image(ui.Image value) { + if (_image == value) return; + _image = value; + markNeedsPaint(); + } + + Rect _srcRect; + Rect get srcRect => _srcRect; + set srcRect(Rect value) { + if (_srcRect == value) return; + _srcRect = value; + markNeedsPaint(); + } + + Rect _dstRect; + Rect get dstRect => _dstRect; + set dstRect(Rect value) { + if (_dstRect == value) return; + _dstRect = value; + markNeedsPaint(); + } + + RRect _rrect; + RRect get rrect => _rrect; + set rrect(RRect value) { + if (_rrect == value) return; + _rrect = value; + markNeedsPaint(); + } + + Paint _imgPaint; + Paint get imgPaint => _imgPaint; + set imgPaint(Paint value) { + if (_imgPaint == value) return; + _imgPaint = value; + markNeedsPaint(); + } + + Paint _borderPaint; + Paint get borderPaint => _borderPaint; + set borderPaint(Paint value) { + if (_borderPaint == value) return; + _borderPaint = value; + markNeedsPaint(); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.constrain(_preferredSize); + } + + @override + void paint(PaintingContext context, Offset offset) { + context.canvas + ..drawImageRect(image, srcRect, dstRect, _imgPaint) + ..drawRRect(rrect, _borderPaint); + } + + @override + bool get isRepaintBoundary => true; +} diff --git a/lib/common/widgets/custom_arc.dart b/lib/common/widgets/custom_arc.dart new file mode 100644 index 000000000..4244851b7 --- /dev/null +++ b/lib/common/widgets/custom_arc.dart @@ -0,0 +1,117 @@ +import 'dart:math' show pi; + +import 'package:flutter/widgets.dart'; + +class Arc extends LeafRenderObjectWidget { + const Arc({ + super.key, + required this.size, + required this.color, + required this.sweepAngle, + this.strokeWidth = 2, + }); + + final double size; + final Color color; + final double sweepAngle; + final double strokeWidth; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderArc( + size: size, + color: color, + sweepAngle: sweepAngle, + strokeWidth: strokeWidth, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderArc renderObject, + ) { + renderObject + ..color = color + ..sweepAngle = sweepAngle + ..strokeWidth = strokeWidth; + } +} + +class RenderArc extends RenderBox { + RenderArc({ + required double size, + required Color color, + required double sweepAngle, + required double strokeWidth, + }) : _preferredSize = Size.square(size), + _color = color, + _sweepAngle = sweepAngle, + _strokeWidth = strokeWidth; + + Color _color; + Color get color => _color; + set color(Color value) { + if (_color == value) return; + _color = value; + markNeedsPaint(); + } + + double _sweepAngle; + double get sweepAngle => _sweepAngle; + set sweepAngle(double value) { + if (_sweepAngle == value) return; + _sweepAngle = value; + markNeedsPaint(); + } + + double _strokeWidth; + double get strokeWidth => _strokeWidth; + set strokeWidth(double value) { + if (_strokeWidth == value) return; + _strokeWidth = value; + markNeedsPaint(); + } + + Size _preferredSize; + set preferredSize(Size value) { + if (_preferredSize == value) return; + _preferredSize = value; + markNeedsLayout(); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.constrain(_preferredSize); + } + + @override + void paint(PaintingContext context, Offset offset) { + if (sweepAngle == 0) { + return; + } + + final paint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke; + + final size = this.size; + final rect = Rect.fromCircle( + center: Offset(size.width / 2, size.height / 2), + radius: size.width / 2, + ); + + const startAngle = -pi / 2; + + context.canvas.drawArc(rect, startAngle, sweepAngle, false, paint); + } + + @override + bool get isRepaintBoundary => true; +} diff --git a/lib/common/widgets/custom_tooltip.dart b/lib/common/widgets/custom_tooltip.dart index cbf7cf678..31a227051 100644 --- a/lib/common/widgets/custom_tooltip.dart +++ b/lib/common/widgets/custom_tooltip.dart @@ -3,7 +3,7 @@ import 'dart:ui' show clampDouble; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; enum TooltipType { top, right } @@ -261,19 +261,83 @@ class _CustomMultiTooltipPositionDelegate extends MultiChildLayoutDelegate { } } -class TrianglePainter extends CustomPainter { - TrianglePainter(this.color, {this.type = TooltipType.top}); - final TooltipType type; +class Triangle extends LeafRenderObjectWidget { + const Triangle({ + super.key, + required this.color, + required this.size, + this.type = .top, + }); + final Color color; + final Size size; + final TooltipType type; @override - void paint(Canvas canvas, Size size) { + RenderObject createRenderObject(BuildContext context) { + return RenderTriangle( + color: color, + size: size, + type: type, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderTriangle renderObject, + ) { + renderObject + ..color = color + ..preferredSize = size; + } +} + +class RenderTriangle extends RenderBox { + RenderTriangle({ + required Color color, + required Size size, + required TooltipType type, + }) : _color = color, + _preferredSize = size, + _type = type; + + Color _color; + Color get color => _color; + set color(Color value) { + if (_color == value) return; + _color = value; + markNeedsPaint(); + } + + Size _preferredSize; + set preferredSize(Size value) { + if (_preferredSize == value) return; + _preferredSize = value; + markNeedsLayout(); + } + + final TooltipType _type; + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.constrain(_preferredSize); + } + + @override + void paint(PaintingContext context, Offset offset) { + final size = this.size; final paint = Paint() ..color = color ..style = PaintingStyle.fill; Path path; - switch (type) { + switch (_type) { case TooltipType.top: path = Path() ..moveTo(0, 0) @@ -288,11 +352,11 @@ class TrianglePainter extends CustomPainter { ..close(); } - canvas.drawPath(path, paint); + context.canvas.drawPath(path, paint); } @override - bool shouldRepaint(TrianglePainter oldDelegate) => color != oldDelegate.color; + bool get isRepaintBoundary => true; } Offset positionDependentBox({ diff --git a/lib/common/widgets/disabled_icon.dart b/lib/common/widgets/disabled_icon.dart index f51e95ffb..97d32f9d5 100644 --- a/lib/common/widgets/disabled_icon.dart +++ b/lib/common/widgets/disabled_icon.dart @@ -4,10 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; class DisabledIcon extends SingleChildRenderObjectWidget { - final Color? color; - final double lineLengthScale; - final StrokeCap strokeCap; - const DisabledIcon({ super.key, required T child, @@ -18,27 +14,70 @@ class DisabledIcon extends SingleChildRenderObjectWidget { strokeCap = strokeCap ?? StrokeCap.butt, super(child: child); + final Color? color; + final StrokeCap strokeCap; + final double lineLengthScale; + + T enable() => child as T; + @override RenderObject createRenderObject(BuildContext context) { return RenderMaskedIcon( - color ?? + color: + color ?? (child is Icon ? (child as Icon).color ?? IconTheme.of(context).color! : IconTheme.of(context).color!), - lineLengthScale, - strokeCap, + strokeCap: strokeCap, + lineLengthScale: lineLengthScale, ); } - T enable() => child as T; + @override + void updateRenderObject(BuildContext context, RenderMaskedIcon renderObject) { + renderObject + ..color = + color ?? + (child is Icon + ? (child as Icon).color ?? IconTheme.of(context).color! + : IconTheme.of(context).color!) + ..strokeCap = strokeCap + ..lineLengthScale = lineLengthScale; + } } class RenderMaskedIcon extends RenderProxyBox { - final Color color; - final double lineLengthScale; - final StrokeCap strokeCap; + RenderMaskedIcon({ + required Color color, + required StrokeCap strokeCap, + required double lineLengthScale, + }) : _color = color, + _strokeCap = strokeCap, + _lineLengthScale = lineLengthScale; - RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap); + Color _color; + Color get color => _color; + set color(Color value) { + if (_color == value) return; + _color = value; + markNeedsPaint(); + } + + StrokeCap _strokeCap; + StrokeCap get strokeCap => _strokeCap; + set strokeCap(StrokeCap value) { + if (_strokeCap == value) return; + _strokeCap = value; + markNeedsPaint(); + } + + double _lineLengthScale; + double get lineLengthScale => _lineLengthScale; + set lineLengthScale(double value) { + if (_lineLengthScale == value) return; + _lineLengthScale = value; + markNeedsPaint(); + } @override void paint(PaintingContext context, Offset offset) { @@ -77,7 +116,7 @@ class RenderMaskedIcon extends RenderProxyBox { ..clipPath(path, doAntiAlias: false); super.paint(context, offset); - context.canvas.restore(); + canvas.restore(); final linePaint = Paint() ..color = color @@ -94,6 +133,9 @@ class RenderMaskedIcon extends RenderProxyBox { linePaint, ); } + + @override + bool get isRepaintBoundary => true; } extension DisabledIconExt on Icon { diff --git a/lib/common/widgets/loading_widget.dart b/lib/common/widgets/loading_widget.dart index 4605eec53..3897ffae2 100644 --- a/lib/common/widgets/loading_widget.dart +++ b/lib/common/widgets/loading_widget.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; +import 'package:PiliPlus/common/widgets/custom_arc.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -30,18 +30,13 @@ class LoadingWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ //loading animation - RepaintBoundary.wrap( - Obx( - () => CustomPaint( - size: const Size.square(40), - painter: ArcPainter( - color: onSurfaceVariant, - strokeWidth: 3, - sweepAngle: progress.value * 2 * pi, - ), - ), + Obx( + () => Arc( + size: 40, + color: onSurfaceVariant, + strokeWidth: 3, + sweepAngle: progress.value * 2 * pi, ), - 0, ), //msg Text(msg, style: TextStyle(color: onSurfaceVariant)), diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart index 1ea9d1813..ce1c5ad31 100644 --- a/lib/pages/emote/view.dart +++ b/lib/pages/emote/view.dart @@ -104,9 +104,9 @@ class _EmotePanelState extends State ); if (!isTextEmote) { child = CustomTooltip( - indicator: () => CustomPaint( + indicator: () => Triangle( + color: color, size: const Size(14, 8), - painter: TrianglePainter(color), ), overlayWidget: () => Container( padding: const EdgeInsets.all(8), diff --git a/lib/pages/live_emote/view.dart b/lib/pages/live_emote/view.dart index a4dbe119a..3991ba6ee 100644 --- a/lib/pages/live_emote/view.dart +++ b/lib/pages/live_emote/view.dart @@ -105,9 +105,9 @@ class _LiveEmotePanelState extends State } }, child: CustomTooltip( - indicator: () => CustomPaint( + indicator: () => Triangle( + color: color, size: const Size(14, 8), - painter: TrianglePainter(color), ), overlayWidget: () => Container( padding: const EdgeInsets.all(8), diff --git a/lib/pages/video/introduction/ugc/widgets/action_item.dart b/lib/pages/video/introduction/ugc/widgets/action_item.dart index 09ba3c304..1e8788ff5 100644 --- a/lib/pages/video/introduction/ugc/widgets/action_item.dart +++ b/lib/pages/video/introduction/ugc/widgets/action_item.dart @@ -1,5 +1,4 @@ -import 'dart:math' show pi; - +import 'package:PiliPlus/common/widgets/custom_arc.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart'; @@ -52,16 +51,12 @@ class ActionItem extends StatelessWidget { clipBehavior: Clip.none, alignment: Alignment.center, children: [ - RepaintBoundary( - child: AnimatedBuilder( - animation: animation!, - builder: (context, child) => CustomPaint( - size: const Size.square(28), - painter: ArcPainter( - color: primary, - sweepAngle: animation!.value, - ), - ), + AnimatedBuilder( + animation: animation!, + builder: (context, child) => Arc( + size: 28, + color: primary, + sweepAngle: animation!.value, ), ), child, @@ -115,40 +110,3 @@ class ActionItem extends StatelessWidget { return child; } } - -class ArcPainter extends CustomPainter { - const ArcPainter({ - required this.color, - required this.sweepAngle, - this.strokeWidth = 2, - }); - final Color color; - final double sweepAngle; - final double strokeWidth; - - @override - void paint(Canvas canvas, Size size) { - if (sweepAngle == 0) { - return; - } - - final paint = Paint() - ..color = color - ..strokeWidth = strokeWidth - ..style = PaintingStyle.stroke; - - final rect = Rect.fromCircle( - center: Offset(size.width / 2, size.height / 2), - radius: size.width / 2, - ); - - const startAngle = -pi / 2; - - canvas.drawArc(rect, startAngle, sweepAngle, false, paint); - } - - @override - bool shouldRepaint(covariant ArcPainter oldDelegate) { - return sweepAngle != oldDelegate.sweepAngle || color != oldDelegate.color; - } -} diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index c3b8214f5..a5b1900ad 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; import 'dart:ui' as ui; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/cropped_image.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/gesture/immediate_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/mouse_interactive_viewer.dart'; @@ -63,6 +64,7 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderProxyBox; import 'package:flutter/services.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -2352,131 +2354,129 @@ class _PLVideoPlayerState extends State return Positioned( right: right, top: top, - child: RepaintBoundary( - child: CustomPaint( - painter: _DanmakuTipPainter(offset: triangleOffset), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: switch (extra) { - null => throw UnimplementedError(), - VideoDanmaku() => [ - Stack( - clipBehavior: Clip.none, - children: [ - _dmActionItem( - extra.isLike - ? const Icon( - size: 20, - CustomIcons.player_dm_tip_like_solid, - color: Colors.white, - ) - : const Icon( - size: 20, - CustomIcons.player_dm_tip_like, - color: Colors.white, - ), - onTap: () => HeaderControl.likeDanmaku( - extra, - plPlayerController.cid!, - ), - ), - if (extra.like > 0) - Positioned( - left: _actionItemWidth - 10.5, - top: 0, - child: Text( - extra.like.toString(), - style: const TextStyle( - fontSize: 10.5, + child: _DanmakuTip( + offset: triangleOffset, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: switch (extra) { + null => throw UnimplementedError(), + VideoDanmaku() => [ + Stack( + clipBehavior: Clip.none, + children: [ + _dmActionItem( + extra.isLike + ? const Icon( + size: 20, + CustomIcons.player_dm_tip_like_solid, + color: Colors.white, + ) + : const Icon( + size: 20, + CustomIcons.player_dm_tip_like, color: Colors.white, ), - ), - ), - ], - ), - - _dmActionItem( - const Icon( - size: 19, - CustomIcons.player_dm_tip_copy, - color: Colors.white, - ), - onTap: () => Utils.copyText(item.content.text), - ), - if (item.content.selfSend) - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_recall, - color: Colors.white, - ), - onTap: () => HeaderControl.deleteDanmaku( - extra.id, + onTap: () => HeaderControl.likeDanmaku( + extra, plPlayerController.cid!, ), - ) - else - _dmActionItem( - const Icon( - size: 20, - CustomIcons.player_dm_tip_back, - color: Colors.white, - ), - onTap: () => HeaderControl.reportDanmaku( - context, - extra: extra, - ctr: plPlayerController, - ), ), - if (seekOffset != null) - _dmActionItem( - const Icon( - size: 18, - Icons.gps_fixed_outlined, - color: Colors.white, + if (extra.like > 0) + Positioned( + left: _actionItemWidth - 10.5, + top: 0, + child: Text( + extra.like.toString(), + style: const TextStyle( + fontSize: 10.5, + color: Colors.white, + ), + ), ), - onTap: () => plPlayerController.seekTo( - Duration(seconds: seekOffset), - isSeek: false, - ), - ), - ], - LiveDanmaku() => [ + ], + ), + + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + if (item.content.selfSend) _dmActionItem( const Icon( size: 20, - MdiIcons.accountOutline, + CustomIcons.player_dm_tip_recall, color: Colors.white, ), - onTap: () => Get.toNamed('/member?mid=${extra.mid}'), - ), - _dmActionItem( - const Icon( - size: 19, - CustomIcons.player_dm_tip_copy, - color: Colors.white, + onTap: () => HeaderControl.deleteDanmaku( + extra.id, + plPlayerController.cid!, ), - onTap: () => Utils.copyText(item.content.text), - ), + ) + else _dmActionItem( const Icon( size: 20, CustomIcons.player_dm_tip_back, color: Colors.white, ), - onTap: () => HeaderControl.reportLiveDanmaku( + onTap: () => HeaderControl.reportDanmaku( context, - roomId: (widget.bottomControl as live_bottom.BottomControl) - .liveRoomCtr - .roomId, - msg: item.content.text, extra: extra, + ctr: plPlayerController, ), ), - ], - }, - ), + if (seekOffset != null) + _dmActionItem( + const Icon( + size: 18, + Icons.gps_fixed_outlined, + color: Colors.white, + ), + onTap: () => plPlayerController.seekTo( + Duration(seconds: seekOffset), + isSeek: false, + ), + ), + ], + LiveDanmaku() => [ + _dmActionItem( + const Icon( + size: 20, + MdiIcons.accountOutline, + color: Colors.white, + ), + onTap: () => Get.toNamed('/member?mid=${extra.mid}'), + ), + _dmActionItem( + const Icon( + size: 19, + CustomIcons.player_dm_tip_copy, + color: Colors.white, + ), + onTap: () => Utils.copyText(item.content.text), + ), + _dmActionItem( + const Icon( + size: 20, + CustomIcons.player_dm_tip_back, + color: Colors.white, + ), + onTap: () => HeaderControl.reportLiveDanmaku( + context, + roomId: (widget.bottomControl as live_bottom.BottomControl) + .liveRoomCtr + .roomId, + msg: item.content.text, + extra: extra, + ), + ), + ], + }, ), ), ); @@ -2661,6 +2661,7 @@ Future _loadImg(String path) async { class _VideoShotImageState extends State { late Size _size; + late Rect _srcRect; late Rect _dstRect; late RRect _rrect; ui.Image? _image; @@ -2686,14 +2687,17 @@ class _VideoShotImageState extends State { final height = widget.height; final width = height * imgXSize / imgYSize; _setRect(width, height); + _setSrcRect(imgXSize, imgYSize); widget.onSetSize(imgXSize, imgYSize); } else { _setRect(double.nan, double.nan); + _setSrcRect(widget.imgXSize, widget.imgYSize); } } else { final height = widget.height; final width = height * widget.imgXSize / widget.imgYSize; _setRect(width, height); + _setSrcRect(widget.imgXSize, widget.imgYSize); } } @@ -2703,6 +2707,15 @@ class _VideoShotImageState extends State { _rrect = RRect.fromRectAndRadius(_dstRect, const Radius.circular(10)); } + void _setSrcRect(double imgXSize, double imgYSize) { + _srcRect = Rect.fromLTWH( + widget.x * imgXSize, + widget.y * imgYSize, + imgXSize, + imgYSize, + ); + } + void _loadImg() { final url = widget.url; _image = widget.imageCache[url]; @@ -2733,6 +2746,9 @@ class _VideoShotImageState extends State { if (oldWidget.url != widget.url) { _loadImg(); } + if (oldWidget.x != widget.x || oldWidget.y != widget.y) { + _setSrcRect(widget.imgXSize, widget.imgYSize); + } } late final _imgPaint = Paint()..filterQuality = FilterQuality.medium; @@ -2744,101 +2760,56 @@ class _VideoShotImageState extends State { @override Widget build(BuildContext context) { if (_image != null) { - return RepaintBoundary( - child: CustomPaint( - painter: _CroppedImagePainter( - image: _image!, - x: widget.x, - y: widget.y, - imgXSize: widget.imgXSize, - imgYSize: widget.imgYSize, - dstRect: _dstRect, - rrect: _rrect, - imgPaint: _imgPaint, - borderPaint: _borderPaint, - ), - size: _size, - ), + return CroppedImage( + size: _size, + image: _image!, + srcRect: _srcRect, + dstRect: _dstRect, + rrect: _rrect, + imgPaint: _imgPaint, + borderPaint: _borderPaint, ); } return const SizedBox.shrink(); } } -class _CroppedImagePainter extends CustomPainter { - final ui.Image image; - final Rect srcRect; - final Rect dstRect; - final RRect rrect; - final Paint imgPaint; - final Paint borderPaint; - - _CroppedImagePainter({ - required this.image, - required int x, - required int y, - required double imgXSize, - required double imgYSize, - required this.dstRect, - required this.rrect, - required this.imgPaint, - required this.borderPaint, - }) : srcRect = Rect.fromLTWH(x * imgXSize, y * imgYSize, imgXSize, imgYSize); - - @override - void paint(Canvas canvas, Size size) { - canvas - ..drawImageRect(image, srcRect, dstRect, imgPaint) - ..drawRRect(rrect, borderPaint); - } - - @override - bool shouldRepaint(_CroppedImagePainter oldDelegate) { - return oldDelegate.image != image || oldDelegate.srcRect != srcRect; - } -} - -Widget buildViewPointWidget( - VideoDetailController videoDetailController, - PlPlayerController plPlayerController, - double offset, - double maxWidth, -) { - return Container( - height: 16, - margin: EdgeInsets.only(bottom: offset), - child: Listener( - behavior: HitTestBehavior.opaque, - onPointerDown: (event) { - try { - double seg = event.localPosition.dx / maxWidth; - Segment item = videoDetailController.viewPointList - .where((item) => item.start >= seg) - .reduce((a, b) => a.start < b.start ? a : b); - if (item.from != null) { - plPlayerController - ..danmakuController?.clear() - ..videoPlayerController?.seek(Duration(seconds: item.from!)); - } - // if (kDebugMode) debugPrint('${item.title},,${item.from}'); - } catch (e) { - if (kDebugMode) rethrow; - } - }, - ), - ); -} - const double _triangleHeight = 5.6; -class _DanmakuTipPainter extends CustomPainter { +class _DanmakuTip extends SingleChildRenderObjectWidget { + const _DanmakuTip({ + this.offset = 0, + super.child, + }); + final double offset; - const _DanmakuTipPainter({this.offset = 0}); + @override + RenderObject createRenderObject(BuildContext context) { + return RenderDanmakuTip(offset: offset); + } @override + void updateRenderObject(BuildContext context, RenderDanmakuTip renderObject) { + renderObject.offset = offset; + } +} + +class RenderDanmakuTip extends RenderProxyBox { + RenderDanmakuTip({ + required double offset, + }) : _offset = offset; + + double _offset; + double get offset => _offset; + set offset(double value) { + if (_offset == value) return; + _offset = value; + markNeedsPaint(); + } + @override - void paint(Canvas canvas, Size size) { + void paint(PaintingContext context, Offset offset) { final paint = Paint() ..color = const Color(0xB3000000) ..style = PaintingStyle.fill; @@ -2851,7 +2822,7 @@ class _DanmakuTipPainter extends CustomPainter { final radius = size.height / 2; const triangleBase = _triangleHeight * 2 / 3; - final triangleCenterX = (size.width / 2 + offset).clamp( + final triangleCenterX = (size.width / 2 + _offset).clamp( radius + triangleBase, size.width - radius - triangleBase, ); @@ -2876,12 +2847,13 @@ class _DanmakuTipPainter extends CustomPainter { ) ..close(); - canvas + context.canvas ..drawPath(path, paint) ..drawPath(path, strokePaint); + + super.paint(context, offset); } @override - bool shouldRepaint(covariant _DanmakuTipPainter oldDelegate) => - oldDelegate.offset != offset; + bool get isRepaintBoundary => true; }