【问题标题】:How to know if a widget is visible within a viewport?如何知道小部件在视口中是否可见?
【发布时间】:2018-12-06 18:12:19
【问题描述】:

我有一个视图,它的主体中包含一个 Scaffold 和一个 ListView,列表的每个子项都是一个不同的小部件,代表视图的各个“部分”(部分范围从简单的 TextViews 到排列Columns 和 Rows),我只想在用户滚动到某些 Widgets 时才显示 FloatingActionButon(由于在列表的下方,它们最初不可见)。

【问题讨论】:

标签: flutter dart viewport


【解决方案1】:

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

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

【讨论】:

  • 有谁知道VisibilityDetector 需要什么样的性能?我想将每个项目包装在 ListView.builder 中,当它可见时,我想将展示事件记录到 Firebase Analytics。
  • @MosheG,事件报告的间隔时间
  • @MosheG 这个包是由谷歌开发的,所以我猜它必须被优化。
  • 我已经有一段时间不用再回到 Flutter 了,但会尝试这个并可能改变接受的答案!
  • 它在视频中表现不佳,纵横比 + vid 使用其中之一包装时会出现错误的效果
【解决方案2】:

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

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

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

【讨论】:

  • 谢谢它对我有用
【解决方案3】:

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

我不保证它会起作用,但值得一试。

前提条件:您必须拥有要检查其可见性的小部件的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...
}

【讨论】:

  • 这不能可靠地工作。 F.e.列表视图中的项目可以具有渲染对象,但仍处于非活动状态且不显示。
  • @user3249027 我写了“我不保证它会起作用,但值得一试。”因此,您可以尝试一下,如果它不起作用,请不要感到惊讶。 :D 但是,它对我和其他一些人也有效.. 你能举出你的例子吗?
  • @user3249027 也如您所见,如果存在RenderObject,我正在计算yPosition。也许您可以使用它(在 y 轴上的偏移量)并将其与屏幕高度进行比较,以检查小部件是否在屏幕中可见。此外,这是否可行取决于您的具体情况,即当前布局和上面 sn-p 执行的时刻。
  • 嘿@Aleksandar,你是对的,我说的是一个特殊的用例;在使用ListView.builder 时遇到了这个问题:该小部件懒惰地构建其子级并“预缓存”,其中一些子级在当前可见的子级之前和之后创建 - 因此“拥有”renderObject - 但被标记(bc屏幕外)为"inactive"。在非活动的renderObject 上调用.localToGlobal() 将引发异常。
  • @user3249027 我猜你找到了判断小部件是否可见的方法,恭喜! ? 如果您认为我应该在回答中添加一些内容,请告诉我。
猜你喜欢
  • 1970-01-01
  • 2022-01-06
  • 1970-01-01
  • 1970-01-01
  • 2016-05-22
  • 1970-01-01
  • 1970-01-01
  • 2023-01-17
  • 1970-01-01
相关资源
最近更新 更多