提交 442c55b4 编写于 作者: H Hixie

Make the drawer, popup menus, dialogs, and settings page scrollable.

Also, fixes the stocks list to work properly including hit testing at
the bottom of the screen.

New classes:

RenderViewport: a class that supports positioning a child inside
itself and offsetting it.

Viewport: a RenderObjectWrapper that wraps RenderViewport.

ScrollableViewport: a Component that hooks Viewport up to some
scrolling behaviour.

Code changes:

RenderBlock now only works when it has an unbounded height constraint.
I removed the clipping in there since it's no longer needed.

I made FixedHeightScrollable use Viewport instead of hand-rolling its
clipping with Transform and Clip. This is what fixes the stocks list
hit testing at the bottom of the screen.

I made anywhere that used to use Block now use ScrollableViewport.

RenderFlex now takes a list of children.

Justifications for test changes:

tests/examples/stocks: changing FixedHeightScrollable to use a
RenderViewport instead of a RenderClipRect/RenderTransform combination
removes the use of an actual transform.

R=abarth@chromium.org

Review URL: https://codereview.chromium.org/1223153004 .
上级 56ef4631
......@@ -12,6 +12,11 @@ abstract class OffsetBase {
bool get isInfinite => _dx >= double.INFINITY || _dy >= double.INFINITY;
bool operator <(OffsetBase other) => _dx < other._dx && _dy < other._dy;
bool operator <=(OffsetBase other) => _dx <= other._dx && _dy <= other._dy;
bool operator >(OffsetBase other) => _dx > other._dx && _dy > other._dy;
bool operator >=(OffsetBase other) => _dx > other._dx && _dy >= other._dy;
bool operator ==(other) {
return other is OffsetBase &&
other.runtimeType == runtimeType &&
......
......@@ -79,6 +79,7 @@ dart_pkg("sky") {
"lib/widgets/scaffold.dart",
"lib/widgets/scrollable.dart",
"lib/widgets/scrollable_list.dart",
"lib/widgets/scrollable_viewport.dart",
"lib/widgets/snack_bar.dart",
"lib/widgets/switch.dart",
"lib/widgets/tabs.dart",
......
......@@ -4,14 +4,15 @@
import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/checkbox.dart';
import 'package:sky/widgets/switch.dart';
import 'package:sky/widgets/flat_button.dart';
import 'package:sky/widgets/dialog.dart';
import 'package:sky/widgets/flat_button.dart';
import 'package:sky/widgets/icon_button.dart';
import 'package:sky/widgets/material.dart';
import 'package:sky/widgets/menu_item.dart';
import 'package:sky/widgets/navigator.dart';
import 'package:sky/widgets/scaffold.dart';
import 'package:sky/widgets/scrollable_viewport.dart';
import 'package:sky/widgets/switch.dart';
import 'package:sky/widgets/tool_bar.dart';
import 'stock_types.dart';
......@@ -89,26 +90,28 @@ class StockSettings extends StatefulComponent {
// (whereby tapping the widgets below causes both the widget and the menu item to fire their callbacks)
return new Material(
type: MaterialType.canvas,
child: new Container(
padding: const EdgeDims.symmetric(vertical: 20.0),
child: new Block([
new MenuItem(
icon: 'action/thumb_up',
onPressed: () => _confirmOptimismChange(),
children: [
new Flexible(child: new Text('Everything is awesome')),
new Checkbox(value: optimism == StockMode.optimistic, onChanged: _handleOptimismChanged)
]
),
new MenuItem(
icon: 'action/backup',
onPressed: () { _handleBackupChanged(!(backup == BackupMode.enabled)); },
children: [
new Flexible(child: new Text('Back up stock list to the cloud')),
new Switch(value: backup == BackupMode.enabled, onChanged: _handleBackupChanged)
]
),
])
child: new ScrollableViewport(
child: new Container(
padding: const EdgeDims.symmetric(vertical: 20.0),
child: new Block([
new MenuItem(
icon: 'action/thumb_up',
onPressed: () => _confirmOptimismChange(),
children: [
new Flexible(child: new Text('Everything is awesome')),
new Checkbox(value: optimism == StockMode.optimistic, onChanged: _handleOptimismChanged)
]
),
new MenuItem(
icon: 'action/backup',
onPressed: () { _handleBackupChanged(!(backup == BackupMode.enabled)); },
children: [
new Flexible(child: new Text('Back up stock list to the cloud')),
new Switch(value: backup == BackupMode.enabled, onChanged: _handleBackupChanged)
]
),
])
)
)
);
}
......
......@@ -109,29 +109,15 @@ class RenderBlock extends RenderBlockBase {
return defaultComputeDistanceToFirstActualBaseline(baseline);
}
bool _hasVisualOverflow = false;
void performLayout() {
assert(constraints.maxHeight >= double.INFINITY);
super.performLayout();
size = constraints.constrain(new Size(constraints.maxWidth, childrenHeight));
assert(!size.isInfinite);
// FIXME(eseidel): Block lays out its children with unconstrained height
// yet itself remains constrained. Remember that our children wanted to
// be taller than we are so we know to clip them (and not cause confusing
// mismatch of painting vs. hittesting).
_hasVisualOverflow = childrenHeight > size.height;
}
void paint(PaintingCanvas canvas, Offset offset) {
if (_hasVisualOverflow) {
canvas.save();
canvas.clipRect(offset & size);
}
defaultPaint(canvas, offset);
if (_hasVisualOverflow) {
canvas.restore();
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
......@@ -204,6 +190,7 @@ class RenderBlockViewport extends RenderBlockBase {
bool get debugDoesLayoutWithCallback => true;
void performLayout() {
assert(constraints.maxHeight < double.INFINITY);
if (_callback != null) {
try {
_inCallback = true;
......
......@@ -1019,6 +1019,132 @@ class RenderBaseline extends RenderShiftedBox {
String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}baseline: ${baseline}\nbaselineType: ${baselineType}';
}
enum ViewportScrollDirection { horizontal, vertical, both }
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
RenderViewport({
RenderBox child,
Offset scrollOffset,
ViewportScrollDirection direction: ViewportScrollDirection.vertical
}) : _scrollOffset = scrollOffset,
_scrollDirection = direction {
assert(_offsetIsSane(scrollOffset, direction));
this.child = child;
}
bool _offsetIsSane(Offset offset, ViewportScrollDirection direction) {
switch (direction) {
case ViewportScrollDirection.both:
return true;
case ViewportScrollDirection.horizontal:
return offset.dy == 0.0;
case ViewportScrollDirection.vertical:
return offset.dx == 0.0;
}
}
Offset _scrollOffset;
Offset get scrollOffset => _scrollOffset;
void set scrollOffset(Offset value) {
if (value == _scrollOffset)
return;
assert(_offsetIsSane(value, scrollDirection));
_scrollOffset = value;
markNeedsPaint();
}
ViewportScrollDirection _scrollDirection;
ViewportScrollDirection get scrollDirection => _scrollDirection;
void set scrollDirection(ViewportScrollDirection value) {
if (value == _scrollDirection)
return;
assert(_offsetIsSane(scrollOffset, value));
_scrollDirection = value;
markNeedsLayout();
}
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
BoxConstraints innerConstraints;
switch (scrollDirection) {
case ViewportScrollDirection.both:
innerConstraints = new BoxConstraints();
break;
case ViewportScrollDirection.horizontal:
innerConstraints = constraints.heightConstraints();
break;
case ViewportScrollDirection.vertical:
innerConstraints = constraints.widthConstraints();
break;
}
return innerConstraints;
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
if (child != null)
return child.getMinIntrinsicWidth(_getInnerConstraints(constraints));
return super.getMinIntrinsicWidth(constraints);
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
if (child != null)
return child.getMaxIntrinsicWidth(_getInnerConstraints(constraints));
return super.getMaxIntrinsicWidth(constraints);
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
if (child != null)
return child.getMinIntrinsicHeight(_getInnerConstraints(constraints));
return super.getMinIntrinsicHeight(constraints);
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
if (child != null)
return child.getMaxIntrinsicHeight(_getInnerConstraints(constraints));
return super.getMaxIntrinsicHeight(constraints);
}
// We don't override computeDistanceToActualBaseline(), because we
// want the default behaviour (returning null). Otherwise, as you
// scroll the RenderViewport, it would shift in its parent if the
// parent was baseline-aligned, which makes no sense.
void performLayout() {
if (child != null) {
child.layout(_getInnerConstraints(constraints), parentUsesSize: true);
size = constraints.constrain(child.size);
assert(child.parentData is BoxParentData);
child.parentData.position = Point.origin;
} else {
performResize();
}
}
void paint(PaintingCanvas canvas, Offset offset) {
if (child != null) {
bool _needsClip = offset < Offset.zero ||
!(offset & size).contains(((offset - scrollOffset) & child.size).bottomRight);
if (_needsClip) {
canvas.save();
canvas.clipRect(offset & size);
}
canvas.paintChild(child, (offset - scrollOffset).toPoint());
if (_needsClip)
canvas.restore();
}
}
void hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) {
assert(child.parentData is BoxParentData);
Rect childBounds = child.parentData.position & child.size;
if (childBounds.contains(position + -scrollOffset))
child.hitTest(result, position: position + scrollOffset);
}
}
}
class RenderImage extends RenderBox {
RenderImage(sky.Image image, Size requestedSize)
......
......@@ -47,12 +47,11 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
FlexDirection direction: FlexDirection.horizontal,
FlexJustifyContent justifyContent: FlexJustifyContent.start,
FlexAlignItems alignItems: FlexAlignItems.center
}) : _direction = direction,
_justifyContent = justifyContent,
_alignItems = alignItems {
addAll(children);
}
}) : _direction = direction,
_justifyContent = justifyContent,
_alignItems = alignItems {
addAll(children);
}
FlexDirection _direction;
FlexDirection get direction => _direction;
......
......@@ -281,6 +281,26 @@ class Baseline extends OneChildRenderObjectWrapper {
}
class Viewport extends OneChildRenderObjectWrapper {
Viewport({
String key,
this.offset: 0.0,
Widget child
}) : super(key: key, child: child);
final double offset;
RenderViewport get root => super.root;
RenderViewport createNode() => new RenderViewport(scrollOffset: new Offset(0.0, offset));
void syncRenderObject(Viewport old) {
super.syncRenderObject(old);
root.scrollOffset = new Offset(0.0, offset);
}
}
class SizeObserver extends OneChildRenderObjectWrapper {
SizeObserver({ String key, this.callback, Widget child })
......
......@@ -6,7 +6,8 @@ import '../theme/colors.dart' as colors;
import 'basic.dart';
import 'default_text_style.dart';
import 'material.dart';
import "theme.dart";
import 'scrollable_viewport.dart';
import 'theme.dart';
/// A material design dialog
///
......@@ -88,7 +89,7 @@ class Dialog extends Component {
level: 4,
color: _color,
child: new ShrinkWrapWidth(
child: new Block(children)
child: new ScrollableBlock(children)
)
)
)
......
......@@ -10,6 +10,7 @@ import '../theme/shadows.dart';
import 'animated_component.dart';
import 'animation_builder.dart';
import 'basic.dart';
import 'scrollable_viewport.dart';
import 'theme.dart';
// TODO(eseidel): Draw width should vary based on device size:
......@@ -138,7 +139,7 @@ class Drawer extends AnimatedComponent {
backgroundColor: Theme.of(this).canvasColor,
boxShadow: shadows[level]),
width: _kWidth,
child: new Block(children)
child: new ScrollableBlock(children)
));
return new Listener(
......
......@@ -4,8 +4,6 @@
import 'dart:math' as math;
import 'package:vector_math/vector_math.dart';
import '../animation/scroll_behavior.dart';
import 'basic.dart';
import 'scrollable.dart';
......@@ -61,15 +59,16 @@ abstract class FixedHeightScrollable extends Scrollable {
_updateScrollOffset();
}
var itemShowIndex = 0;
var itemShowCount = 0;
Matrix4 transform = new Matrix4.identity();
int itemShowIndex = 0;
int itemShowCount = 0;
double offsetY = 0.0;
if (_height != null && _height > 0.0) {
if (scrollOffset < 0.0) {
double visibleHeight = _height + scrollOffset;
itemShowCount = (visibleHeight / itemHeight).round() + 1;
transform.translate(0.0, -scrollOffset);
offsetY = scrollOffset;
} else {
itemShowCount = (_height / itemHeight).ceil() + 1;
double alignmentDelta = -scrollOffset % itemHeight;
......@@ -79,7 +78,7 @@ abstract class FixedHeightScrollable extends Scrollable {
double drawStart = scrollOffset + alignmentDelta;
itemShowIndex = math.max(0, (drawStart / itemHeight).floor());
transform.translate(0.0, alignmentDelta);
offsetY = -alignmentDelta;
}
}
......@@ -88,13 +87,11 @@ abstract class FixedHeightScrollable extends Scrollable {
return new SizeObserver(
callback: _handleSizeChanged,
child: new ClipRect(
child: new Transform(
transform: transform,
child: new Container(
padding: padding,
child: new Block(items)
)
child: new Viewport(
offset: offsetY,
child: new Container(
padding: padding,
child: new Block(items)
)
)
);
......
......@@ -13,6 +13,7 @@ import '../theme/shadows.dart';
import 'animated_component.dart';
import 'basic.dart';
import 'popup_menu_item.dart';
import 'scrollable_viewport.dart';
const Duration _kMenuOpenDuration = const Duration(milliseconds: 300);
const Duration _kMenuCloseDuration = const Duration(milliseconds: 200);
......@@ -146,12 +147,14 @@ class PopupMenu extends AnimatedComponent {
),
child: new ShrinkWrapWidth(
stepWidth: _kMenuWidthStep,
child: new Container(
padding: const EdgeDims.symmetric(
horizontal: _kMenuHorizontalPadding,
vertical: _kMenuVerticalPadding
),
child: new Block(children)
child: new ScrollableViewport(
child: new Container(
padding: const EdgeDims.symmetric(
horizontal: _kMenuHorizontalPadding,
vertical: _kMenuVerticalPadding
),
child: new Block(children)
)
)
)
)
......
// 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.
import '../animation/scroll_behavior.dart';
import 'basic.dart';
import 'scrollable.dart';
class ScrollableViewport extends Scrollable {
ScrollableViewport({ String key, this.child }) : super(key: key);
Widget child;
void syncFields(ScrollableViewport source) {
child = source.child;
super.syncFields(source);
}
ScrollBehavior createScrollBehavior() => new FlingBehavior();
FlingBehavior get scrollBehavior => super.scrollBehavior;
double _viewportHeight = 0.0;
double _childHeight = 0.0;
void _handleViewportSizeChanged(Size newSize) {
setState(() {
_viewportHeight = newSize.height;
_updateScrollBehaviour();
});
}
void _handleChildSizeChanged(Size newSize) {
setState(() {
_childHeight = newSize.height;
_updateScrollBehaviour();
});
}
void _updateScrollBehaviour() {
scrollBehavior.contentsSize = _childHeight;
scrollBehavior.containerSize = _viewportHeight;
if (scrollOffset > scrollBehavior.maxScrollOffset)
settleScrollOffset();
}
Widget buildContent() {
return new SizeObserver(
callback: _handleViewportSizeChanged,
child: new Viewport(
offset: scrollOffset,
child: new SizeObserver(
callback: _handleChildSizeChanged,
child: child
)
)
);
}
}
class ScrollableBlock extends Component {
ScrollableBlock(this.children, { String key }) : super(key: key);
final List<Widget> children;
Widget build() {
return new ScrollableViewport(
child: new Block(children)
);
}
}
......@@ -60,18 +60,12 @@ PAINT FOR FRAME #2 ----------------------------------------------
2 | | | | | drawRect(Rect.fromLTRB(0.0, 104.0, 800.0, 600.0), Paint(color:Color(0xfffafafa)))
2 | | | | | paintChild RenderSizeObserver at Point(0.0, 104.0)
2 | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | paintChild RenderClipRect at Point(0.0, 104.0)
2 | | | | | | paintChild RenderViewport at Point(0.0, 104.0)
2 | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | save
2 | | | | | | | clipRect(Rect.fromLTRB(0.0, 104.0, 800.0, 600.0))
2 | | | | | | | paintChild RenderTransform at Point(0.0, 104.0)
2 | | | | | | | paintChild RenderBlock at Point(0.0, 104.0)
2 | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | save
2 | | | | | | | | translate(0.0, 104.0)
2 | | | | | | | | concat([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
2 | | | | | | | | paintChild RenderBlock at Point(0.0, 0.0)
2 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | restore
2 | | | | | | | restore
2 | | | paintChild RenderDecoratedBox at Point(0.0, 0.0)
2 | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
......@@ -176,18 +170,12 @@ PAINT FOR FRAME #3 ----------------------------------------------
3 | | | | | drawRect(Rect.fromLTRB(0.0, 104.0, 800.0, 600.0), Paint(color:Color(0xfffafafa)))
3 | | | | | paintChild RenderSizeObserver at Point(0.0, 104.0)
3 | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | paintChild RenderClipRect at Point(0.0, 104.0)
3 | | | | | | paintChild RenderViewport at Point(0.0, 104.0)
3 | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | | save
3 | | | | | | | clipRect(Rect.fromLTRB(0.0, 104.0, 800.0, 600.0))
3 | | | | | | | paintChild RenderTransform at Point(0.0, 104.0)
3 | | | | | | | paintChild RenderBlock at Point(0.0, 104.0)
3 | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | | | save
3 | | | | | | | | translate(0.0, 104.0)
3 | | | | | | | | concat([1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
3 | | | | | | | | paintChild RenderBlock at Point(0.0, 0.0)
3 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
3 | | | | | | | | restore
3 | | | | | | | restore
3 | | | paintChild RenderDecoratedBox at Point(0.0, 0.0)
3 | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
......
......@@ -24,25 +24,37 @@ PAINT FOR FRAME #2 ----------------------------------------------
2 | | | | | | drawRRect(Instance of 'RRect', Paint(color:Color(0xffffffff), drawLooper:true))
2 | | | | | | paintChild RenderShrinkWrapWidth at Point(260.0, 218.0)
2 | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | paintChild RenderBlock at Point(260.0, 218.0)
2 | | | | | | | paintChild RenderDecoratedBox at Point(260.0, 218.0)
2 | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | paintChild RenderPadding at Point(260.0, 218.0)
2 | | | | | | | | drawRect(Rect.fromLTRB(260.0, 218.0, 540.0, 382.0), Paint(color:Color(0xfffafafa)))
2 | | | | | | | | paintChild RenderSizeObserver at Point(260.0, 218.0)
2 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | paintChild RenderParagraph at Point(284.0, 242.0)
2 | | | | | | | | | paintChild RenderViewport at Point(260.0, 218.0)
2 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | translate(284.0, 242.0)
2 | | | | | | | | | | translate(-284.0, -242.0)
2 | | | | | | | | paintChild RenderPadding at Point(260.0, 270.0)
2 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | paintChild RenderParagraph at Point(284.0, 290.0)
2 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | translate(284.0, 290.0)
2 | | | | | | | | | | translate(-284.0, -290.0)
2 | | | | | | | | paintChild RenderFlex at Point(260.0, 362.0)
2 | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | paintChild RenderParagraph at Point(411.0, 362.0)
2 | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | translate(411.0, 362.0)
2 | | | | | | | | | | translate(-411.0, -362.0)
2 | | | | | | | | | | save
2 | | | | | | | | | | clipRect(Rect.fromLTRB(260.0, 218.0, 540.0, 382.0))
2 | | | | | | | | | | paintChild RenderSizeObserver at Point(260.0, 218.0)
2 | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | paintChild RenderBlock at Point(260.0, 218.0)
2 | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | paintChild RenderPadding at Point(260.0, 218.0)
2 | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | paintChild RenderParagraph at Point(284.0, 242.0)
2 | | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | | translate(284.0, 242.0)
2 | | | | | | | | | | | | | | translate(-284.0, -242.0)
2 | | | | | | | | | | | | paintChild RenderPadding at Point(260.0, 270.0)
2 | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | paintChild RenderParagraph at Point(284.0, 290.0)
2 | | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | | translate(284.0, 290.0)
2 | | | | | | | | | | | | | | translate(-284.0, -290.0)
2 | | | | | | | | | | | | paintChild RenderFlex at Point(260.0, 362.0)
2 | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | paintChild RenderParagraph at Point(411.0, 362.0)
2 | | | | | | | | | | | | | | TestPaintingCanvas() constructor: 800.0 x 600.0
2 | | | | | | | | | | | | | | translate(411.0, 362.0)
2 | | | | | | | | | | | | | | translate(-411.0, -362.0)
2 | | | | | | | | | | restore
------------------------------------------------------------------------
PAINTED 2 FRAMES
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册