我有一个视图,它的主体由Scaffold和单个ListView组成,列表的每个子级都是一个不同的小部件,表示视图的各个"部分"(部分范围从简单的TextView到ColumnRow的排列),我想只在用户滚动到特定的Widgets上时才显示FloatingActionButon(因为它们最初是不可见的,因为它们位于列表的下方太远).

推荐答案

有了这个重新措辞的问题,我对你想要做的事情有了更清楚的理解.您有一个小部件列表,并希望根据这些小部件当前是否显示在视口中来决定是否显示浮动操作按钮.

我已经编写了一个基本的示例,它在实际操作中展示了这一点.我将在下面描述各种元素,但请注意:

  1. 它使用的GlobalKey往往不太高效
  2. 它连续运行,在滚动过程中每一帧都会进行一些非优化计算.

因此,它可能会导致你的应用程序速度减慢.我将把它留给其他人来优化或编写更好的答案,使用更好的渲染树知识来完成同样的事情.

不管怎样,这是代码.首先,我将介绍一种相对简单的方法——直接在变量上使用setState,因为它更简单:

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(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              new MyObservableWidget(key: key),
              ContainerWithBorder(),
              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 != 0.0) {
                setState(() {
                  fabOpacity = 0.0;
                });
              }
            } else {
              if (fabOpacity == 0.0) {
                setState(() {
                  fabOpacity = 1.0;
                });
              }
            }
            return false;
          },
        ),
        floatingActionButton: new Opacity(
          opacity: fabOpacity,
          child: new FloatingActionButton(
            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 Container(height: 100.0, color: Colors.green);
  }
}

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

它有几个容易修复的问题-它没有隐藏按钮,只是使其透明,它每次都呈现整个小部件,并在每一帧计算小部件的位置.

这是一个更优化的版本,如果不需要,它不会进行计算.如果您的列表发生变化,您可能需要添加更多的逻辑(或者您可以每次只做计算,如果性能足够好,则不必担心).请注意它是如何使用AnimationController和AnimatedBuilder来确保每次只构建相关部分的.您还可以通过直接设置AnimationController的value并自己计算不透明度来消除淡入/淡出(即,您可能希望它在开始滚动到视图中时变得不透明,这必须考虑对象的高度):

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> with TickerProviderStateMixin<MyApp> {
  GlobalKey<State> key = new GlobalKey();

  bool fabShowing = false;

  // non-state-managed variables
  AnimationController _controller;
  RenderObject _prevRenderObject;
  double _offsetToRevealBottom = double.infinity;
  double _offsetToRevealTop = double.negativeInfinity;

  @override
  void initState() {
    super.initState();
    _controller = new AnimationController(vsync: this, duration: Duration(milliseconds: 300));
    _controller.addStatusListener((val) {
      if (val == AnimationStatus.dismissed) {
        setState(() => fabShowing = false);
      }
    });
  }

  @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(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              new MyObservableWidget(key: key),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder(),
              ContainerWithBorder()
            ],
          ),
          onNotification: (ScrollNotification scroll) {
            var currentContext = key.currentContext;
            if (currentContext == null) return false;

            var renderObject = currentContext.findRenderObject();

            if (renderObject != _prevRenderObject) {
              RenderAbstractViewport viewport = RenderAbstractViewport.of(renderObject);
              _offsetToRevealBottom = viewport.getOffsetToReveal(renderObject, 1.0).offset;
              _offsetToRevealTop = viewport.getOffsetToReveal(renderObject, 0.0).offset;
            }

            final offset = scroll.metrics.pixels;

            if (_offsetToRevealBottom < offset && offset < _offsetToRevealTop) {
              if (!fabShowing) setState(() => fabShowing = true);

              if (_controller.status != AnimationStatus.forward) {
                _controller.forward();
              }
            } else {
              if (_controller.status != AnimationStatus.reverse) {
                _controller.reverse();
              }
            }
            return false;
          },
        ),
        floatingActionButton: fabShowing
            ? new AnimatedBuilder(
                child: new FloatingActionButton(
                  onPressed: () {
                    print("YAY");
                  },
                ),
                builder: (BuildContext context, Widget child) => Opacity(opacity: _controller.value, child: child),
                animation: this._controller,
              )
            : null,
      ),
    );
  }
}

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 Container(height: 100.0, color: Colors.green);
  }
}

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

Flutter相关问答推荐

如何组合匹配器

如何在用Ffltter成功登录后重定向下一页?

无法在我的Flutter 应用程序中使用GoRouter测试导航

在flutter web上渲染多个Youtube视频

Riverpod 2.0-如何使用FutureOr和AutoDisposeSyncNotificationer处理api状态代码?

2023 年如何使用 youtube_explode_dart 包下载 URL?

如何在flutter中加载并显示adx锚定的自适应广告?

要禁用按钮上的通知声音,请点击Flutter

Flutter应用无法上传到Microsoft Store

Flutter开发中,如何通过'nfc_manager'插件在Android设备上避免默认的New Tag Scanned弹出窗口?

如何使Flutter 屏幕无法关闭?

围绕父组件旋转位置组件

如何删除图像边框半径与其父容器边框半径之间的空白

如何使用 riverpod_annotation 创建非自动处置提供程序?

应用程序和应用程序内的 webview 之间的单点登录

在 CircleAvatar 中放置芯片

Flutter 错误:正文可能正常完成,导致返回null

如何防止卡子在底部溢出?

在另一个页面编辑数据后更新 Flutter 页面的惯用方法?

Firebase Firestore 查询