Skip to content

Commit

Permalink
[SuperEditor][SuperTextField][mobile] - Do not crop magnifier at scre…
Browse files Browse the repository at this point in the history
…en edges (Resolves #2167) (#2482)
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Jan 10, 2025
1 parent 7120ea1 commit 3a89307
Show file tree
Hide file tree
Showing 21 changed files with 383 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,10 @@ class _MobileEditingIOSDemoState extends State<MobileEditingIOSDemo> with Single
child: IOSFollowingMagnifier.roundedRectangle(
magnifierKey: magnifierKey,
leaderLink: focalPoint,
// The bottom of the magnifier sits above the focal point.
// Leave a few pixels between the bottom of the magnifier and the focal point. This
// value was chosen empirically.
offsetFromFocalPoint: const Offset(0, -20),
// The magnifier is centered with the focal point. Translate it so that it sits
// above the focal point and leave a few pixels between the bottom of the magnifier
// and the focal point. This value was chosen empirically.
offsetFromFocalPoint: Offset(0, (-defaultIosMagnifierSize.height / 2) - 20),
show: isVisible,
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1945,23 +1945,20 @@ class SuperEditorAndroidControlsOverlayManagerState extends State<SuperEditorAnd
return const SizedBox();
}

final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
return Follower.withOffset(
link: _controlsController!.magnifierFocalPoint,
offset: const Offset(0, -150),
offset: Offset(0, -54 * devicePixelRatio),
leaderAnchor: Alignment.center,
followerAnchor: Alignment.topLeft,
// Theoretically, we should be able to use a leaderAnchor and followerAnchor of "center"
// and avoid the following FractionalTranslation. However, when centering the follower,
// we don't get the expect focal point within the magnified area. It's off-center. I'm not
// sure why that happens, but using a followerAnchor of "topLeft" and then pulling back
// by 50% solve the problem.
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: AndroidMagnifyingGlass(
key: magnifierKey,
magnificationScale: 1.5,
offsetFromFocalPoint: Offset(0, -150 / MediaQuery.devicePixelRatioOf(context)),
),
followerAnchor: Alignment.center,
boundary: ScreenFollowerBoundary(
screenSize: MediaQuery.sizeOf(context),
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
),
child: AndroidMagnifyingGlass(
key: magnifierKey,
magnificationScale: 1.5,
offsetFromFocalPoint: const Offset(0, -54),
),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1506,23 +1506,25 @@ class SuperEditorIosMagnifierOverlayManagerState extends State<SuperEditorIosMag
// When the user is dragging an overlay handle, SuperEditor
// position a Leader with a LeaderLink. This magnifier follows that Leader
// via the LeaderLink.
return ValueListenableBuilder(
valueListenable: _controlsController!.shouldShowMagnifier,
builder: (context, shouldShowMagnifier, child) {
return _controlsController!.magnifierBuilder != null //
? _controlsController!.magnifierBuilder!(
context,
DocumentKeys.magnifier,
_controlsController!.magnifierFocalPoint,
shouldShowMagnifier,
)
: _buildDefaultMagnifier(
context,
DocumentKeys.magnifier,
_controlsController!.magnifierFocalPoint,
shouldShowMagnifier,
);
},
return IgnorePointer(
child: ValueListenableBuilder(
valueListenable: _controlsController!.shouldShowMagnifier,
builder: (context, shouldShowMagnifier, child) {
return _controlsController!.magnifierBuilder != null //
? _controlsController!.magnifierBuilder!(
context,
DocumentKeys.magnifier,
_controlsController!.magnifierFocalPoint,
shouldShowMagnifier,
)
: _buildDefaultMagnifier(
context,
DocumentKeys.magnifier,
_controlsController!.magnifierFocalPoint,
shouldShowMagnifier,
);
},
),
);
}

Expand All @@ -1537,10 +1539,10 @@ class SuperEditorIosMagnifierOverlayManagerState extends State<SuperEditorIosMag
magnifierKey: magnifierKey,
leaderLink: magnifierFocalPoint,
show: isVisible,
// The bottom of the magnifier sits above the focal point.
// Leave a few pixels between the bottom of the magnifier and the focal point. This
// value was chosen empirically.
offsetFromFocalPoint: const Offset(0, -20),
// The magnifier is centered with the focal point. Translate it so that it sits
// above the focal point and leave a few pixels between the bottom of the magnifier
// and the focal point. This value was chosen empirically.
offsetFromFocalPoint: Offset(0, (-defaultIosMagnifierSize.height / 2) - 20),
handleColor: _controlsController!.handleColor,
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/super_textfield/infrastructure/magnifier.dart';
import 'package:super_editor/src/super_textfield/infrastructure/outer_box_shadow.dart';

/// An Android magnifying glass that follows a [LayerLink].
/// An Android magnifying glass that follows a [LeaderLink].
class AndroidFollowingMagnifier extends StatelessWidget {
const AndroidFollowingMagnifier({
Key? key,
required this.layerLink,
this.offsetFromFocalPoint = Offset.zero,
}) : super(key: key);

final LayerLink layerLink;
final LeaderLink layerLink;
final Offset offsetFromFocalPoint;

@override
Widget build(BuildContext context) {
return CompositedTransformFollower(
final devicePixelRatio = MediaQuery.devicePixelRatioOf(context);

return Follower.withOffset(
link: layerLink,
offset: offsetFromFocalPoint,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: AndroidMagnifyingGlass(
offsetFromFocalPoint: offsetFromFocalPoint,
leaderAnchor: Alignment.center,
followerAnchor: Alignment.center,
boundary: ScreenFollowerBoundary(
screenSize: MediaQuery.sizeOf(context),
devicePixelRatio: devicePixelRatio,
),
child: AndroidMagnifyingGlass(
magnificationScale: 1.5,
offsetFromFocalPoint: Offset(
offsetFromFocalPoint.dx / devicePixelRatio,
offsetFromFocalPoint.dy / devicePixelRatio,
),
),
);
Expand Down
39 changes: 17 additions & 22 deletions super_editor/lib/src/infrastructure/platforms/ios/magnifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,30 +116,25 @@ class _IOSFollowingMagnifierState extends State<IOSFollowingMagnifier> with Sing
// Animate the magnfier up on entrance and down on exit.
widget.offsetFromFocalPoint.dy * devicePixelRatio * percentage,
),
// Translate the magnifier so it's displayed above the focal point
// when the animation ends.
child: FractionalTranslation(
translation: Offset(0.0, -0.5 * percentage),
child: widget.magnifierBuilder(
context,
IosMagnifierViewModel(
// In theory, the offsetFromFocalPoint should either be `widget.offsetFromFocalPoint.dy` to match
// the actual offset, or it should be `widget.offsetFromFocalPoint.dy / magnificationLevel`. Neither
// of those align the focal point correctly. The following offset was found empirically to give the
// desired results. These values seem to work even with different pixel densities.
offsetFromFocalPoint: Offset(
-22 * percentage,
(-defaultIosMagnifierSize.height + 14) * percentage,
),
animationValue: _animationController.value,
animationDirection:
const [AnimationStatus.forward, AnimationStatus.completed].contains(_animationController.status)
? AnimationDirection.forward
: AnimationDirection.reverse,
borderColor: widget.handleColor ?? Theme.of(context).primaryColor,
boundary: ScreenFollowerBoundary(
screenSize: MediaQuery.sizeOf(context),
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
),
child: widget.magnifierBuilder(
context,
IosMagnifierViewModel(
offsetFromFocalPoint: Offset(
widget.offsetFromFocalPoint.dx * percentage,
widget.offsetFromFocalPoint.dy * percentage,
),
widget.magnifierKey,
animationValue: _animationController.value,
animationDirection:
const [AnimationStatus.forward, AnimationStatus.completed].contains(_animationController.status)
? AnimationDirection.forward
: AnimationDirection.reverse,
borderColor: widget.handleColor ?? Theme.of(context).primaryColor,
),
widget.magnifierKey,
),
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class GestureEditingController with ChangeNotifier {
GestureEditingController({
required this.selectionLinks,
required MagnifierAndToolbarController overlayController,
required LayerLink magnifierFocalPointLink,
required LeaderLink magnifierFocalPointLink,
}) : _magnifierFocalPointLink = magnifierFocalPointLink,
_overlayController = overlayController {
_overlayController.addListener(_toolbarChanged);
Expand All @@ -386,8 +386,8 @@ class GestureEditingController with ChangeNotifier {

/// A `LayerLink` whose top-left corner sits at the location where the
/// magnifier should magnify.
LayerLink get magnifierFocalPointLink => _magnifierFocalPointLink;
final LayerLink _magnifierFocalPointLink;
LeaderLink get magnifierFocalPointLink => _magnifierFocalPointLink;
final LeaderLink _magnifierFocalPointLink;

/// Controls the magnifier and the toolbar.
MagnifierAndToolbarController get overlayController => _overlayController;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class _ReadOnlyAndroidDocumentTouchInteractorState extends State<ReadOnlyAndroid
GroupedOverlayPortalController(displayPriority: OverlayGroupPriority.editingControls);
final _overlayPortalRebuildSignal = SignalNotifier();
late AndroidDocumentGestureEditingController _editingController;
final _magnifierFocalPointLink = LayerLink();
final _magnifierFocalPointLink = LeaderLink();

late DragHandleAutoScroller _handleAutoScrolling;
Offset? _globalStartDragOffset;
Expand Down Expand Up @@ -1480,7 +1480,7 @@ class _AndroidDocumentTouchEditingControlsState extends State<AndroidDocumentTou
left: magnifierOffset.dx,
// TODO: select focal position based on type of content
top: magnifierOffset.dy,
child: CompositedTransformTarget(
child: Leader(
link: widget.editingController.magnifierFocalPointLink,
child: const SizedBox(width: 1, height: 1),
),
Expand All @@ -1492,11 +1492,9 @@ class _AndroidDocumentTouchEditingControlsState extends State<AndroidDocumentTou
//
// When the user is dragging an overlay handle, we place a LayerLink
// target. This magnifier follows that target.
return Center(
child: AndroidFollowingMagnifier(
layerLink: widget.editingController.magnifierFocalPointLink,
offsetFromFocalPoint: const Offset(0, -72),
),
return AndroidFollowingMagnifier(
layerLink: widget.editingController.magnifierFocalPointLink,
offsetFromFocalPoint: Offset(0, -54 * MediaQuery.devicePixelRatioOf(context)),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1164,10 +1164,10 @@ class SuperReaderIosMagnifierOverlayManagerState extends State<SuperReaderIosMag
magnifierKey: magnifierKey,
show: visible,
leaderLink: magnifierFocalPoint,
// The bottom of the magnifier sits above the focal point.
// Leave a few pixels between the bottom of the magnifier and the focal point. This
// value was chosen empirically.
offsetFromFocalPoint: const Offset(0, -20),
// The magnifier is centered with the focal point. Translate it so that it sits
// above the focal point and leave a few pixels between the bottom of the magnifier
// and the focal point. This value was chosen empirically.
offsetFromFocalPoint: Offset(0, (-defaultIosMagnifierSize.height / 2) - 20),
handleColor: _controlsContext!.handleColor,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
import 'package:super_editor/src/infrastructure/multi_listenable_builder.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
Expand Down Expand Up @@ -805,7 +806,7 @@ class _AndroidEditingOverlayControlsState extends State<AndroidEditingOverlayCon
return Positioned(
left: _localDragOffset!.dx,
top: focalPointOffsetInMiddleOfLine.dy,
child: CompositedTransformTarget(
child: Leader(
link: widget.editingController.magnifierFocalPoint,
child: const SizedBox(width: 1, height: 1),
),
Expand All @@ -821,11 +822,9 @@ class _AndroidEditingOverlayControlsState extends State<AndroidEditingOverlayCon
// When some other interaction wants to show the magnifier, then
// that other area of the widget tree is responsible for
// positioning the LayerLink target.
return Center(
child: AndroidFollowingMagnifier(
layerLink: widget.editingController.magnifierFocalPoint,
offsetFromFocalPoint: const Offset(0, -72),
),
return AndroidFollowingMagnifier(
layerLink: widget.editingController.magnifierFocalPoint,
offsetFromFocalPoint: Offset(0, -54 * MediaQuery.devicePixelRatioOf(context)),
);
}

Expand All @@ -838,7 +837,7 @@ class AndroidEditingOverlayController with ChangeNotifier {
AndroidEditingOverlayController({
required this.textController,
required this.caretBlinkController,
required LayerLink magnifierFocalPoint,
required LeaderLink magnifierFocalPoint,
}) : _magnifierFocalPoint = magnifierFocalPoint;

@override
Expand Down Expand Up @@ -894,8 +893,8 @@ class AndroidEditingOverlayController with ChangeNotifier {
notifyListeners();
}

final LayerLink _magnifierFocalPoint;
LayerLink get magnifierFocalPoint => _magnifierFocalPoint;
final LeaderLink _magnifierFocalPoint;
LeaderLink get magnifierFocalPoint => _magnifierFocalPoint;

bool _isMagnifierVisible = false;
bool get isMagnifierVisible => _isMagnifierVisible;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/document_gestures_interaction_overrides.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
Expand Down Expand Up @@ -691,7 +692,7 @@ class AndroidTextFieldTouchInteractorState extends State<AndroidTextFieldTouchIn
return Positioned(
left: extentOffsetInViewport.dx,
top: extentOffsetInViewport.dy + (extentLineHeight / 2),
child: CompositedTransformTarget(
child: Leader(
link: widget.editingOverlayController.magnifierFocalPoint,
child: widget.showDebugPaint
? FractionalTranslation(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/flutter/build_context.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
Expand Down Expand Up @@ -174,7 +175,7 @@ class SuperAndroidTextFieldState extends State<SuperAndroidTextField>

late ImeAttributedTextEditingController _textEditingController;

final _magnifierLayerLink = LayerLink();
final _magnifierLayerLink = LeaderLink();
late AndroidEditingOverlayController _editingOverlayController;

late TextScrollController _textScrollController;
Expand Down
Loading

0 comments on commit 3a89307

Please sign in to comment.