diff --git a/lib/common/widgets/image/custom_grid_view.dart b/lib/common/widgets/image/custom_grid_view.dart index 6f79c1ffd..e76115911 100644 --- a/lib/common/widgets/image/custom_grid_view.dart +++ b/lib/common/widgets/image/custom_grid_view.dart @@ -97,8 +97,7 @@ class CustomGridView extends StatelessWidget { liveUrl: isLive ? item.liveUrl : null, width: isLive ? item.width.toInt() : null, height: isLive ? item.height.toInt() : null, - // width: item.width.toInt(), - // height: item.height.toInt(), + isLongPic: item.isLongPic, ); }, ).toList(); @@ -258,6 +257,40 @@ class CustomGridView extends StatelessWidget { children: List.generate(length, (index) { final item = picArr[index]; final borderRadius = _borderRadius(column, length, index); + Widget child = Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + NetworkImgLayer( + src: item.url, + width: imageWidth, + height: imageHeight, + borderRadius: borderRadius, + alignment: item.isLongPic ? .topCenter : .center, + cacheWidth: item.width <= item.height, + getPlaceHolder: () => placeHolder, + ), + if (item.isLivePhoto) + const PBadge( + text: 'Live', + right: 8, + bottom: 8, + type: PBadgeType.gray, + ) + else if (item.isLongPic) + const PBadge( + text: '长图', + right: 8, + bottom: 8, + ), + ], + ); + if (!item.isLongPic) { + child = Hero( + tag: item.url, + child: child, + ); + } return LayoutId( id: index, child: GestureDetector( @@ -270,37 +303,7 @@ class CustomGridView extends StatelessWidget { ? (details) => _showMenu(context, details.globalPosition, item) : null, - child: Hero( - tag: item.url, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - NetworkImgLayer( - src: item.url, - width: imageWidth, - height: imageHeight, - borderRadius: borderRadius, - alignment: item.isLongPic ? .topCenter : .center, - cacheWidth: item.width <= item.height, - getPlaceHolder: () => placeHolder, - ), - if (item.isLivePhoto) - const PBadge( - text: 'Live', - right: 8, - bottom: 8, - type: PBadgeType.gray, - ) - else if (item.isLongPic) - const PBadge( - text: '长图', - right: 8, - bottom: 8, - ), - ], - ), - ), + child: child, ), ); }), diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index d05d1dca3..8d052c9b3 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -167,8 +167,6 @@ class _GalleryViewerState extends State } } - bool isAnimating() => _animateController.value != 0; - void _onDragStart(ScaleStartDetails details) { _dragging = true; @@ -343,25 +341,26 @@ class _GalleryViewerState extends State Widget _itemBuilder(BuildContext context, int index) { final item = widget.sources[index]; - return Hero( - tag: item.url, - child: switch (item.sourceType) { - .fileImage => Image.file( + Widget child; + switch (item.sourceType) { + case SourceType.fileImage: + child = Image.file( key: _keys[index], File(item.url), filterQuality: .low, minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, - isAnimating: isAnimating, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, tapGestureRecognizer: _tapGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, onChangePage: _onChangePage, - ), - .networkImage => Image( + ); + case SourceType.networkImage: + final isLongPic = item.isLongPic; + child = Image( key: _keys[index], image: CachedNetworkImageProvider(_getActualUrl(item.url)), minScale: widget.minScale, @@ -385,7 +384,6 @@ class _GalleryViewerState extends State minScale: widget.minScale, maxScale: widget.maxScale, containerSize: _containerSize, - isAnimating: isAnimating, onDragStart: null, onDragUpdate: null, onDragEnd: null, @@ -408,12 +406,15 @@ class _GalleryViewerState extends State return child; }, loadingBuilder: loadingBuilder, - isAnimating: isAnimating, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, - ), - .livePhoto => Obx( + ); + if (isLongPic) { + return child; + } + case SourceType.livePhoto: + child = Obx( key: _keys[index], () => _currIndex.value == index ? Viewer( @@ -421,7 +422,6 @@ class _GalleryViewerState extends State maxScale: widget.maxScale, containerSize: _containerSize, childSize: _containerSize, - isAnimating: isAnimating, onDragStart: _onDragStart, onDragUpdate: _onDragUpdate, onDragEnd: _onDragEnd, @@ -437,9 +437,9 @@ class _GalleryViewerState extends State ), ) : const SizedBox.shrink(), - ), - }, - ); + ); + } + return Hero(tag: item.url, child: child); } void _onTap() { diff --git a/lib/common/widgets/image_viewer/image.dart b/lib/common/widgets/image_viewer/image.dart index ab3add1e3..7e0833b47 100644 --- a/lib/common/widgets/image_viewer/image.dart +++ b/lib/common/widgets/image_viewer/image.dart @@ -3,7 +3,9 @@ // found in the LICENSE file. import 'dart:io' show File; +import 'dart:math' as math; +import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/image_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; @@ -37,7 +39,6 @@ class Image extends StatefulWidget { required this.minScale, required this.maxScale, required this.containerSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -76,7 +77,6 @@ class Image extends StatefulWidget { required this.minScale, required this.maxScale, required this.containerSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -122,7 +122,6 @@ class Image extends StatefulWidget { required this.minScale, required this.maxScale, required this.containerSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -171,7 +170,6 @@ class Image extends StatefulWidget { required this.minScale, required this.maxScale, required this.containerSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -220,7 +218,6 @@ class Image extends StatefulWidget { required this.minScale, required this.maxScale, required this.containerSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -278,7 +275,6 @@ class Image extends StatefulWidget { final double maxScale; final Size containerSize; - final ValueGetter isAnimating; final ValueChanged? onDragStart; final ValueChanged? onDragUpdate; final ValueChanged? onDragEnd; @@ -566,27 +562,27 @@ class _ImageState extends State with WidgetsBindingObserver { _isListeningToStream = false; } - Widget _debugBuildErrorWidget(BuildContext context, Object error) { - return Stack( - alignment: Alignment.center, - children: [ - const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))), - Padding( - padding: const EdgeInsets.all(4.0), - child: FittedBox( - child: Text( - '$error', - textAlign: TextAlign.center, - textDirection: TextDirection.ltr, - style: const TextStyle( - shadows: [Shadow(blurRadius: 1.0)], - ), - ), - ), - ), - ], - ); - } + // Widget _debugBuildErrorWidget(BuildContext context, Object error) { + // return Stack( + // alignment: Alignment.center, + // children: [ + // const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))), + // Padding( + // padding: const EdgeInsets.all(4.0), + // child: FittedBox( + // child: Text( + // '$error', + // textAlign: TextAlign.center, + // textDirection: TextDirection.ltr, + // style: const TextStyle( + // shadows: [Shadow(blurRadius: 1.0)], + // ), + // ), + // ), + // ), + // ], + // ); + // } @override Widget build(BuildContext context) { @@ -594,25 +590,31 @@ class _ImageState extends State with WidgetsBindingObserver { if (widget.errorBuilder != null) { return widget.errorBuilder!(context, _lastException!, _lastStack); } - if (kDebugMode) { - return _debugBuildErrorWidget(context, _lastException!); - } + // if (kDebugMode) { + // return _debugBuildErrorWidget(context, _lastException!); + // } } Widget result; if (_imageInfo != null) { - // final isLongPic = - // _imageInfo!.image.height / _imageInfo!.image.width > - // StyleString.imgMaxRatio; + double? minScale, maxScale; + final imgWidth = _imageInfo!.image.width.toDouble(); + final imgHeight = _imageInfo!.image.height.toDouble(); + final imgRatio = imgHeight / imgWidth; + final isLongPic = + imgRatio > StyleString.imgMaxRatio && + imgHeight > widget.containerSize.height; + if (isLongPic) { + minScale = + widget.containerSize.width / widget.containerSize.height * imgRatio; + maxScale = math.max(widget.maxScale, minScale * 3); + } result = Viewer( - minScale: widget.minScale, - maxScale: widget.maxScale, + minScale: minScale ?? widget.minScale, + maxScale: maxScale ?? widget.maxScale, + isLongPic: isLongPic, containerSize: widget.containerSize, - childSize: Size( - _imageInfo!.image.width.toDouble(), - _imageInfo!.image.height.toDouble(), - ), - isAnimating: widget.isAnimating, + childSize: Size(imgWidth, imgHeight), onDragStart: widget.onDragStart, onDragUpdate: widget.onDragUpdate, onDragEnd: widget.onDragEnd, diff --git a/lib/common/widgets/image_viewer/viewer.dart b/lib/common/widgets/image_viewer/viewer.dart index 7a579726e..b17b2c655 100644 --- a/lib/common/widgets/image_viewer/viewer.dart +++ b/lib/common/widgets/image_viewer/viewer.dart @@ -17,9 +17,9 @@ import 'dart:math' as math; -import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/image_tap_gesture_recognizer.dart'; +import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -39,9 +39,9 @@ class Viewer extends StatefulWidget { super.key, required this.minScale, required this.maxScale, + this.isLongPic = false, required this.containerSize, required this.childSize, - required this.isAnimating, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, @@ -53,11 +53,11 @@ class Viewer extends StatefulWidget { final double minScale; final double maxScale; + final bool isLongPic; final Size containerSize; final Size childSize; final Widget child; - final ValueGetter isAnimating; final ValueChanged? onDragStart; final ValueChanged? onDragUpdate; final ValueChanged? onDragEnd; @@ -76,6 +76,7 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { _GestureType? _gestureType; + Offset? _scalePos; late double _scale; double? _scaleStart; late Offset _position; @@ -107,7 +108,7 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _reset() { - _scale = 1.0; + _scale = widget.minScale; _position = .zero; } @@ -118,21 +119,15 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { widget.childSize, widget.containerSize, ).destination; - // if (_imageSize.height / _imageSize.width > StyleString.imgMaxRatio) { - // _imageSize = applyBoxFit( - // .fitWidth, - // widget.childSize, - // widget.containerSize, - // ).destination; - // final containerWidth = widget.containerSize.width; - // final containerHeight = widget.containerSize.height; - // _scale = containerWidth / _imageSize.width; - // final imageHeight = _imageSize.height * _scale; - // _position = Offset( - // (1 - _scale) * containerWidth / 2, - // (imageHeight - _scale * containerHeight) / 2, - // ); - // } + if (widget.isLongPic) { + final containerWidth = widget.containerSize.width; + final containerHeight = widget.containerSize.height; + final imageHeight = _imageSize.height * _scale; + _position = Offset( + (1 - _scale) * containerWidth / 2, + (imageHeight - _scale * containerHeight) / 2, + ); + } } @override @@ -223,32 +218,27 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _handleDoubleTap() { - final Matrix4 begin; - final Matrix4 end; - if (_scale == 1.0) { - final imageWidth = _imageSize.width; - final imageHeight = _imageSize.height; - final isLongPic = imageHeight / imageWidth >= StyleString.imgMaxRatio; - double scale = widget.maxScale * 0.6; - if (isLongPic) { - scale = widget.containerSize.width / _imageSize.width; - } else { - scale = widget.maxScale * 0.6; + final begin = Matrix4.identity() + ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) + ..scaleByDouble(_scale, _scale, _scale, 1.0); + + double endScale; + if (_scale == widget.minScale) { + endScale = widget.maxScale * 0.6; + if (endScale <= widget.minScale) { + endScale = widget.maxScale; } - if (scale <= widget.minScale) { - scale = widget.maxScale; - } - begin = Matrix4.identity(); - final position = _clampPosition(_downPos! * (1 - scale), scale); - end = Matrix4.identity() - ..translateByDouble(position.dx, position.dy, 0.0, 1.0) - ..scaleByDouble(scale, scale, scale, 1.0); } else { - begin = Matrix4.identity() - ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0) - ..scaleByDouble(_scale, _scale, _scale, 1.0); - end = Matrix4.identity(); + endScale = widget.minScale; } + final position = _clampPosition( + (_downPos! * (_scale - endScale) + _position * endScale) / _scale, + endScale, + ); + final end = Matrix4.identity() + ..translateByDouble(position.dx, position.dy, 0.0, 1.0) + ..scaleByDouble(endScale, endScale, endScale, 1.0); + _tween ..begin = begin ..end = end; @@ -258,9 +248,25 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _onScaleStart(ScaleStartDetails details) { - if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) { - widget.onDragStart?.call(details); - return; + if (details.pointerCount == 1) { + if (widget.isLongPic) { + final imageHeight = _scale * _imageSize.height; + final containerHeight = widget.containerSize.height; + if (_scalePos != null && + (_round(_position.dy) == + _round((imageHeight - _scale * containerHeight) / 2) && + details.focalPoint.dy > _scalePos!.dy) || + (_round(_position.dy) == _round(containerHeight - imageHeight) && + details.focalPoint.dy < _scalePos!.dy)) { + _gestureType = .drag; + widget.onDragStart?.call(details); + return; + } + } else if (_scale == widget.minScale) { + _gestureType = .drag; + widget.onDragStart?.call(details); + return; + } } _scaleStart = _scale; @@ -268,7 +274,7 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _onScaleUpdate(ScaleUpdateDetails details) { - if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) { + if (_gestureType == .drag) { widget.onDragUpdate?.call(details); return; } @@ -298,11 +304,6 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { /// ref [InteractiveViewer] void _onScaleEnd(ScaleEndDetails details) { - if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) { - widget.onDragEnd?.call(details); - return; - } - switch (_gestureType) { case _GestureType.pan: if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { @@ -337,31 +338,35 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { ..duration = Duration(milliseconds: (tFinal * 1000).round()) ..forward(from: 0); case _GestureType.scale: - // if (details.scaleVelocity.abs() < 0.1) { - // return; - // } - // final double scale = _scale; - // final FrictionSimulation frictionSimulation = FrictionSimulation( - // _interactionEndFrictionCoefficient * _scaleFactor, - // scale, - // details.scaleVelocity / 10, - // ); - // final double tFinal = _getFinalTime( - // details.scaleVelocity.abs(), - // _interactionEndFrictionCoefficient, - // effectivelyMotionless: 0.1, - // ); - // _scaleAnimation = _scaleController.drive( - // Tween( - // begin: scale, - // end: frictionSimulation.x(tFinal), - // ).chain(CurveTween(curve: Curves.decelerate)), - // )..addListener(_handleScaleAnimation); - // _effectiveAnimationController - // ..duration = Duration(milliseconds: (tFinal * 1000).round()) - // ..forward(from: 0); + // if (details.scaleVelocity.abs() < 0.1) { + // return; + // } + // final double scale = _scale; + // final FrictionSimulation frictionSimulation = FrictionSimulation( + // _interactionEndFrictionCoefficient * _scaleFactor, + // scale, + // details.scaleVelocity / 10, + // ); + // final double tFinal = _getFinalTime( + // details.scaleVelocity.abs(), + // _interactionEndFrictionCoefficient, + // effectivelyMotionless: 0.1, + // ); + // _scaleAnimation = _scaleController.drive( + // Tween( + // begin: scale, + // end: frictionSimulation.x(tFinal), + // ).chain(CurveTween(curve: Curves.decelerate)), + // )..addListener(_handleScaleAnimation); + // _effectiveAnimationController + // ..duration = Duration(milliseconds: (tFinal * 1000).round()) + // ..forward(from: 0); + break; + case _GestureType.drag: + widget.onDragEnd?.call(details); case null: } + _scalePos = null; _gestureType = null; } @@ -385,6 +390,7 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } void _onPointerDown(PointerDownEvent event) { + _scalePos = event.position; _tapGestureRecognizer.addPointer(event); _doubleTapGestureRecognizer.addPointer(event); _horizontalDragGestureRecognizer @@ -440,7 +446,9 @@ class _ViewerState extends State with SingleTickerProviderStateMixin { } } -enum _GestureType { pan, scale } +double _round(double value) => value.toPrecision(6); + +enum _GestureType { pan, scale, drag } double _getFinalTime( double velocity, diff --git a/lib/models/common/image_preview_type.dart b/lib/models/common/image_preview_type.dart index b882c7e6a..e8dd206d3 100644 --- a/lib/models/common/image_preview_type.dart +++ b/lib/models/common/image_preview_type.dart @@ -1,5 +1,3 @@ -import 'package:PiliPlus/common/constants.dart'; - enum SourceType { fileImage, networkImage, livePhoto } class SourceModel { @@ -8,6 +6,7 @@ class SourceModel { final String? liveUrl; final int? width; final int? height; + final bool isLongPic; const SourceModel({ this.sourceType = SourceType.networkImage, @@ -15,12 +14,6 @@ class SourceModel { this.liveUrl, this.width, this.height, + this.isLongPic = false, }); - - bool get isLongPic { - if (width != null && height != null) { - return height! / width! > StyleString.imgMaxRatio; - } - return false; - } }