single_child_scroll_view.dart 13.7 KB
Newer Older
1 2 3 4 5 6
// Copyright 2017 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:math' as math;

7
import 'package:flutter/foundation.dart';
8 9 10 11
import 'package:flutter/rendering.dart';

import 'basic.dart';
import 'framework.dart';
12
import 'primary_scroll_controller.dart';
13 14
import 'scroll_controller.dart';
import 'scroll_physics.dart';
15 16
import 'scrollable.dart';

17 18 19 20 21 22 23 24 25 26
/// A box in which a single widget can be scrolled.
///
/// This widget is useful when you have a single box that will normally be
/// entirely visible, for example a clock face in a time picker, but you need to
/// make sure it can be scrolled if the container gets too small in one axis
/// (the scroll direction).
///
/// It is also useful if you need to shrink-wrap in both axes (the main
/// scrolling direction as well as the cross axis), as one might see in a dialog
/// or pop-up menu. In that case, you might pair the [SingleChildScrollView]
27
/// with a [ListBody] child.
28 29 30 31
///
/// When you have a list of children and do not require cross-axis
/// shrink-wrapping behavior, for example a scrolling list that is always the
/// width of the screen, consider [ListView], which is vastly more efficient
32
/// that a [SingleChildScrollView] containing a [ListBody] or [Column] with
33 34 35 36 37 38 39
/// many children.
///
/// See also:
///
/// * [ListView], which handles multiple children in a scrolling list.
/// * [GridView], which handles multiple children in a scrolling grid.
/// * [PageView], for a scrollable that works page by page.
Adam Barth's avatar
Adam Barth committed
40
/// * [Scrollable], which handles arbitrary scrolling effects.
41
class SingleChildScrollView extends StatelessWidget {
42
  /// Creates a box in which a single widget can be scrolled.
43 44 45
  SingleChildScrollView({
    Key key,
    this.scrollDirection: Axis.vertical,
46
    this.reverse: false,
47
    this.padding,
48
    bool primary,
49 50
    this.physics,
    this.controller,
51
    this.child,
52 53 54 55 56 57 58
  }) : assert(scrollDirection != null),
       assert(!(controller != null && primary == true),
          'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
          'You cannot both set primary to true and pass an explicit controller.'
       ),
       primary = primary ?? controller == null && scrollDirection == Axis.vertical,
       super(key: key);
59

60 61 62
  /// The axis along which the scroll view scrolls.
  ///
  /// Defaults to [Axis.vertical].
63 64
  final Axis scrollDirection;

65 66 67 68 69 70 71
  /// Whether the scroll view scrolls in the reading direction.
  ///
  /// For example, if the reading direction is left-to-right and
  /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
  /// left to right when [reverse] is false and from right to left when
  /// [reverse] is true.
  ///
72
  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
73 74 75 76
  /// scrolls from top to bottom when [reverse] is false and from bottom to top
  /// when [reverse] is true.
  ///
  /// Defaults to false.
77 78
  final bool reverse;

79
  /// The amount of space by which to inset the child.
80
  final EdgeInsetsGeometry padding;
81

82 83 84 85
  /// An object that can be used to control the position to which this scroll
  /// view is scrolled.
  ///
  /// Must be null if [primary] is true.
86 87 88 89 90 91 92 93
  ///
  /// A [ScrollController] serves several purposes. It can be used to control
  /// the initial scroll position (see [ScrollController.initialScrollOffset]).
  /// It can be used to control whether the scroll view should automatically
  /// save and restore its scroll position in the [PageStorage] (see
  /// [ScrollController.keepScrollOffset]). It can be used to read the current
  /// scroll position (see [ScrollController.offset]), or change it (see
  /// [ScrollController.animateTo]).
94 95
  final ScrollController controller;

96 97 98 99 100 101
  /// Whether this is the primary scroll view associated with the parent
  /// [PrimaryScrollController].
  ///
  /// On iOS, this identifies the scroll view that will scroll to top in
  /// response to a tap in the status bar.
  ///
102
  /// Defaults to true when [scrollDirection] is vertical and [controller] is
103
  /// not specified.
104 105
  final bool primary;

106 107 108 109 110 111
  /// How the scroll view should respond to user input.
  ///
  /// For example, determines how the scroll view continues to animate after the
  /// user stops dragging the scroll view.
  ///
  /// Defaults to matching platform conventions.
112 113
  final ScrollPhysics physics;

114
  /// The widget that scrolls.
115 116
  ///
  /// {@macro flutter.widgets.child}
117 118 119
  final Widget child;

  AxisDirection _getDirection(BuildContext context) {
120
    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
121 122 123 124
  }

  @override
  Widget build(BuildContext context) {
125
    final AxisDirection axisDirection = _getDirection(context);
126 127 128
    Widget contents = child;
    if (padding != null)
      contents = new Padding(padding: padding, child: contents);
129
    final ScrollController scrollController = primary
130 131
        ? PrimaryScrollController.of(context)
        : controller;
132
    final Scrollable scrollable = new Scrollable(
133
      axisDirection: axisDirection,
134
      controller: scrollController,
135
      physics: physics,
136 137 138 139
      viewportBuilder: (BuildContext context, ViewportOffset offset) {
        return new _SingleChildViewport(
          axisDirection: axisDirection,
          offset: offset,
140
          child: contents,
141 142
        );
      },
143
    );
144 145 146
    return primary && scrollController != null
      ? new PrimaryScrollController.none(child: scrollable)
      : scrollable;
147 148 149 150
  }
}

class _SingleChildViewport extends SingleChildRenderObjectWidget {
151
  const _SingleChildViewport({
152 153 154 155
    Key key,
    this.axisDirection: AxisDirection.down,
    this.offset,
    Widget child,
156 157
  }) : assert(axisDirection != null),
       super(key: key, child: child);
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178

  final AxisDirection axisDirection;
  final ViewportOffset offset;

  @override
  _RenderSingleChildViewport createRenderObject(BuildContext context) {
    return new _RenderSingleChildViewport(
      axisDirection: axisDirection,
      offset: offset,
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSingleChildViewport renderObject) {
    // Order dependency: The offset setter reads the axis direction.
    renderObject
      ..axisDirection = axisDirection
      ..offset = offset;
  }
}

179
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
180 181
  _RenderSingleChildViewport({
    AxisDirection axisDirection: AxisDirection.down,
182
    @required ViewportOffset offset,
183
    RenderBox child,
184 185 186
  }) : assert(axisDirection != null),
       assert(offset != null),
       _axisDirection = axisDirection,
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
       _offset = offset {
    this.child = child;
  }

  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
    assert(value != null);
    if (value == _axisDirection)
      return;
    _axisDirection = value;
    markNeedsLayout();
  }

  Axis get axis => axisDirectionToAxis(axisDirection);

  ViewportOffset get offset => _offset;
  ViewportOffset _offset;
  set offset(ViewportOffset value) {
    assert(value != null);
    if (value == _offset)
      return;
    if (attached)
210
      _offset.removeListener(_hasScrolled);
211 212
    _offset = value;
    if (attached)
213
      _offset.addListener(_hasScrolled);
214
    markNeedsLayout();
215 216
  }

217 218 219 220 221
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

222 223 224 225 226 227 228 229 230 231 232
  @override
  void setupParentData(RenderObject child) {
    // We don't actually use the offset argument in BoxParentData, so let's
    // avoid allocating it at all.
    if (child.parentData is! ParentData)
      child.parentData = new ParentData();
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
233
    _offset.addListener(_hasScrolled);
234 235 236 237
  }

  @override
  void detach() {
238
    _offset.removeListener(_hasScrolled);
239 240 241 242 243 244
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

245
  double get _viewportExtent {
246 247 248 249
    assert(hasSize);
    switch (axis) {
      case Axis.horizontal:
        return size.width;
250 251
      case Axis.vertical:
        return size.height;
252 253 254 255 256 257 258 259 260 261 262 263 264
    }
    return null;
  }

  double get _minScrollExtent {
    assert(hasSize);
    return 0.0;
  }

  double get _maxScrollExtent {
    assert(hasSize);
    if (child == null)
      return 0.0;
265 266 267 268 269 270 271
    switch (axis) {
      case Axis.horizontal:
        return math.max(0.0, child.size.width - size.width);
      case Axis.vertical:
        return math.max(0.0, child.size.height - size.height);
    }
    return null;
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325
  }

  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    switch (axis) {
      case Axis.horizontal:
        return constraints.heightConstraints();
      case Axis.vertical:
        return constraints.widthConstraints();
    }
    return null;
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    if (child != null)
      return child.getMinIntrinsicWidth(height);
    return 0.0;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    if (child != null)
      return child.getMaxIntrinsicWidth(height);
    return 0.0;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (child != null)
      return child.getMinIntrinsicHeight(width);
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (child != null)
      return child.getMaxIntrinsicHeight(width);
    return 0.0;
  }

  // We don't override computeDistanceToActualBaseline(), because we
  // want the default behavior (returning null). Otherwise, as you
  // scroll, it would shift in its parent if the parent was baseline-aligned,
  // which makes no sense.

  @override
  void performLayout() {
    if (child == null) {
      size = constraints.smallest;
    } else {
      child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child.size);
    }

326
    offset.applyViewportDimension(_viewportExtent);
327 328 329
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

330
  Offset get _paintOffset {
331 332 333
    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
334
        return new Offset(0.0, _offset.pixels - child.size.height + size.height);
335 336 337
      case AxisDirection.down:
        return new Offset(0.0, -_offset.pixels);
      case AxisDirection.left:
338
        return new Offset(_offset.pixels - child.size.width + size.width, 0.0);
339 340 341 342 343 344 345 346 347 348 349 350 351 352
      case AxisDirection.right:
        return new Offset(-_offset.pixels, 0.0);
    }
    return null;
  }

  bool _shouldClipAtPaintOffset(Offset paintOffset) {
    assert(child != null);
    return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
353
      final Offset paintOffset = _paintOffset;
354 355 356 357 358 359

      void paintContents(PaintingContext context, Offset offset) {
        context.paintChild(child, offset + paintOffset);
      }

      if (_shouldClipAtPaintOffset(paintOffset)) {
360
        context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintContents);
361 362 363 364 365 366 367 368
      } else {
        paintContents(context, offset);
      }
    }
  }

  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
369
    final Offset paintOffset = _paintOffset;
370 371 372 373 374
    transform.translate(paintOffset.dx, paintOffset.dy);
  }

  @override
  Rect describeApproximatePaintClip(RenderObject child) {
375
    if (child != null && _shouldClipAtPaintOffset(_paintOffset))
376
      return Offset.zero & size;
377 378 379 380
    return null;
  }

  @override
381
  bool hitTestChildren(HitTestResult result, { Offset position }) {
382
    if (child != null) {
383
      final Offset transformed = position + -_paintOffset;
384 385 386 387
      return child.hitTest(result, position: transformed);
    }
    return false;
  }
388 389

  @override
390 391
  double getOffsetToReveal(RenderObject target, double alignment) {
    if (target is! RenderBox)
392 393
      return offset.pixels;

394 395 396
    final RenderBox targetBox = target;
    final Matrix4 transform = targetBox.getTransformTo(this);
    final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);
397 398
    final Size contentSize = child.size;

399 400 401
    double leadingScrollOffset;
    double targetMainAxisExtent;
    double mainAxisExtent;
402 403 404 405

    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
406 407 408
        mainAxisExtent = size.height;
        leadingScrollOffset = contentSize.height - bounds.bottom;
        targetMainAxisExtent = bounds.height;
409 410
        break;
      case AxisDirection.right:
411 412 413
        mainAxisExtent = size.width;
        leadingScrollOffset = bounds.left;
        targetMainAxisExtent = bounds.width;
414 415
        break;
      case AxisDirection.down:
416 417 418
        mainAxisExtent = size.height;
        leadingScrollOffset = bounds.top;
        targetMainAxisExtent = bounds.height;
419 420
        break;
      case AxisDirection.left:
421 422 423
        mainAxisExtent = size.width;
        leadingScrollOffset = contentSize.width - bounds.right;
        targetMainAxisExtent = bounds.width;
424 425 426
        break;
    }

427
    return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
428
  }
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447

  @override
  void showOnScreen([RenderObject child]) {
    // Logic duplicated in [RenderViewportBase.showOnScreen].
    if (child != null) {
      // Move viewport the smallest distance to bring [child] on screen.
      final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
      final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
      final double currentOffset = offset.pixels;
      if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
        offset.jumpTo(leadingEdgeOffset);
      } else {
        offset.jumpTo(trailingEdgeOffset);
      }
    }

    // Make sure the viewport itself is on screen.
    super.showOnScreen();
  }
448
}