Unverified Commit 3ac9449a authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix the confusing-zero case with NestedScrollView. (#14133)

* Fix the confusing-zero case with NestedScrollView.

* Update mock_canvas.dart

* Update tabs_demo.dart

* more tweaks
parent c5cbc0df
......@@ -13,6 +13,8 @@ class _Page {
_Page({ this.label });
final String label;
String get id => label[0];
@override
String toString() => '$runtimeType("$label")';
}
class _CardData {
......@@ -69,6 +71,13 @@ final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
imageAsset: 'shrine/products/chucks.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
],
new _Page(label: 'RIGHT'): <_CardData>[
const _CardData(
title: 'Beachball',
imageAsset: 'shrine/products/beachball.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
const _CardData(
title: 'Dipped Brush',
imageAsset: 'shrine/products/brush.png',
......@@ -80,13 +89,6 @@ final Map<_Page, List<_CardData>> _allPages = <_Page, List<_CardData>>{
imageAssetPackage: _kGalleryAssetsPackage,
),
],
new _Page(label: 'RIGHT'): <_CardData>[
const _CardData(
title: 'Beachball',
imageAsset: 'shrine/products/beachball.png',
imageAssetPackage: _kGalleryAssetsPackage,
),
],
};
class _CardDataItem extends StatelessWidget {
......@@ -121,7 +123,10 @@ class _CardDataItem extends StatelessWidget {
),
),
new Center(
child: new Text(data.title, style: Theme.of(context).textTheme.title),
child: new Text(
data.title,
style: Theme.of(context).textTheme.title,
),
),
],
),
......@@ -141,13 +146,18 @@ class TabsDemo extends StatelessWidget {
body: new NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
new SliverAppBar(
title: const Text('Tabs and scrolling'),
pinned: true,
expandedHeight: 150.0,
forceElevated: innerBoxIsScrolled,
bottom: new TabBar(
tabs: _allPages.keys.map((_Page page) => new Tab(text: page.label)).toList(),
new SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
child: new SliverAppBar(
title: const Text('Tabs and scrolling'),
pinned: true,
expandedHeight: 150.0,
forceElevated: innerBoxIsScrolled,
bottom: new TabBar(
tabs: _allPages.keys.map(
(_Page page) => new Tab(text: page.label),
).toList(),
),
),
),
];
......@@ -157,15 +167,41 @@ class TabsDemo extends StatelessWidget {
return new SafeArea(
top: false,
bottom: false,
child: new ListView(
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
itemExtent: _CardDataItem.height,
children: _allPages[page].map((_CardData data) {
return new Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: new _CardDataItem(page: page, data: data),
child: new Builder(
builder: (BuildContext context) {
return new CustomScrollView(
key: new PageStorageKey<_Page>(page),
slivers: <Widget>[
new SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
new SliverPadding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
sliver: new SliverFixedExtentList(
itemExtent: _CardDataItem.height,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
final _CardData data = _allPages[page][index];
return new Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
),
child: new _CardDataItem(
page: page,
data: data,
),
);
},
childCount: _allPages[page].length,
),
),
),
],
);
}).toList(),
},
),
);
}).toList(),
......
......@@ -41,6 +41,7 @@ export 'src/painting/image_provider.dart';
export 'src/painting/image_resolution.dart';
export 'src/painting/image_stream.dart';
export 'src/painting/matrix_utils.dart';
export 'src/painting/paint_utilities.dart';
export 'src/painting/rounded_rectangle_border.dart';
export 'src/painting/shape_decoration.dart';
export 'src/painting/stadium_border.dart';
......
......@@ -38,7 +38,7 @@ typedef void AnimationStatusListener(AnimationStatus status);
///
/// To create a new animation that you can run forward and backward, consider
/// using [AnimationController].
abstract class Animation<T> extends Listenable {
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Animation();
......@@ -71,6 +71,7 @@ abstract class Animation<T> extends Listenable {
AnimationStatus get status;
/// The current value of the animation.
@override
T get value;
/// Whether this animation is stopped at the beginning.
......
......@@ -32,6 +32,16 @@ abstract class Listenable {
void removeListener(VoidCallback listener);
}
/// An interface for subclasses of [Listenable] that expose a [value].
///
/// This interface is implemented by [ValueNotifier<T>] and [Animation<T>], and
/// allows other APIs to accept either of those implementations interchangeably.
abstract class ValueListenable<T> extends Listenable {
/// The current value of the object. When the value changes, the callbacks
/// registered with [addListener] will be invoked.
T get value;
}
/// A class that can be extended or mixed in that provides a change notification
/// API using [VoidCallback] for notifications.
///
......@@ -169,13 +179,14 @@ class _MergingListenable extends ChangeNotifier {
/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
class ValueNotifier<T> extends ChangeNotifier {
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);
/// The current value stored in this notifier.
///
/// When the value is replaced, this class notifies its listeners.
@override
T get value => _value;
T _value;
set value(T newValue) {
......
// Copyright 2018 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 'basic_types.dart';
/// Draw a line between two points, which cuts diagonally back and forth across
/// the line that connects the two points.
///
/// The line will cross the line `zigs - 1` times.
///
/// If `zigs` is 1, then this will draw two sides of a triangle from `start` to
/// `end`, with the third point being `width` away from the line, as measured
/// perpendicular to that line.
///
/// If `width` is positive, the first `zig` will be to the left of the `start`
/// point when facing the `end` point. To reverse the zigging polarity, provide
/// a negative `width`.
///
/// The line is drawn using the provided `paint` on the provided `canvas`.
void paintZigZag(Canvas canvas, Paint paint, Offset start, Offset end, int zigs, double width) {
assert(zigs.isFinite);
assert(zigs > 0);
canvas.save();
canvas.translate(start.dx, start.dy);
end = end - start;
canvas.rotate(math.atan2(end.dy, end.dx));
final double length = end.distance;
final double spacing = length / (zigs * 2.0);
final Path path = new Path()
..moveTo(0.0, 0.0);
for (int index = 0; index < zigs; index += 1) {
final double x = (index * 2.0 + 1.0) * spacing;
final double y = width * ((index % 2.0) * 2.0 - 1.0);
path.lineTo(x, y);
}
path.lineTo(length, 0.0);
canvas.drawPath(path, paint);
canvas.restore();
}
\ No newline at end of file
......@@ -242,7 +242,7 @@ class RenderSliverPadding extends RenderSliver with RenderObjectWithChildMixin<R
@override
bool hitTestChildren(HitTestResult result, { @required double mainAxisPosition, @required double crossAxisPosition }) {
if (child.geometry.hitTestExtent > 0.0)
if (child != null && child.geometry.hitTestExtent > 0.0)
return child.hitTest(result, mainAxisPosition: mainAxisPosition - childMainAxisPosition(child), crossAxisPosition: crossAxisPosition - childCrossAxisPosition(child));
return false;
}
......
......@@ -580,12 +580,27 @@ abstract class SchedulerBinding extends BindingBase with ServicesBinding {
/// Schedules a new frame using [scheduleFrame] if this object is not
/// currently producing a frame.
///
/// After this is called, the framework ensures that the end of the
/// [handleBeginFrame] function will (eventually) be reached.
/// Calling this method ensures that [handleDrawFrame] will eventually be
/// called, unless it's already in progress.
///
/// This has no effect if [schedulerPhase] is
/// [SchedulerPhase.transientCallbacks] or [SchedulerPhase.midFrameMicrotasks]
/// (because a frame is already being prepared in that case), or
/// [SchedulerPhase.persistentCallbacks] (because a frame is actively being
/// rendered in that case). It will schedule a frame if the [schedulerPhase]
/// is [SchedulerPhase.idle] (in between frames) or
/// [SchedulerPhase.postFrameCallbacks] (after a frame).
void ensureVisualUpdate() {
if (schedulerPhase != SchedulerPhase.idle)
return;
scheduleFrame();
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
/// If necessary, schedules a new frame by calling
......
......@@ -508,7 +508,7 @@ abstract class WidgetsBinding extends BindingBase with SchedulerBinding, Gesture
_deferFirstFrameReportCount -= 1;
}
void _handleBuildScheduled() {
void _handleBuildScheduled() {
// If we're in the process of building dirty elements, then changes
// should not trigger a new frame.
assert(() {
......
......@@ -26,7 +26,8 @@ import 'scroll_position_with_single_context.dart';
///
/// A [ScrollController] is a [Listenable]. It notifies its listeners whenever
/// any of the attached [ScrollPosition]s notify _their_ listeners (i.e.
/// whenever any of them scroll).
/// whenever any of them scroll). It does not notify its listeners when the list
/// of attached [ScrollPosition]s changes.
///
/// Typically used with [ListView], [GridView], [CustomScrollView].
///
......
......@@ -479,7 +479,7 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// This notifier's value is true if a scroll is underway and false if the scroll
/// position is idle.
///
/// Listeners added by stateful widgets should be in the widget's
/// Listeners added by stateful widgets should be removed in the widget's
/// [State.dispose] method.
final ValueNotifier<bool> isScrollingNotifier = new ValueNotifier<bool>(false);
......
......@@ -182,11 +182,39 @@ abstract class ScrollView extends StatelessWidget {
return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
}
/// Build the list of widgets to place inside the viewport.
///
/// Subclasses should override this method to build the slivers for the inside
/// of the viewport.
@protected
List<Widget> buildSlivers(BuildContext context);
/// Build the viewport.
///
/// Subclasses may override this method to change how the viewport is built.
/// The default implementation uses a [ShrinkWrappingViewport] if [shrinkWrap]
/// is true, and a regular [Viewport] otherwise.
@protected
Widget buildViewport(
BuildContext context,
ViewportOffset offset,
AxisDirection axisDirection,
List<Widget> slivers,
) {
if (shrinkWrap) {
return new ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
return new Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
......@@ -200,20 +228,8 @@ abstract class ScrollView extends StatelessWidget {
controller: scrollController,
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
if (shrinkWrap) {
return new ShrinkWrappingViewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
} else {
return new Viewport(
axisDirection: axisDirection,
offset: offset,
slivers: slivers,
);
}
}
return buildViewport(context, offset, axisDirection, slivers);
},
);
return primary && scrollController != null
? new PrimaryScrollController.none(child: scrollable)
......
......@@ -12,21 +12,6 @@ export 'package:flutter/rendering.dart' show
AxisDirection,
GrowthDirection;
AxisDirection _getDefaultCrossAxisDirection(BuildContext context, AxisDirection axisDirection) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.right:
return AxisDirection.down;
case AxisDirection.down:
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.left:
return AxisDirection.down;
}
return null;
}
/// A widget that is bigger on the inside.
///
/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a
......@@ -124,11 +109,31 @@ class Viewport extends MultiChildRenderObjectWidget {
/// The [center] must be the key of a child of the viewport.
final Key center;
/// Given a [BuildContext] and an [AxisDirection], determine the correct cross
/// axis direction.
///
/// This depends on the [Directionality] if the `axisDirection` is vertical;
/// otherwise, the default cross axis direction is downwards.
static AxisDirection getDefaultCrossAxisDirection(BuildContext context, AxisDirection axisDirection) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.right:
return AxisDirection.down;
case AxisDirection.down:
return textDirectionToAxisDirection(Directionality.of(context));
case AxisDirection.left:
return AxisDirection.down;
}
return null;
}
@override
RenderViewport createRenderObject(BuildContext context) {
return new RenderViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? _getDefaultCrossAxisDirection(context, axisDirection),
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
anchor: anchor,
offset: offset,
);
......@@ -138,7 +143,7 @@ class Viewport extends MultiChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderViewport renderObject) {
renderObject
..axisDirection = axisDirection
..crossAxisDirection = crossAxisDirection ?? _getDefaultCrossAxisDirection(context, axisDirection)
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
..anchor = anchor
..offset = offset;
}
......@@ -271,7 +276,7 @@ class ShrinkWrappingViewport extends MultiChildRenderObjectWidget {
RenderShrinkWrappingViewport createRenderObject(BuildContext context) {
return new RenderShrinkWrappingViewport(
axisDirection: axisDirection,
crossAxisDirection: crossAxisDirection ?? _getDefaultCrossAxisDirection(context, axisDirection),
crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
offset: offset,
);
}
......@@ -280,7 +285,7 @@ class ShrinkWrappingViewport extends MultiChildRenderObjectWidget {
void updateRenderObject(BuildContext context, RenderShrinkWrappingViewport renderObject) {
renderObject
..axisDirection = axisDirection
..crossAxisDirection = crossAxisDirection ?? _getDefaultCrossAxisDirection(context, axisDirection)
..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
..offset = offset;
}
......
......@@ -292,6 +292,23 @@ abstract class PaintPattern {
/// If no call to [Canvas.drawParagraph] was made, then this results in failure.
void paragraph({ ui.Paragraph paragraph, dynamic offset });
/// Indicates that a shadow is expected next.
///
/// The next shadow is examined. Any arguments that are passed to this method
/// are compared to the actual [Canvas.drawShadow] call's `paint` argument,
/// and any mismatches result in failure.
///
/// To introspect the Path object (as it stands after the painting has
/// completed), the `includes` and `excludes` arguments can be provided to
/// specify points that should be considered inside or outside the path
/// (respectively).
///
/// If no call to [Canvas.drawShadow] was made, then this results in failure.
///
/// Any calls made between the last matched call (if any) and the
/// [Canvas.drawShadow] call are ignored.
void shadow({ Iterable<Offset> includes, Iterable<Offset> excludes, Color color, double elevation, bool transparentOccluder });
/// Indicates that an image is expected next.
///
/// The next call to [Canvas.drawImage] is examined, and its arguments
......@@ -637,6 +654,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
_predicates.add(new _FunctionPaintPredicate(#drawParagraph, <dynamic>[paragraph, offset]));
}
@override
void shadow({ Iterable<Offset> includes, Iterable<Offset> excludes, Color color, double elevation, bool transparentOccluder }) {
_predicates.add(new _ShadowPredicate(includes: includes, excludes: excludes, color: color, elevation: elevation, transparentOccluder: transparentOccluder));
}
@override
void image({ ui.Image image, double x, double y, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) {
_predicates.add(new _DrawImagePaintPredicate(image: image, x: x, y: y, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style));
......@@ -709,6 +731,24 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher imp
abstract class _PaintPredicate {
void match(Iterator<RecordedInvocation> call);
@protected
void checkMethod(Iterator<RecordedInvocation> call, Symbol symbol) {
int others = 0;
final RecordedInvocation firstCall = call.current;
while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
others += 1;
if (!call.moveNext())
throw new _MismatchedCall(
'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'the first of which was $firstCall, but did not '
'call ${_symbolName(symbol)}() at the time where $this was expected.',
'The first method that was called when the call to ${_symbolName(symbol)}() '
'was expected, $firstCall, was called with the following stack:',
firstCall,
);
}
}
@override
String toString() {
throw new FlutterError('$runtimeType does not implement toString.');
......@@ -734,19 +774,7 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate {
@override
void match(Iterator<RecordedInvocation> call) {
int others = 0;
final RecordedInvocation firstCall = call.current;
while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
others += 1;
if (!call.moveNext())
throw new _MismatchedCall(
'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'the first of which was $firstCall, but did not '
'call $methodName at the time where $this was expected.',
'The stack for the call to $firstCall was:',
firstCall,
);
}
checkMethod(call, symbol);
final int actualArgumentCount = call.current.invocation.positionalArguments.length;
if (actualArgumentCount != argumentCount)
throw 'It called $methodName with $actualArgumentCount argument${actualArgumentCount == 1 ? "" : "s"}; expected $argumentCount.';
......@@ -1008,6 +1036,81 @@ class _ArcPaintPredicate extends _DrawCommandPaintPredicate {
);
}
class _ShadowPredicate extends _PaintPredicate {
_ShadowPredicate({ this.includes, this.excludes, this.color, this.elevation, this.transparentOccluder });
final Iterable<Offset> includes;
final Iterable<Offset> excludes;
final Color color;
final double elevation;
final bool transparentOccluder;
static const Symbol symbol = #drawShadow;
String get methodName => _symbolName(symbol);
@protected
void verifyArguments(List<dynamic> arguments) {
if (arguments.length != 4)
throw 'It called $methodName with ${arguments.length} arguments; expected 4.';
final Path pathArgument = arguments[0];
if (includes != null) {
for (Offset offset in includes) {
if (!pathArgument.contains(offset))
throw 'It called $methodName with a path that unexpectedly did not contain $offset.';
}
}
if (excludes != null) {
for (Offset offset in excludes) {
if (pathArgument.contains(offset))
throw 'It called $methodName with a path that unexpectedly contained $offset.';
}
}
final Color actualColor = arguments[1];
if (color != null && actualColor != color)
throw 'It called $methodName with a color, $actualColor, which was not exactly the expected color ($color).';
final double actualElevation = arguments[2];
if (elevation != null && actualElevation != elevation)
throw 'It called $methodName with an elevation, $actualElevation, which was not exactly the expected value ($elevation).';
final bool actualTransparentOccluder = arguments[3];
if (transparentOccluder != null && actualTransparentOccluder != transparentOccluder)
throw 'It called $methodName with a transparentOccluder value, $actualTransparentOccluder, which was not exactly the expected value ($transparentOccluder).';
}
@override
void match(Iterator<RecordedInvocation> call) {
checkMethod(call, symbol);
verifyArguments(call.current.invocation.positionalArguments);
call.moveNext();
}
@protected
void debugFillDescription(List<String> description) {
if (includes != null && excludes != null) {
description.add('that contains $includes and does not contain $excludes');
} else if (includes != null) {
description.add('that contains $includes');
} else if (excludes != null) {
description.add('that does not contain $excludes');
}
if (color != null)
description.add('$color');
if (elevation != null)
description.add('elevation: $elevation');
if (transparentOccluder != null)
description.add('transparentOccluder: $transparentOccluder');
}
@override
String toString() {
final List<String> description = <String>[];
debugFillDescription(description);
String result = methodName;
if (description.isNotEmpty)
result += ' with ${description.join(", ")}';
return result;
}
}
class _DrawImagePaintPredicate extends _DrawCommandPaintPredicate {
_DrawImagePaintPredicate({ this.image, this.x, this.y, Color color, double strokeWidth, bool hasMaskFilter, PaintingStyle style }) : super(
#drawImage, 'an image', 3, 2, color: color, strokeWidth: strokeWidth, hasMaskFilter: hasMaskFilter, style: style
......@@ -1128,20 +1231,7 @@ class _FunctionPaintPredicate extends _PaintPredicate {
@override
void match(Iterator<RecordedInvocation> call) {
int others = 0;
final RecordedInvocation firstCall = call.current;
while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) {
others += 1;
if (!call.moveNext())
throw new _MismatchedCall(
'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'the first of which was $firstCall, but did not '
'call ${_symbolName(symbol)}() at the time where $this was expected.',
'The first method that was called when the call to ${_symbolName(symbol)}() '
'was expected, $firstCall, was called with the following stack:',
firstCall,
);
}
checkMethod(call, symbol);
if (call.current.invocation.positionalArguments.length != arguments.length)
throw 'It called ${_symbolName(symbol)} with ${call.current.invocation.positionalArguments.length} arguments; expected ${arguments.length}.';
for (int index = 0; index < arguments.length; index += 1) {
......@@ -1169,20 +1259,7 @@ class _FunctionPaintPredicate extends _PaintPredicate {
class _SaveRestorePairPaintPredicate extends _PaintPredicate {
@override
void match(Iterator<RecordedInvocation> call) {
int others = 0;
final RecordedInvocation firstCall = call.current;
while (!call.current.invocation.isMethod || call.current.invocation.memberName != #save) {
others += 1;
if (!call.moveNext())
throw new _MismatchedCall(
'It called $others other method${ others == 1 ? "" : "s" } on the canvas, '
'the first of which was $firstCall, but did not '
'call save() at the time where $this was expected.',
'The first method that was called when the call to save() '
'was expected, $firstCall, was called with the following stack:',
firstCall,
);
}
checkMethod(call, #save);
int depth = 1;
while (depth > 0) {
if (!call.moveNext())
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment