refa custom painter

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-01-18 22:21:48 +08:00
parent a3ddc83430
commit f5657d2d4c
9 changed files with 586 additions and 275 deletions

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}

View File

@@ -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;
}

View File

@@ -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({

View File

@@ -4,10 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class DisabledIcon<T extends Widget> extends SingleChildRenderObjectWidget {
final Color? color;
final double lineLengthScale;
final StrokeCap strokeCap;
const DisabledIcon({
super.key,
required T child,
@@ -18,27 +14,70 @@ class DisabledIcon<T extends Widget> 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 ??
(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 {

View File

@@ -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,19 +30,14 @@ class LoadingWidget extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
//loading animation
RepaintBoundary.wrap(
Obx(
() => CustomPaint(
size: const Size.square(40),
painter: ArcPainter(
() => Arc(
size: 40,
color: onSurfaceVariant,
strokeWidth: 3,
sweepAngle: progress.value * 2 * pi,
),
),
),
0,
),
//msg
Text(msg, style: TextStyle(color: onSurfaceVariant)),
],

View File

@@ -104,9 +104,9 @@ class _EmotePanelState extends State<EmotePanel>
);
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),

View File

@@ -105,9 +105,9 @@ class _LiveEmotePanelState extends State<LiveEmotePanel>
}
},
child: CustomTooltip(
indicator: () => CustomPaint(
indicator: () => Triangle(
color: color,
size: const Size(14, 8),
painter: TrianglePainter(color),
),
overlayWidget: () => Container(
padding: const EdgeInsets.all(8),

View File

@@ -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,18 +51,14 @@ class ActionItem extends StatelessWidget {
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
RepaintBoundary(
child: AnimatedBuilder(
AnimatedBuilder(
animation: animation!,
builder: (context, child) => CustomPaint(
size: const Size.square(28),
painter: ArcPainter(
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;
}
}

View File

@@ -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,9 +2354,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
return Positioned(
right: right,
top: top,
child: RepaintBoundary(
child: CustomPaint(
painter: _DanmakuTipPainter(offset: triangleOffset),
child: _DanmakuTip(
offset: triangleOffset,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@@ -2478,7 +2479,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
),
),
),
);
}
}
@@ -2661,6 +2661,7 @@ Future<ui.Image?> _loadImg(String path) async {
class _VideoShotImageState extends State<VideoShotImage> {
late Size _size;
late Rect _srcRect;
late Rect _dstRect;
late RRect _rrect;
ui.Image? _image;
@@ -2686,14 +2687,17 @@ class _VideoShotImageState extends State<VideoShotImage> {
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<VideoShotImage> {
_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<VideoShotImage> {
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<VideoShotImage> {
@override
Widget build(BuildContext context) {
if (_image != null) {
return RepaintBoundary(
child: CustomPaint(
painter: _CroppedImagePainter(
return CroppedImage(
size: _size,
image: _image!,
x: widget.x,
y: widget.y,
imgXSize: widget.imgXSize,
imgYSize: widget.imgYSize,
srcRect: _srcRect,
dstRect: _dstRect,
rrect: _rrect,
imgPaint: _imgPaint,
borderPaint: _borderPaint,
),
size: _size,
),
);
}
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;
}