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 117
  final Widget child;

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

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

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

  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;
  }
}

177
class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport {
178 179
  _RenderSingleChildViewport({
    AxisDirection axisDirection: AxisDirection.down,
180
    @required ViewportOffset offset,
181
    RenderBox child,
182 183 184
  }) : assert(axisDirection != null),
       assert(offset != null),
       _axisDirection = axisDirection,
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
       _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)
208
      _offset.removeListener(_hasScrolled);
209 210
    _offset = value;
    if (attached)
211
      _offset.addListener(_hasScrolled);
212
    markNeedsLayout();
213 214
  }

215 216 217 218 219
  void _hasScrolled() {
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

220 221 222 223 224 225 226 227 228 229 230
  @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);
231
    _offset.addListener(_hasScrolled);
232 233 234 235
  }

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

  @override
  bool get isRepaintBoundary => true;

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

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

  double get _maxScrollExtent {
    assert(hasSize);
    if (child == null)
      return 0.0;
263 264 265 266 267 268 269
    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;
270 271 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
  }

  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);
    }

324
    offset.applyViewportDimension(_viewportExtent);
325 326 327
    offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
  }

328
  Offset get _paintOffset {
329 330 331
    assert(axisDirection != null);
    switch (axisDirection) {
      case AxisDirection.up:
332
        return new Offset(0.0, _offset.pixels - child.size.height + size.height);
333 334 335
      case AxisDirection.down:
        return new Offset(0.0, -_offset.pixels);
      case AxisDirection.left:
336
        return new Offset(_offset.pixels - child.size.width + size.width, 0.0);
337 338 339 340 341 342 343 344 345 346 347 348 349 350
      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) {
351
      final Offset paintOffset = _paintOffset;
352 353 354 355 356 357

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

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

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

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

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

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

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

397 398 399
    double leadingScrollOffset;
    double targetMainAxisExtent;
    double mainAxisExtent;
400 401 402 403

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

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

  @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();
  }
446
}