viewport.dart 15.1 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:ui' as ui show window;
6

7
import 'package:flutter/foundation.dart';
8
import 'package:vector_math/vector_math_64.dart';
9

10 11 12
import 'box.dart';
import 'object.dart';

13
/// The end of the viewport from which the paint offset is computed.
14
enum ViewportAnchor {
15 16 17
  /// The start (e.g., top or left, depending on the axis) of the first item
  /// should be aligned with the start (e.g., top or left, depending on the
  /// axis) of the viewport.
18
  start,
19 20 21 22

  /// The end (e.g., bottom or right, depending on the axis) of the last item
  /// should be aligned with the end (e.g., bottom or right, depending on the
  /// axis) of the viewport.
23 24 25
  end,
}

26
/// The interior and exterior dimensions of a viewport.
27
class ViewportDimensions {
28 29 30
  /// Creates dimensions for a viewport.
  ///
  /// By default, the content and container sizes are zero.
31 32 33 34 35
  const ViewportDimensions({
    this.contentSize: Size.zero,
    this.containerSize: Size.zero
  });

36
  /// A viewport that has zero size, both inside and outside.
37 38
  static const ViewportDimensions zero = const ViewportDimensions();

39
  /// The size of the content inside the viewport.
40
  final Size contentSize;
41 42

  /// The size of the outside of the viewport.
43 44 45 46 47 48 49
  final Size containerSize;

  bool get _debugHasAtLeastOneCommonDimension {
    return contentSize.width == containerSize.width
        || contentSize.height == containerSize.height;
  }

50 51
  /// Returns the offset at which to paint the content, accounting for the given
  /// anchor and the dimensions of the viewport.
52 53 54 55 56 57 58 59
  Offset getAbsolutePaintOffset({ Offset paintOffset, ViewportAnchor anchor }) {
    assert(_debugHasAtLeastOneCommonDimension);
    switch (anchor) {
      case ViewportAnchor.start:
        return paintOffset;
      case ViewportAnchor.end:
        return paintOffset + (containerSize - contentSize);
    }
pq's avatar
pq committed
60
    assert(anchor != null);
pq's avatar
pq committed
61
    return null;
62
  }
63

64
  @override
65 66 67 68 69 70 71 72 73 74
  bool operator ==(dynamic other) {
    if (identical(this, other))
      return true;
    if (other is! ViewportDimensions)
      return false;
    final ViewportDimensions typedOther = other;
    return contentSize == typedOther.contentSize &&
           containerSize == typedOther.containerSize;
  }

75
  @override
76 77
  int get hashCode => hashValues(contentSize, containerSize);

78
  @override
79
  String toString() => 'ViewportDimensions(container: $containerSize, content: $contentSize)';
80 81
}

82
/// A base class for render objects that are bigger on the inside.
83
///
84 85 86
/// This class holds the common fields for viewport render objects but does not
/// have a child model. See [RenderViewport] for a viewport with a single child
/// and [RenderVirtualViewport] for a viewport with multiple children.
87
class RenderViewportBase extends RenderBox {
88 89 90 91 92 93
  /// Initializes fields for subclasses.
  ///
  /// The [paintOffset] and [mainAxis] arguments must not be null.
  ///
  /// This constructor uses positional arguments rather than named arguments to
  /// work around limitations of mixins.
94 95
  RenderViewportBase(
    Offset paintOffset,
96
    Axis mainAxis,
97
    ViewportAnchor anchor
98
  ) : _paintOffset = paintOffset,
99
      _mainAxis = mainAxis,
100
      _anchor = anchor {
101
    assert(paintOffset != null);
102 103
    assert(mainAxis != null);
    assert(_offsetIsSane(_paintOffset, mainAxis));
104 105
  }

106
  bool _offsetIsSane(Offset offset, Axis direction) {
107
    switch (direction) {
108
      case Axis.horizontal:
109
        return offset.dy == 0.0;
110
      case Axis.vertical:
111 112
        return offset.dx == 0.0;
    }
pq's avatar
pq committed
113 114
    assert(direction != null);
    return null;
115 116
  }

117
  /// The offset at which to paint the child.
118
  ///
119
  /// The offset can be non-zero only in the [mainAxis].
120 121
  Offset get paintOffset => _paintOffset;
  Offset _paintOffset;
122
  set paintOffset(Offset value) {
123 124
    assert(value != null);
    if (value == _paintOffset)
125
      return;
126
    assert(_offsetIsSane(value, mainAxis));
127
    _paintOffset = value;
128
    markNeedsPaint();
Hixie's avatar
Hixie committed
129
    markNeedsSemanticsUpdate();
130 131
  }

132
  /// The direction in which the child is permitted to be larger than the viewport.
133
  ///
134
  /// The child is given layout constraints that are fully unconstrained along
135 136
  /// the main axis (e.g., the child can be as tall as it wants if the main axis
  /// is vertical).
137 138
  Axis get mainAxis => _mainAxis;
  Axis _mainAxis;
139
  set mainAxis(Axis value) {
140
    assert(value != null);
141
    if (value == _mainAxis)
142
      return;
143
    assert(_offsetIsSane(_paintOffset, value));
144
    _mainAxis = value;
145 146 147
    markNeedsLayout();
  }

148 149 150
  /// The end of the viewport from which the paint offset is computed.
  ///
  /// See [ViewportAnchor] for more detail.
151 152
  ViewportAnchor get anchor => _anchor;
  ViewportAnchor _anchor;
153
  set anchor(ViewportAnchor value) {
154
    assert(value != null);
155
    if (value == _anchor)
156
      return;
157
    _anchor = value;
158 159 160 161
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

162
  /// The interior and exterior extent of the viewport.
163 164
  ViewportDimensions get dimensions => _dimensions;
  ViewportDimensions _dimensions = ViewportDimensions.zero;
165
  set dimensions(ViewportDimensions value) {
166 167 168 169 170
    assert(debugDoingThisLayout);
    _dimensions = value;
  }

  Offset get _effectivePaintOffset {
171 172 173
    final double devicePixelRatio = ui.window.devicePixelRatio;
    int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
    int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
174 175
    return _dimensions.getAbsolutePaintOffset(
      paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio),
176
      anchor: _anchor
177
    );
178 179
  }

180
  @override
181
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
182
    final Offset effectivePaintOffset = _effectivePaintOffset;
183
    super.applyPaintTransform(child, transform..translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
184 185
  }

186
  @override
Hixie's avatar
Hixie committed
187 188 189
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('paintOffset: $paintOffset');
190
    description.add('mainAxis: $mainAxis');
191
    description.add('anchor: $anchor');
Hixie's avatar
Hixie committed
192
  }
193 194
}

195
/// Signature for notifications about [RenderViewport] dimensions changing.
196 197
///
/// Used by [RenderViewport.onPaintOffsetUpdateNeeded].
198 199
typedef Offset ViewportDimensionsChangeCallback(ViewportDimensions dimensions);

200 201
/// A render object that's bigger on the inside.
///
202 203 204 205
/// The child of a viewport can layout to a larger size along the viewport's
/// [mainAxis] than the viewport itself. If that happens, only a portion of the
/// child will be visible through the viewport. The portion of the child that is
/// visible can be controlled with the [paintOffset].
206 207 208 209
///
/// See also:
///
///  * [RenderVirtualViewport] (which works with more than one child)
210
class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<RenderBox> {
211 212 213
  /// Creates a render object that's bigger on the inside.
  ///
  /// The [paintOffset] and [mainAxis] arguments must not be null.
214 215 216
  RenderViewport({
    RenderBox child,
    Offset paintOffset: Offset.zero,
217
    Axis mainAxis: Axis.vertical,
218
    ViewportAnchor anchor: ViewportAnchor.start,
219
    this.onPaintOffsetUpdateNeeded
220
  }) : super(paintOffset, mainAxis, anchor) {
221 222 223
    this.child = child;
  }

224 225
  /// Called during [layout] to report the dimensions of the viewport
  /// and its child.
226 227 228
  ///
  /// The return value of this function is used as the new [paintOffset] and
  /// must not be null.
229 230
  ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;

231 232
  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
    BoxConstraints innerConstraints;
233
    switch (mainAxis) {
234
      case Axis.horizontal:
235 236
        innerConstraints = constraints.heightConstraints();
        break;
237
      case Axis.vertical:
238 239 240 241 242 243
        innerConstraints = constraints.widthConstraints();
        break;
    }
    return innerConstraints;
  }

244
  @override
245
  double computeMinIntrinsicWidth(double height) {
246
    if (child != null)
247 248
      return child.getMinIntrinsicWidth(height);
    return 0.0;
249 250
  }

251
  @override
252
  double computeMaxIntrinsicWidth(double height) {
253
    if (child != null)
254 255
      return child.getMaxIntrinsicWidth(height);
    return 0.0;
256 257
  }

258
  @override
259
  double computeMinIntrinsicHeight(double width) {
260
    if (child != null)
261 262
      return child.getMinIntrinsicHeight(width);
    return 0.0;
263 264
  }

265
  @override
266
  double computeMaxIntrinsicHeight(double width) {
267
    if (child != null)
268 269
      return child.getMaxIntrinsicHeight(width);
    return 0.0;
270 271 272
  }

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

277
  @override
278
  void performLayout() {
279
    final ViewportDimensions oldDimensions = dimensions;
280 281 282
    if (child != null) {
      child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
      size = constraints.constrain(child.size);
Hixie's avatar
Hixie committed
283
      final BoxParentData childParentData = child.parentData;
284
      childParentData.offset = Offset.zero;
285
      dimensions = new ViewportDimensions(containerSize: size, contentSize: child.size);
286 287
    } else {
      performResize();
288
      dimensions = new ViewportDimensions(containerSize: size);
289
    }
290 291 292
    if (onPaintOffsetUpdateNeeded != null && dimensions != oldDimensions)
      paintOffset = onPaintOffsetUpdateNeeded(dimensions);
    assert(paintOffset != null);
293 294
  }

295
  bool _shouldClipAtPaintOffset(Offset paintOffset) {
Hixie's avatar
Hixie committed
296
    assert(child != null);
297
    return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
Hixie's avatar
Hixie committed
298 299
  }

300
  @override
301 302
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
303
      final Offset effectivePaintOffset = _effectivePaintOffset;
304 305 306 307 308 309 310

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

      if (_shouldClipAtPaintOffset(effectivePaintOffset)) {
        context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
Adam Barth's avatar
Adam Barth committed
311
      } else {
312
        paintContents(context, offset);
Adam Barth's avatar
Adam Barth committed
313
      }
314 315 316
    }
  }

317
  @override
Hixie's avatar
Hixie committed
318
  Rect describeApproximatePaintClip(RenderObject child) {
319
    if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset))
Hixie's avatar
Hixie committed
320 321 322 323
      return Point.origin & size;
    return null;
  }

324 325 326 327 328 329
  // Workaround for https://github.com/dart-lang/sdk/issues/25232
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    super.applyPaintTransform(child, transform);
  }

330
  @override
Adam Barth's avatar
Adam Barth committed
331
  bool hitTestChildren(HitTestResult result, { Point position }) {
332 333
    if (child != null) {
      assert(child.parentData is BoxParentData);
334
      Point transformed = position + -_effectivePaintOffset;
Adam Barth's avatar
Adam Barth committed
335
      return child.hitTest(result, position: transformed);
336
    }
Adam Barth's avatar
Adam Barth committed
337
    return false;
338 339
  }
}
Adam Barth's avatar
Adam Barth committed
340

341 342
/// A render object that shows a subset of its children.
///
343 344 345 346
/// The children of a viewport can layout to a larger size along the viewport's
/// [mainAxis] than the viewport itself. If that happens, only a subset of the
/// children will be visible through the viewport. The subset of children that
/// are visible can be controlled with the [paintOffset].
347 348 349 350 351 352
///
/// See also:
///
///  * [RenderList] (which arranges its children linearly)
///  * [RenderGrid] (which arranges its children into tiles)
///  * [RenderViewport] (which is easier to use with a single child)
Adam Barth's avatar
Adam Barth committed
353
abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>>
354 355
    extends RenderViewportBase with ContainerRenderObjectMixin<RenderBox, T>,
                                    RenderBoxContainerDefaultsMixin<RenderBox, T> {
356 357 358
  /// Initializes fields for subclasses.
  ///
  /// The [paintOffset] and [mainAxis] arguments must not be null.
Adam Barth's avatar
Adam Barth committed
359 360
  RenderVirtualViewport({
    int virtualChildCount,
361
    LayoutCallback<BoxConstraints> callback,
362
    Offset paintOffset: Offset.zero,
363
    Axis mainAxis: Axis.vertical,
364
    ViewportAnchor anchor: ViewportAnchor.start
Adam Barth's avatar
Adam Barth committed
365
  }) : _virtualChildCount = virtualChildCount,
366
       _callback = callback,
367
       super(paintOffset, mainAxis, anchor);
Adam Barth's avatar
Adam Barth committed
368

369 370 371
  /// The overall number of children this viewport could potentially display.
  ///
  /// If null, the viewport might display an unbounded number of children.
372
  int get virtualChildCount => _virtualChildCount;
Adam Barth's avatar
Adam Barth committed
373
  int _virtualChildCount;
374
  set virtualChildCount(int value) {
Adam Barth's avatar
Adam Barth committed
375 376 377 378 379 380
    if (_virtualChildCount == value)
      return;
    _virtualChildCount = value;
    markNeedsLayout();
  }

381
  /// Called during [layout] to determine the render object's children.
Adam Barth's avatar
Adam Barth committed
382 383 384
  ///
  /// Typically the callback will mutate the child list appropriately, for
  /// example so the child list contains only visible children.
385 386 387
  LayoutCallback<BoxConstraints> get callback => _callback;
  LayoutCallback<BoxConstraints> _callback;
  set callback(LayoutCallback<BoxConstraints> value) {
Adam Barth's avatar
Adam Barth committed
388 389 390 391 392 393
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

394 395 396 397 398 399
  /// Throws an exception if asserts are enabled, unless the
  /// [RenderObject.debugCheckingIntrinsics] flag is set.
  ///
  /// This is a convenience function for subclasses to call from their
  /// intrinsic-sizing functions if they don't have a good way to generate the
  /// numbers.
400
  @protected
401 402 403 404
  bool debugThrowIfNotCheckingIntrinsics() {
    assert(() {
      if (!RenderObject.debugCheckingIntrinsics) {
        throw new FlutterError(
Ian Hickson's avatar
Ian Hickson committed
405
          '$runtimeType does not support returning intrinsic dimensions.\n'
406 407 408 409 410 411 412 413 414 415 416
          'Calculating the intrinsic dimensions would require walking the entire '
          'child list, which cannot reliably and efficiently be done for render '
          'objects that potentially generate their child list during layout.'
        );
      }
      return true;
    });
    return true;
  }

  @override
417
  double computeMinIntrinsicWidth(double height) {
418
    assert(debugThrowIfNotCheckingIntrinsics());
419 420 421 422
    return 0.0;
  }

  @override
423
  double computeMaxIntrinsicWidth(double height) {
424
    assert(debugThrowIfNotCheckingIntrinsics());
425 426 427 428
    return 0.0;
  }

  @override
429
  double computeMinIntrinsicHeight(double width) {
430
    assert(debugThrowIfNotCheckingIntrinsics());
431 432 433 434
    return 0.0;
  }

  @override
435
  double computeMaxIntrinsicHeight(double width) {
436
    assert(debugThrowIfNotCheckingIntrinsics());
437 438 439
    return 0.0;
  }

440
  @override
Adam Barth's avatar
Adam Barth committed
441
  bool hitTestChildren(HitTestResult result, { Point position }) {
442
    return defaultHitTestChildren(result, position: position + -_effectivePaintOffset);
Adam Barth's avatar
Adam Barth committed
443 444 445
  }

  void _paintContents(PaintingContext context, Offset offset) {
446
    defaultPaint(context, offset + _effectivePaintOffset);
Adam Barth's avatar
Adam Barth committed
447 448
  }

449
  @override
Adam Barth's avatar
Adam Barth committed
450 451 452
  void paint(PaintingContext context, Offset offset) {
    context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
  }
Hixie's avatar
Hixie committed
453

454
  @override
Hixie's avatar
Hixie committed
455
  Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
Hixie's avatar
Hixie committed
456

457 458 459 460 461 462
  // Workaround for https://github.com/dart-lang/sdk/issues/25232
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    super.applyPaintTransform(child, transform);
  }

463
  @override
Hixie's avatar
Hixie committed
464 465 466 467
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('virtual child count: $virtualChildCount');
  }
Adam Barth's avatar
Adam Barth committed
468
}