Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-07-23 16:47:11 +08:00
parent 148e0872b4
commit 418a1e8d39
821 changed files with 29467 additions and 25520 deletions

View File

@@ -22,8 +22,8 @@ import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3;
///
/// * [InteractiveViewer.builder], whose builder is of this type.
/// * [WidgetBuilder], which is similar, but takes no viewport.
typedef InteractiveViewerWidgetBuilder = Widget Function(
BuildContext context, Quad viewport);
typedef InteractiveViewerWidgetBuilder =
Widget Function(BuildContext context, Quad viewport);
/// A widget that enables pan and zoom interactions with its child.
///
@@ -82,23 +82,23 @@ class InteractiveViewer extends StatefulWidget {
this.onReset,
this.isAnimating,
required Widget this.child,
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
builder = null;
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
builder = null;
/// Creates an InteractiveViewer for a child that is created on demand.
///
@@ -132,24 +132,24 @@ class InteractiveViewer extends StatefulWidget {
this.onReset,
this.isAnimating,
required InteractiveViewerWidgetBuilder this.builder,
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
constrained = false,
child = null;
}) : assert(minScale > 0),
assert(interactionEndFrictionCoefficient > 0),
assert(minScale.isFinite),
assert(maxScale > 0),
assert(!maxScale.isNaN),
assert(maxScale >= minScale),
// boundaryMargin must be either fully infinite or fully finite, but not
// a mix of both.
assert(
(boundaryMargin.horizontal.isInfinite &&
boundaryMargin.vertical.isInfinite) ||
(boundaryMargin.top.isFinite &&
boundaryMargin.right.isFinite &&
boundaryMargin.bottom.isFinite &&
boundaryMargin.left.isFinite),
),
constrained = false,
child = null;
final Function? isAnimating;
final VoidCallback? onReset;
@@ -402,7 +402,8 @@ class InteractiveViewer extends StatefulWidget {
/// Returns the closest point to the given point on the given line segment.
@visibleForTesting
static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) {
final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() +
final double lengthSquared =
math.pow(l2.x - l1.x, 2.0).toDouble() +
math.pow(l2.y - l1.y, 2.0).toDouble();
// In this case, l1 == l2.
@@ -414,8 +415,11 @@ class InteractiveViewer extends StatefulWidget {
// the point.
final Vector3 l1P = point - l1;
final Vector3 l1L2 = l2 - l1;
final double fraction =
clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0);
final double fraction = clampDouble(
l1P.dot(l1L2) / lengthSquared,
0.0,
1.0,
);
return l1 + l1L2 * fraction;
}
@@ -558,8 +562,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
final RenderBox childRenderBox =
_childKey.currentContext!.findRenderObject()! as RenderBox;
final Size childSize = childRenderBox.size;
final Rect boundaryRect =
widget.boundaryMargin.inflateRect(Offset.zero & childSize);
final Rect boundaryRect = widget.boundaryMargin.inflateRect(
Offset.zero & childSize,
);
assert(
!boundaryRect.isEmpty,
"InteractiveViewer's child must have nonzero dimensions.",
@@ -631,8 +636,10 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
// If the given translation fits completely within the boundaries, allow it.
final Offset offendingDistance =
_exceedsBy(boundariesAabbQuad, nextViewport);
final Offset offendingDistance = _exceedsBy(
boundariesAabbQuad,
nextViewport,
);
if (offendingDistance == Offset.zero) {
return nextMatrix;
}
@@ -651,17 +658,23 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// complicated than this when rotated.
// https://github.com/flutter/flutter/issues/57698
final Matrix4 correctedMatrix = matrix.clone()
..setTranslation(Vector3(
correctedTotalTranslation.dx,
correctedTotalTranslation.dy,
0.0,
));
..setTranslation(
Vector3(
correctedTotalTranslation.dx,
correctedTotalTranslation.dy,
0.0,
),
);
// Double check that the corrected translation fits.
final Quad correctedViewport =
_transformViewport(correctedMatrix, _viewport);
final Offset offendingCorrectedDistance =
_exceedsBy(boundariesAabbQuad, correctedViewport);
final Quad correctedViewport = _transformViewport(
correctedMatrix,
_viewport,
);
final Offset offendingCorrectedDistance = _exceedsBy(
boundariesAabbQuad,
correctedViewport,
);
if (offendingCorrectedDistance == Offset.zero) {
return correctedMatrix;
}
@@ -680,12 +693,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0,
offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0,
);
return matrix.clone()
..setTranslation(Vector3(
return matrix.clone()..setTranslation(
Vector3(
unidirectionalCorrectedTotalTranslation.dx,
unidirectionalCorrectedTotalTranslation.dy,
0.0,
));
),
);
}
// Return a new matrix representing the given matrix after applying the given
@@ -698,8 +712,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// Don't allow a scale that results in an overall scale beyond min/max
// scale.
final double currentScale =
_transformationController!.value.getMaxScaleOnAxis();
final double currentScale = _transformationController!.value
.getMaxScaleOnAxis();
final double totalScale = math.max(
currentScale * scale,
// Ensure that the scale cannot make the child so big that it can't fit
@@ -933,10 +947,12 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentAxis = null;
return;
}
final Vector3 translationVector =
_transformationController!.value.getTranslation();
final Offset translation =
Offset(translationVector.x, translationVector.y);
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Offset translation = Offset(
translationVector.x,
translationVector.y,
);
final FrictionSimulation frictionSimulationX = FrictionSimulation(
widget.interactionEndFrictionCoefficient,
translation.dx,
@@ -951,13 +967,19 @@ class _InteractiveViewerState extends State<InteractiveViewer>
details.velocity.pixelsPerSecond.distance,
widget.interactionEndFrictionCoefficient,
);
_animation = Tween<Offset>(
begin: translation,
end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
));
_animation =
Tween<Offset>(
begin: translation,
end: Offset(
frictionSimulationX.finalX,
frictionSimulationY.finalX,
),
).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.decelerate,
),
);
_controller.duration = Duration(milliseconds: (tFinal * 1000).round());
_animation!.addListener(_onAnimate);
_controller.forward();
@@ -966,21 +988,31 @@ class _InteractiveViewerState extends State<InteractiveViewer>
_currentAxis = null;
return;
}
final double scale =
_transformationController!.value.getMaxScaleOnAxis();
final double scale = _transformationController!.value
.getMaxScaleOnAxis();
final FrictionSimulation frictionSimulation = FrictionSimulation(
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10);
final double tFinal = _getFinalTime(details.scaleVelocity.abs(),
widget.interactionEndFrictionCoefficient,
effectivelyMotionless: 0.1);
widget.interactionEndFrictionCoefficient * widget.scaleFactor,
scale,
details.scaleVelocity / 10,
);
final double tFinal = _getFinalTime(
details.scaleVelocity.abs(),
widget.interactionEndFrictionCoefficient,
effectivelyMotionless: 0.1,
);
_scaleAnimation =
Tween<double>(begin: scale, end: frictionSimulation.x(tFinal))
.animate(CurvedAnimation(
parent: _scaleController, curve: Curves.decelerate));
_scaleController.duration =
Duration(milliseconds: (tFinal * 1000).round());
Tween<double>(
begin: scale,
end: frictionSimulation.x(tFinal),
).animate(
CurvedAnimation(
parent: _scaleController,
curve: Curves.decelerate,
),
);
_scaleController.duration = Duration(
milliseconds: (tFinal * 1000).round(),
);
_scaleAnimation!.addListener(_onScaleAnimate);
_scaleController.forward();
case _GestureType.rotate || null:
@@ -1009,11 +1041,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
if (!_gestureIsSupported(_GestureType.pan)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - event.scrollDelta,
focalPointDelta: -localDelta,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - event.scrollDelta,
focalPointDelta: -localDelta,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1027,13 +1061,17 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
newFocalPointScene - focalPointScene);
_transformationController!.value,
newFocalPointScene - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position - event.scrollDelta,
localFocalPoint: event.localPosition - localDelta,
focalPointDelta: -localDelta));
focalPointDelta: -localDelta,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1055,11 +1093,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
@@ -1083,11 +1123,13 @@ class _InteractiveViewerState extends State<InteractiveViewer>
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionUpdate?.call(
ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
),
);
widget.onInteractionEnd?.call(ScaleEndDetails());
}
@@ -1101,8 +1143,8 @@ class _InteractiveViewerState extends State<InteractiveViewer>
return;
}
// Translate such that the resulting translation is _animation.value.
final Vector3 translationVector =
_transformationController!.value.getTranslation();
final Vector3 translationVector = _transformationController!.value
.getTranslation();
final Offset translation = Offset(translationVector.x, translationVector.y);
final Offset translationScene = _transformationController!.toScene(
translation,
@@ -1176,27 +1218,33 @@ class _InteractiveViewerState extends State<InteractiveViewer>
// transformationControllers.
if (oldWidget.transformationController == null) {
if (widget.transformationController != null) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController!.dispose();
_transformationController = widget.transformationController;
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
} else {
if (widget.transformationController == null) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = TransformationController();
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
} else if (widget.transformationController !=
oldWidget.transformationController) {
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
_transformationController = widget.transformationController;
_transformationController!
.addListener(_onTransformationControllerChange);
_transformationController!.addListener(
_onTransformationControllerChange,
);
}
}
}
@@ -1205,8 +1253,9 @@ class _InteractiveViewerState extends State<InteractiveViewer>
void dispose() {
_controller.dispose();
_scaleController.dispose();
_transformationController!
.removeListener(_onTransformationControllerChange);
_transformationController!.removeListener(
_onTransformationControllerChange,
);
if (widget.transformationController == null) {
_transformationController!.dispose();
}
@@ -1329,7 +1378,7 @@ class TransformationController extends ValueNotifier<Matrix4> {
/// The [value] defaults to the identity matrix, which corresponds to no
/// transformation.
TransformationController([Matrix4? value])
: super(value ?? Matrix4.identity());
: super(value ?? Matrix4.identity());
/// Return the scene point at the given viewport point.
///
@@ -1365,11 +1414,13 @@ class TransformationController extends ValueNotifier<Matrix4> {
// On viewportPoint, perform the inverse transformation of the scene to get
// where the point would be in the scene before the transformation.
final Matrix4 inverseMatrix = Matrix4.inverted(value);
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
));
final Vector3 untransformed = inverseMatrix.transform3(
Vector3(
viewportPoint.dx,
viewportPoint.dy,
0,
),
);
return Offset(untransformed.x, untransformed.y);
}
}
@@ -1384,8 +1435,11 @@ enum _GestureType {
// Given a velocity and drag, calculate the time at which motion will come to
// a stop, within the margin of effectivelyMotionless.
double _getFinalTime(double velocity, double drag,
{double effectivelyMotionless = 10}) {
double _getFinalTime(
double velocity,
double drag, {
double effectivelyMotionless = 10,
}) {
return math.log(effectivelyMotionless / velocity) / math.log(drag / 100);
}
@@ -1402,26 +1456,34 @@ Offset _getMatrixTranslation(Matrix4 matrix) {
Quad _transformViewport(Matrix4 matrix, Rect viewport) {
final Matrix4 inverseMatrix = matrix.clone()..invert();
return Quad.points(
inverseMatrix.transform3(Vector3(
viewport.topLeft.dx,
viewport.topLeft.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.topRight.dx,
viewport.topRight.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.bottomRight.dx,
viewport.bottomRight.dy,
0.0,
)),
inverseMatrix.transform3(Vector3(
viewport.bottomLeft.dx,
viewport.bottomLeft.dy,
0.0,
)),
inverseMatrix.transform3(
Vector3(
viewport.topLeft.dx,
viewport.topLeft.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.topRight.dx,
viewport.topRight.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomRight.dx,
viewport.bottomRight.dy,
0.0,
),
),
inverseMatrix.transform3(
Vector3(
viewport.bottomLeft.dx,
viewport.bottomLeft.dy,
0.0,
),
),
);
}
@@ -1453,8 +1515,10 @@ Offset _exceedsBy(Quad boundary, Quad viewport) {
];
Offset largestExcess = Offset.zero;
for (final Vector3 point in viewportPoints) {
final Vector3 pointInside =
InteractiveViewer.getNearestPointInside(point, boundary);
final Vector3 pointInside = InteractiveViewer.getNearestPointInside(
point,
boundary,
);
final Offset excess = Offset(
pointInside.x - point.x,
pointInside.y - point.y,

View File

@@ -213,15 +213,15 @@ class InteractiveViewerBoundaryState extends State<InteractiveViewerBoundary>
}
Widget get content => DecoratedBoxTransition(
decoration: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
decoration: _opacityAnimation,
child: SlideTransition(
position: _slideAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
),
),
);
@override
Widget build(BuildContext context) {

View File

@@ -28,8 +28,13 @@ import 'package:media_kit_video/media_kit_video.dart';
/// source is hit after zooming in to disable or enable the swiping gesture of
/// the [PageView].
///
typedef IndexedFocusedWidgetBuilder = Widget Function(
BuildContext context, int index, bool isFocus, bool enablePageView);
typedef IndexedFocusedWidgetBuilder =
Widget Function(
BuildContext context,
int index,
bool isFocus,
bool enablePageView,
);
typedef IndexedTagStringBuilder = String Function(int index);
@@ -233,12 +238,13 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
if (_transformationController!.value != Matrix4.identity()) {
// animate the reset for the transformation of the interactive viewer
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animation =
Matrix4Tween(
begin: _transformationController!.value,
end: Matrix4.identity(),
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController.forward(from: 0);
}
@@ -287,20 +293,27 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
child: PageView.builder(
onPageChanged: _onPageChanged,
controller: _pageController,
physics:
_enablePageView ? null : const NeverScrollableScrollPhysics(),
physics: _enablePageView
? null
: const NeverScrollableScrollPhysics(),
itemCount: widget.sources.length,
itemBuilder: (BuildContext context, int index) {
final item = widget.sources[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => EasyThrottle.throttle(
'preview', const Duration(milliseconds: 555), onClose),
'preview',
const Duration(milliseconds: 555),
onClose,
),
onDoubleTapDown: (TapDownDetails details) {
_doubleTapLocalPosition = details.localPosition;
},
onDoubleTap: () => EasyThrottle.throttle(
'preview', const Duration(milliseconds: 555), onDoubleTap),
'preview',
const Duration(milliseconds: 555),
onDoubleTap,
),
onLongPress: item.sourceType == SourceType.fileImage
? null
: () => onLongPress(item),
@@ -321,7 +334,8 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
left: 0,
right: 0,
child: Container(
padding: MediaQuery.paddingOf(context) +
padding:
MediaQuery.paddingOf(context) +
const EdgeInsets.fromLTRB(12, 8, 20, 8),
decoration: _enablePageView
? BoxDecoration(
@@ -330,7 +344,7 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.3)
Colors.black.withValues(alpha: 0.3),
],
),
)
@@ -419,30 +433,32 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
tag: item.url,
child: switch (item.sourceType) {
SourceType.fileImage => Image(
filterQuality: FilterQuality.low,
image: FileImage(File(item.url)),
),
filterQuality: FilterQuality.low,
image: FileImage(File(item.url)),
),
SourceType.networkImage => CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: _getActualUrl(item.url),
placeholderFadeInDuration: Duration.zero,
placeholder: (context, url) {
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: ImageUtil.thumbnailUrl(item.url, widget.quality),
);
},
),
SourceType.livePhoto => Obx(() => currentIndex.value == index
? IgnorePointer(
child: Video(
controller: _videoController!,
fill: Colors.transparent,
),
)
: const SizedBox.shrink()),
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: _getActualUrl(item.url),
placeholderFadeInDuration: Duration.zero,
placeholder: (context, url) {
return CachedNetworkImage(
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
imageUrl: ImageUtil.thumbnailUrl(item.url, widget.quality),
);
},
),
SourceType.livePhoto => Obx(
() => currentIndex.value == index
? IgnorePointer(
child: Video(
controller: _videoController!,
fill: Colors.transparent,
),
)
: const SizedBox.shrink(),
),
},
),
);
@@ -481,15 +497,16 @@ class _InteractiveviewerGalleryState extends State<InteractiveviewerGallery>
offSetX,
offSetY,
matrix.row2.w,
matrix.row3.w
matrix.row3.w,
]);
_animation = Matrix4Tween(
begin: _transformationController!.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animation =
Matrix4Tween(
begin: _transformationController!.value,
end: matrix,
).animate(
CurveTween(curve: Curves.easeOut).animate(_animationController),
);
_animationController
.forward(from: 0)
.whenComplete(() => _onScaleChanged(targetScale));