oladapo
oladapo

Reputation: 1

Flutter SlideTransition begin with Offset CENTER SCREEN

I am trying to animate an image from the center of the screen to a new position. I am trying to calculate the offset at runtime relative to the widget and screen size. The link below describes how to start the offset offscreen but in my case, i want the transition to start from the center of the screen regardless of the device size.

Flutter SlideTransition begin with Offset OFF SCREEN.

EDIT:

[First image is the splash screen] imagelink

[Second image is the next screen where i want the image to move to from center] imagelink2

Code i have tried. Here i am trying to get the height of the screen in half and use that as the start position. But the maths is wrong.

Future<void>.delayed(Duration(seconds: 0), () {
  final Size screenSize = MediaQuery.of(context).size;
  print('screen size: $screenSize');
  double offsetY = screenSize.height / 2 / 1000;
  print('offsetY: $offsetY');
  tween = Tween<Offset>(
    begin: Offset(0.0, offsetY),
    end: Offset(0.0, 0.0),
  );
  _offsetAnimation = tween.animate(
    CurvedAnimation(
      parent: _controller,
      curve: Curves.bounceIn,
    ),
  );
  this.setState(() {
    // Animate it.
    Timer(
      Duration(milliseconds: 3000),
      () => _controller.forward(),
    );
  });
});

Upvotes: 0

Views: 6918

Answers (1)

EdwynZN
EdwynZN

Reputation: 5601

import 'package:flutter/material.dart';

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

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

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

class _PageState extends State<Page> with SingleTickerProviderStateMixin {
  AnimationController animationController;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    animationController.repeat(); //just to show it can be animated
  }

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

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: Tween<Offset>(begin: Offset.zero,
                              end: Offset(1,0)).animate(animationController),
      child: Container(
          height: double.infinity,
          child: Center(
            child: CircleAvatar(
              backgroundImage: NetworkImage(
                'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
              ),
              radius: 50.0,
            ),
          )),
    );
  }
}

The trick here is to animate the whole container (the size of the screen) but with a child in a center of another size. In the Tween you define the begining with Offset.zero (the position at the center) and at the end the Offset where you want it to move. If you want a bit more control you can also use a PositionTransition with a Stack, but for that I would need to see a bit of your code or some pictures of what are you aiming for

UPDATE

Ok what you can do to achieve that animation is to use an Animation Controller and having 2 Tweens (one for the alignment of the image and one for opacity of the buttons)

class Page2 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PageState2();
}

class _PageState2 extends State<Page2>  with SingleTickerProviderStateMixin{
  AnimationController animationController;
  Animation<AlignmentGeometry> _align;
  Animation<double> _opacity;

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );
    _align = AlignmentGeometryTween(begin: Alignment.bottomCenter, end: Alignment.center).animate(animationController);
    _opacity = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(parent: animationController, curve: Interval(0.5, 1),    reverseCurve: Interval(0.5, 1.0)));

    //The next piece of code is just for test
    animationController.forward();
    animationController.addStatusListener((status) {
      if(status == AnimationStatus.completed)
        Future.delayed(const Duration(milliseconds: 800), () => animationController.reverse());
      else if(status == AnimationStatus.dismissed) 
        Future.delayed(const Duration(milliseconds: 800), () => animationController.forward());
    });
  }
  
  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        decoration: BoxDecoration(
            gradient: LinearGradient(
                colors: [Colors.blue, Colors.teal[200]],
                begin: Alignment.bottomLeft,
                end: Alignment.topRight,
                stops: [0.5, 0.85])
        ),
        child: Column(
          children: [
            Expanded(
              flex: 1,
              child: AnimatedBuilder( //create an AnimatedBuilder and provide the Animation of the align
                animation: _align,
                  child: CircleAvatar(
                    backgroundImage: NetworkImage(
                        'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
                    ),
                    radius: 50.0,
                  ),
                  builder: (context, child){
                    return Align(
                      alignment: _align.value, //use the align value
                      child: child
                    );
                  },
               )
            ),
            Expanded(
              child: FadeTransition( //simply use this widget that receives an Animation<double> to animate opacity from 0 to 1
                 opacity: _opacity,
                  child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
                      width: double.infinity,
                      child: RaisedButton(
                        child: Text('Log in', style: TextStyle(color: Colors.teal[300], fontSize: 12)),
                        color: Colors.white,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(16.0),
                        ),
                        onPressed: () => print('Log in'),
                     ),
                   ),
                    Container(
                     padding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
                     width: double.infinity,
                     child: RaisedButton(
                     child: Text('Sign up', style: TextStyle(color: Colors.white70, fontSize: 12)),
                       color: Colors.teal[300],
                       shape: RoundedRectangleBorder(
                         borderRadius: BorderRadius.circular(16.0),
                       ),
                       onPressed: () => print('Sign up'),
                    ),
                   )
                  ]
                ),
              )
            ),
          ]
        )
    );
  }
}

If you don't care about the animationController and just want to animate once (maybe when first displaying the page) you can make use of some widgets that extend ImplicitAnimations, this class does all the animations for you without you creating the controllers, just by setState and changing their value

class Page1 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _PageState1();
}

class _PageState1 extends State<Page1>{
  double opacity = 0;
  double padding = 0;
  bool align = false;

  @override
  void initState() {
    super.initState();
    //setState after 300ms just to see the changes
    Future.delayed(const Duration(milliseconds: 300), () => _setAnimation());
  }
  
  _setAnimation() => setState((){
      opacity = opacity == 0 ? 1 : 0; //when you setState to different values it will animate to those values
      align = !align;
    });

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        decoration: BoxDecoration(
            gradient: LinearGradient(
                colors: [Colors.blue, Colors.teal[200]],
                begin: Alignment.bottomLeft,
                end: Alignment.topRight,
                stops: [0.5, 0.85])
        ),
        child: Column(
          children: [
            Expanded(
              flex: 1,
              child: AnimatedAlign(
                duration: const Duration(milliseconds: 300),
                alignment: align ? Alignment.center : Alignment.bottomCenter, //pass an Align based on the bool value, or if you want to directly pass an Alignment
                  child: CircleAvatar(
                    backgroundImage: NetworkImage(
                      'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
                    ),
                    radius: 50.0,
                  ),
              )
            ),
            Expanded(
              child: AnimatedOpacity(
                opacity: opacity, //give the opacity value
                duration: const Duration(milliseconds: 500),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  children: [
                    Container(
                      padding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
                      width: double.infinity,
                      child: RaisedButton(
                        child: Text('Log in', style: TextStyle(color: Colors.teal[300], fontSize: 12)),
                        color: Colors.white,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(16.0),
                        ),
                        onPressed: () => print('Log in'),
                     ),
                   ),
                    Container(
                     padding: EdgeInsets.symmetric(horizontal: 16, vertical: 0),
                     width: double.infinity,
                     child: RaisedButton(
                     child: Text('Sign up', style: TextStyle(color: Colors.white70, fontSize: 12)),
                       color: Colors.teal[300],
                       shape: RoundedRectangleBorder(
                         borderRadius: BorderRadius.circular(16.0),
                       ),
                       onPressed: () => _setAnimation(),
                    ),
                   )
                  ]
                )
              ),
            ),
          ]
        )
    );
  }
}

This kind of widgets also have a VoidCallback called onEnd, so if you want to stager some animation, let's say move the image and until finished change the opacity just change it like this

_setAnimation() => setState((){
     //don't change opacity here 
     align = !align;
 });

and then in the onEnd of the AnimatedAlign

AnimatedAlign(
  duration: const Duration(milliseconds: 300),
  alignment: align ? Alignment.center : Alignment.bottomCenter,
  child: CircleAvatar(
    backgroundImage: NetworkImage(
      'https://pbs.twimg.com/media/DpeOMc3XgAIMyx_.jpg',
    ),
  radius: 50.0,
  onEnd: () => setState(() => opacity = opacity == 0 ? 1 : 0) //when the align animation ends, it will change the opacity
),

Upvotes: 1

Related Questions