Commit 30fd06f7 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

getOffsetToReveal deals with pinned slivers (#11878)

* getOffsetToReveal deals with pinned slivers

If a Sliver can potentially be pinned at the edge of a viewport, getOffsetToReveal will take that into account and scroll further up/down to ensure that the object to reveal doesn't end up covered by a pinned sliver.

This is important for accessibility scrolling with app bars.

Since how much a pinned sliver is covering is dynamic (it can change with scroll offset, etc), getOffsetToReveal deals with the worst case and tries to ensure that the object to uncover is visible when the pinned slivers are at their max pinned extent.

* name fixes

* review feedback

* typos

* renaming

* fix analyzer

* fix test

* analyzer fixes
parent 8eb6ad27
......@@ -525,6 +525,7 @@ class SliverGeometry extends Diagnosticable {
this.paintOrigin: 0.0,
double layoutExtent,
this.maxPaintExtent: 0.0,
this.maxScrollObstructionExtent: 0.0,
double hitTestExtent,
bool visible,
this.hasVisualOverflow: false,
......@@ -592,6 +593,16 @@ class SliverGeometry extends Diagnosticable {
/// By definition, this cannot be less than [paintExtent].
final double maxPaintExtent;
/// The maximum extent by which this sliver can reduce the area in which
/// content can scroll if the sliver were pinned at the edge.
///
/// Slivers that never get pinned at the edge, should return zero.
///
/// A pinned app bar is an example for a sliver that would use this setting:
/// When the app bar is pinned to the top, the area in which content can
/// actually scroll is reduced by the height of the app bar.
final double maxScrollObstructionExtent;
/// The distance from where this sliver started painting to the bottom of
/// where it should accept hits.
///
......
......@@ -297,6 +297,7 @@ abstract class RenderSliverPinnedPersistentHeader extends RenderSliverPersistent
paintExtent: math.min(childExtent, constraints.remainingPaintExtent),
layoutExtent: (maxExtent - constraints.scrollOffset).clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: minExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
}
......@@ -410,6 +411,7 @@ abstract class RenderSliverFloatingPersistentHeader extends RenderSliverPersiste
paintExtent: paintExtent.clamp(0.0, constraints.remainingPaintExtent),
layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent),
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
return math.min(0.0, paintExtent - childExtent);
......@@ -519,6 +521,7 @@ abstract class RenderSliverFloatingPinnedPersistentHeader extends RenderSliverFl
paintExtent: paintExtent.clamp(minExtent, constraints.remainingPaintExtent),
layoutExtent: layoutExtent.clamp(0.0, constraints.remainingPaintExtent - minExtent),
maxPaintExtent: maxExtent,
maxScrollObstructionExtent: maxExtent,
hasVisualOverflow: true, // Conservatively say we do have overflow to avoid complexity.
);
return 0.0;
......
......@@ -479,15 +479,24 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(child.parent == this);
assert(child is RenderSliver);
final RenderSliver sliver = child;
final double extentOfPinnedSlivers = maxScrollObstructionExtentBefore(sliver);
leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);
switch (sliver.constraints.growthDirection) {
case GrowthDirection.forward:
leadingScrollOffset -= extentOfPinnedSlivers;
break;
case GrowthDirection.reverse:
// Nothing to do.
break;
}
double mainAxisExtent;
switch (axis) {
case Axis.horizontal:
mainAxisExtent = size.width;
mainAxisExtent = size.width - extentOfPinnedSlivers;
break;
case Axis.vertical:
mainAxisExtent = size.height;
mainAxisExtent = size.height - extentOfPinnedSlivers;
break;
}
......@@ -603,6 +612,15 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
@protected
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild);
/// Returns the total scroll obstruction extent of all slivers in theild].
/// of the viewport before [child].
///
/// This is the extent by which the actual area in which content can scroll
/// is reduced. For example, an app bar that is pinned at the top will reduce
/// the are in which content can actually scroll by the height of the app bar.
@protected
double maxScrollObstructionExtentBefore(RenderSliver child);
/// Converts the `parentMainAxisPosition` into the child's coordinate system.
///
/// The `parentMainAxisPosition` is a distance from the top edge (for vertical
......@@ -653,10 +671,13 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
void showOnScreen([RenderObject child]) {
// Logic duplicated in [_RenderSingleChildViewport.showOnScreen].
if (child != null) {
// TODO(goderbauer): Don't scroll if it is already visible.
// TODO(goderbauer): Don't guess if we need to align at leading or trailing edge.
// Move viewport the smallest distance to bring [child] on screen.
final double leadingEdgeOffset = getOffsetToReveal(child, 0.0);
final double trailingEdgeOffset = getOffsetToReveal(child, 1.0);
final double currentOffset = offset.pixels;
// TODO(goderbauer): Don't scroll if that puts us outside of viewport's bounds.
if ((currentOffset - leadingEdgeOffset).abs() < (currentOffset - trailingEdgeOffset).abs()) {
offset.jumpTo(leadingEdgeOffset);
} else {
......@@ -1023,6 +1044,32 @@ class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentDat
return null;
}
@override
double maxScrollObstructionExtentBefore(RenderSliver child) {
assert(child.parent == this);
final GrowthDirection growthDirection = child.constraints.growthDirection;
assert(growthDirection != null);
switch (growthDirection) {
case GrowthDirection.forward:
double pinnedExtent = 0.0;
RenderSliver current = center;
while (current != child) {
pinnedExtent += current.geometry.maxScrollObstructionExtent;
current = childAfter(current);
}
return pinnedExtent;
case GrowthDirection.reverse:
double pinnedExtent = 0.0;
RenderSliver current = childBefore(center);
while (current != child) {
pinnedExtent += current.geometry.maxScrollObstructionExtent;
current = childBefore(current);
}
return pinnedExtent;
}
return null;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
......@@ -1307,6 +1354,19 @@ class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalConta
return scrollOffsetToChild + scrollOffsetWithinChild;
}
@override
double maxScrollObstructionExtentBefore(RenderSliver child) {
assert(child.parent == this);
assert(child.constraints.growthDirection == GrowthDirection.forward);
double pinnedExtent = 0.0;
RenderSliver current = firstChild;
while (current != child) {
pinnedExtent += current.geometry.maxScrollObstructionExtent;
current = childAfter(current);
}
return pinnedExtent;
}
@override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
assert(child != null);
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
......@@ -72,6 +73,128 @@ void main() {
expect(scrollController.offset, 0.0);
});
testWidgets('showOnScreen works with pinned app bar and sliver list', (WidgetTester tester) async {
new SemanticsTester(tester); // enables semantics tree generation
const double kItemHeight = 100.0;
const double kExpandedAppBarHeight = 56.0;
final List<Widget> containers = <Widget>[];
for (int i = 0; i < 80; i++)
containers.add(new MergeSemantics(child: new Container(
height: kItemHeight,
child: new Text('container $i'),
)));
final ScrollController scrollController = new ScrollController(
initialScrollOffset: kItemHeight / 2,
);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child: new MediaQuery(
data: const MediaQueryData(),
child: new Scrollable(
controller: scrollController,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport(
offset: offset,
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: kExpandedAppBarHeight,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('App Bar'),
),
),
new SliverList(
delegate: new SliverChildListDelegate(containers),
)
],
);
}),
),
));
expect(scrollController.offset, kItemHeight / 2);
final int firstContainerId = tester.renderObject(find.byWidget(containers.first)).debugSemantics.id;
tester.binding.pipelineOwner.semanticsOwner.performAction(firstContainerId, SemanticsAction.showOnScreen);
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(tester.getTopLeft(find.byWidget(containers.first)).dy, kExpandedAppBarHeight);
final int secondContainerId = tester.renderObject(find.byWidget(containers[1])).debugSemantics.id;
tester.binding.pipelineOwner.semanticsOwner.performAction(secondContainerId, SemanticsAction.showOnScreen);
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(tester.getTopLeft(find.byWidget(containers[1])).dy, kExpandedAppBarHeight);
});
testWidgets('showOnScreen works with pinned app bar and individual slivers', (WidgetTester tester) async {
new SemanticsTester(tester); // enables semantics tree generation
const double kItemHeight = 100.0;
const double kExpandedAppBarHeight = 256.0;
final List<Widget> semantics = <Widget>[];
final List<Widget> slivers = new List<Widget>.generate(30, (int i) {
final Widget child = new MergeSemantics(
child: new Container(
child: new Text('Item $i'),
height: 72.0,
),
);
semantics.add(child);
return new SliverToBoxAdapter(
child: child,
);
});
final ScrollController scrollController = new ScrollController(
initialScrollOffset: kItemHeight / 2,
);
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
child:new MediaQuery(
data: const MediaQueryData(),
child: new Scrollable(
controller: scrollController,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport(
offset: offset,
slivers: <Widget>[
const SliverAppBar(
pinned: true,
expandedHeight: kExpandedAppBarHeight,
flexibleSpace: const FlexibleSpaceBar(
title: const Text('App Bar'),
),
),
]..addAll(slivers),
);
},
),
),
));
expect(scrollController.offset, kItemHeight / 2);
final int id0 = tester.renderObject(find.byWidget(semantics[0])).debugSemantics.id;
tester.binding.pipelineOwner.semanticsOwner.performAction(id0, SemanticsAction.showOnScreen);
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(tester.getTopLeft(find.byWidget(semantics[0])).dy, kToolbarHeight);
final int id1 = tester.renderObject(find.byWidget(semantics[1])).debugSemantics.id;
tester.binding.pipelineOwner.semanticsOwner.performAction(id1, SemanticsAction.showOnScreen);
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(tester.getTopLeft(find.byWidget(semantics[1])).dy, kToolbarHeight);
});
}
Future<Null> flingUp(WidgetTester tester, { int repetitions: 1 }) async {
......@@ -88,4 +211,4 @@ Future<Null> flingDown(WidgetTester tester, { int repetitions: 1 }) async {
await tester.pump();
await tester.pump(const Duration(seconds: 5));
}
}
\ No newline at end of file
}
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