// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; class TestFlowDelegate extends FlowDelegate { TestFlowDelegate({required this.startOffset}) : super(repaint: startOffset); final Animation<double> startOffset; @override BoxConstraints getConstraintsForChild(int i, BoxConstraints constraints) { return constraints.loosen(); } @override void paintChildren(FlowPaintingContext context) { double dy = startOffset.value; for (int i = 0; i < context.childCount; ++i) { context.paintChild(i, transform: Matrix4.translationValues(0.0, dy, 0.0)); dy += 0.75 * context.getChildSize(i)!.height; } } @override bool shouldRepaint(TestFlowDelegate oldDelegate) => startOffset == oldDelegate.startOffset; } class OpacityFlowDelegate extends FlowDelegate { OpacityFlowDelegate(this.opacity); double opacity; @override void paintChildren(FlowPaintingContext context) { for (int i = 0; i < context.childCount; ++i) { context.paintChild(i, opacity: opacity); } } @override bool shouldRepaint(OpacityFlowDelegate oldDelegate) => opacity != oldDelegate.opacity; } // OpacityFlowDelegate that paints one of its children twice class DuplicatePainterOpacityFlowDelegate extends OpacityFlowDelegate { DuplicatePainterOpacityFlowDelegate(double opacity) : super(opacity); @override void paintChildren(FlowPaintingContext context) { for (int i = 0; i < context.childCount; ++i) { context.paintChild(i, opacity: opacity); } if (context.childCount > 0) { context.paintChild(0, opacity: opacity); } } } void main() { testWidgets('Flow control test', (WidgetTester tester) async { final AnimationController startOffset = AnimationController.unbounded( vsync: tester, ); final List<int> log = <int>[]; Widget buildBox(int i) { return GestureDetector( onTap: () { log.add(i); }, child: Container( width: 100.0, height: 100.0, color: const Color(0xFF0000FF), child: Text('$i', textDirection: TextDirection.ltr), ), ); } await tester.pumpWidget( Flow( delegate: TestFlowDelegate(startOffset: startOffset), children: <Widget>[ buildBox(0), buildBox(1), buildBox(2), buildBox(3), buildBox(4), buildBox(5), buildBox(6), ], ), ); await tester.tap(find.text('0')); expect(log, equals(<int>[0])); await tester.tap(find.text('1')); expect(log, equals(<int>[0, 1])); await tester.tap(find.text('2')); expect(log, equals(<int>[0, 1, 2])); log.clear(); await tester.tapAt(const Offset(20.0, 90.0)); expect(log, equals(<int>[1])); startOffset.value = 50.0; await tester.pump(); log.clear(); await tester.tapAt(const Offset(20.0, 90.0)); expect(log, equals(<int>[0])); }); testWidgets('paintChild gets called twice', (WidgetTester tester) async { await tester.pumpWidget( Flow( delegate: DuplicatePainterOpacityFlowDelegate(1.0), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), SizedBox(width: 100.0, height: 100.0), ], ), ); final dynamic exception = tester.takeException(); expect(exception, isFlutterError); final FlutterError error = exception as FlutterError; expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' Cannot call paintChild twice for the same child.\n' ' The flow delegate of type DuplicatePainterOpacityFlowDelegate\n' ' attempted to paint child 0 multiple times, which is not\n' ' permitted.\n' )); }); testWidgets('Flow opacity layer', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow( delegate: OpacityFlowDelegate(opacity), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), ], ), ); ContainerLayer? layer = RendererBinding.instance!.renderView.debugLayer; while (layer != null && layer is! OpacityLayer) layer = layer.firstChild as ContainerLayer?; expect(layer, isA<OpacityLayer>()); final OpacityLayer? opacityLayer = layer as OpacityLayer?; expect(opacityLayer!.alpha, equals(opacity * 255)); expect(layer!.firstChild, isA<TransformLayer>()); }); testWidgets('Flow can set and update clipBehavior', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow( delegate: OpacityFlowDelegate(opacity), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), ], ), ); // By default, clipBehavior should be Clip.hardEdge final RenderFlow renderObject = tester.renderObject(find.byType(Flow)); expect(renderObject.clipBehavior, equals(Clip.hardEdge)); for(final Clip clip in Clip.values) { await tester.pumpWidget( Flow( delegate: OpacityFlowDelegate(opacity), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), ], clipBehavior: clip, ), ); expect(renderObject.clipBehavior, clip); } }); testWidgets('Flow.unwrapped can set and update clipBehavior', (WidgetTester tester) async { const double opacity = 0.2; await tester.pumpWidget( Flow.unwrapped( delegate: OpacityFlowDelegate(opacity), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), ], ), ); // By default, clipBehavior should be Clip.hardEdge final RenderFlow renderObject = tester.renderObject(find.byType(Flow)); expect(renderObject.clipBehavior, equals(Clip.hardEdge)); for(final Clip clip in Clip.values) { await tester.pumpWidget( Flow.unwrapped( delegate: OpacityFlowDelegate(opacity), children: const <Widget>[ SizedBox(width: 100.0, height: 100.0), ], clipBehavior: clip, ), ); expect(renderObject.clipBehavior, clip); } }); }