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

时间:2018-06-27 18:58:06

标签: flutter

我有一个视图,该视图的主体由Scaffold和单个ListView组成,列表的每个子代都是一个不同的小部件,代表视图的各个“部分”(各部分范围从简单的TextViews来排列ColumnRow),我只想在用户滚动到某个FloatingActionButon时显示Widgets(由于最初Accept不可见到列表的最下方)。

3 个答案:

答案 0 :(得分:14)

https://pub.dev/packages/flutter_widgets通过其VisibilityDetector小部件提供此功能,该小部件可以包装任何其他Widget并在小部件的可见区域更改时通知:

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

答案 1 :(得分:5)

关于已改写的问题,我对您要执行的操作有了更清晰的了解。您有一个小部件列表,并且要根据当前是否在视口中显示这些小部件来决定是否显示浮动操作按钮。

我写了一个基本的例子来说明这一点。我将在下面描述各种元素,但请注意:

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

答案 2 :(得分:0)

很难看到一些代码;尤其是某些元素为何可见的背后逻辑。不过,我可以给您一些一般性的提示。

首先是在Flutter中,您通常将状态保持在较高级别,然后根据该状态构建其下的内容。在这种情况下,我希望包围您的ListView的小部件具有“状态”,该状态描述了是否使该元素可见。在这种情况下,您的FloatingActionButton可以根据该决定简单地显示或不显示。

您可能可以移动逻辑以将用于创建ListView的数据获取到包含ListView的小部件中,或者可能转移到两者共享的另一个类中。然后,您可以直接向下传递数据(如果只有一层或两层小部件),或者如果有多层小部件,则可以使用InheritedWidget。 (This website很好地示例了如何将InheritedWidgets与source here一起使用。)

第二个是可以使用GlobalKey访问堆栈中您小部件下方的小部件的状态。但是,这通常不是获取信息的可靠方法,因为当子窗口小部件的状态更改时,它不会导致重建。