tabs_utils.dart 5.91 KB
// 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/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

// This returns render paragraph of the Tab label text.
RenderParagraph getTabText(WidgetTester tester, String text) {
  return tester.renderObject<RenderParagraph>(find.descendant(
    of: find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_TabStyle'),
    matching: find.text(text),
  ));
}

// This creates and returns a TabController.
TabController createTabController({
  required int length,
  required TickerProvider vsync,
  int initialIndex = 0,
  Duration? animationDuration,
}) {
  final TabController result = TabController(
    length: length,
    vsync: vsync,
    initialIndex: initialIndex,
    animationDuration: animationDuration,
  );
  addTearDown(result.dispose);
  return result;
}

// This widget is used to test widget state in the tabs_test.dart file.
class TabStateMarker extends StatefulWidget {
  const TabStateMarker({ super.key, this.child });

  final Widget? child;

  @override
  TabStateMarkerState createState() => TabStateMarkerState();
}

class TabStateMarkerState extends State<TabStateMarker> {
  String? marker;

  @override
  Widget build(BuildContext context) {
    return widget.child ?? Container();
  }
}

// Tab controller builder for TabControllerFrame widget.
typedef TabControllerFrameBuilder = Widget Function(BuildContext context, TabController controller);

// This widget creates a TabController and passes it to the builder.
class TabControllerFrame extends StatefulWidget {
  const TabControllerFrame({
    super.key,
    required this.length,
    this.initialIndex = 0,
    required this.builder,
  });

  final int length;
  final int initialIndex;
  final TabControllerFrameBuilder builder;

  @override
  TabControllerFrameState createState() => TabControllerFrameState();
}

class TabControllerFrameState extends State<TabControllerFrame> with SingleTickerProviderStateMixin {
  late TabController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TabController(
      vsync: this,
      length: widget.length,
      initialIndex: widget.initialIndex,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _controller);
  }
}

// Test utility class to test tab indicator drawing.
class TabIndicatorRecordingCanvas extends TestRecordingCanvas {
  TabIndicatorRecordingCanvas(this.indicatorColor);

  final Color indicatorColor;
  late Rect indicatorRect;

  @override
  void drawLine(Offset p1, Offset p2, Paint paint) {
    // Assuming that the indicatorWeight is 2.0, the default.
    const double indicatorWeight = 2.0;
    if (paint.color == indicatorColor) {
      indicatorRect = Rect.fromPoints(p1, p2).inflate(indicatorWeight / 2.0);
    }
  }
}

// This creates a Fake implementation of ScrollMetrics.
class TabMockScrollMetrics extends Fake implements ScrollMetrics { }

class TabBarTestScrollPhysics extends ScrollPhysics {
  const TabBarTestScrollPhysics({ super.parent });

  @override
  TabBarTestScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return TabBarTestScrollPhysics(parent: buildParent(ancestor));
  }

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
    return offset == 10 ? 20 : offset;
  }

  static final SpringDescription _kDefaultSpring = SpringDescription.withDampingRatio(
    mass: 0.5,
    stiffness: 500.0,
    ratio: 1.1,
  );

  @override
  SpringDescription get spring => _kDefaultSpring;
}

// This widget is used to log the lifecycle of the TabBarView children.
class TabBody extends StatefulWidget {
  const TabBody({
    super.key,
    required this.index,
    required this.log,
    this.marker = '',
  });

  final int index;
  final List<String> log;
  final String marker;

  @override
  State<TabBody> createState() => TabBodyState();
}

class TabBodyState extends State<TabBody> {
  @override
  void initState() {
    widget.log.add('init: ${widget.index}');
    super.initState();
  }

  @override
  void didUpdateWidget(TabBody oldWidget) {
    super.didUpdateWidget(oldWidget);
    // To keep the logging straight, widgets must not change their index.
    assert(oldWidget.index == widget.index);
  }

  @override
  void dispose() {
    widget.log.add('dispose: ${widget.index}');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: widget.marker.isEmpty
        ? Text('${widget.index}')
        : Text('${widget.index}-${widget.marker}'),
    );
  }
}

// This widget is used to test the lifecycle of the TabBarView children with Ink widget.
class TabKeepAliveInk extends StatefulWidget {
  const TabKeepAliveInk({ super.key, required this.title });

  final String title;

  @override
  State<StatefulWidget> createState() => _TabKeepAliveInkState();
}

class _TabKeepAliveInkState extends State<TabKeepAliveInk> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Ink(
      child: Text(widget.title),
    );
  }
}

// This widget is used to test the lifecycle of the TabBarView children.
class TabAlwaysKeepAliveWidget extends StatefulWidget {
  const TabAlwaysKeepAliveWidget({super.key});

  static String text = 'AlwaysKeepAlive';

  @override
  State<TabAlwaysKeepAliveWidget> createState() => _TabAlwaysKeepAliveWidgetState();
}

class _TabAlwaysKeepAliveWidgetState extends State<TabAlwaysKeepAliveWidget> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return Text(TabAlwaysKeepAliveWidget.text);
  }
}