bluefireteam / photo_view

📸 Easy to use yet very customizable zoomable image widget for Flutter, Photo View provides a gesture sensitive zoomable widget. Photo View is largely used to show interacive images and other stuff such as SVG.
MIT License
1.91k stars 548 forks source link

[BUG] Wrapping PhotoViewGallery.builder inside GestureDetector, make GestureDetector not working on iOS #220

Open bierbaumtim opened 4 years ago

bierbaumtim commented 4 years ago

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

JeasonLaung commented 4 years ago

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

bierbaumtim commented 4 years ago

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

JeasonLaung commented 4 years ago

you can try it by PhotoViewGallery ,it will Smile at you 🤣

bierbaumtim commented 4 years ago

Here a short list on which devices it’s working and on which not.

Working:

Not Working

Aoi-hosizora commented 4 years ago

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(...),
  ),
)
agordeev commented 4 years ago

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

luckmlc commented 4 years ago

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

NaikSoftware commented 3 years ago

Gallery wrapped into Dismissable working when image horizontal or square and randomly not working (50/50) when image is vertical. Setting custom size Size(mediaQuery.size.width - 3, mediaQuery.size.height) solved problem. I dont know why, but intercepting drag events depends on image size and aspect ratio:)

NaikSoftware commented 3 years ago

I added logs and found error in math operations with floating point. Image width multiplied on scale sometimes different then widget width and it blocks any gestures except zooming.

HitCorners _hitCornersX() {
    final double childWidth = scaleBoundaries.childSize.width * scale;
    final double screenWidth = scaleBoundaries.outerSize.width;
    log('HIT: childW=${scaleBoundaries.childSize.width} childWScaled=$childWidth screenW=$screenWidth');
    if (screenWidth >= childWidth) {
      return const HitCorners(true, true);
    }
    final x = -position.dx;
    final cornersX = this.cornersX();
    return HitCorners(x <= cornersX.min, x >= cornersX.max);
  }

image

Fixed:

if (screenWidth - childWidth > -0.001) {
      return const HitCorners(true, true);
}