How to animate one big image to only show a part of itself inside screen?

Viewed 229

I have one big image.

width = 500 px;
height = 2000 px;

I want to show that image inside viewport of a widget. In this case the widget is going to be background and it is going to take the full width and height of the screen.

I have couple of breakpoints on that image that need to align with bottom of the screen. [ 1000px, 1500 px, 2000 px ]

What needs to be shown is everything from that bottom line until it fills the screen to the top, while the width is being stretched to fit the screen.

I also want to animate the changes from one background to another.

How would I approach this solution?

I tried to use alignment from Image.asset but I cannot map precisely the alignment to needed breakpoints.

    return TweenAnimationBuilder(
      curve: Curves.easeInOut,
      duration: Duration(milliseconds: 300),
      tween: Tween<double>(
          begin: alignmentForScreen(previousScreen),
          end: alignmentForScreen(currentScreen)),
      builder: (context, value, _) => Image.asset(
        "assets/imgs/bg/home_bg.jpeg",
        width: size.width,
        height: size.height,
        fit: BoxFit.fitWidth,
        alignment: Alignment(0,value),
      ),
    );

Here is the image that I want to use:

It is 553*2048 and breakpoints are drawn as a line at the height of

 1365
 1712
 2032

Any suggestions?

2 Answers

This is easy to do with a ListView nested inside a SizedBox

SizedBox(
  height: MediaQuery.of(context).size.height,  //Constraints of viewport to fit screen
  width: MediaQuery.of(context).size.width,  //Constraints of viewport to fit screen
  child: ListView(  //Scrollable widget
    shrinkWrap: true, //Reduce ListView's height to the childrens's
    controller: listViewController,  //Allows to programatically animate between offsets of the ListView (1365, 1712, 2032)
    physics: NeverScrollableScrollPhysics(),  //The user can't swipe the picture up or down
    scrollDirection: Axis.vertical,
    children: [
      Image.asset("assets/svg/ZFh5j.jpg", fit: BoxFit.fitHeight) //BigImage
    ],
  ),
),

To animate between offsets in the ListView add a ScrollController:

class Screen extends StatefulWidget {
  Screen({Key key}) : super(key: key);

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

class _ScreenState extends State<Screen> {
  ScrollController listViewController = ScrollController(initialScrollOffset: 0);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          SizedBox(
            height: MediaQuery.of(context).size.height,  //Constraints of viewport to fit screen
            width: MediaQuery.of(context).size.width,  //Constraints of viewport to fit screen
            child: ListView(  //Scrollable widget
              shrinkWrap: true, //Reduce ListView's height to the childrens's
              controller: listViewController,  //Allows to programatically animate between offsets of the ListView (1365, 1712, 2032)
              physics: NeverScrollableScrollPhysics(),  //The user can't swipe the picture up or down
              scrollDirection: Axis.vertical,
              children: [
                Image.asset("assets/svg/ZFh5j.jpg") //BigImage
              ],
            ),
          ),
          //contents on top of the background here
        ],
      ),
    );
  }
}

Then you can animate the ListView's position like this:

(You can put this function in a GestureDetector. The ListView must already be built before animating it)

listViewController.animateTo(/*offset*/, duration: Duration(milliseconds: 500), curve: Curves.easeInOutQuad);

This will scroll the ListView to "offset" pixels from the top, so animating to the offset 2032 won't align the lower line of the image with the bottom constraint of the viewport. To make it easy, add a SizedBox in the ListView before the image

SizedBox(
  height: MediaQuery.of(context).size.height,  //Constraints of viewport to fit screen
  width: MediaQuery.of(context).size.width,  //Constraints of viewport to fit screen
  child: ListView(  //Scrollable widget
    shrinkWrap: true, //Reduce ListView's height to the childrens's
    controller: listViewController,  //Allows to programatically animate between offsets of the ListView (1365, 1712, 2032)
    physics: NeverScrollableScrollPhysics(),  //The user can't swipe the picture up or down
    scrollDirection: Axis.vertical,
    children: [
      SizedBox(height: MediaQuery.of(context).size.height), //Must be height of viewport in order to work
      SizedBox(
        height: 2048,
        width: 553,
        child: Image.asset("assets/svg/ZFh5j.jpg", fit: BoxFit.fitHeight,) //BigImage
      )
    ],
  ),
),

Now calling listViewController.animateTo(2032, duration: Duration(milliseconds: 500), curve: Curves.easeInOutQuad) should align the lower line with the bottom of the viewport

Thanks to the answer above, the solution I used was to wrap the image in sized box of the same size like image in pixels. When you put that in SingleChildScrollView with reversed = true, animating scroll to offset will be pixel precise.

SingleChildScrollView(
      reverse: true,
      controller: scrollController,
      physics: NeverScrollableScrollPhysics(),
      child: SizedBox(
        height: 2048,
        width: 553,
        child: Image.asset(
          "assets/imgs/bg/home_bg.jpeg",
          fit: BoxFit.fill,
          alignment: Alignment.center,
        ),
      ),
    );
      bgOffsets = [100, 336.0, 411.0, 1200.0];
      scrollController.animateTo(bgOffsets[currentScreen.index],
          duration: Duration(milliseconds: 500), curve: Curves.bounceOut);
Related