如何验证小部件 "offscreen"

How to verify a widget is "offscreen"

悬赏信息:如果满足以下条件,我会接受你的回答:


[edit : 07/02/21] following Miyoyo#5957 on flutter community on discord @iapicca Convert widget position to global, get width height, add both, and see if the resulting bottom right position is on screen? and using the following answers as reference:

给出下面的代码示例(也 runnable on dartpad

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

final _testKey = GlobalKey();
const _fabKey = ValueKey('fab');
final _onScreen = ValueNotifier<bool>(true);

void main() => runApp(_myApp);

const _myApp = MaterialApp(
  home: Scaffold(
    body: MyStage(),
    floatingActionButton: MyFAB(),
  ),
);

class MyFAB extends StatelessWidget {
  const MyFAB() : super(key: const ValueKey('MyFAB'));

  @override
  Widget build(BuildContext context) => FloatingActionButton(
        key: _fabKey,
        onPressed: () => _onScreen.value = !_onScreen.value,
      );
}

class MyStage extends StatelessWidget {
  const MyStage() : super(key: const ValueKey('MyStage'));

  @override
  Widget build(BuildContext context) => Stack(
        children: [
          ValueListenableBuilder(
            child: FlutterLogo(
              key: _testKey,
            ),
            valueListenable: _onScreen,
            builder: (context, isOnStage, child) => AnimatedPositioned(
              top: MediaQuery.of(context).size.height *
                  (_onScreen.value ? .5 : -1),
              child: child,
              duration: const Duration(milliseconds: 100),
            ),
          ),
        ],
      );
}

我要测试的是widget是off screen 这是到目前为止的测试代码

void main() {
  testWidgets('...', (tester) async {
    await tester.pumpWidget(_myApp);
    final rect = _testKey.currentContext.findRenderObject().paintBounds;

    expect(tester.getSize(find.byKey(_testKey)), rect.size,
        reason: 'size should match');

    final lowestPointBefore = rect.bottomRight.dy;
    print('lowest point **BEFORE** $lowestPointBefore ${DateTime.now()}');
    expect(lowestPointBefore > .0, true, reason: 'should be on-screen');

    await tester.tap(find.byKey(_fabKey));
    await tester.pump(const Duration(milliseconds: 300));
    final lowestPointAfter =
        _testKey.currentContext.findRenderObject().paintBounds.bottomRight.dy;

    print('lowest point **AFTER** $lowestPointAfter ${DateTime.now()}');
    expect(lowestPointAfter > .0, false, reason: 'should be off-screen');
  });
}


并生成日志

00:03 +0: ...                                                                                                                                                                                               
lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
  Expected: <false>
  Actual: <true>

When the exception was thrown, this was the stack:
#4      main.<anonymous closure> (file:///home/francesco/projects/issue/test/widget_test.dart:83:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...

This was caught by the test expectation on the following line:
  file:///home/francesco/projects/issue/test/widget_test.dart line 83
The test description was:
  ...
════════════════════════════════════════════════════════════════════════════════════════════════════
00:03 +0 -1: ... [E]                                                                                                                                                                                        
  Test failed. See exception logs above.
  The test description was: ...
  
00:03 +0 -1: Some tests failed.                                            

我不确定我的做法是否正确 印刷品上的时间暗示我

lowest point **BEFORE** 24.0 2021-02-07 16:28:08.715558
lowest point **AFTER** 24.0 2021-02-07 16:28:08.850733

建议我 await tester.pumpAndSettle(Duration(milliseconds: 300)); 不符合我的想法

我找到了 answer (in particular the code inside the onNotification), which kind of does what (I think) you want. It finds the RenderObject using the current context of the key. Afterwards it finds the RenderAbstractViewport using this RenderObject, and checks the offSetToReveal。使用此偏移量可以确定当前 RenderObject 是否正在显示(使用简单比较)。

我不是 100% 确定这会奏效/是你想要的,但希望它能把你推向正确的方向。

另外(即使你说你不想要任何外部包),在同一个问题上有人推荐了 this 包,这对其他有同样问题但愿意使用的人很有用一个外包。

问题是:

  1. 我们试图找到 FlutterLogo 的矩形,但 FlutterLogo 矩形将保持不变,父 AnimatedPositioned 小部件的位置实际上正在改变。
  2. 即使我们现在开始检查 AnimatedPositioned paintBounds,它仍然是一样的,因为我们没有改变宽度,而是改变它自己的位置。

解决方案:

  1. 通过 topWidget 获取屏幕 rect 对我来说它是 Scaffold。 (如果我们有不同的小部件,比如包含 FAB 按钮的 HomeScreen,我们只需要找到那个矩形)
  2. 点击前我正在检查 fab 按钮是否在屏幕上
  3. 点击并泵送小部件,让它稳定下来。
  4. 搜索 widget rect,它将在屏幕之外,即在我们的例子中为 -600

在自己的代码中添加了注释

testWidgets('...', (tester) async {
    await tester.pumpWidget(MyApp);
    //check screen width height - here I'm checking for scaffold but you can put some other logic for screen size or parent widget type
    Rect screenRect = tester.getRect(find.byType(Scaffold));
    print("screenRect: $screenRect");

    //checking previous position of the widget - on our case we are animating widget position via AnimatedPositioned
    // which in itself is a statefulwidget and has Positioned widget inside
    //also if we have multiple widgets of same type give them uniqueKey
    AnimatedPositioned widget =
        tester.firstWidget(find.byType(AnimatedPositioned));
    double topPosition = widget.top;
    print(widget);
    print("AnimatedPositioned topPosition: $topPosition}");
    expect(
        screenRect.bottom > topPosition && screenRect.top < topPosition, true,
        reason: 'should be on-screen');

    //click button to animate the widget and wait
    await tester.tap(find.byKey(fabKey));
    //this will wait for animation to settle or call pump after duration
    await tester.pumpAndSettle(const Duration(milliseconds: 300));

    //check after position of the widget
    AnimatedPositioned afterAnimationWidget =
        tester.firstWidget(find.byType(AnimatedPositioned));

    double afterAnimationTopPosition = afterAnimationWidget.top;
    Rect animatedWidgetRect = tester.getRect(find.byType(AnimatedPositioned));
    print("rect of widget : $animatedWidgetRect");
    expect(
        screenRect.bottom > afterAnimationTopPosition &&
            screenRect.top < afterAnimationTopPosition,
        false,
        reason: 'should be off-screen');
  });

注意:从代码中替换了 _,因为它在测试文件中隐藏了对象。

输出:

screenRect: Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)
fab clicked
rect of widget : Rect.fromLTRB(0.0, -600.0, 24.0, -576.0)

我要感谢@parth-dave 对于他的 ,我很乐意用赏金奖励

Miyoyo在问题中被引用

我想提供基于他的方法的我自己的实现

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

// !! uncomment tge line below to run as test app
// void main() => runApp(_myApp);

class Keys {
  static final subject = UniqueKey();
  static final parent = UniqueKey();
  static final trigger = UniqueKey();
}

final _onScreen = ValueNotifier<bool>(true);

Widget get app => MaterialApp(
      home: Scaffold(
        key: Keys.parent,
        body: MyStage(),
        floatingActionButton: MyFAB(),
      ),
    );

class MyFAB extends StatelessWidget {
  const MyFAB() : super(key: const ValueKey('MyFAB'));

  @override
  Widget build(BuildContext context) => FloatingActionButton(
        key: Keys.trigger,
        onPressed: () => _onScreen.value = !_onScreen.value,
      );
}

class MyStage extends StatelessWidget {
  const MyStage() : super(key: const ValueKey('MyStage'));

  @override
  Widget build(BuildContext context) => Stack(
        children: [
          ValueListenableBuilder(
            child: FlutterLogo(
              key: Keys.subject,
            ),
            valueListenable: _onScreen,
            builder: (context, isOnStage, child) => AnimatedPositioned(
              top: MediaQuery.of(context).size.height *
                  (_onScreen.value ? .5 : -1),
              child: child,
              duration: const Duration(milliseconds: 100),
            ),
          ),
        ],
      );
}

void main() {
  group('`AnimatedPositined` test', () {
    testWidgets(
        'WHEN no interaction with `trigger` THEN `subject` is ON SCREEN',
        (tester) async {
      await tester.pumpWidget(app);

      final parent = tester.getRect(find.byKey(Keys.parent));
      final subject = tester.getRect(find.byKey(Keys.subject));

      expect(parent.overlaps(subject), true, reason: 'should be ON-screen');
    });

    testWidgets('WHEN `trigger` tapped THEN `subject` is OFF SCREEN`',
        (tester) async {
      await tester.pumpWidget(app);

      await tester.tap(find.byKey(Keys.trigger));
      await tester.pumpAndSettle(const Duration(milliseconds: 300));

      final parent = tester.getRect(find.byKey(Keys.parent));
      final subject = tester.getRect(find.byKey(Keys.subject));

      expect(parent.overlaps(subject), false, reason: 'should be OFF-screen');
    });
  });
}