Skip to content

Commit

Permalink
feat: improve ios touch gesture (#648)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasXu0 authored Jan 5, 2024
1 parent 07afd7a commit 7d2b456
Show file tree
Hide file tree
Showing 5 changed files with 502 additions and 442 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'dart:io';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_magnifier.dart';
import 'package:appflowy_editor/src/render/selection/mobile_basic_handle.dart';
import 'package:appflowy_editor/src/render/selection/mobile_collapsed_handle.dart';
import 'package:appflowy_editor/src/render/selection/mobile_selection_handle.dart';
import 'package:appflowy_editor/src/service/selection/mobile_selection_gesture.dart';
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
Expand Down Expand Up @@ -147,10 +149,6 @@ class _MobileSelectionServiceWidgetState
}

Widget _buildCollapsedHandle() {
if (Platform.isIOS) {
return const SizedBox.shrink();
}

return ValueListenableBuilder(
valueListenable: editorState.selectionNotifier,
builder: (context, selection, _) {
Expand All @@ -175,7 +173,7 @@ class _MobileSelectionServiceWidgetState
}

final editorStyle = editorState.editorStyle;
return AndroidCollapsedHandle(
return MobileCollapsedHandle(
layerLink: node.layerLink,
rect: rect,
handleColor: editorStyle.dragHandleColor,
Expand Down Expand Up @@ -226,7 +224,7 @@ class _MobileSelectionServiceWidgetState
}

final editorStyle = editorState.editorStyle;
return MobileSelectionArea(
return MobileSelectionHandle(
layerLink: node.layerLink,
rect: handleType == HandleType.left ? rects.first : rects.last,
handleType: handleType,
Expand Down
350 changes: 350 additions & 0 deletions lib/src/render/selection/mobile_basic_handle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
import 'dart:io';
import 'dart:math';

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';

enum HandleType {
none,
left,
right,
collapsed;

MobileSelectionDragMode get dragMode {
switch (this) {
case HandleType.none:
throw UnsupportedError('Unsupported handle type');
case HandleType.left:
return MobileSelectionDragMode.leftSelectionHandler;
case HandleType.right:
return MobileSelectionDragMode.rightSelectionHandler;
case HandleType.collapsed:
return MobileSelectionDragMode.cursor;
}
}

CrossAxisAlignment get crossAxisAlignment {
switch (this) {
case HandleType.none:
throw UnsupportedError('Unsupported handle type');
case HandleType.left:
return CrossAxisAlignment.end;
case HandleType.right:
return CrossAxisAlignment.start;
case HandleType.collapsed:
return CrossAxisAlignment.center;
}
}
}

abstract class _IDragHandle extends StatelessWidget {
const _IDragHandle({
super.key,
required this.handleHeight,
this.handleColor = Colors.black,
this.handleWidth = 2.0,
this.handleBallWidth = 6.0,
this.debugPaintSizeEnabled = false,
required this.handleType,
});

final Color handleColor;
final double handleWidth;
final double handleHeight;
final double handleBallWidth;
final HandleType handleType;
final bool debugPaintSizeEnabled;
}

class DragHandle extends _IDragHandle {
const DragHandle({
super.key,
required super.handleHeight,
super.handleColor,
super.handleWidth,
super.handleBallWidth,
required super.handleType,
super.debugPaintSizeEnabled,
});

@override
Widget build(BuildContext context) {
Widget child;

if (Platform.isIOS) {
child = _IOSDragHandle(
handleHeight: handleHeight,
handleColor: handleColor,
handleWidth: handleWidth,
handleBallWidth: handleBallWidth,
handleType: handleType,
debugPaintSizeEnabled: debugPaintSizeEnabled,
);
} else if (Platform.isAndroid) {
child = _AndroidDragHandle(
handleHeight: handleHeight,
handleColor: handleColor,
handleWidth: handleWidth,
handleBallWidth: handleBallWidth,
handleType: handleType,
debugPaintSizeEnabled: debugPaintSizeEnabled,
);
} else {
throw UnsupportedError('Unsupported platform');
}

if (debugPaintSizeEnabled) {
child = ColoredBox(
color: Colors.red.withOpacity(0.5),
child: child,
);
}

if (handleType != HandleType.none && handleType != HandleType.collapsed) {
final offset = Platform.isIOS ? -handleWidth : 0.0;
child = Stack(
clipBehavior: Clip.none,
children: [
if (handleType == HandleType.left)
Positioned(
left: offset,
child: child,
),
if (handleType == HandleType.right)
Positioned(
right: offset,
child: child,
),
],
);
}

return child;
}
}

class _IOSDragHandle extends _IDragHandle {
const _IOSDragHandle({
required super.handleHeight,
super.handleColor,
super.handleWidth,
super.handleBallWidth,
required super.handleType,
super.debugPaintSizeEnabled,
});

@override
Widget build(BuildContext context) {
Widget child;
if (handleType == HandleType.collapsed) {
child = Container(
width: handleWidth,
color: handleColor,
height: handleHeight,
);
} else {
child = Column(
mainAxisSize: MainAxisSize.max,
children: [
if (handleType == HandleType.left)
Container(
width: handleBallWidth,
height: handleBallWidth,
decoration: BoxDecoration(
color: handleColor,
shape: BoxShape.circle,
),
),
if (handleType == HandleType.right)
SizedBox(
width: handleBallWidth,
height: handleBallWidth,
),
Container(
width: handleWidth,
color: handleColor,
height: handleHeight - 2.0 * handleBallWidth,
),
if (handleType == HandleType.right)
Container(
width: handleBallWidth,
height: handleBallWidth,
decoration: BoxDecoration(
color: handleColor,
shape: BoxShape.circle,
),
),
if (handleType == HandleType.left)
SizedBox(
width: handleBallWidth,
height: handleBallWidth,
),
],
);
}

final editorState = context.read<EditorState>();
final ballWidth = handleBallWidth;

child = GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onPanStart: (details) {
editorState.service.selectionService.onPanStart(
details.translate(0, -ballWidth),
handleType.dragMode,
);
},
onPanUpdate: (details) {
editorState.service.selectionService.onPanUpdate(
details.translate(0, -ballWidth),
handleType.dragMode,
);
},
onPanEnd: (details) {
editorState.service.selectionService.onPanEnd(
details,
handleType.dragMode,
);
},
child: child,
);

return child;
}
}

// ignore: must_be_immutable
class _AndroidDragHandle extends _IDragHandle {
_AndroidDragHandle({
required super.handleHeight,
super.handleColor,
super.handleWidth,
super.handleBallWidth,
required super.handleType,
super.debugPaintSizeEnabled,
});

Selection? selection;

@override
Widget build(BuildContext context) {
final editorState = context.read<EditorState>();
Widget child = SizedBox(
width: handleWidth,
height: handleHeight - 2.0 * handleBallWidth,
);

if (handleType == HandleType.none) {
return child;
}

final ballWidth = handleBallWidth * 2.0;

child = Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: handleType.crossAxisAlignment,
children: [
child,
if (handleType == HandleType.collapsed)
Transform.rotate(
angle: pi / 4.0,
child: Container(
width: ballWidth,
height: ballWidth,
decoration: BoxDecoration(
color: handleColor,
borderRadius: BorderRadius.only(
topRight: Radius.circular(handleBallWidth),
bottomLeft: Radius.circular(handleBallWidth),
bottomRight: Radius.circular(handleBallWidth),
),
),
),
),
if (handleType == HandleType.left)
Container(
width: ballWidth,
height: ballWidth,
decoration: BoxDecoration(
color: handleColor,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(handleBallWidth),
bottomLeft: Radius.circular(handleBallWidth),
bottomRight: Radius.circular(handleBallWidth),
),
),
),
if (handleType == HandleType.right)
Container(
width: ballWidth,
height: ballWidth,
decoration: BoxDecoration(
color: handleColor,
borderRadius: BorderRadius.only(
topRight: Radius.circular(handleBallWidth),
bottomLeft: Radius.circular(handleBallWidth),
bottomRight: Radius.circular(handleBallWidth),
),
),
),
],
);

child = GestureDetector(
behavior: HitTestBehavior.opaque,
dragStartBehavior: DragStartBehavior.down,
onPanStart: (details) {
selection = editorState.service.selectionService.onPanStart(
details.translate(0, -ballWidth),
handleType.dragMode,
);
},
onPanUpdate: (details) {
final selection = editorState.service.selectionService.onPanUpdate(
details.translate(0, -ballWidth),
handleType.dragMode,
);
if (this.selection != selection) {
HapticFeedback.selectionClick();
}
this.selection = selection;
},
onPanEnd: (details) {
editorState.service.selectionService.onPanEnd(
details,
handleType.dragMode,
);
},
child: child,
);

return child;
}
}

extension on DragStartDetails {
DragStartDetails translate(double dx, double dy) {
return DragStartDetails(
sourceTimeStamp: sourceTimeStamp,
globalPosition: Offset(globalPosition.dx + dx, globalPosition.dy + dy),
localPosition: Offset(localPosition.dx + dx, localPosition.dy + dy),
);
}
}

extension on DragUpdateDetails {
DragUpdateDetails translate(double dx, double dy) {
return DragUpdateDetails(
sourceTimeStamp: sourceTimeStamp,
globalPosition: Offset(globalPosition.dx + dx, globalPosition.dy + dy),
localPosition: Offset(localPosition.dx + dx, localPosition.dy + dy),
delta: Offset(delta.dx + dx, delta.dy + dy),
primaryDelta: primaryDelta,
);
}
}
Loading

0 comments on commit 7d2b456

Please sign in to comment.