Photo_view: [BUG] Wrapping PhotoViewGallery.builder inside GestureDetector, make GestureDetector not working on iOS

Created on 1 Dec 2019  路  7Comments  路  Source: fireslime/photo_view

Describe the bug
I've wrapped the PhotoViewGallery inside an GestureDetector. If i tapped on a random position on the screen, the onTap event of the upper GestureDetector never fired. Dragging vertical, only be fired the onDrag events only in maybe 2 of 10 times.

The widget is the child of a PopupRoute.

What is the current behavior?
Upper GestureDetector not properly receiving events on iOS.

Expected behavior
If onTapDown and onTapUp on PhotoView is null, a available upper GestureDetector should receive all events, including onTap and onDragStart......

my code:

class ImageOverlayIOS extends StatefulWidget {
  final PageController imagePageViewController;
  final List<String> images;

  const ImageOverlayIOS({
    Key key,
    this.imagePageViewController,
    this.images,
  }) : super(key: key);

  @override
  _ImageOverlayIOSState createState() => _ImageOverlayIOSState();
}

class _ImageOverlayIOSState extends State<ImageOverlayIOS> with TickerProviderStateMixin {
  double _scale;
  Offset _offset;
  AnimationController _moveAnimationController, _scaleAnimationController;
  Animation<Offset> _moveAnimation, _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _scale = 1;
    _offset = Offset(0, 0);
    _moveAnimationController = AnimationController(
      vsync: this,
      value: 1,
      duration: Duration(milliseconds: 600),
    );
    _scaleAnimationController = AnimationController(
      vsync: this,
      value: 1,
      duration: Duration(milliseconds: 600),
    );
    _handleDragOffsetAndScale(Offset.zero);
  }

  @override
  void dispose() {
    _moveAnimationController?.dispose();
    _scaleAnimationController?.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onVerticalDragStart: _handleVerticalDragStart,
      onVerticalDragUpdate: _handleVerticalDragUpdate,
      onVerticalDragEnd: _handleVerticalDragEnd,
      onTap: Navigator.of(context).pop,
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: 2.5, sigmaY: 2.5),
        child: AnimatedBuilder(
          animation: _moveAnimationController,
          builder: _buildAnimation,
          child: AnimatedBuilder(
            animation: _scaleAnimationController,
            builder: _buildChildAnimation,
            child: PhotoViewGallery.builder(
              scrollPhysics: const BouncingScrollPhysics(),
              builder: (context, index) => PhotoViewGalleryPageOptions(
                imageProvider: NetworkImage(widget.images.elementAt(index)),
                maxScale: 3.0,
                minScale: PhotoViewComputedScale.contained,
                tightMode: true,
                gestureDetectorBehavior: HitTestBehavior.translucent,
                heroAttributes: PhotoViewHeroAttributes(
                  tag: widget.images.elementAt(index),
                  transitionOnUserGestures: true,
                ),
              ),
              backgroundDecoration: BoxDecoration(
                color: Colors.transparent,
              ),
              itemCount: widget.images.length,
              loadingChild: Loading(
                content: 'Bild wird geladen',
              ),
              pageController: widget.imagePageViewController,
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildChildAnimation(BuildContext context, Widget child) {
    print('ScaleAnimation Value: ${_scaleAnimation.value.dy}');
    _scale = _getScale(
      MediaQuery.of(context).size.height,
      _scaleAnimation.value.dy,
    );
    print('scale: $_scale');
    return Transform.scale(
      scale: _scale,
      child: child,
    );
  }

  Widget _buildAnimation(BuildContext context, Widget child) {
    return Transform.translate(
      offset: _moveAnimation.value,
      child: child,
    );
  }

  void _handleVerticalDragStart(DragStartDetails details) {
    _moveAnimationController.value = 1.0;
    _handleDragOffsetAndScale(Offset.zero);
  }

  void _handleVerticalDragUpdate(DragUpdateDetails details) {
    _handleDragOffsetAndScale(_offset + details.delta);
  }

  void _handleVerticalDragEnd(DragEndDetails details) {
    if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
      final flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
      final finalPosition = flingIsAway ? _moveAnimation.value.dy + 100.0 : 0.0;

      _moveAnimation = Tween<Offset>(
        begin: Offset(0.0, _moveAnimation.value.dy),
        end: Offset(0.0, finalPosition),
      ).animate(_moveAnimationController);
      _moveAnimationController.reset();
      _moveAnimationController.duration = const Duration(
        milliseconds: 64,
      );
      _scaleAnimation = Tween<Offset>(
        begin: Offset(0.0, _moveAnimation.value.dy),
        end: Offset(0.0, finalPosition),
      ).animate(_scaleAnimationController);
      _scaleAnimationController.reset();
      _scaleAnimationController.duration = const Duration(
        milliseconds: 64,
      );
      _scaleAnimationController.forward();
      _scaleAnimationController.addStatusListener(_scaleFlingStatusListener);
      _moveAnimationController.forward();
      _moveAnimationController.addStatusListener(_moveFlingStatusListener);

      return;
    }

    if (_scale <= 0.75) {
      Navigator.of(context).pop();
      return;
    }

    _moveAnimationController.reverse();
    _scaleAnimationController.animateTo(1.0);
    _scaleAnimation = Tween<Offset>(
      begin: _scaleAnimation.value,
      end: Offset(1, 1),
    ).animate(_scaleAnimationController);
    setState(() {
      _offset = Offset.zero;
      _scale = 1;
    });
    return;
  }

  void _handleDragOffsetAndScale(Offset offset) {
    final scaleEndY = offset.dy >= 0.0 ? offset.dy : offset.dy / 400;
    final moveEndY = offset.dy;
    print(scaleEndY);
    setState(() {
      _offset = offset;
      _moveAnimation = Tween<Offset>(
        begin: Offset.zero,
        end: Offset(
          0,
          moveEndY,
        ),
      ).animate(
        CurvedAnimation(
          curve: Curves.easeInOut,
          parent: _moveAnimationController,
        ),
      );
      _scaleAnimation = Tween<Offset>(
        begin: Offset.zero,
        end: Offset(
          0,
          scaleEndY,
        ),
      ).animate(
        CurvedAnimation(
          curve: Curves.easeInOut,
          parent: _scaleAnimationController,
        ),
      );
    });
  }

  void _moveFlingStatusListener(AnimationStatus status) {
    if (status != AnimationStatus.completed) {
      return;
    }

    // Reset the duration back to its original value.
    _moveAnimationController.duration = Duration(milliseconds: 600);

    _moveAnimationController.removeStatusListener(_moveFlingStatusListener);

    // If it was a fling back to the start, it has reset itself, and it should
    // not be dismissed.
    if (_moveAnimation.value.dy == 0.0) {
      return;
    }
    Navigator.of(context).pop();
  }

  void _scaleFlingStatusListener(AnimationStatus status) {
    if (status == AnimationStatus.dismissed) {
      _scaleAnimationController.value = 1.0;
    }
    if (status != AnimationStatus.completed) {
      return;
    }

    // Reset the duration back to its original value.
    _scaleAnimationController.duration = Duration(milliseconds: 600);

    _scaleAnimationController.removeStatusListener(_scaleFlingStatusListener);
  }

  // The scale of the child changes as a function of the distance it is dragged.
  static double _getScale(double maxDragDistance, double dy) {
    final dyDirectional = dy <= 0.0 ? dy : -dy;
    final fraction = (maxDragDistance + dyDirectional) / maxDragDistance;
    return math.max(
      -0.8, // 0.8 from CupertinoContextMenu
      fraction,
    );
  }
}

Which versions of Flutter/Photo View, and which browser / OS are affected by this issue? Did this work in previous versions of Photo View?

flutter doctor:

[鈭歖 Flutter (Channel beta, v1.11.0, on Microsoft Windows [Version 10.0.18363.476], locale de-DE)

[鈭歖 Android toolchain - develop for Android devices (Android SDK version 29.0.2)
[鈭歖 Android Studio (version 3.5)
[鈭歖 VS Code (version 1.40.2)
[!] Connected device
    ! No devices available

! Doctor found issues in 1 category.

OS Version: iOS 13.x
photo_view: 0.9.0

Gallery Priority bug

Most helpful comment

Here a short list on which devices it鈥檚 working and on which not.

Working:

  • iPhone 11 - iOS 13.2.3
  • iPhone SE

Not Working

  • iPhone X - iOS 13.2.3
  • iPhone XS - iOS 13.2.3

All 7 comments

It's not woking in Android too, must pop button to dismiss it.
so funny馃槀

OK, thats interesting, because on Android Emulator everything works fine

you can try it by PhotoViewGallery ,it will Smile at you 馃ぃ

Here a short list on which devices it鈥檚 working and on which not.

Working:

  • iPhone 11 - iOS 13.2.3
  • iPhone SE

Not Working

  • iPhone X - iOS 13.2.3
  • iPhone XS - iOS 13.2.3

Oh, I also have this bug, it works fine on Android Emulator, but something wrong on my Android mobile phone.

GestureDetector(
  onLongPress: _onImageLongPress,
  child: Listener(
    onPointerUp: _onPointerUp,
    onPointerDown: _onPointerDown,
    child: PhotoViewGallery.builder(...),
  ),
)

I'm having the same issue. Works good on iPhone 11 Pro, but doesn't work on iPhone XS Max

onPanUpdate event of GestureDetector have the same issue. drag doesn't work on iPhone.

Was this page helpful?
0 / 5 - 0 ratings