11

How can I create this sticky buy button animation of Adidas app in Flutter. I have tried to use a scroll controller to listen for the position of user and then use an animated container but it is of no use since I have to define my scroll controller in my initstate while the height of my containers are relative to my device's height. here is the link of video for the animation: https://drive.google.com/file/d/1TzIUBr6abRQI87xAVu4NOPG67aftzceK/view?usp=sharing

this is what the widget tree looks like:

 Scaffold(appbar,FAB,_body),
_body= SingleChildSrollView child:Column[Container(child:Listview)
,Container(child:PageView(children:[GridView])
,Container
,Container(this is where the shop button should be, the one that replaces the FAB)
,GridView,])
geekymano
  • 1,420
  • 5
  • 22
  • 53

3 Answers3

4

Output:

enter image description here

void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
  ScrollController _controller = ScrollController();
  double _boxHeight = 200, _screenHeight;
  int _itemIndex = 5;
  bool _itemVisibility = true;

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

    double offsetEnd;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      RenderBox box = context.findRenderObject();
      _screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
      offsetEnd = ((_itemIndex + 1) - (_screenHeight / _boxHeight)) * _boxHeight;
    });

    _controller.addListener(() {
      if (_controller.position.pixels >= offsetEnd) {
        if (_itemVisibility) setState(() => _itemVisibility = false);
      } else {
        if (!_itemVisibility) setState(() => _itemVisibility = true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ListView.builder(
          controller: _controller,
          itemCount: 8,
          itemBuilder: (context, index) {
            return _buildBox(
              index: index,
              color: index == _itemIndex ? Colors.cyan : Colors.blue[((index + 1) * 100) % 900],
            );
          },
        ),
        Positioned(
          bottom: 0,
          right: 0,
          left: 0,
          child: Visibility(
            visible: _itemVisibility,
            child: _buildBox(index: _itemIndex, color: Colors.cyan),
          ),
        ),
      ],
    );
  }

  Widget _buildBox({int index, Color color}) {
    return Container(
      height: _boxHeight,
      color: color,
      alignment: Alignment.center,
      child: Text(
        "${index}",
        style: TextStyle(fontSize: 52, fontWeight: FontWeight.bold),
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • great answer. but I am using a column so I cannot use the index. any ideas? – geekymano Jun 29 '19 at 22:44
  • yes I am using singlechild scrollview. no there is no reason why I cannot switch to that. but how would I use the index that you have used in your example? – geekymano Jun 30 '19 at 12:49
  • I have added the screenshot since my code is pretty verbose. – geekymano Jun 30 '19 at 12:57
  • there is my FAB that I want to hide when it reaches a certain widget – geekymano Jun 30 '19 at 12:58
  • the widget tree is like Scaffold(appbar,FAB,_body),_body= SingleChildSrollView child:Column[Container(child:Listview),Container(child:PageView(children:[GridView]),Container,Container(this is where the shop button should be, the one that replaces the FAB),GridView,]) – geekymano Jun 30 '19 at 13:51
  • I have added the schema of widget tree in the question too, for better readability – geekymano Jun 30 '19 at 13:54
  • the screen shot of my app is exactly like this: https://drive.google.com/file/d/1TzIUBr6abRQI87xAVu4NOPG67aftzceK/view?usp=sharing – geekymano Jun 30 '19 at 19:20
  • Thanks, I am off today, I will take a look tomorrow. – CopsOnRoad Jul 01 '19 at 14:52
3

Another answer (variable height boxes)

enter image description here

void main() => runApp(MaterialApp(home: Scaffold(body: HomePage(), appBar: AppBar())));

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
  ScrollController _controller = ScrollController();
  double _screenHeight, _hRatings = 350, _hSize = 120, _hWidth = 130, _hComfort = 140, _hQuality = 150, _hBuy = 130, _hQuestions = 400;
  bool _itemVisibility = true;

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

    double offsetEnd;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      RenderBox box = context.findRenderObject();
      _screenHeight = box.globalToLocal(Offset(0, MediaQuery.of(context).size.height)).dy;
      offsetEnd = (_hRatings + _hSize + _hWidth + _hComfort + _hQuality + _hBuy) - _screenHeight;
    });

    _controller.addListener(() {
      if (_controller.position.pixels >= offsetEnd) {
        if (_itemVisibility) setState(() => _itemVisibility = false);
      } else {
        if (!_itemVisibility) setState(() => _itemVisibility = true);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        ListView(
          controller: _controller,
          children: <Widget>[
            _buildBox(_hRatings, "Ratings box", Colors.blue[200]),
            _buildBox(_hSize, "Size box", Colors.blue[300]),
            _buildBox(_hWidth, "Width box", Colors.blue[400]),
            _buildBox(_hComfort, "Comfort box", Colors.blue[500]),
            _buildBox(_hQuality, "Quality box", Colors.blue[600]),
            _buildBox(_hBuy, "Buy box", Colors.orange[700]),
            _buildBox(_hQuestions, "Questions part", Colors.blue[800]),
          ],
        ),
        Positioned(
          bottom: 0,
          right: 0,
          left: 0,
          child: Visibility(
            visible: _itemVisibility,
            child: _buildBox(_hBuy, "Buy box", Colors.orange[700]),
          ),
        ),
      ],
    );
  }

  Widget _buildBox(double height, String text, Color color) {
    return Container(
      height: height,
      color: color,
      alignment: Alignment.center,
      child: Text(
        text,
        style: TextStyle(
          fontSize: 32,
          color: Colors.black,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
0

I would show a floatingActionButton when the embedded button is not visible. Here's a solution based on this thread : How to know if a widget is visible within a viewport?

 import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

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

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

class MyAppState extends State<MyApp> {
  GlobalKey<State> key = new GlobalKey();

  double fabOpacity = 1.0;

  @override
  Widget build(BuildContext context) {
return new MaterialApp(
  home: new Scaffold(
    appBar: new AppBar(
      title: new Text("Scrolling."),
    ),
    body: NotificationListener<ScrollNotification>(
      child: new ListView(
        itemExtent: 100.0,
        children: [
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          new MyObservableWidget(key: key),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder(),
          ContainerWithBorder()
        ],
      ),
      onNotification: (ScrollNotification scroll) {
        var currentContext = key.currentContext;
        if (currentContext == null) return false;

        var renderObject = currentContext.findRenderObject();
        RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
        var offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0);
        var offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0);

        if (offsetToRevealBottom.offset > scroll.metrics.pixels ||
            scroll.metrics.pixels > offsetToRevealTop.offset) {
          if (fabOpacity != 1.0) {
            setState(() {
              fabOpacity = 1.0;
            });
          }
        } else {
          if (fabOpacity == 1.0) {
            setState(() {
              fabOpacity = 0.0;
            });
          }
        }
        return false;
      },
    ),
    floatingActionButton: new Opacity(
      opacity: fabOpacity,
      child: Align(
        alignment: Alignment.bottomCenter,
        child: new FloatingActionButton.extended(
          label: Text('sticky buy button'),
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4.0)),
          onPressed: () {
            print("YAY");
          },
        ),
      ),
    ),
  ),
);
  }
}

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

  @override
  State<StatefulWidget> createState() => new MyObservableWidgetState();
}

class MyObservableWidgetState extends State<MyObservableWidget> {
  @override
  Widget build(BuildContext context) {
    return new RaisedButton(
      onPressed: () {

      },
      color: Colors.lightGreenAccent,
      child: Text('This is my buy button', style: TextStyle(color: Colors.blue),),
    );
  }
}

class ContainerWithBorder extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Container(
      decoration: new BoxDecoration(border: new Border.all(), color: Colors.grey),
    );
  }
}
hawkbee
  • 1,442
  • 3
  • 18
  • 26