// Copyright 2015 The Chromium 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 'dart:async'; import 'package:flutter/animation.dart'; import 'package:meta/meta.dart'; import 'scroll_position.dart'; class ScrollController { ScrollController({ this.initialScrollOffset: 0.0, }) { assert(initialScrollOffset != null); } /// The initial value to use for [offset]. /// /// New [ScrollPosition] objects that are created and attached to this /// controller will have their offset initialized to this value. final double initialScrollOffset; final List _positions = []; /// Whether any [ScrollPosition] objects have attached themselves to the /// [ScrollController] using the [attach] method. /// /// If this is false, then members that interact with the [ScrollPosition], /// such as [position], [offset], [animateTo], and [jumpTo], must not be /// called. bool get hasClients => _positions.isNotEmpty; ScrollPosition get position { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.'); return _positions.single; } double get offset => position.pixels; /// Animates the position from its current value to the given value. /// /// Any active animation is canceled. If the user is currently scrolling, that /// action is canceled. /// /// The returned [Future] will complete when the animation ends, whether it /// completed successfully or whether it was interrupted prematurely. /// /// An animation will be interrupted whenever the user attempts to scroll /// manually, or whenever another activity is started, or whenever the /// animation reaches the edge of the viewport and attempts to overscroll. (If /// the [ScrollPosition] does not overscroll but instead allows scrolling /// beyond the extents, then going beyond the extents will not interrupt the /// animation.) /// /// The animation is indifferent to changes to the viewport or content /// dimensions. /// /// Once the animation has completed, the scroll position will attempt to /// begin a ballistic activity in case its value is not stable (for example, /// if it is scrolled beyond the extents and in that situation the scroll /// position would normally bounce back). /// /// The duration must not be zero. To jump to a particular value without an /// animation, use [jumpTo]. Future animateTo(double offset, { @required Duration duration, @required Curve curve, }) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); final List> animations = new List>(_positions.length); for (int i = 0; i < _positions.length; i++) animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve); return Future.wait(animations).then((List _) => null); } /// Jumps the scroll position from its current value to the given value, /// without animation, and without checking if the new value is in range. /// /// Any active animation is canceled. If the user is currently scrolling, that /// action is canceled. /// /// If this method changes the scroll position, a sequence of start/update/end /// scroll notifications will be dispatched. No overscroll notifications can /// be generated by this method. /// /// Immediately after the jump, a ballistic activity is started, in case the /// value was out of range. void jumpTo(double value) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); for (ScrollPosition position in new List.from(_positions)) position.jumpTo(value); } /// Register the given position with this controller. /// /// After this function returns, the [animateTo] and [jumpTo] methods on this /// controller will manipulate the given position. void attach(ScrollPosition position) { assert(!_positions.contains(position)); _positions.add(position); } /// Unregister the given position with this controller. /// /// After this function returns, the [animateTo] and [jumpTo] methods on this /// controller will not manipulate the given position. void detach(ScrollPosition position) { assert(_positions.contains(position)); _positions.remove(position); } static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { return new ScrollPosition( physics: physics, state: state, oldPosition: oldPosition, ); } ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { return new ScrollPosition( physics: physics, state: state, initialPixels: initialScrollOffset, oldPosition: oldPosition, ); } @override String toString() { final StringBuffer result = new StringBuffer(); result.write('$runtimeType#$hashCode('); if (initialScrollOffset != 0.0) result.write('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, '); if (_positions.isEmpty) { result.write('no clients'); } else if (_positions.length == 1) { result.write('one client, offset $offset'); } else { result.write('${_positions.length} clients'); } result.write(')'); return result.toString(); } }