Flutter: Implementing a peek & pop effect

会有一股神秘感。 提交于 2021-02-07 04:18:08

问题


I was trying to achieve a peek & pop effect - when the user taps and hold on to a card, a dialog opens, and dismissed when the user no longer tap holding on the screen:

Listener(
    onPointerDown: (PointerDownEvent e) {
      // open dialog
      showDialog(
          context: context,
          builder: (context) => Container(
                child: Card(),
              ));
    },
    onPointerUp: (PointerUpEvent e) {
      // dismiss dialog
      if (Navigator.of(context).canPop()) {
        Navigator.of(context).pop('dialog');
      }
    },
    child: Card()
)

It worked nicely which shows the dialog when I hold on the card and dismisses when I am no longer holding my tap.

But I wanted to have some delay before onPointerDown is called, like onLongPress from the GestureDectector - I was able to get the dialog to show up when I long pressed, but onTapUp was never invoked when I left the screen:

GestureDetector(
  onLongPress: () {
    // open dialog
    showDialog(
        context: context,
        builder: (context) => Container(child: Card()));
  },
  onTapUp: (TapUpDetails d) {
    // dismiss dialog
    if (Navigator.of(context).canPop()) {
      Navigator.of(context).pop();
    }
  },
  child: Card()
)

I tried to do it the following way, but onTapUp was also never invoked:

  GestureDetector(
  onTapDown: (TapDownDetails d) {
    // open dialog
    showDialog(context: context, builder: (context) => Card());
  },
  onTapUp: (TapUpDetails d) {
    // dismiss dialog
    if (Navigator.of(context).canPop()) {
      Navigator.of(context).pop();
    }
  },
  child: Card())

but the following shows that the taps were registering correctly:

GestureDetector(
  onTapDown: (TapDownDetails d) {
    print("down")
  },
  onTapUp: (TapUpDetails d) {
    print("up")
  },
  child: Card()
)

After looking around I noticed this flutter PR which adds onLongPressUp - I added those changes to my flutter, then tried to re-implement my previous code like this:

GestureDetector(
  onLongPress: () {
    // open dialog
    showDialog(
        context: context,
        builder: (context) => Container(child: Card()));
  },
  onLongPressUp: () {
    // dismiss dialog
    if (Navigator.of(context).canPop()) {
      Navigator.of(context).pop();
    }
  },
  child: Card()
)

The dialog showed up on long press, but was never dismissed when I am no longer longp pressing - onLongPressUp doesn't seem to be invoked, but the following shows it is registering my taps correctly:

GestureDetector(
  onLongPress: () {
    print("longpress")
  },
  onLongPressUp: () {
    print("longpressup")
  },
  child: Card()
)

Out of all these, only using Listener I was able to open and dismiss a dialog by a tap down and tap up, but I wanted to add a delay before onPointerDown is invoked, which I also tried to add a Timer in onPointerDown but doesn't seem to work.

Any solutions?


回答1:


Here is a solution that utilizes a StatefulWidget with a Stack instead of the Navigator.

The problem I hit with your code is that the Listener no longer receives the pointer events when you open the dialog with showDialog, so it's better to keep everything under a single top-level widget.

I'm also using a Timer that is cancelled when the pointer goes up or when the user leaves the page.

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  Timer _showDialogTimer;
  bool _dialogVisible = false;

  @override
  void dispose() {
    _showDialogTimer?.cancel();
    super.dispose();
  }

  void _onPointerDown(PointerDownEvent event) {
    _showDialogTimer = Timer(Duration(seconds: 1), _showDialog);
  }

  void _onPointerUp(PointerUpEvent event) {
    _showDialogTimer?.cancel();
    _showDialogTimer = null;
    setState(() {
      _dialogVisible = false;
    });
  }

  void _showDialog() {
    setState(() {
      _dialogVisible = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    final layers = <Widget>[];

    layers.add(_buildPage());

    if(_dialogVisible) {
      layers.add(_buildDialog());
    }

    return Listener(
      onPointerDown: _onPointerDown,
      onPointerUp: _onPointerUp,
      child: Stack(
        fit: StackFit.expand,
        children: layers,
      ),
    );
  }

  Widget _buildPage() {
    return Scaffold(
      appBar: AppBar(
        title: Text('Example App'),
      ),
      body: Center(
        child: Text('Press me'),
      ),
    );
  }

  Widget _buildDialog() {
    return Container(
      color: Colors.black.withOpacity(0.5),
      padding: EdgeInsets.all(50.0),
      child: Card(),
    );
  }
}



回答2:


EDIT 3: v1.0.0 is finally released! More fluent, more optimised and more beautiful than ever. Very customisable and very easy to use. See it on Pub or GitHub.

EDIT 2: v0.1.9 no longer requires any modifications to Flutter's normal "binding.dart"! You can leave your Flutter source code alone and happy. If you are updating from an earlier version, you can revert your "binding.dart" to its original format. Thanks to everyone for their feedback.

EDIT: People have expressed worries concerning modifying Flutter's normal "binding.dart". Don't worry- v0.1.9 which will be released soon will not require this modification. For now, you can temporarily follow the installation instructions and start developing with v0.1.8. When it is updated, you can revert your 'binding.dart' to its original format.

I don't know if its still relevant but I am developing a Flutter package for this specific purpose which you can find here and here.

This is a Peek & Pop implementation for Flutter based on the iOS functionality of the same name.

The power move of this package is what I like to call "Gesture Recognition Rerouting". Normally, when a new widget with GestureDetector or similar is pushed over an initial widget used for detecting Force Press, the user has to restart the gesture for Flutter to resume updating it. This package fixes that problem. As explained in the documentation:

///This function is called by the instantiated [PeekAndPopChild] once it is ready to be included in the Peek & Pop process. Perhaps the most
///essential functionality of this package also takes places in this function: The gesture recognition is rerouted from the  [PeekAndPopDetector]
///to the instantiated [PeekAndPopChild]. This is important for avoiding the necessity of having the user stop and restart their Force Press.
///Instead, the [PeekAndPopController] does this automatically so that the existing Force Press can continue to update even when if
///[PeekAndPopDetector] is blocked by the view which is often the case especially when using PlatformViews.

The core of the package is the "PeekAndPopController" widget. This widget is highly customisable. You can control the entire process, even prevent default behaviours as you prefer and run your own sequences instead.

Take a look at this video for some examples. This is a video from V0.1.0 so the package is much more optimised now- it runs better and more fluent.

Let me know if you have any questions!



来源:https://stackoverflow.com/questions/51480827/flutter-implementing-a-peek-pop-effect

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!