16

My goal is to change the color and the opacity of the appbar when user scrolls down.

My logic is:

  • scroll offset = 0 : appbar is red with opacity = 1
  • 0 < scroll offset < 40 : appbar is blue with opacity = 0.4
  • 40 <= scroll offset : appbar is blue with opacity proportional to scroll offset

I came up with the following code:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  var _gradientColor1 = Colors.red[400];
  var _gradientColor2 = Colors.red[800];
  ScrollController _scrollViewController;

  void changeColor(){
    if((_scrollViewController.offset == 0) && (_gradientColor1 != Colors.red[400])){
      setState(() {
        _gradientColor1 = Colors.red[400];
        _gradientColor2 = Colors.red[800];
      });
    }else if((_scrollViewController.offset <= 40) && (_gradientColor1 != Color.fromRGBO(66,165,245 ,0.4))){
      setState(() {
        _gradientColor1 = Color.fromRGBO(66,165,245 ,0.4);
        _gradientColor2 = Color.fromRGBO(21,101,192 ,0.4);
      });
    }else if((_scrollViewController.offset <= 100) && (_scrollViewController.offset > 40)){
      var opacity = _scrollViewController.offset/100;
      setState(() {
        _gradientColor1 = Color.fromRGBO(66,165,245 ,opacity);
        _gradientColor2 = Color.fromRGBO(21,101,192 ,opacity);
      });
    }
  }

  @override
  void initState() {
    _scrollViewController = ScrollController(initialScrollOffset: 0.0);
    _scrollViewController.addListener(changeColor);
  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      appBar: GradientAppBar(
        backgroundColorStart: _gradientColor1,
        backgroundColorEnd: _gradientColor2,
        elevation: 0,
      ),
      body: SingleChildScrollView(
        controller: _scrollViewController,
        child: Column(
          children: <Widget>[
            Container(color: Colors.red, height: 400,),
            Container(color: Colors.purple, height: 400,),
          ],
        ),
      ),
    );
  }
}

It works as expected but it becomes laggy with a more complicated UI. In my example I'm using GradientAppbar: https://github.com/joostlek/GradientAppBar

woshitom
  • 4,811
  • 8
  • 38
  • 62
  • 1
    You are calling `setState` every frame the user scrolls. Maybe add a check if the color is already the one you want to set, then don't call `setState`. For example in the first `if` check that the colors also are not already `Colors.red[400]` and `Colors.red[800]`. This way you won't have as many calls to `setState`. – tudorprodan Feb 17 '19 at 15:18
  • 1
    @tudorprodan I'm already doing that check in every if, ie: _gradientColor1 != Colors.red[400] – woshitom Feb 17 '19 at 17:47

2 Answers2

37

i think the best approach Will be using AnimatedBuilder and you will see that first container in body will not changed its color because widget state have not changed and the result :

enter image description here

code:

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

class ProductDetails extends StatefulWidget {
  @override
  _ProductDetailsState createState() => _ProductDetailsState();
}

class _ProductDetailsState extends State<ProductDetails>
    with TickerProviderStateMixin {
  AnimationController _ColorAnimationController;
  AnimationController _TextAnimationController;
  Animation _colorTween, _iconColorTween;
  Animation<Offset> _transTween;

  @override
  void initState() {
    _ColorAnimationController =
        AnimationController(vsync: this, duration: Duration(seconds: 0));
    _colorTween = ColorTween(begin: Colors.transparent, end: Color(0xFFee4c4f))
        .animate(_ColorAnimationController);
    _iconColorTween = ColorTween(begin: Colors.grey, end: Colors.white)
        .animate(_ColorAnimationController);


    _TextAnimationController =
        AnimationController(vsync: this, duration: Duration(seconds: 0));

    _transTween = Tween(begin: Offset(-10, 40), end: Offset(-10, 0))
        .animate(_TextAnimationController);

    super.initState();
  }

  bool _scrollListener(ScrollNotification scrollInfo) {
    if (scrollInfo.metrics.axis == Axis.vertical) {
      _ColorAnimationController.animateTo(scrollInfo.metrics.pixels / 350);

      _TextAnimationController.animateTo(
          (scrollInfo.metrics.pixels - 350) / 50);
      return true;
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color(0xFFEEEEEE),
      body: NotificationListener<ScrollNotification>(
        onNotification: _scrollListener,
        child: Container(
          height: double.infinity,
          child: Stack(
            children: <Widget>[
              SingleChildScrollView(
                child: Column(
                  children: <Widget>[
                    Container(
                      height: 150,
                      color:
                          Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0)
                              .withOpacity(1),
                      width: 250,
                    ),
                    Container(
                      height: 150,
                      color: Colors.pink,
                      width: 250,
                    ),
                    Container(
                      height: 150,
                      color: Colors.deepOrange,
                      width: 250,
                    ),
                    Container(
                      height: 150,
                      color: Colors.red,
                      width: 250,
                    ),
                    Container(
                      height: 150,
                      color: Colors.white70,
                      width: 250,
                    ),
                  ],
                ),
              ),
              Container(
                height: 80,
                child: AnimatedBuilder(
                  animation: _ColorAnimationController,
                  builder: (context, child) => AppBar(
                    backgroundColor: _colorTween.value,
                    elevation: 0,
                    titleSpacing: 0.0,
                    title: Transform.translate(
                      offset: _transTween.value,
                      child: Text(
                        "اسم کالا اینجا",
                        style: TextStyle(
                            color: Colors.white,
                            fontWeight: FontWeight.bold,
                            fontSize: 16),
                      ),
                    ),
                    iconTheme: IconThemeData(
                      color: _iconColorTween.value,
                    ),
                    actions: <Widget>[
                      IconButton(
                        icon: Icon(
                          Icons.local_grocery_store,
                        ),
                        onPressed: () {
//                          Navigator.of(context).push(TutorialOverlay());
                        },
                      ),
                      IconButton(
                        icon: Icon(
                          Icons.more_vert,
                        ),
                        onPressed: () {},
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Mortada Jafar
  • 3,529
  • 1
  • 18
  • 33
0

In this little example I do the following: I change the opacity of my AnimatedOpacity depending on some conditions, namely if its offset is either greater or less than 100 pixels from the top to the bottom of the screen. I obtain the mentioned offset with the help of RenderBox and GlobalKey. This validations and events happen within the function passed to my scrollListener. That means that they get triggered every time I scroll. Here is the full code.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  ScrollController _scrollController;
  GlobalKey widgetKey = GlobalKey();
  Offset widgetOffset;
  double _currentPosition;
  double opacity = 1;

  @override
  void initState() {
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
    super.initState();
  }

  _scrollListener() {
    print('scrolling');

    RenderBox textFieldRenderBox = widgetKey.currentContext.findRenderObject();
    widgetOffset = textFieldRenderBox.localToGlobal(Offset.zero);
    _currentPosition = widgetOffset.dy;

    print(
        "widget position: $_currentPosition against: 100");

    if (100 > _currentPosition && _currentPosition > 1) {
      setState(() {
        opacity = _currentPosition / 100;
      });
    } else if (_currentPosition > 100 && opacity != 1) {
      opacity = 1;
    }
    else if (_currentPosition < 0 && opacity != 0) {
      opacity = 0;
    }
    print("opacity is: $opacity");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
          controller: _scrollController,
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.red,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.red,
              ),
              AnimatedOpacity(
                key: widgetKey,
                duration: Duration(milliseconds: 1),
                opacity: opacity,
                child: Center(
                  child: Container(
                    margin: EdgeInsets.only(bottom: 50),
                    height: 100,
                    width: 100,
                    color: Colors.purpleAccent,
                  ),
                ),
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.red,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.red,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.red,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.teal,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.teal,
              ),
              Container(
                margin: EdgeInsets.only(bottom: 50),
                height: 100,
                width: 100,
                color: Colors.teal,
              ),
            ],
          )),
    );
  }
}

Scroll and animation behavior

Iván Yoed
  • 3,878
  • 31
  • 44
  • 1
    calling `setState` every time the scroll updates is terrible for performance. – JediBurrell Jul 28 '21 at 00:03
  • 1
    You have a good point there. Whoever wants to implement this should make sure `setState` only gets gets called when necessary. Suggestions are very welcomed. – Iván Yoed Jul 28 '21 at 14:50