opt image preview

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-02-14 14:41:48 +08:00
parent 352e314ee1
commit e7e79eb62a
5 changed files with 180 additions and 174 deletions

View File

@@ -97,8 +97,7 @@ class CustomGridView extends StatelessWidget {
liveUrl: isLive ? item.liveUrl : null, liveUrl: isLive ? item.liveUrl : null,
width: isLive ? item.width.toInt() : null, width: isLive ? item.width.toInt() : null,
height: isLive ? item.height.toInt() : null, height: isLive ? item.height.toInt() : null,
// width: item.width.toInt(), isLongPic: item.isLongPic,
// height: item.height.toInt(),
); );
}, },
).toList(); ).toList();
@@ -258,6 +257,40 @@ class CustomGridView extends StatelessWidget {
children: List.generate(length, (index) { children: List.generate(length, (index) {
final item = picArr[index]; final item = picArr[index];
final borderRadius = _borderRadius(column, length, 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( return LayoutId(
id: index, id: index,
child: GestureDetector( child: GestureDetector(
@@ -270,37 +303,7 @@ class CustomGridView extends StatelessWidget {
? (details) => ? (details) =>
_showMenu(context, details.globalPosition, item) _showMenu(context, details.globalPosition, item)
: null, : null,
child: Hero( child: child,
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,
),
],
),
),
), ),
); );
}), }),

View File

@@ -167,8 +167,6 @@ class _GalleryViewerState extends State<GalleryViewer>
} }
} }
bool isAnimating() => _animateController.value != 0;
void _onDragStart(ScaleStartDetails details) { void _onDragStart(ScaleStartDetails details) {
_dragging = true; _dragging = true;
@@ -343,25 +341,26 @@ class _GalleryViewerState extends State<GalleryViewer>
Widget _itemBuilder(BuildContext context, int index) { Widget _itemBuilder(BuildContext context, int index) {
final item = widget.sources[index]; final item = widget.sources[index];
return Hero( Widget child;
tag: item.url, switch (item.sourceType) {
child: switch (item.sourceType) { case SourceType.fileImage:
.fileImage => Image.file( child = Image.file(
key: _keys[index], key: _keys[index],
File(item.url), File(item.url),
filterQuality: .low, filterQuality: .low,
minScale: widget.minScale, minScale: widget.minScale,
maxScale: widget.maxScale, maxScale: widget.maxScale,
containerSize: _containerSize, containerSize: _containerSize,
isAnimating: isAnimating,
onDragStart: _onDragStart, onDragStart: _onDragStart,
onDragUpdate: _onDragUpdate, onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd, onDragEnd: _onDragEnd,
tapGestureRecognizer: _tapGestureRecognizer, tapGestureRecognizer: _tapGestureRecognizer,
horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer,
onChangePage: _onChangePage, onChangePage: _onChangePage,
), );
.networkImage => Image( case SourceType.networkImage:
final isLongPic = item.isLongPic;
child = Image(
key: _keys[index], key: _keys[index],
image: CachedNetworkImageProvider(_getActualUrl(item.url)), image: CachedNetworkImageProvider(_getActualUrl(item.url)),
minScale: widget.minScale, minScale: widget.minScale,
@@ -385,7 +384,6 @@ class _GalleryViewerState extends State<GalleryViewer>
minScale: widget.minScale, minScale: widget.minScale,
maxScale: widget.maxScale, maxScale: widget.maxScale,
containerSize: _containerSize, containerSize: _containerSize,
isAnimating: isAnimating,
onDragStart: null, onDragStart: null,
onDragUpdate: null, onDragUpdate: null,
onDragEnd: null, onDragEnd: null,
@@ -408,12 +406,15 @@ class _GalleryViewerState extends State<GalleryViewer>
return child; return child;
}, },
loadingBuilder: loadingBuilder, loadingBuilder: loadingBuilder,
isAnimating: isAnimating,
onDragStart: _onDragStart, onDragStart: _onDragStart,
onDragUpdate: _onDragUpdate, onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd, onDragEnd: _onDragEnd,
), );
.livePhoto => Obx( if (isLongPic) {
return child;
}
case SourceType.livePhoto:
child = Obx(
key: _keys[index], key: _keys[index],
() => _currIndex.value == index () => _currIndex.value == index
? Viewer( ? Viewer(
@@ -421,7 +422,6 @@ class _GalleryViewerState extends State<GalleryViewer>
maxScale: widget.maxScale, maxScale: widget.maxScale,
containerSize: _containerSize, containerSize: _containerSize,
childSize: _containerSize, childSize: _containerSize,
isAnimating: isAnimating,
onDragStart: _onDragStart, onDragStart: _onDragStart,
onDragUpdate: _onDragUpdate, onDragUpdate: _onDragUpdate,
onDragEnd: _onDragEnd, onDragEnd: _onDragEnd,
@@ -437,9 +437,9 @@ class _GalleryViewerState extends State<GalleryViewer>
), ),
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
), );
}, }
); return Hero(tag: item.url, child: child);
} }
void _onTap() { void _onTap() {

View File

@@ -3,7 +3,9 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:io' show 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_horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/gesture/image_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/image_tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart';
@@ -37,7 +39,6 @@ class Image extends StatefulWidget {
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
required this.containerSize, required this.containerSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -76,7 +77,6 @@ class Image extends StatefulWidget {
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
required this.containerSize, required this.containerSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -122,7 +122,6 @@ class Image extends StatefulWidget {
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
required this.containerSize, required this.containerSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -171,7 +170,6 @@ class Image extends StatefulWidget {
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
required this.containerSize, required this.containerSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -220,7 +218,6 @@ class Image extends StatefulWidget {
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
required this.containerSize, required this.containerSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -278,7 +275,6 @@ class Image extends StatefulWidget {
final double maxScale; final double maxScale;
final Size containerSize; final Size containerSize;
final ValueGetter<bool> isAnimating;
final ValueChanged<ScaleStartDetails>? onDragStart; final ValueChanged<ScaleStartDetails>? onDragStart;
final ValueChanged<ScaleUpdateDetails>? onDragUpdate; final ValueChanged<ScaleUpdateDetails>? onDragUpdate;
final ValueChanged<ScaleEndDetails>? onDragEnd; final ValueChanged<ScaleEndDetails>? onDragEnd;
@@ -566,27 +562,27 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
_isListeningToStream = false; _isListeningToStream = false;
} }
Widget _debugBuildErrorWidget(BuildContext context, Object error) { // Widget _debugBuildErrorWidget(BuildContext context, Object error) {
return Stack( // return Stack(
alignment: Alignment.center, // alignment: Alignment.center,
children: <Widget>[ // children: <Widget>[
const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))), // const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))),
Padding( // Padding(
padding: const EdgeInsets.all(4.0), // padding: const EdgeInsets.all(4.0),
child: FittedBox( // child: FittedBox(
child: Text( // child: Text(
'$error', // '$error',
textAlign: TextAlign.center, // textAlign: TextAlign.center,
textDirection: TextDirection.ltr, // textDirection: TextDirection.ltr,
style: const TextStyle( // style: const TextStyle(
shadows: <Shadow>[Shadow(blurRadius: 1.0)], // shadows: <Shadow>[Shadow(blurRadius: 1.0)],
), // ),
), // ),
), // ),
), // ),
], // ],
); // );
} // }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -594,25 +590,31 @@ class _ImageState extends State<Image> with WidgetsBindingObserver {
if (widget.errorBuilder != null) { if (widget.errorBuilder != null) {
return widget.errorBuilder!(context, _lastException!, _lastStack); return widget.errorBuilder!(context, _lastException!, _lastStack);
} }
if (kDebugMode) { // if (kDebugMode) {
return _debugBuildErrorWidget(context, _lastException!); // return _debugBuildErrorWidget(context, _lastException!);
} // }
} }
Widget result; Widget result;
if (_imageInfo != null) { if (_imageInfo != null) {
// final isLongPic = double? minScale, maxScale;
// _imageInfo!.image.height / _imageInfo!.image.width > final imgWidth = _imageInfo!.image.width.toDouble();
// StyleString.imgMaxRatio; 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( result = Viewer(
minScale: widget.minScale, minScale: minScale ?? widget.minScale,
maxScale: widget.maxScale, maxScale: maxScale ?? widget.maxScale,
isLongPic: isLongPic,
containerSize: widget.containerSize, containerSize: widget.containerSize,
childSize: Size( childSize: Size(imgWidth, imgHeight),
_imageInfo!.image.width.toDouble(),
_imageInfo!.image.height.toDouble(),
),
isAnimating: widget.isAnimating,
onDragStart: widget.onDragStart, onDragStart: widget.onDragStart,
onDragUpdate: widget.onDragUpdate, onDragUpdate: widget.onDragUpdate,
onDragEnd: widget.onDragEnd, onDragEnd: widget.onDragEnd,

View File

@@ -17,9 +17,9 @@
import 'dart:math' as math; 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_horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/gesture/image_tap_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:easy_debounce/easy_throttle.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@@ -39,9 +39,9 @@ class Viewer extends StatefulWidget {
super.key, super.key,
required this.minScale, required this.minScale,
required this.maxScale, required this.maxScale,
this.isLongPic = false,
required this.containerSize, required this.containerSize,
required this.childSize, required this.childSize,
required this.isAnimating,
required this.onDragStart, required this.onDragStart,
required this.onDragUpdate, required this.onDragUpdate,
required this.onDragEnd, required this.onDragEnd,
@@ -53,11 +53,11 @@ class Viewer extends StatefulWidget {
final double minScale; final double minScale;
final double maxScale; final double maxScale;
final bool isLongPic;
final Size containerSize; final Size containerSize;
final Size childSize; final Size childSize;
final Widget child; final Widget child;
final ValueGetter<bool> isAnimating;
final ValueChanged<ScaleStartDetails>? onDragStart; final ValueChanged<ScaleStartDetails>? onDragStart;
final ValueChanged<ScaleUpdateDetails>? onDragUpdate; final ValueChanged<ScaleUpdateDetails>? onDragUpdate;
final ValueChanged<ScaleEndDetails>? onDragEnd; final ValueChanged<ScaleEndDetails>? onDragEnd;
@@ -76,6 +76,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
_GestureType? _gestureType; _GestureType? _gestureType;
Offset? _scalePos;
late double _scale; late double _scale;
double? _scaleStart; double? _scaleStart;
late Offset _position; late Offset _position;
@@ -107,7 +108,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _reset() { void _reset() {
_scale = 1.0; _scale = widget.minScale;
_position = .zero; _position = .zero;
} }
@@ -118,21 +119,15 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
widget.childSize, widget.childSize,
widget.containerSize, widget.containerSize,
).destination; ).destination;
// if (_imageSize.height / _imageSize.width > StyleString.imgMaxRatio) { if (widget.isLongPic) {
// _imageSize = applyBoxFit( final containerWidth = widget.containerSize.width;
// .fitWidth, final containerHeight = widget.containerSize.height;
// widget.childSize, final imageHeight = _imageSize.height * _scale;
// widget.containerSize, _position = Offset(
// ).destination; (1 - _scale) * containerWidth / 2,
// final containerWidth = widget.containerSize.width; (imageHeight - _scale * containerHeight) / 2,
// final containerHeight = widget.containerSize.height; );
// _scale = containerWidth / _imageSize.width; }
// final imageHeight = _imageSize.height * _scale;
// _position = Offset(
// (1 - _scale) * containerWidth / 2,
// (imageHeight - _scale * containerHeight) / 2,
// );
// }
} }
@override @override
@@ -223,32 +218,27 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _handleDoubleTap() { void _handleDoubleTap() {
final Matrix4 begin; final begin = Matrix4.identity()
final Matrix4 end; ..translateByDouble(_position.dx, _position.dy, 0.0, 1.0)
if (_scale == 1.0) { ..scaleByDouble(_scale, _scale, _scale, 1.0);
final imageWidth = _imageSize.width;
final imageHeight = _imageSize.height; double endScale;
final isLongPic = imageHeight / imageWidth >= StyleString.imgMaxRatio; if (_scale == widget.minScale) {
double scale = widget.maxScale * 0.6; endScale = widget.maxScale * 0.6;
if (isLongPic) { if (endScale <= widget.minScale) {
scale = widget.containerSize.width / _imageSize.width; endScale = widget.maxScale;
} else {
scale = widget.maxScale * 0.6;
} }
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 { } else {
begin = Matrix4.identity() endScale = widget.minScale;
..translateByDouble(_position.dx, _position.dy, 0.0, 1.0)
..scaleByDouble(_scale, _scale, _scale, 1.0);
end = Matrix4.identity();
} }
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 _tween
..begin = begin ..begin = begin
..end = end; ..end = end;
@@ -258,9 +248,25 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _onScaleStart(ScaleStartDetails details) { void _onScaleStart(ScaleStartDetails details) {
if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) { if (details.pointerCount == 1) {
widget.onDragStart?.call(details); if (widget.isLongPic) {
return; 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; _scaleStart = _scale;
@@ -268,7 +274,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) { if (_gestureType == .drag) {
widget.onDragUpdate?.call(details); widget.onDragUpdate?.call(details);
return; return;
} }
@@ -298,11 +304,6 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
/// ref [InteractiveViewer] /// ref [InteractiveViewer]
void _onScaleEnd(ScaleEndDetails details) { void _onScaleEnd(ScaleEndDetails details) {
if (widget.isAnimating() || (details.pointerCount < 2 && _scale == 1.0)) {
widget.onDragEnd?.call(details);
return;
}
switch (_gestureType) { switch (_gestureType) {
case _GestureType.pan: case _GestureType.pan:
if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) {
@@ -337,31 +338,35 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
..duration = Duration(milliseconds: (tFinal * 1000).round()) ..duration = Duration(milliseconds: (tFinal * 1000).round())
..forward(from: 0); ..forward(from: 0);
case _GestureType.scale: case _GestureType.scale:
// if (details.scaleVelocity.abs() < 0.1) { // if (details.scaleVelocity.abs() < 0.1) {
// return; // return;
// } // }
// final double scale = _scale; // final double scale = _scale;
// final FrictionSimulation frictionSimulation = FrictionSimulation( // final FrictionSimulation frictionSimulation = FrictionSimulation(
// _interactionEndFrictionCoefficient * _scaleFactor, // _interactionEndFrictionCoefficient * _scaleFactor,
// scale, // scale,
// details.scaleVelocity / 10, // details.scaleVelocity / 10,
// ); // );
// final double tFinal = _getFinalTime( // final double tFinal = _getFinalTime(
// details.scaleVelocity.abs(), // details.scaleVelocity.abs(),
// _interactionEndFrictionCoefficient, // _interactionEndFrictionCoefficient,
// effectivelyMotionless: 0.1, // effectivelyMotionless: 0.1,
// ); // );
// _scaleAnimation = _scaleController.drive( // _scaleAnimation = _scaleController.drive(
// Tween<double>( // Tween<double>(
// begin: scale, // begin: scale,
// end: frictionSimulation.x(tFinal), // end: frictionSimulation.x(tFinal),
// ).chain(CurveTween(curve: Curves.decelerate)), // ).chain(CurveTween(curve: Curves.decelerate)),
// )..addListener(_handleScaleAnimation); // )..addListener(_handleScaleAnimation);
// _effectiveAnimationController // _effectiveAnimationController
// ..duration = Duration(milliseconds: (tFinal * 1000).round()) // ..duration = Duration(milliseconds: (tFinal * 1000).round())
// ..forward(from: 0); // ..forward(from: 0);
break;
case _GestureType.drag:
widget.onDragEnd?.call(details);
case null: case null:
} }
_scalePos = null;
_gestureType = null; _gestureType = null;
} }
@@ -385,6 +390,7 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
void _onPointerDown(PointerDownEvent event) { void _onPointerDown(PointerDownEvent event) {
_scalePos = event.position;
_tapGestureRecognizer.addPointer(event); _tapGestureRecognizer.addPointer(event);
_doubleTapGestureRecognizer.addPointer(event); _doubleTapGestureRecognizer.addPointer(event);
_horizontalDragGestureRecognizer _horizontalDragGestureRecognizer
@@ -440,7 +446,9 @@ class _ViewerState extends State<Viewer> with SingleTickerProviderStateMixin {
} }
} }
enum _GestureType { pan, scale } double _round(double value) => value.toPrecision(6);
enum _GestureType { pan, scale, drag }
double _getFinalTime( double _getFinalTime(
double velocity, double velocity,

View File

@@ -1,5 +1,3 @@
import 'package:PiliPlus/common/constants.dart';
enum SourceType { fileImage, networkImage, livePhoto } enum SourceType { fileImage, networkImage, livePhoto }
class SourceModel { class SourceModel {
@@ -8,6 +6,7 @@ class SourceModel {
final String? liveUrl; final String? liveUrl;
final int? width; final int? width;
final int? height; final int? height;
final bool isLongPic;
const SourceModel({ const SourceModel({
this.sourceType = SourceType.networkImage, this.sourceType = SourceType.networkImage,
@@ -15,12 +14,6 @@ class SourceModel {
this.liveUrl, this.liveUrl,
this.width, this.width,
this.height, this.height,
this.isLongPic = false,
}); });
bool get isLongPic {
if (width != null && height != null) {
return height! / width! > StyleString.imgMaxRatio;
}
return false;
}
} }