viewport.dart 13.3 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:vector_math/vector_math_64.dart';
8

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

12
/// The end of the viewport from which the paint offset is computed.
13
enum ViewportAnchor {
14 15 16
  /// 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.
17
  start,
18 19 20 21

  /// 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.
22 23 24
  end,
}

25
/// The interior and exterior dimensions of a viewport.
26 27 28 29 30 31
class ViewportDimensions {
  const ViewportDimensions({
    this.contentSize: Size.zero,
    this.containerSize: Size.zero
  });

32
  /// A viewport that has zero size, both inside and outside.
33 34
  static const ViewportDimensions zero = const ViewportDimensions();

35
  /// The size of the content inside the viewport.
36
  final Size contentSize;
37 38

  /// The size of the outside of the viewport.
39 40 41 42 43 44 45
  final Size containerSize;

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

46 47
  /// Returns the offset at which to paint the content, accounting for the given
  /// anchor and the dimensions of the viewport.
48 49 50 51 52 53 54 55 56
  Offset getAbsolutePaintOffset({ Offset paintOffset, ViewportAnchor anchor }) {
    assert(_debugHasAtLeastOneCommonDimension);
    switch (anchor) {
      case ViewportAnchor.start:
        return paintOffset;
      case ViewportAnchor.end:
        return paintOffset + (containerSize - contentSize);
    }
  }
57

58
  @override
59 60 61 62 63 64 65 66 67 68
  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;
  }

69
  @override
70 71
  int get hashCode => hashValues(contentSize, containerSize);

72
  @override
73
  String toString() => 'ViewportDimensions(container: $containerSize, content: $contentSize)';
74 75
}

76
/// A base class for render objects that are bigger on the inside.
77
///
78 79 80
/// 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.
81
class RenderViewportBase extends RenderBox {
82 83
  RenderViewportBase(
    Offset paintOffset,
84
    Axis mainAxis,
85
    ViewportAnchor anchor,
86
    RenderObjectPainter overlayPainter
87
  ) : _paintOffset = paintOffset,
88
      _mainAxis = mainAxis,
89
      _anchor = anchor,
90 91
      _overlayPainter = overlayPainter {
    assert(paintOffset != null);
92 93
    assert(mainAxis != null);
    assert(_offsetIsSane(_paintOffset, mainAxis));
94 95
  }

96
  bool _offsetIsSane(Offset offset, Axis direction) {
97
    switch (direction) {
98
      case Axis.horizontal:
99
        return offset.dy == 0.0;
100
      case Axis.vertical:
101 102 103 104
        return offset.dx == 0.0;
    }
  }

105
  /// The offset at which to paint the child.
106
  ///
107
  /// The offset can be non-zero only in the [mainAxis].
108 109
  Offset get paintOffset => _paintOffset;
  Offset _paintOffset;
110
  set paintOffset(Offset value) {
111 112
    assert(value != null);
    if (value == _paintOffset)
113
      return;
114
    assert(_offsetIsSane(value, mainAxis));
115
    _paintOffset = value;
116
    markNeedsPaint();
Hixie's avatar
Hixie committed
117
    markNeedsSemanticsUpdate();
118 119
  }

120
  /// The direction in which the child is permitted to be larger than the viewport
121
  ///
122 123 124
  /// The child is given layout constraints that are fully unconstrainted along
  /// the main axis (e.g., the child can be as tall as it wants if the main axis
  /// is vertical).
125 126
  Axis get mainAxis => _mainAxis;
  Axis _mainAxis;
127
  set mainAxis(Axis value) {
128
    assert(value != null);
129
    if (value == _mainAxis)
130
      return;
131
    assert(_offsetIsSane(_paintOffset, value));
132
    _mainAxis = value;
133 134 135
    markNeedsLayout();
  }

136 137 138
  /// The end of the viewport from which the paint offset is computed.
  ///
  /// See [ViewportAnchor] for more detail.
139 140
  ViewportAnchor get anchor => _anchor;
  ViewportAnchor _anchor;
141
  set anchor(ViewportAnchor value) {
142
    assert(value != null);
143
    if (value == _anchor)
144
      return;
145
    _anchor = value;
146 147 148 149
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

150 151
  RenderObjectPainter get overlayPainter => _overlayPainter;
  RenderObjectPainter _overlayPainter;
152
  set overlayPainter(RenderObjectPainter value) {
153 154 155 156 157 158 159 160 161 162
    if (_overlayPainter == value)
      return;
    if (attached)
      _overlayPainter?.detach();
    _overlayPainter = value;
    if (attached)
      _overlayPainter?.attach(this);
    markNeedsPaint();
  }

163
  @override
164 165
  void attach(PipelineOwner owner) {
    super.attach(owner);
166 167 168
    _overlayPainter?.attach(this);
  }

169
  @override
170 171 172 173 174
  void detach() {
    super.detach();
    _overlayPainter?.detach();
  }

175 176
  ViewportDimensions get dimensions => _dimensions;
  ViewportDimensions _dimensions = ViewportDimensions.zero;
177
  set dimensions(ViewportDimensions value) {
178 179 180 181 182
    assert(debugDoingThisLayout);
    _dimensions = value;
  }

  Offset get _effectivePaintOffset {
183 184 185
    final double devicePixelRatio = ui.window.devicePixelRatio;
    int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
    int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
186 187
    return _dimensions.getAbsolutePaintOffset(
      paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio),
188
      anchor: _anchor
189
    );
190 191
  }

192
  @override
193
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
194
    final Offset effectivePaintOffset = _effectivePaintOffset;
195 196 197
    super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
  }

198
  @override
Hixie's avatar
Hixie committed
199 200 201
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('paintOffset: $paintOffset');
202
    description.add('mainAxis: $mainAxis');
203
    description.add('anchor: $anchor');
Hixie's avatar
Hixie committed
204 205 206
    if (overlayPainter != null)
      description.add('overlay painter: $overlayPainter');
  }
207 208
}

209 210
typedef Offset ViewportDimensionsChangeCallback(ViewportDimensions dimensions);

211 212 213 214 215 216 217 218 219 220 221
/// A render object that's bigger on the inside.
///
/// The child of a viewport can layout to a larger size 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 is controlled by the
/// paint offset.
class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<RenderBox> {

  RenderViewport({
    RenderBox child,
    Offset paintOffset: Offset.zero,
222
    Axis mainAxis: Axis.vertical,
223
    ViewportAnchor anchor: ViewportAnchor.start,
224
    RenderObjectPainter overlayPainter,
225
    this.onPaintOffsetUpdateNeeded
226
  }) : super(paintOffset, mainAxis, anchor, overlayPainter) {
227 228 229
    this.child = child;
  }

230 231 232 233
  /// Called during [layout] to report the dimensions of the viewport
  /// and its child.
  ViewportDimensionsChangeCallback onPaintOffsetUpdateNeeded;

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

247
  @override
248
  double getMinIntrinsicWidth(BoxConstraints constraints) {
249
    assert(constraints.debugAssertIsValid());
250
    if (child != null)
251
      return constraints.constrainWidth(child.getMinIntrinsicWidth(_getInnerConstraints(constraints)));
252 253 254
    return super.getMinIntrinsicWidth(constraints);
  }

255
  @override
256
  double getMaxIntrinsicWidth(BoxConstraints constraints) {
257
    assert(constraints.debugAssertIsValid());
258
    if (child != null)
259
      return constraints.constrainWidth(child.getMaxIntrinsicWidth(_getInnerConstraints(constraints)));
260 261 262
    return super.getMaxIntrinsicWidth(constraints);
  }

263
  @override
264
  double getMinIntrinsicHeight(BoxConstraints constraints) {
265
    assert(constraints.debugAssertIsValid());
266
    if (child != null)
267
      return constraints.constrainHeight(child.getMinIntrinsicHeight(_getInnerConstraints(constraints)));
268 269 270
    return super.getMinIntrinsicHeight(constraints);
  }

271
  @override
272
  double getMaxIntrinsicHeight(BoxConstraints constraints) {
273
    assert(constraints.debugAssertIsValid());
274
    if (child != null)
275
      return constraints.constrainHeight(child.getMaxIntrinsicHeight(_getInnerConstraints(constraints)));
276 277 278 279
    return super.getMaxIntrinsicHeight(constraints);
  }

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

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

302
  bool _shouldClipAtPaintOffset(Offset paintOffset) {
Hixie's avatar
Hixie committed
303
    assert(child != null);
304
    return paintOffset < Offset.zero || !(Offset.zero & size).contains((paintOffset & child.size).bottomRight);
Hixie's avatar
Hixie committed
305 306
  }

307
  @override
308 309
  void paint(PaintingContext context, Offset offset) {
    if (child != null) {
310
      final Offset effectivePaintOffset = _effectivePaintOffset;
311 312 313 314 315 316 317 318

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

      if (_shouldClipAtPaintOffset(effectivePaintOffset)) {
        context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
Adam Barth's avatar
Adam Barth committed
319
      } else {
320
        paintContents(context, offset);
Adam Barth's avatar
Adam Barth committed
321
      }
322 323 324
    }
  }

325
  @override
Hixie's avatar
Hixie committed
326
  Rect describeApproximatePaintClip(RenderObject child) {
327
    if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset))
Hixie's avatar
Hixie committed
328 329 330 331
      return Point.origin & size;
    return null;
  }

332 333 334 335 336 337
  // Workaround for https://github.com/dart-lang/sdk/issues/25232
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    super.applyPaintTransform(child, transform);
  }

338
  @override
Adam Barth's avatar
Adam Barth committed
339
  bool hitTestChildren(HitTestResult result, { Point position }) {
340 341
    if (child != null) {
      assert(child.parentData is BoxParentData);
342
      Point transformed = position + -_effectivePaintOffset;
Adam Barth's avatar
Adam Barth committed
343
      return child.hitTest(result, position: transformed);
344
    }
Adam Barth's avatar
Adam Barth committed
345
    return false;
346 347
  }
}
Adam Barth's avatar
Adam Barth committed
348 349

abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<RenderBox>>
350 351
    extends RenderViewportBase with ContainerRenderObjectMixin<RenderBox, T>,
                                    RenderBoxContainerDefaultsMixin<RenderBox, T> {
Adam Barth's avatar
Adam Barth committed
352 353
  RenderVirtualViewport({
    int virtualChildCount,
354
    LayoutCallback callback,
355
    Offset paintOffset: Offset.zero,
356
    Axis mainAxis: Axis.vertical,
357
    ViewportAnchor anchor: ViewportAnchor.start,
358
    RenderObjectPainter overlayPainter
Adam Barth's avatar
Adam Barth committed
359
  }) : _virtualChildCount = virtualChildCount,
360
       _callback = callback,
361
       super(paintOffset, mainAxis, anchor, overlayPainter);
Adam Barth's avatar
Adam Barth committed
362

363
  int get virtualChildCount => _virtualChildCount;
Adam Barth's avatar
Adam Barth committed
364
  int _virtualChildCount;
365
  set virtualChildCount(int value) {
Adam Barth's avatar
Adam Barth committed
366 367 368 369 370 371
    if (_virtualChildCount == value)
      return;
    _virtualChildCount = value;
    markNeedsLayout();
  }

372
  /// Called during [layout] to determine the render object's children.
Adam Barth's avatar
Adam Barth committed
373 374 375 376 377
  ///
  /// Typically the callback will mutate the child list appropriately, for
  /// example so the child list contains only visible children.
  LayoutCallback get callback => _callback;
  LayoutCallback _callback;
378
  set callback(LayoutCallback value) {
Adam Barth's avatar
Adam Barth committed
379 380 381 382 383 384
    if (value == _callback)
      return;
    _callback = value;
    markNeedsLayout();
  }

385
  @override
Adam Barth's avatar
Adam Barth committed
386
  bool hitTestChildren(HitTestResult result, { Point position }) {
387
    return defaultHitTestChildren(result, position: position + -_effectivePaintOffset);
Adam Barth's avatar
Adam Barth committed
388 389 390
  }

  void _paintContents(PaintingContext context, Offset offset) {
391
    defaultPaint(context, offset + _effectivePaintOffset);
392
    _overlayPainter?.paint(context, offset);
Adam Barth's avatar
Adam Barth committed
393 394
  }

395
  @override
Adam Barth's avatar
Adam Barth committed
396 397 398
  void paint(PaintingContext context, Offset offset) {
    context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
  }
Hixie's avatar
Hixie committed
399

400
  @override
Hixie's avatar
Hixie committed
401
  Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
Hixie's avatar
Hixie committed
402

403 404 405 406 407 408
  // Workaround for https://github.com/dart-lang/sdk/issues/25232
  @override
  void applyPaintTransform(RenderBox child, Matrix4 transform) {
    super.applyPaintTransform(child, transform);
  }

409
  @override
Hixie's avatar
Hixie committed
410 411 412 413
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('virtual child count: $virtualChildCount');
  }
Adam Barth's avatar
Adam Barth committed
414
}