// Copyright 2016 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 'package:meta/meta.dart';
import 'framework.dart';
import 'scrollable.dart';
/// A widget that controls whether viewport descendants will overscroll their contents.
/// Overscrolling is clamped at the beginning or end or both according to the
/// [edge] parameter.
///
/// Scroll offset limits are defined by the enclosing Scrollable's [ScrollBehavior].
class ClampOverscrolls extends InheritedWidget {
/// Creates a widget that controls whether viewport descendants will overscroll
/// their contents.
///
/// The [edge] and [child] arguments must not be null.
ClampOverscrolls({
Key key,
this.edge: ScrollableEdge.none,
@required Widget child,
}) : super(key: key, child: child) {
assert(edge != null);
assert(child != null);
}
/// Creates a widget that controls whether viewport descendants will overscroll
/// based on the given [edge] and the inherited ClampOverscrolls widget for
/// the given [context]. For example if edge is ScrollableEdge.leading
/// and a ClampOverscrolls ancestor exists that specified ScrollableEdge.trailing,
/// then this widget would clamp both scrollable edges.
///
/// The [context], [edge] and [child] arguments must not be null.
factory ClampOverscrolls.inherit({
Key key,
@required BuildContext context,
@required ScrollableEdge edge: ScrollableEdge.none,
@required Widget child
}) {
assert(context != null);
assert(edge != null);
assert(child != null);
// The child's clamped edge is the union of the given edge and the
// parent's clamped edge.
ScrollableEdge parentEdge = ClampOverscrolls.of(context)?.edge ?? ScrollableEdge.none;
ScrollableEdge childEdge = edge;
switch (parentEdge) {
case ScrollableEdge.leading:
if (edge == ScrollableEdge.trailing || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.trailing:
if (edge == ScrollableEdge.leading || edge == ScrollableEdge.both)
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.both:
childEdge = ScrollableEdge.both;
break;
case ScrollableEdge.none:
break;
}
return new ClampOverscrolls(
key: key,
edge: childEdge,
child: child
);
}
/// Defines when viewport scrollOffsets are clamped in terms of the scrollDirection.
/// If edge is `leading` the viewport's scrollOffset will be clamped at its minimum
/// value (often 0.0). If edge is `trailing` then the scrollOffset will be clamped
/// to its maximum value. If edge is `both` then both the leading and trailing
/// constraints are applied.
final ScrollableEdge edge;
/// Return the [newScrollOffset] clamped according to [edge] and [scrollable]'s
/// scroll behavior. The value of [newScrollOffset] defaults to `scrollable.scrollOffset`.
double clampScrollOffset(ScrollableState scrollable, [double newScrollOffset]) {
final double scrollOffset = newScrollOffset ?? scrollable.scrollOffset;
final double minScrollOffset = scrollable.scrollBehavior.minScrollOffset;
final double maxScrollOffset = scrollable.scrollBehavior.maxScrollOffset;
switch (edge) {
case ScrollableEdge.both:
return scrollOffset.clamp(minScrollOffset, maxScrollOffset);
case ScrollableEdge.leading:
return scrollOffset.clamp(minScrollOffset, double.INFINITY);
case ScrollableEdge.trailing:
return scrollOffset.clamp(double.NEGATIVE_INFINITY, maxScrollOffset);
case ScrollableEdge.none:
return scrollOffset;
}
return scrollOffset;
}
/// The closest instance of this class that encloses the given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// ScrollableEdge edge = ClampOverscrolls.of(context).edge;
/// ```
static ClampOverscrolls of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ClampOverscrolls);
}
@override
bool updateShouldNotify(ClampOverscrolls old) => edge != old.edge;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('edge: $edge');
}
}