312 lines
9.6 KiB
Dart
312 lines
9.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'dart:math';
|
|
|
|
class GestureZoomBox extends StatefulWidget {
|
|
final double maxScale;
|
|
final double doubleTapScale;
|
|
final Widget child;
|
|
final Duration duration;
|
|
|
|
const GestureZoomBox({
|
|
Key? key,
|
|
this.maxScale = 2.0,
|
|
this.doubleTapScale = 2.0,
|
|
required this.child,
|
|
this.duration = const Duration(milliseconds: 200),
|
|
}) : assert(maxScale >= 1.0),
|
|
assert(doubleTapScale >= 1.0 && doubleTapScale <= maxScale),
|
|
super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _GestureZoomBoxState();
|
|
}
|
|
}
|
|
|
|
class _GestureZoomBoxState extends State<GestureZoomBox>
|
|
with TickerProviderStateMixin {
|
|
AnimationController? _scaleAnimController; // 缩放动画控制器
|
|
AnimationController? _offsetAnimController; // 偏移动画控制器
|
|
ScaleUpdateDetails? _latestScaleUpdateDetails; // 上次缩放变化数据
|
|
|
|
double _scale = 1.0; // 当前缩放值
|
|
Offset _offset = Offset.zero; // 当前偏移值
|
|
Offset? _doubleTapPosition; // 双击缩放的点击位置
|
|
|
|
bool _isScaling = false;
|
|
bool _isDragging = false;
|
|
|
|
double _maxDragOver = 100; // 拖动超出边界的最大值
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Transform(
|
|
alignment: Alignment.center,
|
|
transform: Matrix4.identity()
|
|
..translate(_offset.dx, _offset.dy)
|
|
..scale(_scale, _scale),
|
|
child: Listener(
|
|
onPointerUp: _onPointerUp,
|
|
child: GestureDetector(
|
|
onDoubleTap: _onDoubleTap,
|
|
onScaleStart: _onScaleStart,
|
|
onScaleUpdate: _onScaleUpdate,
|
|
onScaleEnd: _onScaleEnd,
|
|
child: AbsorbPointer(
|
|
absorbing: _scale != 1,
|
|
child: widget.child,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scaleAnimController?.dispose();
|
|
_offsetAnimController?.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
/// 处理手指抬起事件 [event]
|
|
_onPointerUp(PointerUpEvent event) {
|
|
_doubleTapPosition = event.localPosition;
|
|
}
|
|
|
|
/// 处理双击
|
|
_onDoubleTap() {
|
|
double targetScale = _scale == 1.0 ? widget.doubleTapScale : 1.0;
|
|
_animationScale(targetScale);
|
|
if (targetScale == 1.0) {
|
|
_animationOffset(Offset.zero);
|
|
}
|
|
}
|
|
|
|
_onScaleStart(ScaleStartDetails details) {
|
|
_scaleAnimController?.stop();
|
|
_offsetAnimController?.stop();
|
|
_isScaling = false;
|
|
_isDragging = false;
|
|
_latestScaleUpdateDetails = null;
|
|
}
|
|
|
|
/// 处理缩放变化 [details]
|
|
_onScaleUpdate(ScaleUpdateDetails details) {
|
|
setState(() {
|
|
if (details.scale != 1.0) {
|
|
_scaling(details);
|
|
} else {
|
|
_dragging(details);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 执行缩放
|
|
_scaling(ScaleUpdateDetails details) {
|
|
if (_isDragging) {
|
|
return;
|
|
}
|
|
final latestScaleUpdateDetails = _latestScaleUpdateDetails,
|
|
size = context.size;
|
|
_isScaling = true;
|
|
if (latestScaleUpdateDetails == null || size == null) {
|
|
_latestScaleUpdateDetails = details;
|
|
return;
|
|
}
|
|
|
|
// 计算缩放比例
|
|
double scaleIncrement = details.scale - latestScaleUpdateDetails.scale;
|
|
if (details.scale < 1.0 && _scale > 1.0) {
|
|
scaleIncrement *= _scale;
|
|
}
|
|
if (_scale < 1.0 && scaleIncrement < 0) {
|
|
scaleIncrement *= (_scale - 0.5);
|
|
} else if (_scale > widget.maxScale && scaleIncrement > 0) {
|
|
scaleIncrement *= (2.0 - (_scale - widget.maxScale));
|
|
}
|
|
_scale = max(_scale + scaleIncrement, 0.0);
|
|
|
|
// 计算缩放后偏移前(缩放前后的内容中心对齐)的左上角坐标变化
|
|
double scaleOffsetX = size.width * (_scale - 1.0) / 2;
|
|
double scaleOffsetY = size.height * (_scale - 1.0) / 2;
|
|
// 将缩放前的触摸点映射到缩放后的内容上
|
|
double scalePointDX =
|
|
(details.localFocalPoint.dx + scaleOffsetX - _offset.dx) / _scale;
|
|
double scalePointDY =
|
|
(details.localFocalPoint.dy + scaleOffsetY - _offset.dy) / _scale;
|
|
// 计算偏移,使缩放中心在屏幕上的位置保持不变
|
|
_offset += Offset(
|
|
(size.width / 2 - scalePointDX) * scaleIncrement,
|
|
(size.height / 2 - scalePointDY) * scaleIncrement,
|
|
);
|
|
|
|
_latestScaleUpdateDetails = details;
|
|
}
|
|
|
|
/// 执行拖动
|
|
_dragging(ScaleUpdateDetails details) {
|
|
if (_isScaling) {
|
|
return;
|
|
}
|
|
final latestScaleUpdateDetails = _latestScaleUpdateDetails,
|
|
size = context.size;
|
|
_isDragging = true;
|
|
if (latestScaleUpdateDetails == null || size == null) {
|
|
_latestScaleUpdateDetails = details;
|
|
return;
|
|
}
|
|
|
|
// 计算本次拖动增量
|
|
double offsetXIncrement = (details.localFocalPoint.dx -
|
|
latestScaleUpdateDetails.localFocalPoint.dx) *
|
|
_scale;
|
|
double offsetYIncrement = (details.localFocalPoint.dy -
|
|
latestScaleUpdateDetails.localFocalPoint.dy) *
|
|
_scale;
|
|
// 处理 X 轴边界
|
|
double scaleOffsetX = size.width * (_scale - 1.0) / 2;
|
|
if (scaleOffsetX <= 0) {
|
|
offsetXIncrement = 0;
|
|
} else if (_offset.dx > scaleOffsetX) {
|
|
offsetXIncrement *=
|
|
(_maxDragOver - (_offset.dx - scaleOffsetX)) / _maxDragOver;
|
|
} else if (_offset.dx < -scaleOffsetX) {
|
|
offsetXIncrement *=
|
|
(_maxDragOver - (-scaleOffsetX - _offset.dx)) / _maxDragOver;
|
|
}
|
|
// 处理 Y 轴边界
|
|
double scaleOffsetY =
|
|
(size.height * _scale - MediaQuery.of(context).size.height) / 2;
|
|
if (scaleOffsetY <= 0) {
|
|
offsetYIncrement = 0;
|
|
} else if (_offset.dy > scaleOffsetY) {
|
|
offsetYIncrement *=
|
|
(_maxDragOver - (_offset.dy - scaleOffsetY)) / _maxDragOver;
|
|
} else if (_offset.dy < -scaleOffsetY) {
|
|
offsetYIncrement *=
|
|
(_maxDragOver - (-scaleOffsetY - _offset.dy)) / _maxDragOver;
|
|
}
|
|
|
|
_offset += Offset(offsetXIncrement, offsetYIncrement);
|
|
|
|
_latestScaleUpdateDetails = details;
|
|
}
|
|
|
|
/// 缩放/拖动结束
|
|
_onScaleEnd(ScaleEndDetails details) {
|
|
final size = context.size;
|
|
if (size == null) {
|
|
return;
|
|
}
|
|
if (_scale < 1.0) {
|
|
// 缩放值过小,恢复到 1.0
|
|
_animationScale(1.0);
|
|
} else if (_scale > widget.maxScale) {
|
|
// 缩放值过大,恢复到最大值
|
|
_animationScale(widget.maxScale);
|
|
}
|
|
if (_scale <= 1.0) {
|
|
// 缩放值过小,修改偏移值,使内容居中
|
|
_animationOffset(Offset.zero);
|
|
} else if (_isDragging) {
|
|
// 处理拖动超过边界的情况(自动回弹到边界)
|
|
double realScale = _scale > widget.maxScale ? widget.maxScale : _scale;
|
|
double targetOffsetX = _offset.dx, targetOffsetY = _offset.dy;
|
|
// 处理 X 轴边界
|
|
double scaleOffsetX = size.width * (realScale - 1.0) / 2;
|
|
if (scaleOffsetX <= 0) {
|
|
targetOffsetX = 0;
|
|
} else if (_offset.dx > scaleOffsetX) {
|
|
targetOffsetX = scaleOffsetX;
|
|
} else if (_offset.dx < -scaleOffsetX) {
|
|
targetOffsetX = -scaleOffsetX;
|
|
}
|
|
// 处理 Y 轴边界
|
|
double scaleOffsetY =
|
|
(size.height * realScale - MediaQuery.of(context).size.height) / 2;
|
|
if (scaleOffsetY < 0) {
|
|
targetOffsetY = 0;
|
|
} else if (_offset.dy > scaleOffsetY) {
|
|
targetOffsetY = scaleOffsetY;
|
|
} else if (_offset.dy < -scaleOffsetY) {
|
|
targetOffsetY = -scaleOffsetY;
|
|
}
|
|
if (_offset.dx != targetOffsetX || _offset.dy != targetOffsetY) {
|
|
// 启动越界回弹
|
|
_animationOffset(Offset(targetOffsetX, targetOffsetY));
|
|
} else {
|
|
// 处理 X 轴边界
|
|
double duration =
|
|
(widget.duration.inSeconds + widget.duration.inMilliseconds / 1000);
|
|
Offset targetOffset =
|
|
_offset + details.velocity.pixelsPerSecond * duration;
|
|
targetOffsetX = targetOffset.dx;
|
|
if (targetOffsetX > scaleOffsetX) {
|
|
targetOffsetX = scaleOffsetX;
|
|
} else if (targetOffsetX < -scaleOffsetX) {
|
|
targetOffsetX = -scaleOffsetX;
|
|
}
|
|
// 处理 X 轴边界
|
|
targetOffsetY = targetOffset.dy;
|
|
if (targetOffsetY > scaleOffsetY) {
|
|
targetOffsetY = scaleOffsetY;
|
|
} else if (targetOffsetY < -scaleOffsetY) {
|
|
targetOffsetY = -scaleOffsetY;
|
|
}
|
|
// 启动惯性滚动
|
|
_animationOffset(Offset(targetOffsetX, targetOffsetY));
|
|
}
|
|
}
|
|
|
|
_isScaling = false;
|
|
_isDragging = false;
|
|
_latestScaleUpdateDetails = null;
|
|
}
|
|
|
|
/// 执行动画缩放内容到 [targetScale]
|
|
_animationScale(double targetScale) {
|
|
_scaleAnimController?.dispose();
|
|
final scaleAnimController = _scaleAnimController =
|
|
AnimationController(vsync: this, duration: widget.duration);
|
|
Animation anim = Tween<double>(begin: _scale, end: targetScale)
|
|
.animate(scaleAnimController);
|
|
anim.addListener(() {
|
|
setState(() {
|
|
_scaling(ScaleUpdateDetails(
|
|
focalPoint: _doubleTapPosition!,
|
|
localFocalPoint: _doubleTapPosition!,
|
|
scale: anim.value,
|
|
horizontalScale: anim.value,
|
|
verticalScale: anim.value,
|
|
));
|
|
});
|
|
});
|
|
anim.addStatusListener((status) {
|
|
if (status == AnimationStatus.completed) {
|
|
_onScaleEnd(ScaleEndDetails());
|
|
}
|
|
});
|
|
scaleAnimController.forward();
|
|
}
|
|
|
|
/// 执行动画偏移内容到 [targetOffset]
|
|
_animationOffset(Offset targetOffset) {
|
|
_offsetAnimController?.dispose();
|
|
final offsetAnimController = _offsetAnimController =
|
|
AnimationController(vsync: this, duration: widget.duration);
|
|
Animation anim = offsetAnimController
|
|
.drive(Tween<Offset>(begin: _offset, end: targetOffset));
|
|
anim.addListener(() {
|
|
setState(() {
|
|
_offset = anim.value;
|
|
});
|
|
});
|
|
offsetAnimController.fling();
|
|
}
|
|
}
|