1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:math' as math;
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/rendering.dart';
9
10/// A description of a [Scrollable]'s contents, useful for modeling the state
11/// of its viewport.
12///
13/// This class defines a current position, [pixels], and a range of values
14/// considered "in bounds" for that position. The range has a minimum value at
15/// [minScrollExtent] and a maximum value at [maxScrollExtent] (inclusive). The
16/// viewport scrolls in the direction and axis described by [axisDirection]
17/// and [axis].
18///
19/// The [outOfRange] getter will return true if [pixels] is outside this defined
20/// range. The [atEdge] getter will return true if the [pixels] position equals
21/// either the [minScrollExtent] or the [maxScrollExtent].
22///
23/// The dimensions of the viewport in the given [axis] are described by
24/// [viewportDimension].
25///
26/// The above values are also exposed in terms of [extentBefore],
27/// [extentInside], and [extentAfter], which may be more useful for use cases
28/// such as scroll bars; for example, see [Scrollbar].
29///
30/// {@tool dartpad}
31/// This sample shows how a [ScrollMetricsNotification] is dispatched when
32/// the [ScrollMetrics] changed as a result of resizing the [Viewport].
33/// Press the floating action button to increase the scrollable window's size.
34///
35/// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart **
36/// {@end-tool}
37///
38/// See also:
39///
40/// * [FixedScrollMetrics], which is an immutable object that implements this
41/// interface.
42mixin ScrollMetrics {
43 /// Creates a [ScrollMetrics] that has the same properties as this object.
44 ///
45 /// This is useful if this object is mutable, but you want to get a snapshot
46 /// of the current state.
47 ///
48 /// The named arguments allow the values to be adjusted in the process. This
49 /// is useful to examine hypothetical situations, for example "would applying
50 /// this delta unmodified take the position [outOfRange]?".
51 ScrollMetrics copyWith({
52 double? minScrollExtent,
53 double? maxScrollExtent,
54 double? pixels,
55 double? viewportDimension,
56 AxisDirection? axisDirection,
57 double? devicePixelRatio,
58 }) {
59 return FixedScrollMetrics(
60 minScrollExtent: minScrollExtent ?? (hasContentDimensions ? this.minScrollExtent : null),
61 maxScrollExtent: maxScrollExtent ?? (hasContentDimensions ? this.maxScrollExtent : null),
62 pixels: pixels ?? (hasPixels ? this.pixels : null),
63 viewportDimension: viewportDimension ?? (hasViewportDimension ? this.viewportDimension : null),
64 axisDirection: axisDirection ?? this.axisDirection,
65 devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
66 );
67 }
68
69 /// The minimum in-range value for [pixels].
70 ///
71 /// The actual [pixels] value might be [outOfRange].
72 ///
73 /// This value is typically less than or equal to
74 /// [maxScrollExtent]. It can be negative infinity, if the scroll is unbounded.
75 double get minScrollExtent;
76
77 /// The maximum in-range value for [pixels].
78 ///
79 /// The actual [pixels] value might be [outOfRange].
80 ///
81 /// This value is typically greater than or equal to
82 /// [minScrollExtent]. It can be infinity, if the scroll is unbounded.
83 double get maxScrollExtent;
84
85 /// Whether the [minScrollExtent] and the [maxScrollExtent] properties are available.
86 bool get hasContentDimensions;
87
88 /// The current scroll position, in logical pixels along the [axisDirection].
89 double get pixels;
90
91 /// Whether the [pixels] property is available.
92 bool get hasPixels;
93
94 /// The extent of the viewport along the [axisDirection].
95 double get viewportDimension;
96
97 /// Whether the [viewportDimension] property is available.
98 bool get hasViewportDimension;
99
100 /// The direction in which the scroll view scrolls.
101 AxisDirection get axisDirection;
102
103 /// The axis in which the scroll view scrolls.
104 Axis get axis => axisDirectionToAxis(axisDirection);
105
106 /// Whether the [pixels] value is outside the [minScrollExtent] and
107 /// [maxScrollExtent].
108 bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent;
109
110 /// Whether the [pixels] value is exactly at the [minScrollExtent] or the
111 /// [maxScrollExtent].
112 bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
113
114 /// The quantity of content conceptually "above" the viewport in the scrollable.
115 /// This is the content above the content described by [extentInside].
116 double get extentBefore => math.max(pixels - minScrollExtent, 0.0);
117
118 /// The quantity of content conceptually "inside" the viewport in the
119 /// scrollable (including empty space if the total amount of content is less
120 /// than the [viewportDimension]).
121 ///
122 /// The value is typically the extent of the viewport ([viewportDimension])
123 /// when [outOfRange] is false. It can be less when overscrolling.
124 ///
125 /// The value is always non-negative, and less than or equal to [viewportDimension].
126 double get extentInside {
127 assert(minScrollExtent <= maxScrollExtent);
128 return viewportDimension
129 // "above" overscroll value
130 - clampDouble(minScrollExtent - pixels, 0, viewportDimension)
131 // "below" overscroll value
132 - clampDouble(pixels - maxScrollExtent, 0, viewportDimension);
133 }
134
135 /// The quantity of content conceptually "below" the viewport in the scrollable.
136 /// This is the content below the content described by [extentInside].
137 double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
138
139 /// The total quantity of content available.
140 ///
141 /// This is the sum of [extentBefore], [extentInside], and [extentAfter], modulo
142 /// any rounding errors.
143 double get extentTotal => maxScrollExtent - minScrollExtent + viewportDimension;
144
145 /// The [FlutterView.devicePixelRatio] of the view that the [Scrollable]
146 /// associated with this metrics object is drawn into.
147 double get devicePixelRatio;
148}
149
150/// An immutable snapshot of values associated with a [Scrollable] viewport.
151///
152/// For details, see [ScrollMetrics], which defines this object's interfaces.
153///
154/// {@tool dartpad}
155/// This sample shows how a [ScrollMetricsNotification] is dispatched when
156/// the [ScrollMetrics] changed as a result of resizing the [Viewport].
157/// Press the floating action button to increase the scrollable window's size.
158///
159/// ** See code in examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart **
160/// {@end-tool}
161class FixedScrollMetrics with ScrollMetrics {
162 /// Creates an immutable snapshot of values associated with a [Scrollable] viewport.
163 FixedScrollMetrics({
164 required double? minScrollExtent,
165 required double? maxScrollExtent,
166 required double? pixels,
167 required double? viewportDimension,
168 required this.axisDirection,
169 required this.devicePixelRatio,
170 }) : _minScrollExtent = minScrollExtent,
171 _maxScrollExtent = maxScrollExtent,
172 _pixels = pixels,
173 _viewportDimension = viewportDimension;
174
175 @override
176 double get minScrollExtent => _minScrollExtent!;
177 final double? _minScrollExtent;
178
179 @override
180 double get maxScrollExtent => _maxScrollExtent!;
181 final double? _maxScrollExtent;
182
183 @override
184 bool get hasContentDimensions => _minScrollExtent != null && _maxScrollExtent != null;
185
186 @override
187 double get pixels => _pixels!;
188 final double? _pixels;
189
190 @override
191 bool get hasPixels => _pixels != null;
192
193 @override
194 double get viewportDimension => _viewportDimension!;
195 final double? _viewportDimension;
196
197 @override
198 bool get hasViewportDimension => _viewportDimension != null;
199
200 @override
201 final AxisDirection axisDirection;
202
203 @override
204 final double devicePixelRatio;
205
206 @override
207 String toString() {
208 return '${objectRuntimeType(this, 'FixedScrollMetrics')}(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)})';
209 }
210}
211