tab_controller.dart 12.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Hans Muller's avatar
Hans Muller committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

Hans Muller's avatar
Hans Muller committed
7 8 9 10
import 'package:flutter/widgets.dart';

import 'constants.dart';

11 12 13
// Examples can assume:
// BuildContext context;

Hans Muller's avatar
Hans Muller committed
14 15 16
/// Coordinates tab selection between a [TabBar] and a [TabBarView].
///
/// The [index] property is the index of the selected tab and the [animation]
Aditya Sharma's avatar
Aditya Sharma committed
17
/// represents the current scroll positions of the tab bar and the tab bar view.
Hans Muller's avatar
Hans Muller committed
18 19
/// The selected tab's index can be changed with [animateTo].
///
20
/// A stateful widget that builds a [TabBar] or a [TabBarView] can create
21 22 23
/// a [TabController] and share it directly.
///
/// When the [TabBar] and [TabBarView] don't have a convenient stateful
24 25
/// ancestor, a [TabController] can be shared by providing a
/// [DefaultTabController] inherited widget.
Hans Muller's avatar
Hans Muller committed
26
///
27 28
/// {@animation 700 540 https://flutter.github.io/assets-for-api-docs/assets/material/tabs.mp4}
///
29
/// {@tool sample}
30
///
31 32
/// This widget introduces a [Scaffold] with an [AppBar] and a [TabBar].
///
33
/// ```dart
34 35 36
/// class MyTabbedPage extends StatefulWidget {
///   const MyTabbedPage({ Key key }) : super(key: key);
///   @override
37
///   _MyTabbedPageState createState() => _MyTabbedPageState();
38 39 40
/// }
///
/// class _MyTabbedPageState extends State<MyTabbedPage> with SingleTickerProviderStateMixin {
41
///   final List<Tab> myTabs = <Tab>[
42 43
///     Tab(text: 'LEFT'),
///     Tab(text: 'RIGHT'),
44 45 46 47 48 49 50
///   ];
///
///   TabController _tabController;
///
///   @override
///   void initState() {
///     super.initState();
51
///     _tabController = TabController(vsync: this, length: myTabs.length);
52 53 54 55 56 57 58 59 60 61
///   }
///
///  @override
///  void dispose() {
///    _tabController.dispose();
///    super.dispose();
///  }
///
///   @override
///   Widget build(BuildContext context) {
62 63 64
///     return Scaffold(
///       appBar: AppBar(
///         bottom: TabBar(
65 66 67 68
///           controller: _tabController,
///           tabs: myTabs,
///         ),
///       ),
69
///       body: TabBarView(
70 71
///         controller: _tabController,
///         children: myTabs.map((Tab tab) {
72 73 74 75 76 77 78
///           final String label = tab.text.toLowerCase();
///           return Center(
///             child: Text(
///               'This is the $label tab',
///               style: const TextStyle(fontSize: 36),
///             ),
///           );
79 80 81 82 83 84
///         }).toList(),
///       ),
///     );
///   }
/// }
/// ```
85
/// {@end-tool}
Hans Muller's avatar
Hans Muller committed
86
class TabController extends ChangeNotifier {
87 88
  /// Creates an object that manages the state required by [TabBar] and a
  /// [TabBarView].
89
  ///
90
  /// The [length] must not be null or negative. Typically it's a value greater
91 92
  /// than one, i.e. typically there are two or more tabs. The [length] must
  /// match [TabBar.tabs]'s and [TabBarView.children]'s length.
93
  ///
94 95
  /// The `initialIndex` must be valid given [length] and must not be null. If
  /// [length] is zero, then `initialIndex` must be 0 (the default).
96
  TabController({ int initialIndex = 0, @required this.length, @required TickerProvider vsync })
97 98
    : assert(length != null && length >= 0),
      assert(initialIndex != null && initialIndex >= 0 && (length == 0 || initialIndex < length)),
99
      _index = initialIndex,
Hans Muller's avatar
Hans Muller committed
100
      _previousIndex = initialIndex,
101
      _animationController = AnimationController.unbounded(
Hans Muller's avatar
Hans Muller committed
102
        value: initialIndex.toDouble(),
103
        vsync: vsync,
104
      );
Hans Muller's avatar
Hans Muller committed
105

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  // Private constructor used by `_copyWith`. This allows a new TabController to
  // be created without having to create a new animationController.
  TabController._({
    int index,
    int previousIndex,
    AnimationController animationController,
    @required this.length,
  }) : _index = index,
       _previousIndex = previousIndex,
       _animationController = animationController;


  /// Creates a new [TabController] with `index`, `previousIndex`, and `length`
  /// if they are non-null.
  ///
121
  /// This method is used by [DefaultTabController].
122
  ///
123 124
  /// When [DefaultTabController.length] is updated, this method is called to
  /// create a new [TabController] without creating a new [AnimationController].
125 126 127 128 129 130 131 132 133
  TabController _copyWith({ int index, int length, int previousIndex }) {
    return TabController._(
      index: index ?? _index,
      length: length ?? this.length,
      animationController: _animationController,
      previousIndex: previousIndex ?? _previousIndex,
    );
  }

Hans Muller's avatar
Hans Muller committed
134 135 136 137 138 139 140 141
  /// An animation whose value represents the current position of the [TabBar]'s
  /// selected tab indicator as well as the scrollOffsets of the [TabBar]
  /// and [TabBarView].
  ///
  /// The animation's value ranges from 0.0 to [length] - 1.0. After the
  /// selected tab is changed, the animation's value equals [index]. The
  /// animation's value can be [offset] by +/- 1.0 to reflect [TabBarView]
  /// drag scrolling.
142
  ///
143
  /// If this [TabController] was disposed, then return null.
144 145
  Animation<double> get animation => _animationController?.view;
  AnimationController _animationController;
Hans Muller's avatar
Hans Muller committed
146

147 148 149 150
  /// The total number of tabs.
  ///
  /// Typically greater than one. Must match [TabBar.tabs]'s and
  /// [TabBarView.children]'s length.
Hans Muller's avatar
Hans Muller committed
151 152 153 154
  final int length;

  void _changeIndex(int value, { Duration duration, Curve curve }) {
    assert(value != null);
155
    assert(value >= 0 && (value < length || length == 0));
156
    assert(duration != null || curve == null);
Hans Muller's avatar
Hans Muller committed
157
    assert(_indexIsChangingCount >= 0);
158
    if (value == _index || length < 2)
Hans Muller's avatar
Hans Muller committed
159 160 161 162 163
      return;
    _previousIndex = index;
    _index = value;
    if (duration != null) {
      _indexIsChangingCount += 1;
164
      notifyListeners(); // Because the value of indexIsChanging may have changed.
Hans Muller's avatar
Hans Muller committed
165
      _animationController
166
        .animateTo(_index.toDouble(), duration: duration, curve: curve)
167 168 169 170
        .whenCompleteOrCancel(() {
          _indexIsChangingCount -= 1;
          notifyListeners();
        });
Hans Muller's avatar
Hans Muller committed
171 172 173 174 175 176 177 178
    } else {
      _indexIsChangingCount += 1;
      _animationController.value = _index.toDouble();
      _indexIsChangingCount -= 1;
      notifyListeners();
    }
  }

179 180 181 182
  /// The index of the currently selected tab.
  ///
  /// Changing the index also updates [previousIndex], sets the [animation]'s
  /// value to index, resets [indexIsChanging] to false, and notifies listeners.
Hans Muller's avatar
Hans Muller committed
183 184
  ///
  /// To change the currently selected tab and play the [animation] use [animateTo].
185 186 187
  ///
  /// The value of [index] must be valid given [length]. If [length] is zero,
  /// then [index] will also be zero.
Hans Muller's avatar
Hans Muller committed
188 189 190 191 192 193
  int get index => _index;
  int _index;
  set index(int value) {
    _changeIndex(value);
  }

194 195 196
  /// The index of the previously selected tab.
  ///
  /// Initially the same as [index].
Hans Muller's avatar
Hans Muller committed
197 198 199
  int get previousIndex => _previousIndex;
  int _previousIndex;

200 201 202 203 204 205
  /// True while we're animating from [previousIndex] to [index] as a
  /// consequence of calling [animateTo].
  ///
  /// This value is true during the [animateTo] animation that's triggered when
  /// the user taps a [TabBar] tab. It is false when [offset] is changing as a
  /// consequence of the user dragging (and "flinging") the [TabBarView].
Hans Muller's avatar
Hans Muller committed
206 207 208 209 210 211 212 213
  bool get indexIsChanging => _indexIsChangingCount != 0;
  int _indexIsChangingCount = 0;

  /// Immediately sets [index] and [previousIndex] and then plays the
  /// [animation] from its current value to [index].
  ///
  /// While the animation is running [indexIsChanging] is true. When the
  /// animation completes [offset] will be 0.0.
214
  void animateTo(int value, { Duration duration = kTabScrollDuration, Curve curve = Curves.ease }) {
Hans Muller's avatar
Hans Muller committed
215 216 217
    _changeIndex(value, duration: duration, curve: curve);
  }

218 219 220
  /// The difference between the [animation]'s value and [index].
  ///
  /// The offset value must be between -1.0 and 1.0.
Hans Muller's avatar
Hans Muller committed
221 222 223 224 225
  ///
  /// This property is typically set by the [TabBarView] when the user
  /// drags left or right. A value between -1.0 and 0.0 implies that the
  /// TabBarView has been dragged to the left. Similarly a value between
  /// 0.0 and 1.0 implies that the TabBarView has been dragged to the right.
226
  double get offset => _animationController.value - _index.toDouble();
227 228 229
  set offset(double value) {
    assert(value != null);
    assert(value >= -1.0 && value <= 1.0);
Hans Muller's avatar
Hans Muller committed
230
    assert(!indexIsChanging);
231
    if (value == offset)
Hans Muller's avatar
Hans Muller committed
232
      return;
233
    _animationController.value = value + _index.toDouble();
Hans Muller's avatar
Hans Muller committed
234 235 236 237
  }

  @override
  void dispose() {
238
    _animationController?.dispose();
239
    _animationController = null;
Hans Muller's avatar
Hans Muller committed
240 241 242 243 244
    super.dispose();
  }
}

class _TabControllerScope extends InheritedWidget {
245
  const _TabControllerScope({
Hans Muller's avatar
Hans Muller committed
246 247 248
    Key key,
    this.controller,
    this.enabled,
249
    Widget child,
Hans Muller's avatar
Hans Muller committed
250 251 252 253 254 255 256 257 258 259 260
  }) : super(key: key, child: child);

  final TabController controller;
  final bool enabled;

  @override
  bool updateShouldNotify(_TabControllerScope old) {
    return enabled != old.enabled || controller != old.controller;
  }
}

261 262
/// The [TabController] for descendant widgets that don't specify one
/// explicitly.
263
///
264 265 266 267 268
/// [DefaultTabController] is an inherited widget that is used to share a
/// [TabController] with a [TabBar] or a [TabBarView]. It's used when sharing an
/// explicitly created [TabController] isn't convenient because the tab bar
/// widgets are created by a stateless parent widget or by different parent
/// widgets.
269
///
270 271
/// {@animation 700 540 https://flutter.github.io/assets-for-api-docs/assets/material/tabs.mp4}
///
272 273 274
/// ```dart
/// class MyDemo extends StatelessWidget {
///   final List<Tab> myTabs = <Tab>[
275 276
///     Tab(text: 'LEFT'),
///     Tab(text: 'RIGHT'),
277 278 279 280
///   ];
///
///   @override
///   Widget build(BuildContext context) {
281
///     return DefaultTabController(
282
///       length: myTabs.length,
283 284 285
///       child: Scaffold(
///         appBar: AppBar(
///           bottom: TabBar(
286 287 288
///             tabs: myTabs,
///           ),
///         ),
289
///         body: TabBarView(
290
///           children: myTabs.map((Tab tab) {
291 292 293 294 295 296 297
///             final String label = tab.text.toLowerCase();
///             return Center(
///               child: Text(
///                 'This is the $label tab',
///                 style: const TextStyle(fontSize: 36),
///               ),
///             );
298 299 300 301 302 303 304
///           }).toList(),
///         ),
///       ),
///     );
///   }
/// }
/// ```
Hans Muller's avatar
Hans Muller committed
305
class DefaultTabController extends StatefulWidget {
306 307
  /// Creates a default tab controller for the given [child] widget.
  ///
308 309
  /// The [length] argument is typically greater than one. The [length] must
  /// match [TabBar.tabs]'s and [TabBarView.children]'s length.
310 311
  ///
  /// The [initialIndex] argument must not be null.
312
  const DefaultTabController({
Hans Muller's avatar
Hans Muller committed
313 314
    Key key,
    @required this.length,
315
    this.initialIndex = 0,
316
    @required this.child,
317
  }) : assert(initialIndex != null),
318
       assert(length >= 0),
319
       assert(length == 0 || (initialIndex >= 0 && initialIndex < length)),
320
       super(key: key);
Hans Muller's avatar
Hans Muller committed
321

322 323 324 325
  /// The total number of tabs.
  ///
  /// Typically greater than one. Must match [TabBar.tabs]'s and
  /// [TabBarView.children]'s length.
Hans Muller's avatar
Hans Muller committed
326 327 328
  final int length;

  /// The initial index of the selected tab.
329 330
  ///
  /// Defaults to zero.
Hans Muller's avatar
Hans Muller committed
331 332
  final int initialIndex;

333 334 335 336 337
  /// The widget below this widget in the tree.
  ///
  /// Typically a [Scaffold] whose [AppBar] includes a [TabBar].
  ///
  /// {@macro flutter.widgets.child}
Hans Muller's avatar
Hans Muller committed
338 339 340 341
  final Widget child;

  /// The closest instance of this class that encloses the given context.
  ///
342 343
  /// {@tool sample}
  /// Typical usage is as follows:
Hans Muller's avatar
Hans Muller committed
344 345
  ///
  /// ```dart
346
  /// TabController controller = DefaultTabController.of(context);
Hans Muller's avatar
Hans Muller committed
347
  /// ```
348
  /// {@end-tool}
Hans Muller's avatar
Hans Muller committed
349
  static TabController of(BuildContext context) {
350
    final _TabControllerScope scope = context.dependOnInheritedWidgetOfExactType<_TabControllerScope>();
Hans Muller's avatar
Hans Muller committed
351 352 353 354
    return scope?.controller;
  }

  @override
355
  _DefaultTabControllerState createState() => _DefaultTabControllerState();
Hans Muller's avatar
Hans Muller committed
356 357 358 359 360 361 362 363
}

class _DefaultTabControllerState extends State<DefaultTabController> with SingleTickerProviderStateMixin {
  TabController _controller;

  @override
  void initState() {
    super.initState();
364
    _controller = TabController(
Hans Muller's avatar
Hans Muller committed
365
      vsync: this,
366 367
      length: widget.length,
      initialIndex: widget.initialIndex,
Hans Muller's avatar
Hans Muller committed
368 369 370 371 372 373 374 375 376 377 378
    );
  }

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

  @override
  Widget build(BuildContext context) {
379
    return _TabControllerScope(
Hans Muller's avatar
Hans Muller committed
380 381
      controller: _controller,
      enabled: TickerMode.of(context),
382
      child: widget.child,
Hans Muller's avatar
Hans Muller committed
383 384
    );
  }
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404

  @override
  void didUpdateWidget(DefaultTabController oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.length != widget.length) {
      // If the length is shortened while the last tab is selected, we should
      // automatically update the index of the controller to be the new last tab.
      int newIndex;
      int previousIndex = _controller.previousIndex;
      if (_controller.index >= widget.length) {
        newIndex = math.max(0, widget.length - 1);
        previousIndex = _controller.index;
      }
      _controller = _controller._copyWith(
        length: widget.length,
        index: newIndex,
        previousIndex: previousIndex,
      );
    }
  }
Hans Muller's avatar
Hans Muller committed
405
}