mirror of
https://github.com/block/buzz.git
synced 2026-11-14 05:12:27 +00:00
chore(mobile): vendor scrollable_positioned_list 0.3.8
Copy the published 0.3.8 package unchanged into mobile/third_party and depend on it by path, so the next commit can patch how it keys items. Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ analyzer:
|
||||
- windows/**
|
||||
- macos/**
|
||||
- linux/**
|
||||
- third_party/**
|
||||
|
||||
linter:
|
||||
rules:
|
||||
|
||||
@@ -1275,10 +1275,9 @@ packages:
|
||||
scrollable_positioned_list:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: scrollable_positioned_list
|
||||
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
path: "third_party/scrollable_positioned_list"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.3.8"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
|
||||
@@ -43,7 +43,9 @@ dependencies:
|
||||
package_info_plus: ^10.0.0
|
||||
app_badge_plus: ^1.2.10
|
||||
app_links: ^6.4.0
|
||||
scrollable_positioned_list: ^0.3.8
|
||||
# Vendored 0.3.8 with stable item keys; see third_party/scrollable_positioned_list/VENDORED.md.
|
||||
scrollable_positioned_list:
|
||||
path: third_party/scrollable_positioned_list
|
||||
open_filex: ^4.7.0
|
||||
path_provider: ^2.1.6
|
||||
share_plus: ^13.3.0
|
||||
|
||||
1
mobile/third_party/scrollable_positioned_list/.gitignore
vendored
Normal file
1
mobile/third_party/scrollable_positioned_list/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
pubspec.lock
|
||||
126
mobile/third_party/scrollable_positioned_list/CHANGELOG.md
vendored
Normal file
126
mobile/third_party/scrollable_positioned_list/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
# 0.3.8
|
||||
* Add ScrollOffsetController to allow pixel-based changes in offset.
|
||||
* Bump min sdk version to 2.15.0
|
||||
|
||||
# 0.3.7
|
||||
* Add ScrollOffsetListener to allow listening to changes in scroll offset.
|
||||
|
||||
# 0.3.6
|
||||
* Fix cache extents for horizontal lists
|
||||
* scrollTo future doesn't complete until scrolling is done.
|
||||
|
||||
# 0.3.5
|
||||
* Fix extraneous animation controller declaration in 0.3.4.
|
||||
|
||||
# 0.3.4
|
||||
* Disposed the animation controller when disposing the scrollable list.
|
||||
|
||||
# 0.3.3
|
||||
* Fix potential crash when reading from RenderBox.size.
|
||||
|
||||
# 0.3.2
|
||||
* Re-apply Flutter framework bindings' null safety calls but set SDK
|
||||
constraints correctly to 2.12.0 instead.
|
||||
|
||||
# 0.3.1
|
||||
* Reverts change from 0.3.0 where the Flutter version constraint should have
|
||||
been set to 2.12.0 instead of 2.10.5.
|
||||
|
||||
# 0.3.0
|
||||
* Move to Flutter version 2.10.5 and update dependencies' null safety calls.
|
||||
|
||||
# 0.2.3
|
||||
* Support shrink wrap.
|
||||
# 0.2.2
|
||||
* Move dependencies from pre-release versions to released versions.
|
||||
|
||||
# 0.2.1
|
||||
* Fix crash on NaN or infinite offset.
|
||||
|
||||
# 0.2.0-nullsafety.0
|
||||
* Update to null safety.
|
||||
|
||||
# 0.1.10
|
||||
* Update the home page URL to fix
|
||||
[issue #190](https://github.com/google/flutter.widgets/issues/190).
|
||||
* Miscellaneous tweaks to the example.
|
||||
* Added documentation to address
|
||||
[issue #96](https://github.com/google/flutter.widgets/issues/96).
|
||||
* Miscellaneous other cleanup.
|
||||
* Restructured `_ScrollablePositionedListState` to try to simplify logic.
|
||||
* Fixed an issue with `ItemScrollController.scrollTo` where it could scroll to
|
||||
the wrong item if a non-zero `alignment` was specified and if the list was
|
||||
manually scrolled by dragging.
|
||||
|
||||
# 0.1.9
|
||||
* Fixed the example in `README.md`. Fixes
|
||||
[issue #191](https://github.com/google/flutter.widgets/issues/191).
|
||||
* Made the example runnable with `flutter run`. Fixes
|
||||
[issue #211](https://github.com/google/flutter.widgets/issues/211).
|
||||
* Updates to computation of semantic clip.
|
||||
* Smoother transition between views on long scrolls.
|
||||
* New controls over transition between views on long scrolls.
|
||||
|
||||
# 0.1.8
|
||||
* Set updateScheduled to false when short circuiting due to empty list.
|
||||
To fix https://github.com/google/flutter.widgets/issues/182.
|
||||
|
||||
# 0.1.7
|
||||
* Apply viewport dimensions in UnboundedRenderedViewport.performResize.
|
||||
To work around change in https://github.com/flutter/flutter/pull/61973
|
||||
causing breakage
|
||||
|
||||
# 0.1.6
|
||||
* Change to do local scroll (without a fade) whenever target item is found
|
||||
within the cache.
|
||||
* Added sdk constraints to the example.
|
||||
* Moved `itemScrollControllerDetachment` to
|
||||
`_ScrollablePositionedListState.deactivate`.
|
||||
|
||||
# 0.1.5
|
||||
|
||||
* Added minCacheExtent to ScrollablePositionedList
|
||||
* Fixes the issue when item count updated from zero to one and `index` in
|
||||
`itemBuilder` becomes `-1`. Fixes
|
||||
[issue #104](https://github.com/google/flutter.widgets/issues/104).
|
||||
|
||||
# 0.1.4
|
||||
|
||||
* itemBuilders should not be called with indices > itemCount - 1. Fixes
|
||||
[issue #42](https://github.com/google/flutter.widgets/issues/42) and
|
||||
[issue #77](https://github.com/google/flutter.widgets/issues/77).
|
||||
|
||||
# 0.1.3
|
||||
|
||||
* Don't build items when `itemCount` is 0. Fixes
|
||||
[issue #78](https://github.com/google/flutter.widgets/issues/78).
|
||||
|
||||
* Fix typos in `README.md`.
|
||||
|
||||
# 0.1.2
|
||||
|
||||
* Store scroll state in page storage to fix
|
||||
[issue #43](https://github.com/google/flutter.widgets/issues/43).
|
||||
|
||||
# 0.1.1
|
||||
|
||||
* Fix padding for horizontal lists.
|
||||
|
||||
* Add `ScrollablePositionedList.separated` constructor to complete
|
||||
[issue #34](https://github.com/google/flutter.widgets/issues/34).
|
||||
|
||||
* Add `isAttached` method to `ItemScrollController`.
|
||||
|
||||
# 0.1.0
|
||||
|
||||
* Properly bound `ScrollablePositionedList` to fix
|
||||
[issue #23](https://github.com/google/flutter.widgets/issues/23).
|
||||
|
||||
* Allow `ScrollablePositionedList` alignment outside `[0..1]` to fix
|
||||
[issue #31](https://github.com/google/flutter.widgets/issues/31).
|
||||
|
||||
* Moved `ScrollablePositionedList` example into `example` subdirectory.
|
||||
|
||||
# 0.0.1
|
||||
|
||||
* Added `ScrollablePositionedList`.
|
||||
26
mobile/third_party/scrollable_positioned_list/LICENSE
vendored
Normal file
26
mobile/third_party/scrollable_positioned_list/LICENSE
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
Copyright 2018 the Dart project authors, Inc. All rights reserved.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
74
mobile/third_party/scrollable_positioned_list/README.md
vendored
Normal file
74
mobile/third_party/scrollable_positioned_list/README.md
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
# scrollable_positioned_list
|
||||
|
||||
A flutter list that allows scrolling to a specific item in the list.
|
||||
|
||||
Also allows determining what items are currently visible.
|
||||
|
||||
## Usage
|
||||
|
||||
A `ScrollablePositionedList` works much like the builder version of `ListView`
|
||||
except that the list can be scrolled or jumped to a specific item.
|
||||
|
||||
### Example
|
||||
|
||||
A `ScrollablePositionedList` can be created with:
|
||||
|
||||
```dart
|
||||
final ItemScrollController itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController scrollOffsetController = ScrollOffsetController();
|
||||
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();
|
||||
final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create()
|
||||
|
||||
ScrollablePositionedList.builder(
|
||||
itemCount: 500,
|
||||
itemBuilder: (context, index) => Text('Item $index'),
|
||||
itemScrollController: itemScrollController,
|
||||
scrollOffsetController: scrollOffsetController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
);
|
||||
```
|
||||
|
||||
One then can scroll to a particular item with:
|
||||
|
||||
```dart
|
||||
itemScrollController.scrollTo(
|
||||
index: 150,
|
||||
duration: Duration(seconds: 2),
|
||||
curve: Curves.easeInOutCubic);
|
||||
```
|
||||
|
||||
or jump to a particular item with:
|
||||
|
||||
```dart
|
||||
itemScrollController.jumpTo(index: 150);
|
||||
```
|
||||
|
||||
One can monitor what items are visible on screen with:
|
||||
|
||||
```dart
|
||||
itemPositionsListener.itemPositions.addListener(() => ...);
|
||||
```
|
||||
|
||||
### Experimental APIs (subject to bugs and changes)
|
||||
|
||||
Changes in scroll position can be monitored with:
|
||||
|
||||
```dart
|
||||
scrollOffsetListener.changes.listen((event) => ...)
|
||||
```
|
||||
|
||||
see `ScrollSum` in [this test](test/scroll_offset_listener_test.dart) for an example of how the current offset can be
|
||||
calculated from the stream of scroll change deltas. This feature is new and experimental.
|
||||
|
||||
Changes in scroll position in pixels, relative to the current scroll position, can be made with:
|
||||
|
||||
```dart
|
||||
scrollOffsetController.animateScroll(offset: 100, duration: Duration(seconds: 1));
|
||||
```
|
||||
|
||||
A full example can be found in the example folder.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
This is not an officially supported Google product.
|
||||
10
mobile/third_party/scrollable_positioned_list/VENDORED.md
vendored
Normal file
10
mobile/third_party/scrollable_positioned_list/VENDORED.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Vendored scrollable_positioned_list
|
||||
|
||||
- **Source:** [pub.dev `scrollable_positioned_list` 0.3.8](https://pub.dev/packages/scrollable_positioned_list/versions/0.3.8), from [google/flutter.widgets](https://github.com/google/flutter.widgets/tree/master/packages/scrollable_positioned_list).
|
||||
- **Archive sha256:** `1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287`
|
||||
- **License:** BSD-3-Clause, in `LICENSE`.
|
||||
- **Copied:** `lib/`, `LICENSE`, `README.md`, `CHANGELOG.md`, `pubspec.yaml`, unchanged except one trailing space trimmed from `README.md`. The example and tests were left out.
|
||||
|
||||
## Local changes
|
||||
|
||||
None yet.
|
||||
7
mobile/third_party/scrollable_positioned_list/lib/scrollable_positioned_list.dart
vendored
Normal file
7
mobile/third_party/scrollable_positioned_list/lib/scrollable_positioned_list.dart
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2019 The Fuchsia Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'src/item_positions_listener.dart';
|
||||
export 'src/scrollable_positioned_list.dart';
|
||||
export 'src/scroll_offset_listener.dart';
|
||||
96
mobile/third_party/scrollable_positioned_list/lib/src/element_registry.dart
vendored
Normal file
96
mobile/third_party/scrollable_positioned_list/lib/src/element_registry.dart
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/widgets.dart';
|
||||
|
||||
/// A registry to track some [Element]s in the tree.
|
||||
class RegistryWidget extends StatefulWidget {
|
||||
/// Creates a [RegistryWidget].
|
||||
const RegistryWidget({Key? key, this.elementNotifier, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget child;
|
||||
|
||||
/// Contains the current set of all [Element]s created by
|
||||
/// [RegisteredElementWidget]s in the tree below this widget.
|
||||
///
|
||||
/// Note that if there is another [RegistryWidget] in this widget's subtree
|
||||
/// that registry, and not this one, will collect elements in its subtree.
|
||||
final ValueNotifier<Set<Element>?>? elementNotifier;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _RegistryWidgetState();
|
||||
}
|
||||
|
||||
/// A widget whose [Element] will be added its nearest ancestor
|
||||
/// [RegistryWidget].
|
||||
class RegisteredElementWidget extends ProxyWidget {
|
||||
/// Creates a [RegisteredElementWidget].
|
||||
const RegisteredElementWidget({Key? key, required Widget child})
|
||||
: super(key: key, child: child);
|
||||
|
||||
@override
|
||||
Element createElement() => _RegisteredElement(this);
|
||||
}
|
||||
|
||||
class _RegistryWidgetState extends State<RegistryWidget> {
|
||||
final Set<Element> registeredElements = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _InheritedRegistryWidget(
|
||||
state: this,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
class _InheritedRegistryWidget extends InheritedWidget {
|
||||
final _RegistryWidgetState state;
|
||||
|
||||
const _InheritedRegistryWidget(
|
||||
{Key? key, required this.state, required Widget child})
|
||||
: super(key: key, child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) => true;
|
||||
}
|
||||
|
||||
class _RegisteredElement extends ProxyElement {
|
||||
_RegisteredElement(ProxyWidget widget) : super(widget);
|
||||
|
||||
@override
|
||||
void notifyClients(ProxyWidget oldWidget) {}
|
||||
|
||||
late _RegistryWidgetState _registryWidgetState;
|
||||
|
||||
@override
|
||||
void mount(Element? parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
final _inheritedRegistryWidget =
|
||||
dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!;
|
||||
_registryWidgetState = _inheritedRegistryWidget.state;
|
||||
_registryWidgetState.registeredElements.add(this);
|
||||
_registryWidgetState.widget.elementNotifier?.value =
|
||||
_registryWidgetState.registeredElements;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final _inheritedRegistryWidget =
|
||||
dependOnInheritedWidgetOfExactType<_InheritedRegistryWidget>()!;
|
||||
_registryWidgetState = _inheritedRegistryWidget.state;
|
||||
_registryWidgetState.registeredElements.add(this);
|
||||
_registryWidgetState.widget.elementNotifier?.value =
|
||||
_registryWidgetState.registeredElements;
|
||||
}
|
||||
|
||||
@override
|
||||
void unmount() {
|
||||
_registryWidgetState.registeredElements.remove(this);
|
||||
_registryWidgetState.widget.elementNotifier?.value =
|
||||
_registryWidgetState.registeredElements;
|
||||
super.unmount();
|
||||
}
|
||||
}
|
||||
61
mobile/third_party/scrollable_positioned_list/lib/src/item_positions_listener.dart
vendored
Normal file
61
mobile/third_party/scrollable_positioned_list/lib/src/item_positions_listener.dart
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/foundation.dart';
|
||||
|
||||
import 'item_positions_notifier.dart';
|
||||
import 'scrollable_positioned_list.dart';
|
||||
|
||||
/// Provides a listenable iterable of [itemPositions] of items that are on
|
||||
/// screen and their locations.
|
||||
abstract class ItemPositionsListener {
|
||||
/// Creates an [ItemPositionsListener] that can be used by a
|
||||
/// [ScrollablePositionedList] to return the current position of items.
|
||||
factory ItemPositionsListener.create() => ItemPositionsNotifier();
|
||||
|
||||
/// The position of items that are at least partially visible in the viewport.
|
||||
ValueListenable<Iterable<ItemPosition>> get itemPositions;
|
||||
}
|
||||
|
||||
/// Position information for an item in the list.
|
||||
class ItemPosition {
|
||||
/// Create an [ItemPosition].
|
||||
const ItemPosition(
|
||||
{required this.index,
|
||||
required this.itemLeadingEdge,
|
||||
required this.itemTrailingEdge});
|
||||
|
||||
/// Index of the item.
|
||||
final int index;
|
||||
|
||||
/// Distance in proportion of the viewport's main axis length from the leading
|
||||
/// edge of the viewport to the leading edge of the item.
|
||||
///
|
||||
/// May be negative if the item is partially visible.
|
||||
final double itemLeadingEdge;
|
||||
|
||||
/// Distance in proportion of the viewport's main axis length from the leading
|
||||
/// edge of the viewport to the trailing edge of the item.
|
||||
///
|
||||
/// May be greater than one if the item is partially visible.
|
||||
final double itemTrailingEdge;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
final ItemPosition otherPosition = other;
|
||||
return otherPosition.index == index &&
|
||||
otherPosition.itemLeadingEdge == itemLeadingEdge &&
|
||||
otherPosition.itemTrailingEdge == itemTrailingEdge;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
31 * (31 * (7 + index.hashCode) + itemLeadingEdge.hashCode) +
|
||||
itemTrailingEdge.hashCode;
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'ItemPosition(index: $index, itemLeadingEdge: $itemLeadingEdge, itemTrailingEdge: $itemTrailingEdge)';
|
||||
}
|
||||
13
mobile/third_party/scrollable_positioned_list/lib/src/item_positions_notifier.dart
vendored
Normal file
13
mobile/third_party/scrollable_positioned_list/lib/src/item_positions_notifier.dart
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/foundation.dart';
|
||||
|
||||
import 'item_positions_listener.dart';
|
||||
|
||||
/// Internal implementation of [ItemPositionsListener].
|
||||
class ItemPositionsNotifier implements ItemPositionsListener {
|
||||
@override
|
||||
final ValueNotifier<Iterable<ItemPosition>> itemPositions = ValueNotifier([]);
|
||||
}
|
||||
370
mobile/third_party/scrollable_positioned_list/lib/src/positioned_list.dart
vendored
Normal file
370
mobile/third_party/scrollable_positioned_list/lib/src/positioned_list.dart
vendored
Normal file
@@ -0,0 +1,370 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'element_registry.dart';
|
||||
import 'item_positions_listener.dart';
|
||||
import 'item_positions_notifier.dart';
|
||||
import 'scroll_view.dart';
|
||||
import 'wrapping.dart';
|
||||
|
||||
/// A list of widgets similar to [ListView], except scroll control
|
||||
/// and position reporting is based on index rather than pixel offset.
|
||||
///
|
||||
/// [PositionedList] lays out children in the same way as [ListView].
|
||||
///
|
||||
/// The list can be displayed with the item at [positionIndex] positioned at a
|
||||
/// particular [alignment]. See [ItemScrollController.jumpTo] for an
|
||||
/// explanation of alignment.
|
||||
///
|
||||
/// All other parameters are the same as specified in [ListView].
|
||||
class PositionedList extends StatefulWidget {
|
||||
/// Create a [PositionedList].
|
||||
const PositionedList({
|
||||
Key? key,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
this.separatorBuilder,
|
||||
this.controller,
|
||||
this.itemPositionsNotifier,
|
||||
this.positionedIndex = 0,
|
||||
this.alignment = 0,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
this.shrinkWrap = false,
|
||||
this.physics,
|
||||
this.padding,
|
||||
this.cacheExtent,
|
||||
this.semanticChildCount,
|
||||
this.addSemanticIndexes = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
this.addAutomaticKeepAlives = true,
|
||||
}) : assert(itemCount != null),
|
||||
assert(itemBuilder != null),
|
||||
assert((positionedIndex == 0) || (positionedIndex < itemCount)),
|
||||
super(key: key);
|
||||
|
||||
/// Number of items the [itemBuilder] can produce.
|
||||
final int itemCount;
|
||||
|
||||
/// Called to build children for the list with
|
||||
/// 0 <= index < itemCount.
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
|
||||
/// If not null, called to build separators for between each item in the list.
|
||||
/// Called with 0 <= index < itemCount - 1.
|
||||
final IndexedWidgetBuilder? separatorBuilder;
|
||||
|
||||
/// An object that can be used to control the position to which this scroll
|
||||
/// view is scrolled.
|
||||
final ScrollController? controller;
|
||||
|
||||
/// Notifier that reports the items laid out in the list after each frame.
|
||||
final ItemPositionsNotifier? itemPositionsNotifier;
|
||||
|
||||
/// Index of an item to initially align to a position within the viewport
|
||||
/// defined by [alignment].
|
||||
final int positionedIndex;
|
||||
|
||||
/// Determines where the leading edge of the item at [positionedIndex]
|
||||
/// should be placed.
|
||||
///
|
||||
/// See [ItemScrollController.jumpTo] for an explanation of alignment.
|
||||
final double alignment;
|
||||
|
||||
/// The axis along which the scroll view scrolls.
|
||||
///
|
||||
/// Defaults to [Axis.vertical].
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Whether the view scrolls in the reading direction.
|
||||
///
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// See [ScrollView.reverse].
|
||||
final bool reverse;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||
/// determined by the contents being viewed.
|
||||
///
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// See [ScrollView.shrinkWrap].
|
||||
final bool shrinkWrap;
|
||||
|
||||
/// How the scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
/// user stops dragging the scroll view.
|
||||
///
|
||||
/// See [ScrollView.physics].
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.cacheExtent}
|
||||
final double? cacheExtent;
|
||||
|
||||
/// The number of children that will contribute semantic information.
|
||||
///
|
||||
/// See [ScrollView.semanticChildCount] for more information.
|
||||
final int? semanticChildCount;
|
||||
|
||||
/// Whether to wrap each child in an [IndexedSemantics].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addSemanticIndexes].
|
||||
final bool addSemanticIndexes;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
final EdgeInsets? padding;
|
||||
|
||||
/// Whether to wrap each child in a [RepaintBoundary].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addRepaintBoundaries].
|
||||
final bool addRepaintBoundaries;
|
||||
|
||||
/// Whether to wrap each child in an [AutomaticKeepAlive].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||
final bool addAutomaticKeepAlives;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PositionedListState();
|
||||
}
|
||||
|
||||
class _PositionedListState extends State<PositionedList> {
|
||||
final Key _centerKey = UniqueKey();
|
||||
|
||||
final registeredElements = ValueNotifier<Set<Element>?>(null);
|
||||
late final ScrollController scrollController;
|
||||
|
||||
bool updateScheduled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = widget.controller ?? ScrollController();
|
||||
scrollController.addListener(_schedulePositionNotificationUpdate);
|
||||
_schedulePositionNotificationUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.removeListener(_schedulePositionNotificationUpdate);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(PositionedList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_schedulePositionNotificationUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => RegistryWidget(
|
||||
elementNotifier: registeredElements,
|
||||
child: UnboundedCustomScrollView(
|
||||
anchor: widget.alignment,
|
||||
center: _centerKey,
|
||||
controller: scrollController,
|
||||
scrollDirection: widget.scrollDirection,
|
||||
reverse: widget.reverse,
|
||||
cacheExtent: widget.cacheExtent,
|
||||
physics: widget.physics,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
semanticChildCount: widget.semanticChildCount ?? widget.itemCount,
|
||||
slivers: <Widget>[
|
||||
if (widget.positionedIndex > 0)
|
||||
SliverPadding(
|
||||
padding: _leadingSliverPadding,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => widget.separatorBuilder == null
|
||||
? _buildItem(widget.positionedIndex - (index + 1))
|
||||
: _buildSeparatedListElement(
|
||||
2 * widget.positionedIndex - (index + 1)),
|
||||
childCount: widget.separatorBuilder == null
|
||||
? widget.positionedIndex
|
||||
: 2 * widget.positionedIndex,
|
||||
addSemanticIndexes: false,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
key: _centerKey,
|
||||
padding: _centerSliverPadding,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => widget.separatorBuilder == null
|
||||
? _buildItem(index + widget.positionedIndex)
|
||||
: _buildSeparatedListElement(
|
||||
index + 2 * widget.positionedIndex),
|
||||
childCount: widget.itemCount != 0 ? 1 : 0,
|
||||
addSemanticIndexes: false,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.positionedIndex >= 0 &&
|
||||
widget.positionedIndex < widget.itemCount - 1)
|
||||
SliverPadding(
|
||||
padding: _trailingSliverPadding,
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => widget.separatorBuilder == null
|
||||
? _buildItem(index + widget.positionedIndex + 1)
|
||||
: _buildSeparatedListElement(
|
||||
index + 2 * widget.positionedIndex + 1),
|
||||
childCount: widget.separatorBuilder == null
|
||||
? widget.itemCount - widget.positionedIndex - 1
|
||||
: 2 * (widget.itemCount - widget.positionedIndex - 1),
|
||||
addSemanticIndexes: false,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSeparatedListElement(int index) {
|
||||
if (index.isEven) {
|
||||
return _buildItem(index ~/ 2);
|
||||
} else {
|
||||
return widget.separatorBuilder!(context, index ~/ 2);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildItem(int index) {
|
||||
return RegisteredElementWidget(
|
||||
key: ValueKey(index),
|
||||
child: widget.addSemanticIndexes
|
||||
? IndexedSemantics(
|
||||
index: index, child: widget.itemBuilder(context, index))
|
||||
: widget.itemBuilder(context, index),
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsets get _leadingSliverPadding =>
|
||||
(widget.scrollDirection == Axis.vertical
|
||||
? widget.reverse
|
||||
? widget.padding?.copyWith(top: 0)
|
||||
: widget.padding?.copyWith(bottom: 0)
|
||||
: widget.reverse
|
||||
? widget.padding?.copyWith(left: 0)
|
||||
: widget.padding?.copyWith(right: 0)) ??
|
||||
EdgeInsets.all(0);
|
||||
|
||||
EdgeInsets get _centerSliverPadding => widget.scrollDirection == Axis.vertical
|
||||
? widget.reverse
|
||||
? widget.padding?.copyWith(
|
||||
top: widget.positionedIndex == widget.itemCount - 1
|
||||
? widget.padding!.top
|
||||
: 0,
|
||||
bottom: widget.positionedIndex == 0
|
||||
? widget.padding!.bottom
|
||||
: 0) ??
|
||||
EdgeInsets.all(0)
|
||||
: widget.padding?.copyWith(
|
||||
top: widget.positionedIndex == 0 ? widget.padding!.top : 0,
|
||||
bottom: widget.positionedIndex == widget.itemCount - 1
|
||||
? widget.padding!.bottom
|
||||
: 0) ??
|
||||
EdgeInsets.all(0)
|
||||
: widget.reverse
|
||||
? widget.padding?.copyWith(
|
||||
left: widget.positionedIndex == widget.itemCount - 1
|
||||
? widget.padding!.left
|
||||
: 0,
|
||||
right: widget.positionedIndex == 0
|
||||
? widget.padding!.right
|
||||
: 0) ??
|
||||
EdgeInsets.all(0)
|
||||
: widget.padding?.copyWith(
|
||||
left: widget.positionedIndex == 0 ? widget.padding!.left : 0,
|
||||
right: widget.positionedIndex == widget.itemCount - 1
|
||||
? widget.padding!.right
|
||||
: 0,
|
||||
) ??
|
||||
EdgeInsets.all(0);
|
||||
|
||||
EdgeInsets get _trailingSliverPadding =>
|
||||
widget.scrollDirection == Axis.vertical
|
||||
? widget.reverse
|
||||
? widget.padding?.copyWith(bottom: 0) ?? EdgeInsets.all(0)
|
||||
: widget.padding?.copyWith(top: 0) ?? EdgeInsets.all(0)
|
||||
: widget.reverse
|
||||
? widget.padding?.copyWith(right: 0) ?? EdgeInsets.all(0)
|
||||
: widget.padding?.copyWith(left: 0) ?? EdgeInsets.all(0);
|
||||
|
||||
void _schedulePositionNotificationUpdate() {
|
||||
if (!updateScheduled) {
|
||||
updateScheduled = true;
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
final elements = registeredElements.value;
|
||||
if (elements == null) {
|
||||
updateScheduled = false;
|
||||
return;
|
||||
}
|
||||
final positions = <ItemPosition>[];
|
||||
RenderViewportBase? viewport;
|
||||
for (var element in elements) {
|
||||
final RenderBox box = element.renderObject as RenderBox;
|
||||
viewport ??= RenderAbstractViewport.of(box) as RenderViewportBase?;
|
||||
var anchor = 0.0;
|
||||
if (viewport is RenderViewport) {
|
||||
anchor = viewport.anchor;
|
||||
}
|
||||
|
||||
if (viewport is CustomRenderViewport) {
|
||||
anchor = viewport.anchor;
|
||||
}
|
||||
|
||||
final ValueKey<int> key = element.widget.key as ValueKey<int>;
|
||||
// Skip this element if `box` has never been laid out.
|
||||
if (!box.hasSize) continue;
|
||||
if (widget.scrollDirection == Axis.vertical) {
|
||||
final reveal = viewport!.getOffsetToReveal(box, 0).offset;
|
||||
if (!reveal.isFinite) continue;
|
||||
final itemOffset =
|
||||
reveal - viewport.offset.pixels + anchor * viewport.size.height;
|
||||
positions.add(ItemPosition(
|
||||
index: key.value,
|
||||
itemLeadingEdge: itemOffset.round() /
|
||||
scrollController.position.viewportDimension,
|
||||
itemTrailingEdge: (itemOffset + box.size.height).round() /
|
||||
scrollController.position.viewportDimension));
|
||||
} else {
|
||||
final itemOffset =
|
||||
box.localToGlobal(Offset.zero, ancestor: viewport).dx;
|
||||
if (!itemOffset.isFinite) continue;
|
||||
positions.add(ItemPosition(
|
||||
index: key.value,
|
||||
itemLeadingEdge: (widget.reverse
|
||||
? scrollController.position.viewportDimension -
|
||||
(itemOffset + box.size.width)
|
||||
: itemOffset)
|
||||
.round() /
|
||||
scrollController.position.viewportDimension,
|
||||
itemTrailingEdge: (widget.reverse
|
||||
? scrollController.position.viewportDimension -
|
||||
itemOffset
|
||||
: (itemOffset + box.size.width))
|
||||
.round() /
|
||||
scrollController.position.viewportDimension));
|
||||
}
|
||||
}
|
||||
widget.itemPositionsNotifier?.itemPositions.value = positions;
|
||||
updateScheduled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
35
mobile/third_party/scrollable_positioned_list/lib/src/post_mount_callback.dart
vendored
Normal file
35
mobile/third_party/scrollable_positioned_list/lib/src/post_mount_callback.dart
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/widgets.dart';
|
||||
|
||||
/// Widget whose [Element] calls a callback when the element is mounted.
|
||||
class PostMountCallback extends StatelessWidget {
|
||||
/// Creates a [PostMountCallback] widget.
|
||||
const PostMountCallback({required this.child, this.callback, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
final Widget child;
|
||||
|
||||
/// Callback to call when the element for this widget is mounted.
|
||||
final void Function()? callback;
|
||||
|
||||
@override
|
||||
StatelessElement createElement() => _PostMountCallbackElement(this);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => child;
|
||||
}
|
||||
|
||||
class _PostMountCallbackElement extends StatelessElement {
|
||||
_PostMountCallbackElement(PostMountCallback widget) : super(widget);
|
||||
|
||||
@override
|
||||
void mount(Element? parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
final PostMountCallback postMountCallback = widget as PostMountCallback;
|
||||
postMountCallback.callback?.call();
|
||||
}
|
||||
}
|
||||
21
mobile/third_party/scrollable_positioned_list/lib/src/scroll_offset_listener.dart
vendored
Normal file
21
mobile/third_party/scrollable_positioned_list/lib/src/scroll_offset_listener.dart
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'scroll_offset_notifier.dart';
|
||||
|
||||
/// Provides an affordance for listening to scroll offset changes.
|
||||
///
|
||||
/// This is an experimental API and is subject to change.
|
||||
/// Behavior may be ill-defined in some cases. Please file bugs.
|
||||
abstract class ScrollOffsetListener {
|
||||
/// Stream of scroll offset deltas.
|
||||
Stream<double> get changes;
|
||||
|
||||
/// Construct a ScrollOffsetListener.
|
||||
///
|
||||
/// Set [recordProgrammaticScrolls] to false to prevent reporting of
|
||||
/// programmatic scrolls.
|
||||
factory ScrollOffsetListener.create(
|
||||
{bool recordProgrammaticScrolls = true}) =>
|
||||
ScrollOffsetNotifier(
|
||||
recordProgrammaticScrolls: recordProgrammaticScrolls);
|
||||
}
|
||||
20
mobile/third_party/scrollable_positioned_list/lib/src/scroll_offset_notifier.dart
vendored
Normal file
20
mobile/third_party/scrollable_positioned_list/lib/src/scroll_offset_notifier.dart
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'scroll_offset_listener.dart';
|
||||
|
||||
class ScrollOffsetNotifier implements ScrollOffsetListener {
|
||||
final bool recordProgrammaticScrolls;
|
||||
|
||||
ScrollOffsetNotifier({this.recordProgrammaticScrolls = true});
|
||||
|
||||
final _streamController = StreamController<double>();
|
||||
|
||||
@override
|
||||
Stream<double> get changes => _streamController.stream;
|
||||
|
||||
StreamController get changeController => _streamController;
|
||||
|
||||
void dispose() {
|
||||
_streamController.close();
|
||||
}
|
||||
}
|
||||
83
mobile/third_party/scrollable_positioned_list/lib/src/scroll_view.dart
vendored
Normal file
83
mobile/third_party/scrollable_positioned_list/lib/src/scroll_view.dart
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019 The Fuchsia 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:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'wrapping.dart';
|
||||
import 'viewport.dart';
|
||||
|
||||
/// A version of [CustomScrollView] that allows does not constrict the extents
|
||||
/// to be within 0 and 1. See [CustomScrollView] for more information.
|
||||
class UnboundedCustomScrollView extends CustomScrollView {
|
||||
final bool _shrinkWrap;
|
||||
|
||||
const UnboundedCustomScrollView({
|
||||
Key? key,
|
||||
Axis scrollDirection = Axis.vertical,
|
||||
bool reverse = false,
|
||||
ScrollController? controller,
|
||||
bool? primary,
|
||||
ScrollPhysics? physics,
|
||||
bool shrinkWrap = false,
|
||||
Key? center,
|
||||
double anchor = 0.0,
|
||||
double? cacheExtent,
|
||||
List<Widget> slivers = const <Widget>[],
|
||||
int? semanticChildCount,
|
||||
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
||||
}) : _shrinkWrap = shrinkWrap,
|
||||
_anchor = anchor,
|
||||
super(
|
||||
key: key,
|
||||
scrollDirection: scrollDirection,
|
||||
reverse: reverse,
|
||||
controller: controller,
|
||||
primary: primary,
|
||||
physics: physics,
|
||||
shrinkWrap: false,
|
||||
center: center,
|
||||
cacheExtent: cacheExtent,
|
||||
semanticChildCount: semanticChildCount,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
slivers: slivers,
|
||||
);
|
||||
|
||||
// [CustomScrollView] enforces constraints on [CustomScrollView.anchor], so
|
||||
// we need our own version.
|
||||
final double _anchor;
|
||||
|
||||
@override
|
||||
double get anchor => _anchor;
|
||||
|
||||
/// Build the viewport.
|
||||
@override
|
||||
@protected
|
||||
Widget buildViewport(
|
||||
BuildContext context,
|
||||
ViewportOffset offset,
|
||||
AxisDirection axisDirection,
|
||||
List<Widget> slivers,
|
||||
) {
|
||||
if (_shrinkWrap) {
|
||||
return CustomShrinkWrappingViewport(
|
||||
axisDirection: axisDirection,
|
||||
offset: offset,
|
||||
slivers: slivers,
|
||||
cacheExtent: cacheExtent,
|
||||
center: center,
|
||||
anchor: anchor,
|
||||
);
|
||||
}
|
||||
return UnboundedViewport(
|
||||
axisDirection: axisDirection,
|
||||
offset: offset,
|
||||
slivers: slivers,
|
||||
cacheExtent: cacheExtent,
|
||||
center: center,
|
||||
anchor: anchor,
|
||||
);
|
||||
}
|
||||
}
|
||||
672
mobile/third_party/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart
vendored
Normal file
672
mobile/third_party/scrollable_positioned_list/lib/src/scrollable_positioned_list.dart
vendored
Normal file
@@ -0,0 +1,672 @@
|
||||
// Copyright 2019 The Fuchsia 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:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'item_positions_notifier.dart';
|
||||
import 'positioned_list.dart';
|
||||
import 'post_mount_callback.dart';
|
||||
import 'scroll_offset_notifier.dart';
|
||||
|
||||
/// Number of screens to scroll when scrolling a long distance.
|
||||
const int _screenScrollCount = 2;
|
||||
|
||||
/// A scrollable list of widgets similar to [ListView], except scroll control
|
||||
/// and position reporting is based on index rather than pixel offset.
|
||||
///
|
||||
/// [ScrollablePositionedList] lays out children in the same way as [ListView].
|
||||
///
|
||||
/// The list can be displayed with the item at [initialScrollIndex] positioned
|
||||
/// at a particular [initialAlignment].
|
||||
///
|
||||
/// The [itemScrollController] can be used to scroll or jump to particular items
|
||||
/// in the list. The [itemPositionsNotifier] can be used to get a list of items
|
||||
/// currently laid out by the list.
|
||||
///
|
||||
/// The [scrollOffsetListener] can be used to get updates about scroll position
|
||||
/// changes.
|
||||
///
|
||||
/// All other parameters are the same as specified in [ListView].
|
||||
class ScrollablePositionedList extends StatefulWidget {
|
||||
/// Create a [ScrollablePositionedList] whose items are provided by
|
||||
/// [itemBuilder].
|
||||
const ScrollablePositionedList.builder({
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
Key? key,
|
||||
this.itemScrollController,
|
||||
this.shrinkWrap = false,
|
||||
ItemPositionsListener? itemPositionsListener,
|
||||
this.scrollOffsetController,
|
||||
ScrollOffsetListener? scrollOffsetListener,
|
||||
this.initialScrollIndex = 0,
|
||||
this.initialAlignment = 0,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
this.physics,
|
||||
this.semanticChildCount,
|
||||
this.padding,
|
||||
this.addSemanticIndexes = true,
|
||||
this.addAutomaticKeepAlives = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
this.minCacheExtent,
|
||||
}) : assert(itemCount != null),
|
||||
assert(itemBuilder != null),
|
||||
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||
scrollOffsetNotifier = scrollOffsetListener as ScrollOffsetNotifier?,
|
||||
separatorBuilder = null,
|
||||
super(key: key);
|
||||
|
||||
/// Create a [ScrollablePositionedList] whose items are provided by
|
||||
/// [itemBuilder] and separators provided by [separatorBuilder].
|
||||
const ScrollablePositionedList.separated({
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.separatorBuilder,
|
||||
Key? key,
|
||||
this.shrinkWrap = false,
|
||||
this.itemScrollController,
|
||||
ItemPositionsListener? itemPositionsListener,
|
||||
this.scrollOffsetController,
|
||||
ScrollOffsetListener? scrollOffsetListener,
|
||||
this.initialScrollIndex = 0,
|
||||
this.initialAlignment = 0,
|
||||
this.scrollDirection = Axis.vertical,
|
||||
this.reverse = false,
|
||||
this.physics,
|
||||
this.semanticChildCount,
|
||||
this.padding,
|
||||
this.addSemanticIndexes = true,
|
||||
this.addAutomaticKeepAlives = true,
|
||||
this.addRepaintBoundaries = true,
|
||||
this.minCacheExtent,
|
||||
}) : assert(itemCount != null),
|
||||
assert(itemBuilder != null),
|
||||
assert(separatorBuilder != null),
|
||||
itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?,
|
||||
scrollOffsetNotifier = scrollOffsetListener as ScrollOffsetNotifier?,
|
||||
super(key: key);
|
||||
|
||||
/// Number of items the [itemBuilder] can produce.
|
||||
final int itemCount;
|
||||
|
||||
/// Called to build children for the list with
|
||||
/// 0 <= index < itemCount.
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
|
||||
/// Called to build separators for between each item in the list.
|
||||
/// Called with 0 <= index < itemCount - 1.
|
||||
final IndexedWidgetBuilder? separatorBuilder;
|
||||
|
||||
/// Controller for jumping or scrolling to an item.
|
||||
final ItemScrollController? itemScrollController;
|
||||
|
||||
/// Notifier that reports the items laid out in the list after each frame.
|
||||
final ItemPositionsNotifier? itemPositionsNotifier;
|
||||
|
||||
final ScrollOffsetController? scrollOffsetController;
|
||||
|
||||
/// Notifier that reports the changes to the scroll offset.
|
||||
final ScrollOffsetNotifier? scrollOffsetNotifier;
|
||||
|
||||
/// Index of an item to initially align within the viewport.
|
||||
final int initialScrollIndex;
|
||||
|
||||
/// Determines where the leading edge of the item at [initialScrollIndex]
|
||||
/// should be placed.
|
||||
///
|
||||
/// See [ItemScrollController.jumpTo] for an explanation of alignment.
|
||||
final double initialAlignment;
|
||||
|
||||
/// The axis along which the scroll view scrolls.
|
||||
///
|
||||
/// Defaults to [Axis.vertical].
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Whether the view scrolls in the reading direction.
|
||||
///
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// See [ScrollView.reverse].
|
||||
final bool reverse;
|
||||
|
||||
/// {@template flutter.widgets.scroll_view.shrinkWrap}
|
||||
/// Whether the extent of the scroll view in the [scrollDirection] should be
|
||||
/// determined by the contents being viewed.
|
||||
///
|
||||
/// Defaults to false.
|
||||
///
|
||||
/// See [ScrollView.shrinkWrap].
|
||||
final bool shrinkWrap;
|
||||
|
||||
/// How the scroll view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the scroll view continues to animate after the
|
||||
/// user stops dragging the scroll view.
|
||||
///
|
||||
/// See [ScrollView.physics].
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// The number of children that will contribute semantic information.
|
||||
///
|
||||
/// See [ScrollView.semanticChildCount] for more information.
|
||||
final int? semanticChildCount;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
final EdgeInsets? padding;
|
||||
|
||||
/// Whether to wrap each child in an [IndexedSemantics].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addSemanticIndexes].
|
||||
final bool addSemanticIndexes;
|
||||
|
||||
/// Whether to wrap each child in an [AutomaticKeepAlive].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addAutomaticKeepAlives].
|
||||
final bool addAutomaticKeepAlives;
|
||||
|
||||
/// Whether to wrap each child in a [RepaintBoundary].
|
||||
///
|
||||
/// See [SliverChildBuilderDelegate.addRepaintBoundaries].
|
||||
final bool addRepaintBoundaries;
|
||||
|
||||
/// The minimum cache extent used by the underlying scroll lists.
|
||||
/// See [ScrollView.cacheExtent].
|
||||
///
|
||||
/// Note that the [ScrollablePositionedList] uses two lists to simulate long
|
||||
/// scrolls, so using the [ScrollController.scrollTo] method may result
|
||||
/// in builds of widgets that would otherwise already be built in the
|
||||
/// cache extent.
|
||||
final double? minCacheExtent;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _ScrollablePositionedListState();
|
||||
}
|
||||
|
||||
/// Controller to jump or scroll to a particular position in a
|
||||
/// [ScrollablePositionedList].
|
||||
class ItemScrollController {
|
||||
/// Whether any ScrollablePositionedList objects are attached this object.
|
||||
///
|
||||
/// If `false`, then [jumpTo] and [scrollTo] must not be called.
|
||||
bool get isAttached => _scrollableListState != null;
|
||||
|
||||
_ScrollablePositionedListState? _scrollableListState;
|
||||
|
||||
/// Immediately, without animation, reconfigure the list so that the item at
|
||||
/// [index]'s leading edge is at the given [alignment].
|
||||
///
|
||||
/// The [alignment] specifies the desired position for the leading edge of the
|
||||
/// item. The [alignment] is expected to be a value in the range \[0.0, 1.0\]
|
||||
/// and represents a proportion along the main axis of the viewport.
|
||||
///
|
||||
/// For a vertically scrolling view that is not reversed:
|
||||
/// * 0 aligns the top edge of the item with the top edge of the view.
|
||||
/// * 1 aligns the top edge of the item with the bottom of the view.
|
||||
/// * 0.5 aligns the top edge of the item with the center of the view.
|
||||
///
|
||||
/// For a horizontally scrolling view that is not reversed:
|
||||
/// * 0 aligns the left edge of the item with the left edge of the view
|
||||
/// * 1 aligns the left edge of the item with the right edge of the view.
|
||||
/// * 0.5 aligns the left edge of the item with the center of the view.
|
||||
void jumpTo({required int index, double alignment = 0}) {
|
||||
_scrollableListState!._jumpTo(index: index, alignment: alignment);
|
||||
}
|
||||
|
||||
/// Animate the list over [duration] using the given [curve] such that the
|
||||
/// item at [index] ends up with its leading edge at the given [alignment].
|
||||
/// See [jumpTo] for an explanation of alignment.
|
||||
///
|
||||
/// The [duration] must be greater than 0; otherwise, use [jumpTo].
|
||||
///
|
||||
/// When item position is not available, because it's too far, the scroll
|
||||
/// is composed into three phases:
|
||||
///
|
||||
/// 1. The currently displayed list view starts scrolling.
|
||||
/// 2. Another list view, which scrolls with the same speed, fades over the
|
||||
/// first one and shows items that are close to the scroll target.
|
||||
/// 3. The second list view scrolls and stops on the target.
|
||||
///
|
||||
/// The [opacityAnimationWeights] can be used to apply custom weights to these
|
||||
/// three stages of this animation. The default weights, `[40, 20, 40]`, are
|
||||
/// good with default [Curves.linear]. Different weights might be better for
|
||||
/// other cases. For example, if you use [Curves.easeOut], consider setting
|
||||
/// [opacityAnimationWeights] to `[20, 20, 60]`.
|
||||
///
|
||||
/// See [TweenSequenceItem.weight] for more info.
|
||||
Future<void> scrollTo({
|
||||
required int index,
|
||||
double alignment = 0,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
List<double> opacityAnimationWeights = const [40, 20, 40],
|
||||
}) {
|
||||
assert(_scrollableListState != null);
|
||||
assert(opacityAnimationWeights.length == 3);
|
||||
assert(duration > Duration.zero);
|
||||
return _scrollableListState!._scrollTo(
|
||||
index: index,
|
||||
alignment: alignment,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
opacityAnimationWeights: opacityAnimationWeights,
|
||||
);
|
||||
}
|
||||
|
||||
void _attach(_ScrollablePositionedListState scrollableListState) {
|
||||
assert(_scrollableListState == null);
|
||||
_scrollableListState = scrollableListState;
|
||||
}
|
||||
|
||||
void _detach() {
|
||||
_scrollableListState = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Controller to scroll a certain number of pixels relative to the current
|
||||
/// scroll offset.
|
||||
///
|
||||
/// Scrolls [offset] pixels relative to the current scroll offset. [offset] can
|
||||
/// be positive or negative.
|
||||
///
|
||||
/// This is an experimental API and is subject to change.
|
||||
/// Behavior may be ill-defined in some cases. Please file bugs.
|
||||
class ScrollOffsetController {
|
||||
Future<void> animateScroll(
|
||||
{required double offset,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear}) async {
|
||||
final currentPosition =
|
||||
_scrollableListState!.primary.scrollController.offset;
|
||||
final newPosition = currentPosition + offset;
|
||||
await _scrollableListState!.primary.scrollController.animateTo(
|
||||
newPosition,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
}
|
||||
|
||||
_ScrollablePositionedListState? _scrollableListState;
|
||||
|
||||
void _attach(_ScrollablePositionedListState scrollableListState) {
|
||||
assert(_scrollableListState == null);
|
||||
_scrollableListState = scrollableListState;
|
||||
}
|
||||
|
||||
void _detach() {
|
||||
_scrollableListState = null;
|
||||
}
|
||||
}
|
||||
|
||||
class _ScrollablePositionedListState extends State<ScrollablePositionedList>
|
||||
with TickerProviderStateMixin {
|
||||
/// Details for the primary (active) [ListView].
|
||||
var primary = _ListDisplayDetails(const ValueKey('Ping'));
|
||||
|
||||
/// Details for the secondary (transitional) [ListView] that is temporarily
|
||||
/// shown when scrolling a long distance.
|
||||
var secondary = _ListDisplayDetails(const ValueKey('Pong'));
|
||||
|
||||
final opacity = ProxyAnimation(const AlwaysStoppedAnimation<double>(0));
|
||||
|
||||
void Function() startAnimationCallback = () {};
|
||||
|
||||
bool _isTransitioning = false;
|
||||
|
||||
var _animationController;
|
||||
|
||||
double previousOffset = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ItemPosition? initialPosition = PageStorage.of(context).readState(context);
|
||||
primary.target = initialPosition?.index ?? widget.initialScrollIndex;
|
||||
primary.alignment =
|
||||
initialPosition?.itemLeadingEdge ?? widget.initialAlignment;
|
||||
if (widget.itemCount > 0 && primary.target > widget.itemCount - 1) {
|
||||
primary.target = widget.itemCount - 1;
|
||||
}
|
||||
widget.itemScrollController?._attach(this);
|
||||
widget.scrollOffsetController?._attach(this);
|
||||
primary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
|
||||
secondary.itemPositionsNotifier.itemPositions.addListener(_updatePositions);
|
||||
primary.scrollController.addListener(() {
|
||||
final currentOffset = primary.scrollController.offset;
|
||||
final offsetChange = currentOffset - previousOffset;
|
||||
previousOffset = currentOffset;
|
||||
if (!_isTransitioning |
|
||||
(widget.scrollOffsetNotifier?.recordProgrammaticScrolls ?? false)) {
|
||||
widget.scrollOffsetNotifier?.changeController.add(offsetChange);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void activate() {
|
||||
super.activate();
|
||||
widget.itemScrollController?._attach(this);
|
||||
widget.scrollOffsetController?._attach(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
widget.itemScrollController?._detach();
|
||||
widget.scrollOffsetController?._detach();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
primary.itemPositionsNotifier.itemPositions
|
||||
.removeListener(_updatePositions);
|
||||
secondary.itemPositionsNotifier.itemPositions
|
||||
.removeListener(_updatePositions);
|
||||
_animationController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ScrollablePositionedList oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.itemScrollController?._scrollableListState == this) {
|
||||
oldWidget.itemScrollController?._detach();
|
||||
}
|
||||
if (widget.itemScrollController?._scrollableListState != this) {
|
||||
widget.itemScrollController?._detach();
|
||||
widget.itemScrollController?._attach(this);
|
||||
}
|
||||
|
||||
if (widget.itemCount == 0) {
|
||||
setState(() {
|
||||
primary.target = 0;
|
||||
secondary.target = 0;
|
||||
});
|
||||
} else {
|
||||
if (primary.target > widget.itemCount - 1) {
|
||||
setState(() {
|
||||
primary.target = widget.itemCount - 1;
|
||||
});
|
||||
}
|
||||
if (secondary.target > widget.itemCount - 1) {
|
||||
setState(() {
|
||||
secondary.target = widget.itemCount - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final cacheExtent = _cacheExtent(constraints);
|
||||
return Listener(
|
||||
onPointerDown: (_) => _stopScroll(canceled: true),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
PostMountCallback(
|
||||
key: primary.key,
|
||||
callback: startAnimationCallback,
|
||||
child: FadeTransition(
|
||||
opacity: ReverseAnimation(opacity),
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => _isTransitioning,
|
||||
child: PositionedList(
|
||||
itemBuilder: widget.itemBuilder,
|
||||
separatorBuilder: widget.separatorBuilder,
|
||||
itemCount: widget.itemCount,
|
||||
positionedIndex: primary.target,
|
||||
controller: primary.scrollController,
|
||||
itemPositionsNotifier: primary.itemPositionsNotifier,
|
||||
scrollDirection: widget.scrollDirection,
|
||||
reverse: widget.reverse,
|
||||
cacheExtent: cacheExtent,
|
||||
alignment: primary.alignment,
|
||||
physics: widget.physics,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
addSemanticIndexes: widget.addSemanticIndexes,
|
||||
semanticChildCount: widget.semanticChildCount,
|
||||
padding: widget.padding,
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isTransitioning)
|
||||
PostMountCallback(
|
||||
key: secondary.key,
|
||||
callback: startAnimationCallback,
|
||||
child: FadeTransition(
|
||||
opacity: opacity,
|
||||
child: NotificationListener<ScrollNotification>(
|
||||
onNotification: (_) => false,
|
||||
child: PositionedList(
|
||||
itemBuilder: widget.itemBuilder,
|
||||
separatorBuilder: widget.separatorBuilder,
|
||||
itemCount: widget.itemCount,
|
||||
itemPositionsNotifier: secondary.itemPositionsNotifier,
|
||||
positionedIndex: secondary.target,
|
||||
controller: secondary.scrollController,
|
||||
scrollDirection: widget.scrollDirection,
|
||||
reverse: widget.reverse,
|
||||
cacheExtent: cacheExtent,
|
||||
alignment: secondary.alignment,
|
||||
physics: widget.physics,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
addSemanticIndexes: widget.addSemanticIndexes,
|
||||
semanticChildCount: widget.semanticChildCount,
|
||||
padding: widget.padding,
|
||||
addAutomaticKeepAlives: widget.addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
double _cacheExtent(BoxConstraints constraints) => max(
|
||||
(widget.scrollDirection == Axis.vertical
|
||||
? constraints.maxHeight
|
||||
: constraints.maxWidth) *
|
||||
_screenScrollCount,
|
||||
widget.minCacheExtent ?? 0,
|
||||
);
|
||||
|
||||
void _jumpTo({required int index, required double alignment}) {
|
||||
_stopScroll(canceled: true);
|
||||
if (index > widget.itemCount - 1) {
|
||||
index = widget.itemCount - 1;
|
||||
}
|
||||
setState(() {
|
||||
primary.scrollController.jumpTo(0);
|
||||
primary.target = index;
|
||||
primary.alignment = alignment;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _scrollTo({
|
||||
required int index,
|
||||
required double alignment,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
required List<double> opacityAnimationWeights,
|
||||
}) async {
|
||||
if (index > widget.itemCount - 1) {
|
||||
index = widget.itemCount - 1;
|
||||
}
|
||||
if (_isTransitioning) {
|
||||
final scrollCompleter = Completer<void>();
|
||||
_stopScroll(canceled: true);
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
await _startScroll(
|
||||
index: index,
|
||||
alignment: alignment,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
opacityAnimationWeights: opacityAnimationWeights,
|
||||
);
|
||||
scrollCompleter.complete();
|
||||
});
|
||||
await scrollCompleter.future;
|
||||
} else {
|
||||
await _startScroll(
|
||||
index: index,
|
||||
alignment: alignment,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
opacityAnimationWeights: opacityAnimationWeights,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startScroll({
|
||||
required int index,
|
||||
required double alignment,
|
||||
required Duration duration,
|
||||
Curve curve = Curves.linear,
|
||||
required List<double> opacityAnimationWeights,
|
||||
}) async {
|
||||
final direction = index > primary.target ? 1 : -1;
|
||||
final itemPosition = primary.itemPositionsNotifier.itemPositions.value
|
||||
.firstWhereOrNull(
|
||||
(ItemPosition itemPosition) => itemPosition.index == index);
|
||||
if (itemPosition != null) {
|
||||
// Scroll directly.
|
||||
final localScrollAmount = itemPosition.itemLeadingEdge *
|
||||
primary.scrollController.position.viewportDimension;
|
||||
await primary.scrollController.animateTo(
|
||||
primary.scrollController.offset +
|
||||
localScrollAmount -
|
||||
alignment * primary.scrollController.position.viewportDimension,
|
||||
duration: duration,
|
||||
curve: curve);
|
||||
} else {
|
||||
final scrollAmount = _screenScrollCount *
|
||||
primary.scrollController.position.viewportDimension;
|
||||
final startCompleter = Completer<void>();
|
||||
final endCompleter = Completer<void>();
|
||||
startAnimationCallback = () {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
startAnimationCallback = () {};
|
||||
_animationController?.dispose();
|
||||
_animationController =
|
||||
AnimationController(vsync: this, duration: duration)..forward();
|
||||
opacity.parent = _opacityAnimation(opacityAnimationWeights)
|
||||
.animate(_animationController);
|
||||
secondary.scrollController.jumpTo(-direction *
|
||||
(_screenScrollCount *
|
||||
primary.scrollController.position.viewportDimension -
|
||||
alignment *
|
||||
secondary.scrollController.position.viewportDimension));
|
||||
|
||||
startCompleter.complete(primary.scrollController.animateTo(
|
||||
primary.scrollController.offset + direction * scrollAmount,
|
||||
duration: duration,
|
||||
curve: curve));
|
||||
endCompleter.complete(secondary.scrollController
|
||||
.animateTo(0, duration: duration, curve: curve));
|
||||
});
|
||||
};
|
||||
setState(() {
|
||||
// TODO: _startScroll can be re-entrant, which invalidates this assert.
|
||||
// assert(!_isTransitioning);
|
||||
secondary.target = index;
|
||||
secondary.alignment = alignment;
|
||||
_isTransitioning = true;
|
||||
});
|
||||
await Future.wait<void>([startCompleter.future, endCompleter.future]);
|
||||
_stopScroll();
|
||||
}
|
||||
}
|
||||
|
||||
void _stopScroll({bool canceled = false}) {
|
||||
if (!_isTransitioning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canceled) {
|
||||
if (primary.scrollController.hasClients) {
|
||||
primary.scrollController.jumpTo(primary.scrollController.offset);
|
||||
}
|
||||
if (secondary.scrollController.hasClients) {
|
||||
secondary.scrollController.jumpTo(secondary.scrollController.offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (opacity.value >= 0.5) {
|
||||
// Secondary [ListView] is more visible than the primary; make it the
|
||||
// new primary.
|
||||
var temp = primary;
|
||||
primary = secondary;
|
||||
secondary = temp;
|
||||
}
|
||||
_isTransitioning = false;
|
||||
opacity.parent = const AlwaysStoppedAnimation<double>(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Animatable<double> _opacityAnimation(List<double> opacityAnimationWeights) {
|
||||
final startOpacity = 0.0;
|
||||
final endOpacity = 1.0;
|
||||
return TweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(startOpacity),
|
||||
weight: opacityAnimationWeights[0]),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: startOpacity, end: endOpacity),
|
||||
weight: opacityAnimationWeights[1]),
|
||||
TweenSequenceItem<double>(
|
||||
tween: ConstantTween<double>(endOpacity),
|
||||
weight: opacityAnimationWeights[2]),
|
||||
]);
|
||||
}
|
||||
|
||||
void _updatePositions() {
|
||||
final itemPositions = primary.itemPositionsNotifier.itemPositions.value
|
||||
.where((ItemPosition position) =>
|
||||
position.itemLeadingEdge < 1 && position.itemTrailingEdge > 0);
|
||||
if (itemPositions.isNotEmpty) {
|
||||
PageStorage.of(context).writeState(
|
||||
context,
|
||||
itemPositions.reduce((value, element) =>
|
||||
value.itemLeadingEdge < element.itemLeadingEdge
|
||||
? value
|
||||
: element));
|
||||
}
|
||||
widget.itemPositionsNotifier?.itemPositions.value = itemPositions;
|
||||
}
|
||||
}
|
||||
|
||||
class _ListDisplayDetails {
|
||||
_ListDisplayDetails(this.key);
|
||||
|
||||
final itemPositionsNotifier = ItemPositionsNotifier();
|
||||
final scrollController = ScrollController(keepScrollOffset: false);
|
||||
|
||||
/// The index of the item to scroll to.
|
||||
int target = 0;
|
||||
|
||||
/// The desired alignment for [target].
|
||||
///
|
||||
/// See [ItemScrollController.jumpTo] for an explanation of alignment.
|
||||
double alignment = 0;
|
||||
|
||||
final Key key;
|
||||
}
|
||||
315
mobile/third_party/scrollable_positioned_list/lib/src/viewport.dart
vendored
Normal file
315
mobile/third_party/scrollable_positioned_list/lib/src/viewport.dart
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright 2019 The Fuchsia 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 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A render object that is bigger on the inside.
|
||||
///
|
||||
/// Version of [Viewport] with some modifications to how extents are
|
||||
/// computed to allow scroll extents outside 0 to 1. See [Viewport]
|
||||
/// for more information.
|
||||
class UnboundedViewport extends Viewport {
|
||||
UnboundedViewport({
|
||||
Key? key,
|
||||
AxisDirection axisDirection = AxisDirection.down,
|
||||
AxisDirection? crossAxisDirection,
|
||||
double anchor = 0.0,
|
||||
required ViewportOffset offset,
|
||||
Key? center,
|
||||
double? cacheExtent,
|
||||
List<Widget> slivers = const <Widget>[],
|
||||
}) : _anchor = anchor,
|
||||
super(
|
||||
key: key,
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection,
|
||||
offset: offset,
|
||||
center: center,
|
||||
cacheExtent: cacheExtent,
|
||||
slivers: slivers);
|
||||
|
||||
// [Viewport] enforces constraints on [Viewport.anchor], so we need our own
|
||||
// version.
|
||||
final double _anchor;
|
||||
|
||||
@override
|
||||
double get anchor => _anchor;
|
||||
|
||||
@override
|
||||
RenderViewport createRenderObject(BuildContext context) {
|
||||
return UnboundedRenderViewport(
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection ??
|
||||
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
||||
anchor: anchor,
|
||||
offset: offset,
|
||||
cacheExtent: cacheExtent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A render object that is bigger on the inside.
|
||||
///
|
||||
/// Version of [RenderViewport] with some modifications to how extents are
|
||||
/// computed to allow scroll extents outside 0 to 1. See [RenderViewport]
|
||||
/// for more information.
|
||||
///
|
||||
// Differences from [RenderViewport] are marked with a //***** Differences
|
||||
// comment.
|
||||
class UnboundedRenderViewport extends RenderViewport {
|
||||
/// Creates a viewport for [RenderSliver] objects.
|
||||
UnboundedRenderViewport({
|
||||
AxisDirection axisDirection = AxisDirection.down,
|
||||
required AxisDirection crossAxisDirection,
|
||||
required ViewportOffset offset,
|
||||
double anchor = 0.0,
|
||||
List<RenderSliver>? children,
|
||||
RenderSliver? center,
|
||||
double? cacheExtent,
|
||||
}) : _anchor = anchor,
|
||||
super(
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection,
|
||||
offset: offset,
|
||||
center: center,
|
||||
cacheExtent: cacheExtent,
|
||||
children: children);
|
||||
|
||||
static const int _maxLayoutCycles = 10;
|
||||
|
||||
double _anchor;
|
||||
|
||||
// Out-of-band data computed during layout.
|
||||
late double _minScrollExtent;
|
||||
late double _maxScrollExtent;
|
||||
bool _hasVisualOverflow = false;
|
||||
|
||||
/// This value is set during layout based on the [CacheExtentStyle].
|
||||
///
|
||||
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
|
||||
/// of the viewport multiplied by the requested cache extent, which is still
|
||||
/// expressed in pixels.
|
||||
double? _calculatedCacheExtent;
|
||||
|
||||
@override
|
||||
double get anchor => _anchor;
|
||||
|
||||
@override
|
||||
set anchor(double value) {
|
||||
assert(value != null);
|
||||
if (value == _anchor) return;
|
||||
_anchor = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
super.performResize();
|
||||
// TODO: Figure out why this override is needed as a result of
|
||||
// https://github.com/flutter/flutter/pull/61973 and see if it can be
|
||||
// removed somehow.
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
offset.applyViewportDimension(size.height);
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
offset.applyViewportDimension(size.width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Rect describeSemanticsClip(RenderSliver? child) {
|
||||
assert(axis != null);
|
||||
|
||||
if (_calculatedCacheExtent == null) {
|
||||
return semanticBounds;
|
||||
}
|
||||
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
return Rect.fromLTRB(
|
||||
semanticBounds.left,
|
||||
semanticBounds.top - _calculatedCacheExtent!,
|
||||
semanticBounds.right,
|
||||
semanticBounds.bottom + _calculatedCacheExtent!,
|
||||
);
|
||||
default:
|
||||
return Rect.fromLTRB(
|
||||
semanticBounds.left - _calculatedCacheExtent!,
|
||||
semanticBounds.top,
|
||||
semanticBounds.right + _calculatedCacheExtent!,
|
||||
semanticBounds.bottom,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (center == null) {
|
||||
assert(firstChild == null);
|
||||
_minScrollExtent = 0.0;
|
||||
_maxScrollExtent = 0.0;
|
||||
_hasVisualOverflow = false;
|
||||
offset.applyContentDimensions(0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
assert(center!.parent == this);
|
||||
|
||||
late double mainAxisExtent;
|
||||
late double crossAxisExtent;
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
mainAxisExtent = size.height;
|
||||
crossAxisExtent = size.width;
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
mainAxisExtent = size.width;
|
||||
crossAxisExtent = size.height;
|
||||
break;
|
||||
}
|
||||
|
||||
final centerOffsetAdjustment = center!.centerOffsetAdjustment;
|
||||
|
||||
double correction;
|
||||
var count = 0;
|
||||
do {
|
||||
assert(offset.pixels != null);
|
||||
correction = _attemptLayout(mainAxisExtent, crossAxisExtent,
|
||||
offset.pixels + centerOffsetAdjustment);
|
||||
if (correction != 0.0) {
|
||||
offset.correctBy(correction);
|
||||
} else {
|
||||
// *** Difference from [RenderViewport].
|
||||
final top = _minScrollExtent + mainAxisExtent * anchor;
|
||||
final bottom = _maxScrollExtent - mainAxisExtent * (1.0 - anchor);
|
||||
final maxScrollOffset = math.max(math.min(0.0, top), bottom);
|
||||
final minScrollOffset = math.min(top, maxScrollOffset);
|
||||
if (offset.applyContentDimensions(minScrollOffset, maxScrollOffset))
|
||||
break;
|
||||
// *** End of difference from [RenderViewport].
|
||||
}
|
||||
count += 1;
|
||||
} while (count < _maxLayoutCycles);
|
||||
assert(() {
|
||||
if (count >= _maxLayoutCycles) {
|
||||
assert(count != 1);
|
||||
throw FlutterError(
|
||||
'A RenderViewport exceeded its maximum number of layout cycles.\n'
|
||||
'RenderViewport render objects, during layout, can retry if either their '
|
||||
'slivers or their ViewportOffset decide that the offset should be corrected '
|
||||
'to take into account information collected during that layout.\n'
|
||||
'In the case of this RenderViewport object, however, this happened $count '
|
||||
'times and still there was no consensus on the scroll offset. This usually '
|
||||
'indicates a bug. Specifically, it means that one of the following three '
|
||||
'problems is being experienced by the RenderViewport object:\n'
|
||||
' * One of the RenderSliver children or the ViewportOffset have a bug such'
|
||||
' that they always think that they need to correct the offset regardless.\n'
|
||||
' * Some combination of the RenderSliver children and the ViewportOffset'
|
||||
' have a bad interaction such that one applies a correction then another'
|
||||
' applies a reverse correction, leading to an infinite loop of corrections.\n'
|
||||
' * There is a pathological case that would eventually resolve, but it is'
|
||||
' so complicated that it cannot be resolved in any reasonable number of'
|
||||
' layout passes.');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
double _attemptLayout(
|
||||
double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
|
||||
assert(!mainAxisExtent.isNaN);
|
||||
assert(mainAxisExtent >= 0.0);
|
||||
assert(crossAxisExtent.isFinite);
|
||||
assert(crossAxisExtent >= 0.0);
|
||||
assert(correctedOffset.isFinite);
|
||||
_minScrollExtent = 0.0;
|
||||
_maxScrollExtent = 0.0;
|
||||
_hasVisualOverflow = false;
|
||||
|
||||
// centerOffset is the offset from the leading edge of the RenderViewport
|
||||
// to the zero scroll offset (the line between the forward slivers and the
|
||||
// reverse slivers).
|
||||
final double centerOffset = mainAxisExtent * anchor - correctedOffset;
|
||||
final double reverseDirectionRemainingPaintExtent =
|
||||
centerOffset.clamp(0.0, mainAxisExtent);
|
||||
final double forwardDirectionRemainingPaintExtent =
|
||||
(mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
|
||||
|
||||
switch (cacheExtentStyle) {
|
||||
case CacheExtentStyle.pixel:
|
||||
_calculatedCacheExtent = cacheExtent;
|
||||
break;
|
||||
case CacheExtentStyle.viewport:
|
||||
_calculatedCacheExtent = mainAxisExtent * cacheExtent!;
|
||||
break;
|
||||
}
|
||||
|
||||
final double fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!;
|
||||
final double centerCacheOffset = centerOffset + _calculatedCacheExtent!;
|
||||
final double reverseDirectionRemainingCacheExtent =
|
||||
centerCacheOffset.clamp(0.0, fullCacheExtent);
|
||||
final double forwardDirectionRemainingCacheExtent =
|
||||
(fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
|
||||
|
||||
final RenderSliver? leadingNegativeChild = childBefore(center!);
|
||||
|
||||
if (leadingNegativeChild != null) {
|
||||
// negative scroll offsets
|
||||
final double result = layoutChildSequence(
|
||||
child: leadingNegativeChild,
|
||||
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
|
||||
overlap: 0.0,
|
||||
layoutOffset: forwardDirectionRemainingPaintExtent,
|
||||
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
crossAxisExtent: crossAxisExtent,
|
||||
growthDirection: GrowthDirection.reverse,
|
||||
advance: childBefore,
|
||||
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
|
||||
cacheOrigin: (mainAxisExtent - centerOffset)
|
||||
.clamp(-_calculatedCacheExtent!, 0.0),
|
||||
);
|
||||
if (result != 0.0) return -result;
|
||||
}
|
||||
|
||||
// positive scroll offsets
|
||||
return layoutChildSequence(
|
||||
child: center,
|
||||
scrollOffset: math.max(0.0, -centerOffset),
|
||||
overlap:
|
||||
leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
|
||||
layoutOffset: centerOffset >= mainAxisExtent
|
||||
? centerOffset
|
||||
: reverseDirectionRemainingPaintExtent,
|
||||
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
crossAxisExtent: crossAxisExtent,
|
||||
growthDirection: GrowthDirection.forward,
|
||||
advance: childAfter,
|
||||
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
|
||||
cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent!, 0.0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasVisualOverflow => _hasVisualOverflow;
|
||||
|
||||
@override
|
||||
void updateOutOfBandData(
|
||||
GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
_maxScrollExtent += childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
_minScrollExtent -= childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
}
|
||||
if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true;
|
||||
}
|
||||
}
|
||||
995
mobile/third_party/scrollable_positioned_list/lib/src/wrapping.dart
vendored
Normal file
995
mobile/third_party/scrollable_positioned_list/lib/src/wrapping.dart
vendored
Normal file
@@ -0,0 +1,995 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A widget that is bigger on the inside and shrink wraps its children in the
|
||||
/// main axis.
|
||||
///
|
||||
/// [ShrinkWrappingViewport] displays a subset of its children according to its
|
||||
/// own dimensions and the given [offset]. As the offset varies, different
|
||||
/// children are visible through the viewport.
|
||||
///
|
||||
/// [ShrinkWrappingViewport] differs from [Viewport] in that [Viewport] expands
|
||||
/// to fill the main axis whereas [ShrinkWrappingViewport] sizes itself to match
|
||||
/// its children in the main axis. This shrink wrapping behavior is expensive
|
||||
/// because the children, and hence the viewport, could potentially change size
|
||||
/// whenever the [offset] changes (e.g., because of a collapsing header).
|
||||
///
|
||||
/// [ShrinkWrappingViewport] cannot contain box children directly. Instead, use
|
||||
/// a [SliverList], [SliverFixedExtentList], [SliverGrid], or a
|
||||
/// [SliverToBoxAdapter], for example.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine
|
||||
/// [Scrollable] and [ShrinkWrappingViewport] into widgets that are easier to
|
||||
/// use.
|
||||
/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a
|
||||
/// sliver context (the opposite of this widget).
|
||||
/// * [Viewport], a viewport that does not shrink-wrap its contents.
|
||||
class CustomShrinkWrappingViewport extends CustomViewport {
|
||||
/// Creates a widget that is bigger on the inside and shrink wraps its
|
||||
/// children in the main axis.
|
||||
///
|
||||
/// The viewport listens to the [offset], which means you do not need to
|
||||
/// rebuild this widget when the [offset] changes.
|
||||
///
|
||||
/// The [offset] argument must not be null.
|
||||
CustomShrinkWrappingViewport({
|
||||
Key? key,
|
||||
AxisDirection axisDirection = AxisDirection.down,
|
||||
AxisDirection? crossAxisDirection,
|
||||
double anchor = 0.0,
|
||||
required ViewportOffset offset,
|
||||
List<RenderSliver>? children,
|
||||
Key? center,
|
||||
double? cacheExtent,
|
||||
List<Widget> slivers = const <Widget>[],
|
||||
}) : _anchor = anchor,
|
||||
super(
|
||||
key: key,
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection,
|
||||
offset: offset,
|
||||
center: center,
|
||||
cacheExtent: cacheExtent,
|
||||
slivers: slivers);
|
||||
|
||||
// [Viewport] enforces constraints on [Viewport.anchor], so we need our own
|
||||
// version.
|
||||
final double _anchor;
|
||||
|
||||
@override
|
||||
double get anchor => _anchor;
|
||||
|
||||
@override
|
||||
CustomRenderShrinkWrappingViewport createRenderObject(BuildContext context) {
|
||||
return CustomRenderShrinkWrappingViewport(
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection ??
|
||||
Viewport.getDefaultCrossAxisDirection(context, axisDirection),
|
||||
offset: offset,
|
||||
anchor: anchor,
|
||||
cacheExtent: cacheExtent,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, CustomRenderShrinkWrappingViewport renderObject) {
|
||||
renderObject
|
||||
..axisDirection = axisDirection
|
||||
..crossAxisDirection = crossAxisDirection ??
|
||||
Viewport.getDefaultCrossAxisDirection(context, axisDirection)
|
||||
..anchor = anchor
|
||||
..offset = offset
|
||||
..cacheExtent = cacheExtent
|
||||
..cacheExtentStyle = cacheExtentStyle
|
||||
..clipBehavior = clipBehavior;
|
||||
}
|
||||
}
|
||||
|
||||
/// A render object that is bigger on the inside and shrink wraps its children
|
||||
/// in the main axis.
|
||||
///
|
||||
/// [RenderShrinkWrappingViewport] displays a subset of its children according
|
||||
/// to its own dimensions and the given [offset]. As the offset varies, different
|
||||
/// children are visible through the viewport.
|
||||
///
|
||||
/// [RenderShrinkWrappingViewport] differs from [RenderViewport] in that
|
||||
/// [RenderViewport] expands to fill the main axis whereas
|
||||
/// [RenderShrinkWrappingViewport] sizes itself to match its children in the
|
||||
/// main axis. This shrink wrapping behavior is expensive because the children,
|
||||
/// and hence the viewport, could potentially change size whenever the [offset]
|
||||
/// changes (e.g., because of a collapsing header).
|
||||
///
|
||||
/// [RenderShrinkWrappingViewport] cannot contain [RenderBox] children directly.
|
||||
/// Instead, use a [RenderSliverList], [RenderSliverFixedExtentList],
|
||||
/// [RenderSliverGrid], or a [RenderSliverToBoxAdapter], for example.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RenderViewport], a viewport that does not shrink-wrap its contents.
|
||||
/// * [RenderSliver], which explains more about the Sliver protocol.
|
||||
/// * [RenderBox], which explains more about the Box protocol.
|
||||
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
|
||||
/// placed inside a [RenderSliver] (the opposite of this class).
|
||||
class CustomRenderShrinkWrappingViewport extends CustomRenderViewport {
|
||||
/// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its
|
||||
/// contents.
|
||||
///
|
||||
/// The [offset] must be specified. For testing purposes, consider passing a
|
||||
/// [ViewportOffset.zero] or [ViewportOffset.fixed].
|
||||
CustomRenderShrinkWrappingViewport({
|
||||
AxisDirection axisDirection = AxisDirection.down,
|
||||
required AxisDirection crossAxisDirection,
|
||||
required ViewportOffset offset,
|
||||
double anchor = 0.0,
|
||||
List<RenderSliver>? children,
|
||||
RenderSliver? center,
|
||||
double? cacheExtent,
|
||||
}) : _anchor = anchor,
|
||||
super(
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection,
|
||||
offset: offset,
|
||||
center: center,
|
||||
cacheExtent: cacheExtent,
|
||||
children: children,
|
||||
);
|
||||
|
||||
double _anchor;
|
||||
|
||||
@override
|
||||
double get anchor => _anchor;
|
||||
|
||||
@override
|
||||
bool get sizedByParent => false;
|
||||
|
||||
double lastMainAxisExtent = -1;
|
||||
|
||||
@override
|
||||
set anchor(double value) {
|
||||
if (value == _anchor) return;
|
||||
_anchor = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
late double _shrinkWrapExtent;
|
||||
|
||||
/// This value is set during layout based on the [CacheExtentStyle].
|
||||
///
|
||||
/// When the style is [CacheExtentStyle.viewport], it is the main axis extent
|
||||
/// of the viewport multiplied by the requested cache extent, which is still
|
||||
/// expressed in pixels.
|
||||
double? _calculatedCacheExtent;
|
||||
|
||||
/// While List in a wrapping container, eg. ListView,the mainAxisExtent will
|
||||
/// be infinite. This time need to change mainAxisExtent to this value.
|
||||
final double _maxMainAxisExtent = double.maxFinite;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (center == null) {
|
||||
assert(firstChild == null);
|
||||
_minScrollExtent = 0.0;
|
||||
_maxScrollExtent = 0.0;
|
||||
_hasVisualOverflow = false;
|
||||
offset.applyContentDimensions(0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
assert(center!.parent == this);
|
||||
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
if (firstChild == null) {
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
assert(constraints.hasBoundedWidth);
|
||||
size = Size(constraints.maxWidth, constraints.minHeight);
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
assert(constraints.hasBoundedHeight);
|
||||
size = Size(constraints.minWidth, constraints.maxHeight);
|
||||
break;
|
||||
}
|
||||
offset.applyViewportDimension(0.0);
|
||||
_maxScrollExtent = 0.0;
|
||||
_shrinkWrapExtent = 0.0;
|
||||
_hasVisualOverflow = false;
|
||||
offset.applyContentDimensions(0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
double mainAxisExtent;
|
||||
final double crossAxisExtent;
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
assert(constraints.hasBoundedWidth);
|
||||
mainAxisExtent = constraints.maxHeight;
|
||||
crossAxisExtent = constraints.maxWidth;
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
assert(constraints.hasBoundedHeight);
|
||||
mainAxisExtent = constraints.maxWidth;
|
||||
crossAxisExtent = constraints.maxHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
if (mainAxisExtent.isInfinite) {
|
||||
mainAxisExtent = _maxMainAxisExtent;
|
||||
}
|
||||
|
||||
final centerOffsetAdjustment = center!.centerOffsetAdjustment;
|
||||
|
||||
double correction;
|
||||
double effectiveExtent;
|
||||
do {
|
||||
correction = _attemptLayout(mainAxisExtent, crossAxisExtent,
|
||||
offset.pixels + centerOffsetAdjustment);
|
||||
if (correction != 0.0) {
|
||||
offset.correctBy(correction);
|
||||
} else {
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
|
||||
break;
|
||||
}
|
||||
// *** Difference from [RenderViewport].
|
||||
final top = _minScrollExtent + mainAxisExtent * anchor;
|
||||
final bottom = _maxScrollExtent - mainAxisExtent * (1.0 - anchor);
|
||||
|
||||
final maxScrollOffset = math.max(math.min(0.0, top), bottom);
|
||||
final minScrollOffset = math.min(top, maxScrollOffset);
|
||||
|
||||
final bool didAcceptViewportDimension =
|
||||
offset.applyViewportDimension(effectiveExtent);
|
||||
final bool didAcceptContentDimension =
|
||||
offset.applyContentDimensions(minScrollOffset, maxScrollOffset);
|
||||
if (didAcceptViewportDimension && didAcceptContentDimension) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
size =
|
||||
constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
size =
|
||||
constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double _attemptLayout(
|
||||
double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
|
||||
assert(!mainAxisExtent.isNaN);
|
||||
assert(mainAxisExtent >= 0.0);
|
||||
assert(crossAxisExtent.isFinite);
|
||||
assert(crossAxisExtent >= 0.0);
|
||||
assert(correctedOffset.isFinite);
|
||||
_minScrollExtent = 0.0;
|
||||
_maxScrollExtent = 0.0;
|
||||
_hasVisualOverflow = false;
|
||||
_shrinkWrapExtent = 0.0;
|
||||
|
||||
// centerOffset is the offset from the leading edge of the RenderViewport
|
||||
// to the zero scroll offset (the line between the forward slivers and the
|
||||
// reverse slivers).
|
||||
final centerOffset = mainAxisExtent * anchor - correctedOffset;
|
||||
final reverseDirectionRemainingPaintExtent =
|
||||
centerOffset.clamp(0.0, mainAxisExtent);
|
||||
final forwardDirectionRemainingPaintExtent =
|
||||
(mainAxisExtent - centerOffset).clamp(0.0, mainAxisExtent);
|
||||
|
||||
switch (cacheExtentStyle) {
|
||||
case CacheExtentStyle.pixel:
|
||||
_calculatedCacheExtent = cacheExtent;
|
||||
break;
|
||||
case CacheExtentStyle.viewport:
|
||||
_calculatedCacheExtent = mainAxisExtent * cacheExtent!;
|
||||
break;
|
||||
}
|
||||
|
||||
final fullCacheExtent = mainAxisExtent + 2 * _calculatedCacheExtent!;
|
||||
final centerCacheOffset = centerOffset + _calculatedCacheExtent!;
|
||||
final reverseDirectionRemainingCacheExtent =
|
||||
centerCacheOffset.clamp(0.0, fullCacheExtent);
|
||||
final forwardDirectionRemainingCacheExtent =
|
||||
(fullCacheExtent - centerCacheOffset).clamp(0.0, fullCacheExtent);
|
||||
|
||||
final leadingNegativeChild = childBefore(center!);
|
||||
|
||||
if (leadingNegativeChild != null) {
|
||||
// negative scroll offsets
|
||||
final result = layoutChildSequence(
|
||||
child: leadingNegativeChild,
|
||||
scrollOffset: math.max(mainAxisExtent, centerOffset) - mainAxisExtent,
|
||||
overlap: 0.0,
|
||||
layoutOffset: forwardDirectionRemainingPaintExtent,
|
||||
remainingPaintExtent: reverseDirectionRemainingPaintExtent,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
crossAxisExtent: crossAxisExtent,
|
||||
growthDirection: GrowthDirection.reverse,
|
||||
advance: childBefore,
|
||||
remainingCacheExtent: reverseDirectionRemainingCacheExtent,
|
||||
cacheOrigin: (mainAxisExtent - centerOffset)
|
||||
.clamp(-_calculatedCacheExtent!, 0.0),
|
||||
);
|
||||
if (result != 0.0) return -result;
|
||||
}
|
||||
|
||||
// positive scroll offsets
|
||||
return layoutChildSequence(
|
||||
child: center,
|
||||
scrollOffset: math.max(0.0, -centerOffset),
|
||||
overlap:
|
||||
leadingNegativeChild == null ? math.min(0.0, -centerOffset) : 0.0,
|
||||
layoutOffset: centerOffset >= mainAxisExtent
|
||||
? centerOffset
|
||||
: reverseDirectionRemainingPaintExtent,
|
||||
remainingPaintExtent: forwardDirectionRemainingPaintExtent,
|
||||
mainAxisExtent: mainAxisExtent,
|
||||
crossAxisExtent: crossAxisExtent,
|
||||
growthDirection: GrowthDirection.forward,
|
||||
advance: childAfter,
|
||||
remainingCacheExtent: forwardDirectionRemainingCacheExtent,
|
||||
cacheOrigin: centerOffset.clamp(-_calculatedCacheExtent!, 0.0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get hasVisualOverflow => _hasVisualOverflow;
|
||||
|
||||
@override
|
||||
void updateOutOfBandData(
|
||||
GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
_maxScrollExtent += childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
_minScrollExtent -= childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
}
|
||||
if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true;
|
||||
_shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
|
||||
growSize = _shrinkWrapExtent;
|
||||
}
|
||||
|
||||
@override
|
||||
String labelForChild(int index) => 'child $index';
|
||||
}
|
||||
|
||||
/// A widget that is bigger on the inside.
|
||||
///
|
||||
/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a
|
||||
/// subset of its children according to its own dimensions and the given
|
||||
/// [offset]. As the offset varies, different children are visible through
|
||||
/// the viewport.
|
||||
///
|
||||
/// [Viewport] hosts a bidirectional list of slivers, anchored on a [center]
|
||||
/// sliver, which is placed at the zero scroll offset. The center widget is
|
||||
/// displayed in the viewport according to the [anchor] property.
|
||||
///
|
||||
/// Slivers that are earlier in the child list than [center] are displayed in
|
||||
/// reverse order in the reverse [axisDirection] starting from the [center]. For
|
||||
/// example, if the [axisDirection] is [AxisDirection.down], the first sliver
|
||||
/// before [center] is placed above the [center]. The slivers that are later in
|
||||
/// the child list than [center] are placed in order in the [axisDirection]. For
|
||||
/// example, in the preceding scenario, the first sliver after [center] is
|
||||
/// placed below the [center].
|
||||
///
|
||||
/// [Viewport] cannot contain box children directly. Instead, use a
|
||||
/// [SliverList], [SliverFixedExtentList], [SliverGrid], or a
|
||||
/// [SliverToBoxAdapter], for example.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine
|
||||
/// [Scrollable] and [Viewport] into widgets that are easier to use.
|
||||
/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a
|
||||
/// sliver context (the opposite of this widget).
|
||||
/// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its
|
||||
/// contents along the main axis.
|
||||
abstract class CustomViewport extends MultiChildRenderObjectWidget {
|
||||
/// Creates a widget that is bigger on the inside.
|
||||
///
|
||||
/// The viewport listens to the [offset], which means you do not need to
|
||||
/// rebuild this widget when the [offset] changes.
|
||||
///
|
||||
/// The [offset] argument must not be null.
|
||||
///
|
||||
/// The [cacheExtent] must be specified if the [cacheExtentStyle] is
|
||||
/// not [CacheExtentStyle.pixel].
|
||||
CustomViewport({
|
||||
Key? key,
|
||||
this.axisDirection = AxisDirection.down,
|
||||
this.crossAxisDirection,
|
||||
this.anchor = 0.0,
|
||||
required this.offset,
|
||||
this.center,
|
||||
this.cacheExtent,
|
||||
this.cacheExtentStyle = CacheExtentStyle.pixel,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
List<Widget> slivers = const <Widget>[],
|
||||
}) : assert(offset != null),
|
||||
assert(slivers != null),
|
||||
assert(center == null ||
|
||||
slivers.where((Widget child) => child.key == center).length == 1),
|
||||
assert(cacheExtentStyle != null),
|
||||
assert(cacheExtentStyle != CacheExtentStyle.viewport ||
|
||||
cacheExtent != null),
|
||||
assert(clipBehavior != null),
|
||||
super(key: key, children: slivers);
|
||||
|
||||
/// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
|
||||
///
|
||||
/// For example, if the [axisDirection] is [AxisDirection.down], a scroll
|
||||
/// offset of zero is at the top of the viewport and increases towards the
|
||||
/// bottom of the viewport.
|
||||
final AxisDirection axisDirection;
|
||||
|
||||
/// The direction in which child should be laid out in the cross axis.
|
||||
///
|
||||
/// If the [axisDirection] is [AxisDirection.down] or [AxisDirection.up], this
|
||||
/// property defaults to [AxisDirection.left] if the ambient [Directionality]
|
||||
/// is [TextDirection.rtl] and [AxisDirection.right] if the ambient
|
||||
/// [Directionality] is [TextDirection.ltr].
|
||||
///
|
||||
/// If the [axisDirection] is [AxisDirection.left] or [AxisDirection.right],
|
||||
/// this property defaults to [AxisDirection.down].
|
||||
final AxisDirection? crossAxisDirection;
|
||||
|
||||
/// The relative position of the zero scroll offset.
|
||||
///
|
||||
/// For example, if [anchor] is 0.5 and the [axisDirection] is
|
||||
/// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is
|
||||
/// vertically centered within the viewport. If the [anchor] is 1.0, and the
|
||||
/// [axisDirection] is [AxisDirection.right], then the zero scroll offset is
|
||||
/// on the left edge of the viewport.
|
||||
final double anchor;
|
||||
|
||||
/// Which part of the content inside the viewport should be visible.
|
||||
///
|
||||
/// The [ViewportOffset.pixels] value determines the scroll offset that the
|
||||
/// viewport uses to select which part of its content to display. As the user
|
||||
/// scrolls the viewport, this value changes, which changes the content that
|
||||
/// is displayed.
|
||||
///
|
||||
/// Typically a [ScrollPosition].
|
||||
final ViewportOffset offset;
|
||||
|
||||
/// The first child in the [GrowthDirection.forward] growth direction.
|
||||
///
|
||||
/// Children after [center] will be placed in the [axisDirection] relative to
|
||||
/// the [center]. Children before [center] will be placed in the opposite of
|
||||
/// the [axisDirection] relative to the [center].
|
||||
///
|
||||
/// The [center] must be the key of a child of the viewport.
|
||||
final Key? center;
|
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [cacheExtentStyle], which controls the units of the [cacheExtent].
|
||||
final double? cacheExtent;
|
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtentStyle}
|
||||
final CacheExtentStyle cacheExtentStyle;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// 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:
|
||||
assert(debugCheckHasDirectionality(
|
||||
context,
|
||||
why:
|
||||
'to determine the cross-axis direction when the viewport has an \'up\' axisDirection',
|
||||
alternative:
|
||||
'Alternatively, consider specifying the \'crossAxisDirection\' argument on the Viewport.',
|
||||
));
|
||||
return textDirectionToAxisDirection(Directionality.of(context));
|
||||
case AxisDirection.right:
|
||||
return AxisDirection.down;
|
||||
case AxisDirection.down:
|
||||
assert(debugCheckHasDirectionality(
|
||||
context,
|
||||
why:
|
||||
'to determine the cross-axis direction when the viewport has a \'down\' axisDirection',
|
||||
alternative:
|
||||
'Alternatively, consider specifying the \'crossAxisDirection\' argument on the Viewport.',
|
||||
));
|
||||
return textDirectionToAxisDirection(Directionality.of(context));
|
||||
case AxisDirection.left:
|
||||
return AxisDirection.down;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
CustomRenderViewport createRenderObject(BuildContext context);
|
||||
|
||||
@override
|
||||
_ViewportElement createElement() => _ViewportElement(this);
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
|
||||
properties.add(EnumProperty<AxisDirection>(
|
||||
'crossAxisDirection', crossAxisDirection,
|
||||
defaultValue: null));
|
||||
properties.add(DoubleProperty('anchor', anchor));
|
||||
properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
|
||||
if (center != null) {
|
||||
properties.add(DiagnosticsProperty<Key>('center', center));
|
||||
} else if (children.isNotEmpty && children.first.key != null) {
|
||||
properties.add(DiagnosticsProperty<Key>('center', children.first.key,
|
||||
tooltip: 'implicit'));
|
||||
}
|
||||
properties.add(DiagnosticsProperty<double>('cacheExtent', cacheExtent));
|
||||
properties.add(DiagnosticsProperty<CacheExtentStyle>(
|
||||
'cacheExtentStyle', cacheExtentStyle));
|
||||
}
|
||||
}
|
||||
|
||||
class _ViewportElement extends MultiChildRenderObjectElement {
|
||||
/// Creates an element that uses the given widget as its configuration.
|
||||
_ViewportElement(CustomViewport widget) : super(widget);
|
||||
|
||||
@override
|
||||
CustomViewport get widget => super.widget as CustomViewport;
|
||||
|
||||
@override
|
||||
CustomRenderViewport get renderObject =>
|
||||
super.renderObject as CustomRenderViewport;
|
||||
|
||||
@override
|
||||
void mount(Element? parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
_updateCenter();
|
||||
}
|
||||
|
||||
@override
|
||||
void update(MultiChildRenderObjectWidget newWidget) {
|
||||
super.update(newWidget);
|
||||
_updateCenter();
|
||||
}
|
||||
|
||||
void _updateCenter() {
|
||||
if (widget.center != null) {
|
||||
renderObject.center = children
|
||||
.singleWhere((Element element) => element.widget.key == widget.center)
|
||||
.renderObject as RenderSliver?;
|
||||
} else if (children.isNotEmpty) {
|
||||
renderObject.center = children.first.renderObject as RenderSliver?;
|
||||
} else {
|
||||
renderObject.center = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugVisitOnstageChildren(ElementVisitor visitor) {
|
||||
children.where((Element e) {
|
||||
final RenderSliver renderSliver = e.renderObject! as RenderSliver;
|
||||
return renderSliver.geometry!.visible;
|
||||
}).forEach(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSliverPhysicalContainerParentData
|
||||
extends SliverPhysicalContainerParentData {
|
||||
/// The position of the child relative to the zero scroll offset.
|
||||
///
|
||||
/// The number of pixels from from the zero scroll offset of the parent sliver
|
||||
/// (the line at which its [SliverConstraints.scrollOffset] is zero) to the
|
||||
/// side of the child closest to that offset. A [layoutOffset] can be null
|
||||
/// when it cannot be determined. The value will be set after layout.
|
||||
///
|
||||
/// In a typical list, this does not change as the parent is scrolled.
|
||||
///
|
||||
/// Defaults to null.
|
||||
double? layoutOffset;
|
||||
|
||||
GrowthDirection? growthDirection;
|
||||
}
|
||||
|
||||
/// A render object that is bigger on the inside.
|
||||
///
|
||||
/// [RenderViewport] is the visual workhorse of the scrolling machinery. It
|
||||
/// displays a subset of its children according to its own dimensions and the
|
||||
/// given [offset]. As the offset varies, different children are visible through
|
||||
/// the viewport.
|
||||
///
|
||||
/// [RenderViewport] hosts a bidirectional list of slivers, anchored on a
|
||||
/// [center] sliver, which is placed at the zero scroll offset. The center
|
||||
/// widget is displayed in the viewport according to the [anchor] property.
|
||||
///
|
||||
/// Slivers that are earlier in the child list than [center] are displayed in
|
||||
/// reverse order in the reverse [axisDirection] starting from the [center]. For
|
||||
/// example, if the [axisDirection] is [AxisDirection.down], the first sliver
|
||||
/// before [center] is placed above the [center]. The slivers that are later in
|
||||
/// the child list than [center] are placed in order in the [axisDirection]. For
|
||||
/// example, in the preceding scenario, the first sliver after [center] is
|
||||
/// placed below the [center].
|
||||
///
|
||||
/// [RenderViewport] cannot contain [RenderBox] children directly. Instead, use
|
||||
/// a [RenderSliverList], [RenderSliverFixedExtentList], [RenderSliverGrid], or
|
||||
/// a [RenderSliverToBoxAdapter], for example.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RenderSliver], which explains more about the Sliver protocol.
|
||||
/// * [RenderBox], which explains more about the Box protocol.
|
||||
/// * [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
|
||||
/// placed inside a [RenderSliver] (the opposite of this class).
|
||||
/// * [RenderShrinkWrappingViewport], a variant of [RenderViewport] that
|
||||
/// shrink-wraps its contents along the main axis.
|
||||
abstract class CustomRenderViewport
|
||||
extends RenderViewportBase<CustomSliverPhysicalContainerParentData> {
|
||||
/// Creates a viewport for [RenderSliver] objects.
|
||||
///
|
||||
/// If the [center] is not specified, then the first child in the `children`
|
||||
/// list, if any, is used.
|
||||
///
|
||||
/// The [offset] must be specified. For testing purposes, consider passing a
|
||||
/// [ViewportOffset.zero] or [ViewportOffset.fixed].
|
||||
CustomRenderViewport({
|
||||
AxisDirection axisDirection = AxisDirection.down,
|
||||
required AxisDirection crossAxisDirection,
|
||||
required ViewportOffset offset,
|
||||
double anchor = 0.0,
|
||||
List<RenderSliver>? children,
|
||||
RenderSliver? center,
|
||||
double? cacheExtent,
|
||||
CacheExtentStyle cacheExtentStyle = CacheExtentStyle.pixel,
|
||||
Clip clipBehavior = Clip.hardEdge,
|
||||
}) : assert(anchor != null),
|
||||
assert(anchor >= 0.0 && anchor <= 1.0),
|
||||
assert(cacheExtentStyle != CacheExtentStyle.viewport ||
|
||||
cacheExtent != null),
|
||||
assert(clipBehavior != null),
|
||||
_center = center,
|
||||
super(
|
||||
axisDirection: axisDirection,
|
||||
crossAxisDirection: crossAxisDirection,
|
||||
offset: offset,
|
||||
cacheExtent: cacheExtent,
|
||||
cacheExtentStyle: cacheExtentStyle,
|
||||
clipBehavior: clipBehavior,
|
||||
) {
|
||||
addAll(children);
|
||||
if (center == null && firstChild != null) _center = firstChild;
|
||||
}
|
||||
|
||||
/// If a [RenderAbstractViewport] overrides
|
||||
/// [RenderObject.describeSemanticsConfiguration] to add the [SemanticsTag]
|
||||
/// [useTwoPaneSemantics] to its [SemanticsConfiguration], two semantics nodes
|
||||
/// will be used to represent the viewport with its associated scrolling
|
||||
/// actions in the semantics tree.
|
||||
///
|
||||
/// Two semantics nodes (an inner and an outer node) are necessary to exclude
|
||||
/// certain child nodes (via the [excludeFromScrolling] tag) from the
|
||||
/// scrollable area for semantic purposes: The [SemanticsNode]s of children
|
||||
/// that should be excluded from scrolling will be attached to the outer node.
|
||||
/// The semantic scrolling actions and the [SemanticsNode]s of scrollable
|
||||
/// children will be attached to the inner node, which itself is a child of
|
||||
/// the outer node.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RenderViewportBase.describeSemanticsConfiguration], which adds this
|
||||
/// tag to its [SemanticsConfiguration].
|
||||
static const SemanticsTag useTwoPaneSemantics =
|
||||
SemanticsTag('RenderViewport.twoPane');
|
||||
|
||||
/// When a top-level [SemanticsNode] below a [RenderAbstractViewport] is
|
||||
/// tagged with [excludeFromScrolling] it will not be part of the scrolling
|
||||
/// area for semantic purposes.
|
||||
///
|
||||
/// This behavior is only active if the [RenderAbstractViewport]
|
||||
/// tagged its [SemanticsConfiguration] with [useTwoPaneSemantics].
|
||||
/// Otherwise, the [excludeFromScrolling] tag is ignored.
|
||||
///
|
||||
/// As an example, a [RenderSliver] that stays on the screen within a
|
||||
/// [Scrollable] even though the user has scrolled past it (e.g. a pinned app
|
||||
/// bar) can tag its [SemanticsNode] with [excludeFromScrolling] to indicate
|
||||
/// that it should no longer be considered for semantic actions related to
|
||||
/// scrolling.
|
||||
static const SemanticsTag excludeFromScrolling =
|
||||
SemanticsTag('RenderViewport.excludeFromScrolling');
|
||||
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
if (child.parentData is! CustomSliverPhysicalContainerParentData)
|
||||
child.parentData = CustomSliverPhysicalContainerParentData();
|
||||
}
|
||||
|
||||
/// The relative position of the zero scroll offset.
|
||||
///
|
||||
/// For example, if [anchor] is 0.5 and the [axisDirection] is
|
||||
/// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is
|
||||
/// vertically centered within the viewport. If the [anchor] is 1.0, and the
|
||||
/// [axisDirection] is [AxisDirection.right], then the zero scroll offset is
|
||||
/// on the left edge of the viewport.
|
||||
double get anchor;
|
||||
|
||||
set anchor(double value);
|
||||
|
||||
/// The first child in the [GrowthDirection.forward] growth direction.
|
||||
///
|
||||
/// This child that will be at the position defined by [anchor] when the
|
||||
/// [ViewportOffset.pixels] of [offset] is `0`.
|
||||
///
|
||||
/// Children after [center] will be placed in the [axisDirection] relative to
|
||||
/// the [center]. Children before [center] will be placed in the opposite of
|
||||
/// the [axisDirection] relative to the [center].
|
||||
///
|
||||
/// The [center] must be a child of the viewport.
|
||||
RenderSliver? get center => _center;
|
||||
RenderSliver? _center;
|
||||
|
||||
set center(RenderSliver? value) {
|
||||
if (value == _center) return;
|
||||
_center = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get sizedByParent => true;
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
assert(() {
|
||||
if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
|
||||
switch (axis) {
|
||||
case Axis.vertical:
|
||||
if (!constraints.hasBoundedHeight) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Vertical viewport was given unbounded height.'),
|
||||
ErrorDescription(
|
||||
'Viewports expand in the scrolling direction to fill their container. '
|
||||
'In this case, a vertical viewport was given an unlimited amount of '
|
||||
'vertical space in which to expand. This situation typically happens '
|
||||
'when a scrollable widget is nested inside another scrollable widget.'),
|
||||
ErrorHint(
|
||||
'If this widget is always nested in a scrollable widget there '
|
||||
'is no need to use a viewport because there will always be enough '
|
||||
'vertical space for the children. In this case, consider using a '
|
||||
'Column instead. Otherwise, consider using the "shrinkWrap" property '
|
||||
'(or a ShrinkWrappingViewport) to size the height of the viewport '
|
||||
'to the sum of the heights of its children.')
|
||||
]);
|
||||
}
|
||||
if (!constraints.hasBoundedWidth) {
|
||||
throw FlutterError(
|
||||
'Vertical viewport was given unbounded width.\n'
|
||||
'Viewports expand in the cross axis to fill their container and '
|
||||
'constrain their children to match their extent in the cross axis. '
|
||||
'In this case, a vertical viewport was given an unlimited amount of '
|
||||
'horizontal space in which to expand.');
|
||||
}
|
||||
break;
|
||||
case Axis.horizontal:
|
||||
if (!constraints.hasBoundedWidth) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('Horizontal viewport was given unbounded width.'),
|
||||
ErrorDescription(
|
||||
'Viewports expand in the scrolling direction to fill their container. '
|
||||
'In this case, a horizontal viewport was given an unlimited amount of '
|
||||
'horizontal space in which to expand. This situation typically happens '
|
||||
'when a scrollable widget is nested inside another scrollable widget.'),
|
||||
ErrorHint(
|
||||
'If this widget is always nested in a scrollable widget there '
|
||||
'is no need to use a viewport because there will always be enough '
|
||||
'horizontal space for the children. In this case, consider using a '
|
||||
'Row instead. Otherwise, consider using the "shrinkWrap" property '
|
||||
'(or a ShrinkWrappingViewport) to size the width of the viewport '
|
||||
'to the sum of the widths of its children.')
|
||||
]);
|
||||
}
|
||||
if (!constraints.hasBoundedHeight) {
|
||||
throw FlutterError(
|
||||
'Horizontal viewport was given unbounded height.\n'
|
||||
'Viewports expand in the cross axis to fill their container and '
|
||||
'constrain their children to match their extent in the cross axis. '
|
||||
'In this case, a horizontal viewport was given an unlimited amount of '
|
||||
'vertical space in which to expand.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return constraints.biggest;
|
||||
}
|
||||
|
||||
// Out-of-band data computed during layout.
|
||||
late double _minScrollExtent;
|
||||
late double _maxScrollExtent;
|
||||
bool _hasVisualOverflow = false;
|
||||
|
||||
double growSize = 0;
|
||||
|
||||
@override
|
||||
bool get hasVisualOverflow => _hasVisualOverflow;
|
||||
|
||||
@override
|
||||
void updateOutOfBandData(
|
||||
GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
_maxScrollExtent += childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
case GrowthDirection.reverse:
|
||||
_minScrollExtent -= childLayoutGeometry.scrollExtent;
|
||||
break;
|
||||
}
|
||||
if (childLayoutGeometry.hasVisualOverflow) _hasVisualOverflow = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateChildLayoutOffset(RenderSliver child, double layoutOffset,
|
||||
GrowthDirection growthDirection) {
|
||||
final CustomSliverPhysicalContainerParentData childParentData =
|
||||
child.parentData! as CustomSliverPhysicalContainerParentData;
|
||||
childParentData.layoutOffset = layoutOffset;
|
||||
childParentData.growthDirection = growthDirection;
|
||||
}
|
||||
|
||||
@override
|
||||
Offset paintOffsetOf(RenderSliver child) {
|
||||
final CustomSliverPhysicalContainerParentData childParentData =
|
||||
child.parentData! as CustomSliverPhysicalContainerParentData;
|
||||
return computeAbsolutePaintOffset(
|
||||
child, childParentData.layoutOffset!, childParentData.growthDirection!);
|
||||
}
|
||||
|
||||
@override
|
||||
double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
|
||||
assert(child.parent == this);
|
||||
final GrowthDirection growthDirection = child.constraints.growthDirection;
|
||||
switch (growthDirection) {
|
||||
case GrowthDirection.forward:
|
||||
double scrollOffsetToChild = 0.0;
|
||||
RenderSliver? current = center;
|
||||
while (current != child) {
|
||||
scrollOffsetToChild += current!.geometry!.scrollExtent;
|
||||
current = childAfter(current);
|
||||
}
|
||||
return scrollOffsetToChild + scrollOffsetWithinChild;
|
||||
case GrowthDirection.reverse:
|
||||
double scrollOffsetToChild = 0.0;
|
||||
RenderSliver? current = childBefore(center!);
|
||||
while (current != child) {
|
||||
scrollOffsetToChild -= current!.geometry!.scrollExtent;
|
||||
current = childBefore(current);
|
||||
}
|
||||
return scrollOffsetToChild - scrollOffsetWithinChild;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double maxScrollObstructionExtentBefore(RenderSliver child) {
|
||||
assert(child.parent == this);
|
||||
final GrowthDirection growthDirection = child.constraints.growthDirection;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderObject child, Matrix4 transform) {
|
||||
final Offset offset = paintOffsetOf(child as RenderSliver);
|
||||
transform.translate(offset.dx, offset.dy);
|
||||
}
|
||||
|
||||
@override
|
||||
double computeChildMainAxisPosition(
|
||||
RenderSliver child, double parentMainAxisPosition) {
|
||||
final CustomSliverPhysicalContainerParentData childParentData =
|
||||
child.parentData! as CustomSliverPhysicalContainerParentData;
|
||||
switch (applyGrowthDirectionToAxisDirection(
|
||||
child.constraints.axisDirection, child.constraints.growthDirection)) {
|
||||
case AxisDirection.down:
|
||||
case AxisDirection.right:
|
||||
return parentMainAxisPosition - childParentData.layoutOffset!;
|
||||
case AxisDirection.up:
|
||||
return (size.height - parentMainAxisPosition) -
|
||||
childParentData.layoutOffset!;
|
||||
case AxisDirection.left:
|
||||
return (size.width - parentMainAxisPosition) -
|
||||
childParentData.layoutOffset!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get indexOfFirstChild {
|
||||
assert(center != null);
|
||||
assert(center!.parent == this);
|
||||
assert(firstChild != null);
|
||||
int count = 0;
|
||||
RenderSliver? child = center;
|
||||
while (child != firstChild) {
|
||||
count -= 1;
|
||||
child = childBefore(child!);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@override
|
||||
String labelForChild(int index) {
|
||||
if (index == 0) return 'center child';
|
||||
return 'child $index';
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<RenderSliver> get childrenInPaintOrder sync* {
|
||||
if (firstChild == null) return;
|
||||
RenderSliver? child = firstChild;
|
||||
while (child != center) {
|
||||
yield child!;
|
||||
child = childAfter(child);
|
||||
}
|
||||
child = lastChild;
|
||||
while (true) {
|
||||
yield child!;
|
||||
if (child == center) return;
|
||||
child = childBefore(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<RenderSliver> get childrenInHitTestOrder sync* {
|
||||
if (firstChild == null) return;
|
||||
RenderSliver? child = center;
|
||||
while (child != null) {
|
||||
yield child;
|
||||
child = childAfter(child);
|
||||
}
|
||||
child = childBefore(center!);
|
||||
while (child != null) {
|
||||
yield child;
|
||||
child = childBefore(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('anchor', anchor));
|
||||
}
|
||||
}
|
||||
19
mobile/third_party/scrollable_positioned_list/pubspec.yaml
vendored
Normal file
19
mobile/third_party/scrollable_positioned_list/pubspec.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: scrollable_positioned_list
|
||||
version: 0.3.8
|
||||
description: >
|
||||
A list with helper methods to programmatically scroll to an item.
|
||||
homepage: https://github.com/google/flutter.widgets/tree/master/packages/scrollable_positioned_list
|
||||
|
||||
environment:
|
||||
sdk: '>=2.15.0 <4.0.0'
|
||||
flutter: '>=2.12.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
collection: ^1.15.0
|
||||
|
||||
dev_dependencies:
|
||||
pedantic: ^1.10.0-0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
Reference in New Issue
Block a user