Flutter:在滚动时更改小部件不透明度和颜色的最佳方法

Posted

技术标签:

【中文标题】Flutter:在滚动时更改小部件不透明度和颜色的最佳方法【英文标题】:Flutter: Best way to change a widget opacity and color on scroll 【发布时间】:2019-07-10 17:58:27 【问题描述】:

我的目标是在用户向下滚动时更改应用栏的颜色和不透明度。

我的逻辑是:

滚动偏移 = 0 : appbar 为红色,不透明度 = 1 0 40

我想出了以下代码:

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,),
          ],
        ),
      ),
    );
  

它按预期工作,但随着用户界面的复杂化而变得迟钝。 在我的示例中,我使用的是 GradientAppbar:https://github.com/joostlek/GradientAppBar

【问题讨论】:

您在用户滚动的每一帧都调用setState。如果颜色已经是您想要设置的颜色,可能会添加一个检查,然后不要调用setState。例如在第一个if 中检查颜色是否还不是Colors.red[400]Colors.red[800]。这样一来,您就不会有那么多电话给setState @tudorprodan 我已经在每个 if 中进行检查,即:_gradientColor1 != Colors.red[400] 【参考方案1】:

我认为最好的方法是使用AnimatedBuilder,你会看到body中的第一个容器不会改变它的颜色,因为小部件状态没有改变 结果:

代码:

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: () ,
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  

【讨论】:

这是考虑性能的最佳方法吗? 现在可以使用 extendBodyBehindAppBar: true 将动画 AppBar 直接添加到 Staffold 中,(来自 Flutter stable 1.12+)【参考方案2】:

在这个小示例中,我执行以下操作:我根据某些条件更改 opacityAnimatedOpacity,即从屏幕顶部到底部的偏移量是否大于或小于 100 像素。提到的偏移量是我在RenderBoxGlobalKey 的帮助下获得的。此验证和事件发生在传递给我的scrollListener 的函数中。这意味着每次滚动时都会触发它们。这是完整的代码。

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,
              ),
            ],
          )),
    );
  

【讨论】:

每次滚动更新时调用 setState 对性能来说很糟糕。 你说得很好。想要实现这一点的人应该确保setState 只在必要时被调用。非常欢迎提出建议。

以上是关于Flutter:在滚动时更改小部件不透明度和颜色的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

如何在 PyQt5 Python 中更改表格小部件中滚动条的颜色

Flutter 如何从自定义小部件中获取价值

Flutter:使剪切区域透明以滚动 ListView

三元条件没有重建我的小部件 - Flutter

Flutter 文本小部件

Flutter:使用 Sliver 小部件时如何在滚动时隐藏 BottomAppBar?