| // 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; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| import 'primary_scroll_controller.dart'; |
| import 'scroll_controller.dart'; |
| import 'scroll_physics.dart'; |
| import 'scrollable.dart'; |
| |
| /// 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] |
| /// with a [ListBody] child. |
| /// |
| /// 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 |
| /// that a [SingleChildScrollView] containing a [ListBody] or [Column] with |
| /// 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. |
| /// * [Scrollable], which handles arbitrary scrolling effects. |
| class SingleChildScrollView extends StatelessWidget { |
| /// Creates a box in which a single widget can be scrolled. |
| SingleChildScrollView({ |
| Key key, |
| this.scrollDirection: Axis.vertical, |
| this.reverse: false, |
| this.padding, |
| bool primary, |
| this.physics, |
| this.controller, |
| this.child, |
| }) : 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); |
| |
| /// The axis along which the scroll view scrolls. |
| /// |
| /// Defaults to [Axis.vertical]. |
| final Axis scrollDirection; |
| |
| /// 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. |
| /// |
| /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view |
| /// scrolls from top to bottom when [reverse] is false and from bottom to top |
| /// when [reverse] is true. |
| /// |
| /// Defaults to false. |
| final bool reverse; |
| |
| /// The amount of space by which to inset the child. |
| final EdgeInsetsGeometry padding; |
| |
| /// An object that can be used to control the position to which this scroll |
| /// view is scrolled. |
| /// |
| /// Must be null if [primary] is true. |
| /// |
| /// 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]). |
| final ScrollController controller; |
| |
| /// 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. |
| /// |
| /// Defaults to true when [scrollDirection] is vertical and [controller] is |
| /// not specified. |
| final bool primary; |
| |
| /// 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. |
| final ScrollPhysics physics; |
| |
| /// The widget that scrolls. |
| /// |
| /// {@macro flutter.widgets.child} |
| final Widget child; |
| |
| AxisDirection _getDirection(BuildContext context) { |
| return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| final AxisDirection axisDirection = _getDirection(context); |
| Widget contents = child; |
| if (padding != null) |
| contents = new Padding(padding: padding, child: contents); |
| final ScrollController scrollController = primary |
| ? PrimaryScrollController.of(context) |
| : controller; |
| final Scrollable scrollable = new Scrollable( |
| axisDirection: axisDirection, |
| controller: scrollController, |
| physics: physics, |
| viewportBuilder: (BuildContext context, ViewportOffset offset) { |
| return new _SingleChildViewport( |
| axisDirection: axisDirection, |
| offset: offset, |
| child: contents, |
| ); |
| }, |
| ); |
| return primary && scrollController != null |
| ? new PrimaryScrollController.none(child: scrollable) |
| : scrollable; |
| } |
| } |
| |
| class _SingleChildViewport extends SingleChildRenderObjectWidget { |
| const _SingleChildViewport({ |
| Key key, |
| this.axisDirection: AxisDirection.down, |
| this.offset, |
| Widget child, |
| }) : assert(axisDirection != null), |
| super(key: key, child: child); |
| |
| 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; |
| } |
| } |
| |
| class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> implements RenderAbstractViewport { |
| _RenderSingleChildViewport({ |
| AxisDirection axisDirection: AxisDirection.down, |
| @required ViewportOffset offset, |
| RenderBox child, |
| }) : assert(axisDirection != null), |
| assert(offset != null), |
| _axisDirection = axisDirection, |
| _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) |
| _offset.removeListener(_hasScrolled); |
| _offset = value; |
| if (attached) |
| _offset.addListener(_hasScrolled); |
| markNeedsLayout(); |
| } |
| |
| void _hasScrolled() { |
| markNeedsPaint(); |
| markNeedsSemanticsUpdate(); |
| } |
| |
| @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); |
| _offset.addListener(_hasScrolled); |
| } |
| |
| @override |
| void detach() { |
| _offset.removeListener(_hasScrolled); |
| super.detach(); |
| } |
| |
| @override |
| bool get isRepaintBoundary => true; |
| |
| double get _viewportExtent { |
| assert(hasSize); |
| switch (axis) { |
| case Axis.horizontal: |
| return size.width; |
| case Axis.vertical: |
| return size.height; |
| } |
| return null; |
| } |
| |
| double get _minScrollExtent { |
| assert(hasSize); |
| return 0.0; |
| } |
| |
| double get _maxScrollExtent { |
| assert(hasSize); |
| if (child == null) |
| return 0.0; |
| 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; |
| } |
| |
| 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); |
| } |
| |
| offset.applyViewportDimension(_viewportExtent); |
| offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent); |
| } |
| |
| Offset get _paintOffset { |
| assert(axisDirection != null); |
| switch (axisDirection) { |
| case AxisDirection.up: |
| return new Offset(0.0, _offset.pixels - child.size.height + size.height); |
| case AxisDirection.down: |
| return new Offset(0.0, -_offset.pixels); |
| case AxisDirection.left: |
| return new Offset(_offset.pixels - child.size.width + size.width, 0.0); |
| 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) { |
| final Offset paintOffset = _paintOffset; |
| |
| void paintContents(PaintingContext context, Offset offset) { |
| context.paintChild(child, offset + paintOffset); |
| } |
| |
| if (_shouldClipAtPaintOffset(paintOffset)) { |
| context.pushClipRect(needsCompositing, offset, Offset.zero & size, paintContents); |
| } else { |
| paintContents(context, offset); |
| } |
| } |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| final Offset paintOffset = _paintOffset; |
| transform.translate(paintOffset.dx, paintOffset.dy); |
| } |
| |
| @override |
| Rect describeApproximatePaintClip(RenderObject child) { |
| if (child != null && _shouldClipAtPaintOffset(_paintOffset)) |
| return Offset.zero & size; |
| return null; |
| } |
| |
| @override |
| bool hitTestChildren(HitTestResult result, { Offset position }) { |
| if (child != null) { |
| final Offset transformed = position + -_paintOffset; |
| return child.hitTest(result, position: transformed); |
| } |
| return false; |
| } |
| |
| @override |
| double getOffsetToReveal(RenderObject target, double alignment) { |
| if (target is! RenderBox) |
| return offset.pixels; |
| |
| final RenderBox targetBox = target; |
| final Matrix4 transform = targetBox.getTransformTo(this); |
| final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds); |
| final Size contentSize = child.size; |
| |
| double leadingScrollOffset; |
| double targetMainAxisExtent; |
| double mainAxisExtent; |
| |
| assert(axisDirection != null); |
| switch (axisDirection) { |
| case AxisDirection.up: |
| mainAxisExtent = size.height; |
| leadingScrollOffset = contentSize.height - bounds.bottom; |
| targetMainAxisExtent = bounds.height; |
| break; |
| case AxisDirection.right: |
| mainAxisExtent = size.width; |
| leadingScrollOffset = bounds.left; |
| targetMainAxisExtent = bounds.width; |
| break; |
| case AxisDirection.down: |
| mainAxisExtent = size.height; |
| leadingScrollOffset = bounds.top; |
| targetMainAxisExtent = bounds.height; |
| break; |
| case AxisDirection.left: |
| mainAxisExtent = size.width; |
| leadingScrollOffset = contentSize.width - bounds.right; |
| targetMainAxisExtent = bounds.width; |
| break; |
| } |
| |
| return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment; |
| } |
| |
| @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(); |
| } |
| } |