user1661421
user1661421

Reputation:

Flutter SlideTransition begin with Offset OFF SCREEN

My app is a simple Column with 3 Text widgets wrapped in SlideTransitions. My goal is for the app to load with nothing on the screen, and then animate these Text widgets from the bottom (off screen) up into the screen (settling in the middle).

return Column(
   children: <Widget>[
     SlideTransition(
         position:_curve1,
         child: Text('Hello 1')
     ),
     SlideTransition(
         position:_curve1,
         child: Text('Hello 2')
     ),
     SlideTransition(
         position:_curve1,
         child: Text('Hello 3')
     ),
   ]
);

The problem is that when defining the animation, you have to specify their initial Offset.

@override
void initState(){
    ...
    Offset beginningOffset = Offset(0.0,20.0);
    _curve1 = Tween<Offset>(begin: beginningOffset, end: Offset.zero).animate(
    CurvedAnimation(
        parent: _animationController,
        curve: Curves.easeInOut)));
    ...
}  

Here you can see that beginningOffset is specified as Offset(0.0, 20.0) which does successfully position the widgets off screen. But what happens if I suddenly run this on a tablet? Since Offset is defined as units of the height of the widget, then this is obviously not a good way to position a widget off screen.

Since the animation is defined in "initState()" ... I see no way to use something like MediaQuery.of(context) ... since we don't have context there.

In native iOS this would be simple. Within "viewDidLoad" you would get the frame from self.view. You'd position each text field at the edge of the frame explicitly. You'd then animate a constraint change, positioning them where you want them. Why must this be so hard in Flutter?

I find it especially curious that this exact scenario (starting an animation just off screen) is totally not covered in any of the examples I could find. For instance:

https://github.com/flutter/website/blob/master/examples/_animation/basic_staggered_animation/main.dart

Seems like almost every type of animation is featured here EXCEPT an explicit off screen animation on load... Perhaps I'm just missing something.

EDIT: ThePeanut has solved it! Check his "Second Update".

Upvotes: 14

Views: 18547

Answers (2)

lsaudon
lsaudon

Reputation: 1458

I found this solution, to have the exact value of off-screen.

Screen size / 2 / widget size + half of 1 offset

Without the half offset, only half of the widget is off-screen.

final widgetHeight = 190;
final offsetScreen = MediaQueryData.fromWindow(WidgetsBinding.instance!.window).size.height / 2 / widgetHeight + 0.5;

Upvotes: 0

Thepeanut
Thepeanut

Reputation: 3407

You might want to try using the addPostFrameCallback method in your initState.

    @override
    void initState() {
      super.initState();
      WidgetsBinding.instance.addPostFrameCallback((_){
         // Schedule code execution once after the frame has rendered
         print(MediaQuery.of(context).size.toString());
      });
    }

Flutter Api Docs Link

OR you can also use a Future for this:

    @override
    void initState() {
      super.initState();
      new Future.delayed(Duration.zero, () {
          // Schedule a zero-delay future to be executed
          print(MediaQuery.of(context).size.toString());
      });
    }

Hope this helps.

UPDATED

A bit of a unusual way to do it, but it really does the thing you need.

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Animation<Offset> animation;
  AnimationController animationController;

  @override
  void initState() {
    super.initState();

    animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    animation = Tween<Offset>(
      begin: Offset(0.0, 1.0),
      end: Offset(0.0, 0.0),
    ).animate(CurvedAnimation(
      parent: animationController,
      curve: Curves.fastLinearToSlowEaseIn,
    ));

    Future<void>.delayed(Duration(seconds: 1), () {
      animationController.forward();
    });
  }

  @override
  void dispose() {
    // Don't forget to dispose the animation controller on class destruction
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Stack(
        alignment: Alignment.center,
        fit: StackFit.expand,
        children: <Widget>[
          SlideTransition(
            position: animation,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                CircleAvatar(
                  backgroundImage: NetworkImage(
                    'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
                  ),
                  radius: 50.0,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

It works this way:

1. We create a Stack of items with options to expand any child inside it.

2. Wrap each slide you want to display in to a Column with center alignment of children. The Column will take 100% of the size of the Stack.

3. Set the beginning Offset for animation to Offset(0.0, 1.0).

Keep in mind that dx and dy in Offset are not pixels or something like that, but the ratio of Widget's width or height. For example: if your widget's width is 100.0 and you put 0.25 as dx - it will result in moving your child to the right by 25.0 points.

So setting offset to (0.0, 1.0) will move the Column offscreen to the bottom by it's 100% height (this is how many page transitions work in Flutter).

4. Animate the Column back to it's original position after a 1 second delay.

SECOND UPDATE

This code calculates the offset based on the screen size and widget size. PS. There might be a better way of doing this that I don't know of.

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Page(),
    );
  }
}

class Page extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PageState();
}

class _PageState extends State<Page> with SingleTickerProviderStateMixin {
  // Set the initial position to something that will be offscreen for sure
  Tween<Offset> tween = Tween<Offset>(
    begin: Offset(0.0, 10000.0),
    end: Offset(0.0, 0.0),
  );
  Animation<Offset> animation;
  AnimationController animationController;

  GlobalKey _widgetKey = GlobalKey();

  @override
  void initState() {
    super.initState();

    // initialize animation controller and the animation itself
    animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    animation = tween.animate(animationController);

    Future<void>.delayed(Duration(seconds: 1), () {
      // Get the screen size
      final Size screenSize = MediaQuery.of(context).size;
      // Get render box of the widget
      final RenderBox widgetRenderBox =
          _widgetKey.currentContext.findRenderObject();
      // Get widget's size
      final Size widgetSize = widgetRenderBox.size;

      // Calculate the dy offset.
      // We divide the screen height by 2 because the initial position of the widget is centered.
      // Ceil the value, so we get a position that is a bit lower the bottom edge of the screen.
      final double offset = (screenSize.height / 2 / widgetSize.height).ceilToDouble();

      // Re-set the tween and animation
      tween = Tween<Offset>(
        begin: Offset(0.0, offset),
        end: Offset(0.0, 0.0),
      );
      animation = tween.animate(animationController);

      // Call set state to re-render the widget with the new position.
      this.setState((){
        // Animate it.
        animationController.forward();
      });
    });
  }

  @override
  void dispose() {
    // Don't forget to dispose the animation controller on class destruction
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      fit: StackFit.loose,
      children: <Widget>[
        SlideTransition(
          position: animation,
          child: CircleAvatar(
            key: _widgetKey,
            backgroundImage: NetworkImage(
              'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
            ),
            radius: 50.0,
          ),
        ),
      ],
    );
  }
}

Upvotes: 17

Related Questions