Shuttle: IOS sideslip return

Posted by KC_Geek on Sun, 13 Feb 2022 15:11:31 +0100

Write in front

In fluent, the default is to support the sideslip return of the screen edge of iOS. However, if we rewrite the onWillPop callback of WillPopScope due to some requirements, this feature will become invalid.

content

In general, we do not add WillPopScope to the page and rewrite its onWillPop method. On iOS, we can sideslip back on the left edge of the screen. But if we rewrite the onWillPop method, we will find that this gesture feature is invalid. In router In the dart file, we can know why:

// route.dart
  static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
    ...
    // If attempts to dismiss this route might be vetoed such as in a page
    // with forms, then do not allow the user to dismiss the route with a swipe.
    if (route.hasScopedWillPopCallback)
      return false;
    ....
    // Looks like a back gesture would be welcome!
    return true;
  }

// routes.dart
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ...
    void addScopedWillPopCallback(WillPopCallback callback) {
    assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
    _willPopCallbacks.add(callback);
  }
  
  /// True if one or more [WillPopCallback] callbacks exist.
  ///
  /// This method is used to disable the horizontal swipe pop gesture supported
  /// by [MaterialPageRoute] for [TargetPlatform.iOS] and
  /// [TargetPlatform.macOS]. If a pop might be vetoed, then the back gesture is
  /// disabled.
  ///
  /// The [buildTransitions] method will not be called again if this changes,
  /// since it can change during the build as descendants of the route add or
  /// remove callbacks.
  ///
  /// See also:
  ///
  ///  * [addScopedWillPopCallback], which adds a callback.
  ///  * [removeScopedWillPopCallback], which removes a callback.
  ///  * [willHandlePopInternally], which reports on another reason why
  ///    a pop might be vetoed.
  @protected
  bool get hasScopedWillPopCallback {
    return _willPopCallbacks.isNotEmpty;
  }
 ...
}

// will_pop_scope.dart
class _WillPopScopeState extends State<WillPopScope> {
  ModalRoute<dynamic>? _route;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (widget.onWillPop != null)
      _route?.removeScopedWillPopCallback(widget.onWillPop!);
    _route = ModalRoute.of(context);
    if (widget.onWillPop != null)
      _route?.addScopedWillPopCallback(widget.onWillPop!);
  }

  @override
  void didUpdateWidget(WillPopScope oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(_route == ModalRoute.of(context));
    if (widget.onWillPop != oldWidget.onWillPop && _route != null) {
      if (oldWidget.onWillPop != null)
        _route!.removeScopedWillPopCallback(oldWidget.onWillPop!);
      if (widget.onWillPop != null)
        _route!.addScopedWillPopCallback(widget.onWillPop!);
    }
  }
  }

Since we have rewritten onWillPop, it will add this callback to the route_ willPopCallbacks list, so the gesture operation of exit is disabled here.

Set onWillPop to null

If we really need to use WillPopScope in some cases, what should we do? We can see that the premise of adding the callback to the queue is the widget onWillPop != Null, so we can provide a trigger condition to change the onwillpop according to the specific situation, similar to the following:

  bool condition = true;
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: condition ? () async{
        // do something
        return true; 
        } : null,
      child: Scaffold(
    );
  }

In this way, we can deal with this problem more flexibly.

stay Document that WillPopScope prevents swipe to go back on MaterialPageRoute #14203 In this problem, we can see another way to deal with it.

Override MaterialPageRoute

class CustomMaterialPageRoute extends MaterialPageRoute {

  @override
  @protected
  bool get hasScopedWillPopCallback {
    return false;
  }
  CustomMaterialPageRoute({
    required WidgetBuilder builder,
    RouteSettings? settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  }) : super(
    builder: builder,
    settings: settings,
    maintainState: maintainState,
    fullscreenDialog: fullscreenDialog,
  );
}

By rewriting MaterialPageRoute, directly change hasScopedWillPopCallback to false, and modify the implementation when jumping:

   // old:
   Navigator.push(
      context,
      MaterialPageRoute(builder: (context) {
        return const Second();
      }),
    );
    
    // new:
    Navigator.push(
      context,
      CustomMaterialPageRoute(builder: (context) {
        return const Second();
      }),
    );

But one problem this will cause is that if you want to pass data back when you return to the page, even if you rewrite the onWillPop callback, the data will not be returned by gesture, but the return button on the AppBar can still return the data.

other

When dealing with the return processing of the pop-up window at the bottom, I found a difference in the interaction between Android and iOS. On Android, you can close the pop-up window as long as your finger slides right on the left edge of the screen. iOS needs your fingers to have a downward direction in order to close the pop-up window.

On iOS, we can always feel that its gesture operation is very handy. If this thing comes from, it should go back the same way. You can see the details [WWDC 2018] Designing Fluid Interfaces smooth interface design.

So I think this may be the reason why WillPopScope performs differently on Android and iOS. On Android, finger sideslip is equivalent to the return button at the bottom, which clearly indicates that you want to go back. On iOS, because it is very handy, although you slide on the edge of the screen, you can put it back as long as your finger is still on the screen. In this way, WillPopScope's judgment intention may not be as clear as Android.

Topics: Flutter