如何知道小部件在视口中是否可见?

How to know if a widget is visible within a viewport?

我有一个视图,其主体由一个 Scaffold 和一个 ListView 组成,列表的每个子项都是一个不同的小部件,代表视图的各种 "sections" (部分范围从简单的 TextView 到 Columns 和 Rows 的排列),我只想在用户滚动到某些 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 并自己进行不透明度计算来摆脱淡出 in/fading(即,您可能希望它在开始滚动到视图时变得不透明,这将有考虑到你的物体的高度):

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

https://pub.dev/packages/visibility_detector provides this functionality with its VisibilityDetector 可以包裹任何其他 Widget 的小部件,并在小部件的可见区域更改时发出通知:

VisibilityDetector(
   key: Key("unique key"),
   onVisibilityChanged: (VisibilityInfo info) {
       debugPrint("${info.visibleFraction} of my widget is visible");
   },
   child: MyWidgetToTrack());
)

这里是最简单的方法,只需三行代码!

我不保证一定有用,但值得一试。

先决条件:您必须拥有要检查其可见性的小部件的 Key

final RenderObject? box = _widgetKey.currentContext?.findRenderObject(); //     !
if (box != null) {
  final double yPosition = (box as RenderBox).localToGlobal(Offset.zero).dy; // !
  print('Widget is visible in the viewport at position: $yPosition');
  // do stuff...
}
else {
  print('Widget is not visible.');
  // do stuff...
}