5

Is it possible to copy the transition effect of iOS App Store using Flutter?

I tried using Hero Animation by placing two tags into the root layout of both widgets, but animation looks janky or not what I expected. But good thing about this is I am able to do iOS swipe back as I'm using MaterialPageRoute.

Source

Hero(
   tag: 'heroTag_destinationScreen',
   transitionOnUserGestures: true,
   flightShuttleBuilder: (BuildContext flightContext,
     Animation<double> animation,
     HeroFlightDirection flightDirection,
     BuildContext fromHeroContext,
     BuildContext toHeroContext,) {
        final Hero toHero = toHeroContext.widget;
            return ScaleTransition(
              scale: animation,
              child: toHero,
            );
          },
          child: GestureDetector(
            onTap: () {
              Navigator.of(context).push(
                MaterialPageRoute<void>(
                  builder: (BuildContext context) {
                    return DestinationScreen()
                  },
                ),
              );
            },
            child: Card(
                ...someCardContent
            ),
      ),
)

Destination Screen

@override
Widget build(BuildContext context) {
    return Hero(
       tag: 'heroTag_destinationScreen',
       child: Scaffold(
           appBar: ...someAppBar
            body: ...someMainBodyContent
          ),
      )
    }

Then I have been looking around and there is a package created by Flutter team which can simulate this effect using container transform. I implemented it, works awesome but then I'm not able to do iOS swipe from left to go back and shrink the layout to card view.

https://pub.dev/packages/animations

Example App Store

fadhli-sulaimi
  • 686
  • 12
  • 17
  • most likely the jank is due to the debug version of the app. try again in profile or release mode – Doc Jun 25 '20 at 13:03
  • hi, sorry what i meant by jank is not having the transition animation i want which is to open up the card like reveal effect and bouncing the destination screen on init – fadhli-sulaimi Jun 25 '20 at 13:20
  • I managed to simulate the back swipe and slowly scaling the scaffold, but on navigator pop the scaffold suddenly enlarge itself to full screen – fadhli-sulaimi Jun 25 '20 at 13:56
  • @FadhliS could you share the code you used with the animations package? – Dante Cervantes Jul 06 '21 at 02:55

1 Answers1

14

here is my solution.

https://i.stack.imgur.com/y3Na2.jpg

(Sorry for my reputation, I can't post a image.)

I customize hero transition to remake App store transition as much as possible.

      child: Hero(
        tag: widget.product.id,
        child: Image.asset(widget.product.image, fit: BoxFit.cover),
        flightShuttleBuilder:
            (flightContext, animation, direction, fromcontext, toContext) {
          final Hero toHero = toContext.widget;
          // Change push and pop animation.
          return direction == HeroFlightDirection.push
              ? ScaleTransition(
                  scale: animation.drive(
                    Tween<double>(
                      begin: 0.75,
                      end: 1.02,
                    ).chain(
                      CurveTween(
                          curve: Interval(0.4, 1.0, curve: Curves.easeInOut)),
                    ),
                  ),
                  child: toHero.child,
                )
              : SizeTransition(
                  sizeFactor: animation,
                  child: toHero.child,
                );
        },
      ),

Next, I use ScaleTransition and onVerticalDragUpdate to control pop animation.

https://i.stack.imgur.com/6iyyR.jpg

double _initPoint = 0;
double _pointerDistance = 0;
GestureDetector(
  onVerticalDragDown: (detail) {
    _initPoint = detail.globalPosition.dy;
  },
  onVerticalDragUpdate: (detail) {
    _pointerDistance = detail.globalPosition.dy - _initPoint;
    if (_pointerDistance >= 0 && _pointerDistance < 200) {
        // scroll up
        double _scaleValue = double.parse((_pointerDistance / 100).toStringAsFixed(2));
        if (_pointerDistance < 100) {
          _closeController.animateTo(_scaleValue,
          duration: Duration(milliseconds: 300),
          curve: Curves.linear);
        }
    } else if (_pointerDistance >= 260) {
      if (_pop) {
        _pop = false;
        _closeController.fling(velocity: 1).then((_) {
          setState(() {
          _heightController.reverse();
          });
          Timer(Duration(milliseconds: 100), () {
            Navigator.of(context).pop();
          });
        });
      }
    } else {
      // scroll down
    }
  },
  onVerticalDragEnd: (detail) {
    if (_pointerDistance >= 550) {
      if (_pop) {
        _closeController.fling(velocity: 1).then((_) {
          setState(() {
            _heightController.reverse();
          });
          Timer(Duration(milliseconds: 100), () {
            Navigator.of(context).pop();
          });
        });
      }
    } else {
    _closeController.fling(velocity: -1);
    }
  },
  child: Hero(
    tag: _product.id,
    child: Image.asset(
      _product.image,
      fit: BoxFit.cover,
      height: 300,
    ),
  ),
),

If use Hero as a animation, you need to customize the text section transition.

Here: https://i.stack.imgur.com/AXHPt.jpg

In my case, I control text section transition by Sizetransition.

// horizontal way and vertical way.
SizeTransition(
  axis: Axis.horizontal,
  sizeFactor: Tween<double>(begin: 0.5, end: 1).animate(
    CurvedAnimation(
        curve: Curves.easeInOut, parent: _widthController),
  ),
  child: SizeTransition(
    sizeFactor: Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
          curve: Curves.easeInOut, parent: _heightController),
    ),
    child: Container(
      padding: EdgeInsets.only(
          left: 20, right: 20, top: 50, bottom: 30),
      width: double.infinity,
      color: Colors.white,
      constraints: BoxConstraints(
        minHeight: 650,
      ),
      child: Column(
        // title and text
        children: <Widget>[
          Text('Title', style: TextStyle(fontSize: 18)),
          SizedBox(height: 30),
          Text(_text,
              style: TextStyle(
                fontSize: 15,
              )),
        ],
      ),
    ),
  ),
),

Although it isn't the same as App Store, i hope it is helpful for you.

Source code: https://github.com/HelloJunWei/app_store_transition

If you have any suggestion, feel free to feedback or create a pull request. :)

Neil
  • 140
  • 4
  • Hi there, thank you so much for your effort!! I managed to make it work as well but i think your solution is better. I think its still possible to make the edges rounded as your drag while closing. Maybe i can create a pull request on your git – fadhli-sulaimi Jul 16 '20 at 10:04
  • Hi @FadhliS , I have updated my project. [demo gif](https://user-images.githubusercontent.com/18310281/87765427-607a8a00-c84a-11ea-9718-95883c853e02.gif) – Neil Jul 17 '20 at 08:33
  • Hi @Neil, great work there! I am facing a similar issue and can not get it done... It seems like you really know your stuff, so could you maybe have a look at it ? :) https://stackoverflow.com/questions/66438581/flutter-gesturedetector-if-swiping-down-to-dismiss – Chris Mar 03 '21 at 11:48