sliver_app_bar.dart 11.6 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
// Copyright 2016 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/gestures.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

import 'box.dart';
import 'binding.dart';
import 'object.dart';
import 'sliver.dart';

abstract class RenderSliverAppBar extends RenderSliver with RenderObjectWithChildMixin<RenderBox>, RenderSliverHelpers {
  RenderSliverAppBar({ RenderBox child }) {
    this.child = child;
  }

  double get maxExtent;

  /// The intrinsic size of the child as of the last time the sliver was laid out.
  ///
  /// If the render object is dirty (i.e. if [markNeedsLayout] has been called,
  /// or if the object was newly created), then the returned value will be stale
  /// until [layoutChild] has been called.
  @protected
  double get minExtent => _minExtent;
  double _minExtent;

  @protected
  double get childExtent {
    if (child == null)
      return 0.0;
    assert(child.hasSize);
    assert(constraints.axis != null);
    switch (constraints.axis) {
      case Axis.vertical:
        return child.size.height;
      case Axis.horizontal:
        return child.size.width;
    }
    return null;
  }

  @protected
  double _getChildIntrinsicExtent() {
    if (child == null)
      return 0.0;
    assert(child != null);
    assert(constraints.axis != null);
    switch (constraints.axis) {
      case Axis.vertical:
        return child.getMinIntrinsicHeight(constraints.crossAxisExtent);
      case Axis.horizontal:
        return child.getMinIntrinsicWidth(constraints.crossAxisExtent);
    }
    return null;
  }

  /// The last value that we passed to updateChild().
  double _lastShrinkOffset;

  /// Called during layout if the shrink offset has changed.
  ///
  /// During this callback, the [child] can be set, mutated, or replaced.
  @protected
  void updateChild(double shrinkOffset) { }

  /// Flag the current child as stale and needing updating even if the shrink
  /// offset has not changed.
  ///
  /// Call this whenever [updateChild] would change or mutate the child even if
  /// given the same `shrinkOffset` as the last time it was called.
  ///
  /// This must be implemented by [RenderSliverAppBar] subclasses such that the
  /// next layout after this call will result in [updateChild] being called.
  @protected
  void markNeedsUpdate() {
    markNeedsLayout();
    _lastShrinkOffset = null;
  }

  void layoutChild(double scrollOffset, double maxExtent) {
    assert(maxExtent != null);
    final double shrinkOffset = math.min(scrollOffset, maxExtent);
    if (shrinkOffset != _lastShrinkOffset) {
      invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
        assert(constraints == this.constraints);
        updateChild(shrinkOffset);
        _minExtent = _getChildIntrinsicExtent();
      });
      _lastShrinkOffset = shrinkOffset;
    }
    assert(_minExtent != null);
    assert(() {
      if (_minExtent <= maxExtent)
        return true;
      throw new FlutterError(
        'The maxExtent for this $runtimeType is less than the child\'s intrinsic extent.\n'
        'The specified maxExtent was: ${maxExtent.toStringAsFixed(1)}\n'
        'The child was updated with shrink offset: ${shrinkOffset.toStringAsFixed(1)}\n'
        'The actual measured intrinsic extent of the child was: ${_minExtent.toStringAsFixed(1)}\n'
      );
    });
    child?.layout(
      constraints.asBoxConstraints(maxExtent: math.max(_minExtent, maxExtent - shrinkOffset)),
      parentUsesSize: true,
    );
  }

  /// Returns the distance from the leading _visible_ edge of the sliver to the
  /// side of the child closest to that edge, in the scroll axis direction.
  ///
  /// For example, if the [constraints] describe this sliver as having an axis
  /// direction of [AxisDirection.down], then this is the distance from the top
  /// of the visible portion of the sliver to the top of the child. If the child
  /// is scrolled partially off the top of the viewport, then this will be
  /// negative. On the other hand, if the [constraints] describe this sliver as
  /// having an axis direction of [AxisDirection.up], then this is the distance
  /// from the bottom of the visible portion of the sliver to the bottom of the
  /// child. In both cases, this is the direction of increasing
  /// [SliverConstraints.scrollOffset].
  ///
  /// Calling this when the child is not visible is not valid.
  ///
  /// The argument must be the value of the [child] property.
  ///
  /// This must be implemented by [RenderSliverAppBar] subclasses.
  ///
  /// If there is no child, this should return 0.0.
  @override
135
  double childPosition(@checked RenderObject child) => super.childPosition(child);
Ian Hickson's avatar
Ian Hickson committed
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157

  @override
  bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
    assert(geometry.hitTestExtent > 0.0);
    if (child != null)
      return hitTestBoxChild(result, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition);
    return false;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child != null);
    assert(child == this.child);
    applyPaintTransformForBoxChild(child, transform);
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && geometry.visible) {
      assert(constraints.axisDirection != null);
      switch (applyGrowthDirectionToAxisDirection(constraints.axisDirection, constraints.growthDirection)) {
        case AxisDirection.up:
158
          offset += new Offset(0.0, geometry.paintExtent - childPosition(child) - childExtent);
Ian Hickson's avatar
Ian Hickson committed
159 160
          break;
        case AxisDirection.down:
161
          offset += new Offset(0.0, childPosition(child));
Ian Hickson's avatar
Ian Hickson committed
162 163
          break;
        case AxisDirection.left:
164
          offset += new Offset(geometry.paintExtent - childPosition(child) - childExtent, 0.0);
Ian Hickson's avatar
Ian Hickson committed
165 166
          break;
        case AxisDirection.right:
167
          offset += new Offset(childPosition(child), 0.0);
Ian Hickson's avatar
Ian Hickson committed
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
          break;
      }
      context.paintChild(child, offset);
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    try {
      description.add('maxExtent: ${maxExtent.toStringAsFixed(1)}');
    } catch (e) {
      description.add('maxExtent: EXCEPTION (${e.runtimeType}) WHILE COMPUTING MAX EXTENT');
    }
    try {
183
      description.add('child position: ${childPosition(child).toStringAsFixed(1)}');
Ian Hickson's avatar
Ian Hickson committed
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
    } catch (e) {
      description.add('child position: EXCEPTION (${e.runtimeType}) WHILE COMPUTING CHILD POSITION');
    }
  }
}

/// A sliver with a [RenderBox] child which scrolls normally, except that when
/// it hits the leading edge (typically the top) of the viewport, it shrinks to
/// a minimum size before continuing to scroll.
///
/// This sliver makes no effort to avoid overlapping other content.
abstract class RenderSliverScrollingAppBar extends RenderSliverAppBar {
  RenderSliverScrollingAppBar({
    RenderBox child,
  }) : super(child: child);

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
  double _childPosition;

  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
    layoutChild(constraints.scrollOffset, maxExtent);
    final double paintExtent = maxExtent - constraints.scrollOffset;
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
      maxPaintExtent: maxExtent,
213
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
214 215 216 217 218
    );
    _childPosition = math.min(0.0, paintExtent - childExtent);
  }

  @override
219
  double childPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    assert(child == this.child);
    return _childPosition;
  }
}

/// A sliver with a [RenderBox] child which never scrolls off the viewport in
/// the positive scroll direction, and which first scrolls on at a full size but
/// then shrinks as the viewport continues to scroll.
///
/// This sliver avoids overlapping other earlier slivers where possible.
abstract class RenderSliverPinnedAppBar extends RenderSliverAppBar {
  RenderSliverPinnedAppBar({
    RenderBox child,
  }) : super(child: child);

  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
    layoutChild(constraints.scrollOffset + constraints.overlap, maxExtent);
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: math.min(constraints.overlap + childExtent, constraints.remainingPaintExtent),
      layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
      maxPaintExtent: constraints.overlap + maxExtent,
244
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
245 246 247 248
    );
  }

  @override
249
  double childPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
    assert(child == this.child);
    return constraints?.overlap;
  }
}

abstract class RenderSliverFloatingAppBar extends RenderSliverAppBar {
  RenderSliverFloatingAppBar({
    RenderBox child,
  }) : super(child: child);

  double _lastActualScrollOffset;
  double _effectiveScrollOffset;

  // Distance from our leading edge to the child's leading edge, in the axis
  // direction. Negative if we're scrolled off the top.
  double _childPosition;

  @override
  void performLayout() {
    final double maxExtent = this.maxExtent;
    if (_lastActualScrollOffset != null && // We've laid out at least once to get an initial position, and either
        ((constraints.scrollOffset < _lastActualScrollOffset) || // we are scrolling back, so should reveal, or
         (_effectiveScrollOffset < maxExtent))) { // some part of it is visible, so should shrink or reveal as appropriate.
      double delta = _lastActualScrollOffset - constraints.scrollOffset;
      final bool allowFloatingExpansion = constraints.userScrollDirection == ScrollDirection.forward;
      if (allowFloatingExpansion) {
        if (_effectiveScrollOffset > maxExtent) // We're scrolled off-screen, but should reveal, so
          _effectiveScrollOffset = maxExtent; // pretend we're just at the limit.
      } else {
        if (delta > 0.0) // If we are trying to expand when allowFloatingExpansion is false,
          delta = 0.0; // disallow the expansion. (But allow shrinking, i.e. delta < 0.0 is fine.)
      }
      _effectiveScrollOffset = (_effectiveScrollOffset - delta).clamp(0.0, constraints.scrollOffset);
    } else {
      _effectiveScrollOffset = constraints.scrollOffset;
    }
    layoutChild(_effectiveScrollOffset, maxExtent);
    final double paintExtent = maxExtent - _effectiveScrollOffset;
    final double layoutExtent = (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent);
    geometry = new SliverGeometry(
      scrollExtent: maxExtent,
      paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
      layoutExtent: layoutExtent,
      maxPaintExtent: maxExtent,
294
      hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
Ian Hickson's avatar
Ian Hickson committed
295 296 297 298 299 300
    );
    _childPosition = math.min(0.0, paintExtent - childExtent);
    _lastActualScrollOffset = constraints.scrollOffset;
  }

  @override
301
  double childPosition(RenderBox child) {
Ian Hickson's avatar
Ian Hickson committed
302 303 304 305 306 307 308 309 310 311
    assert(child == this.child);
    return _childPosition;
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('effective scroll offset: ${_effectiveScrollOffset?.toStringAsFixed(1)}');
  }
}