diff --git a/lib/common/widgets/flutter/chat_list_view.dart b/lib/common/widgets/flutter/chat_list_view.dart index 540aaef0c..45d8b38e4 100644 --- a/lib/common/widgets/flutter/chat_list_view.dart +++ b/lib/common/widgets/flutter/chat_list_view.dart @@ -4,8 +4,9 @@ import 'dart:math' as math; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide BoxScrollView; import 'package:flutter/rendering.dart'; class ChatListView extends BoxScrollView { diff --git a/lib/common/widgets/flutter/scroll_view/scroll_view.dart b/lib/common/widgets/flutter/scroll_view/scroll_view.dart new file mode 100644 index 000000000..620b4cdc8 --- /dev/null +++ b/lib/common/widgets/flutter/scroll_view/scroll_view.dart @@ -0,0 +1,2175 @@ +// Copyright 2014 The Flutter 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:PiliPlus/common/widgets/flutter/scroll_view/scrollable.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart' hide Scrollable; + +/// A widget that combines a [Scrollable] and a [Viewport] to create an +/// interactive scrolling pane of content in one dimension. +/// +/// Scrollable widgets consist of three pieces: +/// +/// 1. A [Scrollable] widget, which listens for various user gestures and +/// implements the interaction design for scrolling. +/// 2. A viewport widget, such as [Viewport] or [ShrinkWrappingViewport], which +/// implements the visual design for scrolling by displaying only a portion +/// of the widgets inside the scroll view. +/// 3. One or more slivers, which are widgets that can be composed to created +/// various scrolling effects, such as lists, grids, and expanding headers. +/// +/// [ScrollView] helps orchestrate these pieces by creating the [Scrollable] and +/// the viewport and deferring to its subclass to create the slivers. +/// +/// To learn more about slivers, see [ExtendedCustomScrollView.slivers]. +/// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// +/// {@template flutter.widgets.ScrollView.PageStorage} +/// ## Persisting the scroll position during a session +/// +/// Scroll views attempt to persist their scroll position using [PageStorage]. +/// This can be disabled by setting [ScrollController.keepScrollOffset] to false +/// on the [controller]. If it is enabled, using a [PageStorageKey] for the +/// [key] of this widget is recommended to help disambiguate different scroll +/// views from each other. +/// {@endtemplate} +/// +/// See also: +/// +/// * [ExtendedListView], which is a commonly used [ScrollView] that displays a +/// scrolling, linear list of child widgets. +/// * [PageView], which is a scrolling list of child widgets that are each the +/// size of the viewport. +/// * [ExtendedGridView], which is a [ScrollView] that displays a scrolling, 2D array +/// of child widgets. +/// * [ExtendedCustomScrollView], which is a [ScrollView] that creates custom scroll +/// effects using slivers. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +/// * [TwoDimensionalScrollView], which is a similar widget [ScrollView] that +/// scrolls in two dimensions. +abstract class ScrollView extends StatelessWidget { + /// Creates a widget that scrolls. + /// + /// The [ScrollView.primary] argument defaults to true for vertical + /// scroll views if no [controller] has been provided. The [controller] argument + /// must be null if [primary] is explicitly set to true. If [primary] is true, + /// the nearest [PrimaryScrollController] surrounding the widget is attached + /// to this scroll view. + /// + /// If the [shrinkWrap] argument is true, the [center] argument must be null. + /// + /// The [anchor] argument must be in the range zero to one, inclusive. + const ScrollView({ + super.key, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + ScrollPhysics? physics, + this.scrollBehavior, + this.shrinkWrap = false, + this.center, + this.anchor = 0.0, + this.cacheExtent, + this.semanticChildCount, + this.paintOrder = SliverPaintOrder.firstIsTop, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.hitTestBehavior = HitTestBehavior.opaque, + }) : assert( + !(controller != null && (primary ?? false)), + 'Primary ScrollViews obtain their ScrollController via inheritance ' + 'from a PrimaryScrollController widget. You cannot both set primary to ' + 'true and pass an explicit controller.', + ), + assert(!shrinkWrap || center == null), + assert(anchor >= 0.0 && anchor <= 1.0), + assert(semanticChildCount == null || semanticChildCount >= 0), + physics = + physics ?? + ((primary ?? false) || + (primary == null && + controller == null && + identical(scrollDirection, Axis.vertical)) + ? const AlwaysScrollableScrollPhysics() + : null); + + /// {@template flutter.widgets.scroll_view.scrollDirection} + /// The [Axis] along which the scroll view's offset increases. + /// + /// For the direction in which active scrolling may be occurring, see + /// [ScrollDirection]. + /// + /// Defaults to [Axis.vertical]. + /// {@endtemplate} + final Axis scrollDirection; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if the reading direction is left-to-right and + /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from + /// left to right when [reverse] is false and from right to left when + /// [reverse] is true. + /// + /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? controller; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the application, + /// the ScrollAction will be evaluated using this scroll view, for example, + /// when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// + /// Cannot be true while a [ScrollController] is provided to `controller`, + /// only one ScrollController can be associated with a ScrollView. + /// + /// Setting to false will explicitly prevent inheriting any + /// [PrimaryScrollController]. + /// + /// Defaults to null. When null, and a controller is not provided, + /// [PrimaryScrollController.shouldInherit] is used to decide automatic + /// inheritance. + /// + /// By default, the [PrimaryScrollController] that is injected by each + /// [ModalRoute] is configured to automatically be inherited on + /// [TargetPlatformVariant.mobile] for ScrollViews in the [Axis.vertical] + /// scroll direction. Adding another to your app will override the + /// PrimaryScrollController above it. + /// + /// The following video contains more information about scroll controllers, + /// the PrimaryScrollController widget, and their impact on your apps: + /// + /// {@youtube 560 315 https://www.youtube.com/watch?v=33_0ABjFJUU} + /// + /// {@endtemplate} + final bool? primary; + + /// {@template flutter.widgets.scroll_view.physics} + /// 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. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.widgets.scrollable.scrollBehavior} + final ScrollBehavior? scrollBehavior; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// + /// {@youtube 560 315 https://www.youtube.com/watch?v=LUqDNnv_dh0} + /// {@endtemplate} + final bool shrinkWrap; + + /// The first child in the [GrowthDirection.forward] growth direction. + /// + /// Children after [center] will be placed in the [AxisDirection] determined + /// by [scrollDirection] and [reverse] relative to the [center]. Children + /// before [center] will be placed in the opposite of the axis direction + /// relative to the [center]. This makes the [center] the inflection point of + /// the growth direction. + /// + /// The [center] must be the key of one of the slivers built by [buildSlivers]. + /// + /// Of the built-in subclasses of [ScrollView], only [ExtendedCustomScrollView] + /// supports [center]; for that class, the given key must be the key of one of + /// the slivers in the [ExtendedCustomScrollView.slivers] list. + /// + /// Most scroll views by default are ordered [GrowthDirection.forward]. + /// Changing the default values of [ScrollView.anchor], + /// [ScrollView.center], or both, can configure a scroll view for + /// [GrowthDirection.reverse]. + /// + /// {@tool dartpad} + /// This sample shows a [ExtendedCustomScrollView], with [Radio] buttons in the + /// [AppBar.bottom] that change the [AxisDirection] to illustrate different + /// configurations. The [ExtendedCustomScrollView.anchor] and [ExtendedCustomScrollView.center] + /// properties are also set to have the 0 scroll offset positioned in the middle + /// of the viewport, with [GrowthDirection.forward] and [GrowthDirection.reverse] + /// illustrated on either side. The sliver that shares the + /// [ExtendedCustomScrollView.center] key is positioned at the [ExtendedCustomScrollView.anchor]. + /// + /// ** See code in examples/api/lib/rendering/growth_direction/growth_direction.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [anchor], which controls where the [center] as aligned in the viewport. + final Key? center; + + /// {@template flutter.widgets.scroll_view.anchor} + /// The relative position of the zero scroll offset. + /// + /// For example, if [anchor] is 0.5 and the [AxisDirection] determined by + /// [scrollDirection] and [reverse] 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 axis direction is + /// [AxisDirection.right], then the zero scroll offset is on the left edge of + /// the viewport. + /// + /// Most scroll views by default are ordered [GrowthDirection.forward]. + /// Changing the default values of [ScrollView.anchor], + /// [ScrollView.center], or both, can configure a scroll view for + /// [GrowthDirection.reverse]. + /// + /// {@tool dartpad} + /// This sample shows a [ExtendedCustomScrollView], with [Radio] buttons in the + /// [AppBar.bottom] that change the [AxisDirection] to illustrate different + /// configurations. The [ExtendedCustomScrollView.anchor] and [ExtendedCustomScrollView.center] + /// properties are also set to have the 0 scroll offset positioned in the middle + /// of the viewport, with [GrowthDirection.forward] and [GrowthDirection.reverse] + /// illustrated on either side. The sliver that shares the + /// [ExtendedCustomScrollView.center] key is positioned at the [ExtendedCustomScrollView.anchor]. + /// + /// ** See code in examples/api/lib/rendering/growth_direction/growth_direction.0.dart ** + /// {@end-tool} + /// {@endtemplate} + final double anchor; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// The number of children that will contribute semantic information. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ExtendedListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [ExtendedCustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. If the + /// number is unknown or unbounded this should be left unset or set to null. + /// + /// See also: + /// + /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. + final int? semanticChildCount; + + /// {@macro flutter.rendering.RenderViewportBase.paintOrder} + /// + /// Defaults to [SliverPaintOrder.firstIsTop]. + final SliverPaintOrder paintOrder; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// The [ScrollViewKeyboardDismissBehavior] defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + /// + /// If [keyboardDismissBehavior] is null then it will fallback to + /// [scrollBehavior]. If that is also null, the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior] will be used. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// {@macro flutter.widgets.scrollable.hitTestBehavior} + /// + /// Defaults to [HitTestBehavior.opaque]. + final HitTestBehavior hitTestBehavior; + + /// Returns the [AxisDirection] in which the scroll view scrolls. + /// + /// Combines the [scrollDirection] with the [reverse] boolean to obtain the + /// concrete [AxisDirection]. + /// + /// If the [scrollDirection] is [Axis.horizontal], the ambient + /// [Directionality] is also considered when selecting the concrete + /// [AxisDirection]. For example, if the ambient [Directionality] is + /// [TextDirection.rtl], then the non-reversed [AxisDirection] is + /// [AxisDirection.left] and the reversed [AxisDirection] is + /// [AxisDirection.right]. + @protected + AxisDirection getDirection(BuildContext context) { + return getAxisDirectionFromAxisReverseAndDirectionality( + context, + scrollDirection, + reverse, + ); + } + + /// Build the list of widgets to place inside the viewport. + /// + /// Subclasses should override this method to build the slivers for the inside + /// of the viewport. + /// + /// To learn more about slivers, see [ExtendedCustomScrollView.slivers]. + @protected + List buildSlivers(BuildContext context); + + /// Build the viewport. + /// + /// Subclasses may override this method to change how the viewport is built. + /// The default implementation uses a [ShrinkWrappingViewport] if [shrinkWrap] + /// is true, and a regular [Viewport] otherwise. + /// + /// The `offset` argument is the value obtained from + /// [Scrollable.viewportBuilder]. + /// + /// The `axisDirection` argument is the value obtained from [getDirection], + /// which by default uses [scrollDirection] and [reverse]. + /// + /// The `slivers` argument is the value obtained from [buildSlivers]. + @protected + Widget buildViewport( + BuildContext context, + ViewportOffset offset, + AxisDirection axisDirection, + List slivers, + ) { + assert(() { + switch (axisDirection) { + case AxisDirection.up: + case AxisDirection.down: + return debugCheckHasDirectionality( + context, + why: 'to determine the cross-axis direction of the scroll view', + hint: + 'Vertical scroll views create Viewport widgets that try to determine their cross axis direction ' + 'from the ambient Directionality.', + ); + case AxisDirection.left: + case AxisDirection.right: + return true; + } + }()); + if (shrinkWrap) { + return ShrinkWrappingViewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + paintOrder: paintOrder, + clipBehavior: clipBehavior, + ); + } + return Viewport( + axisDirection: axisDirection, + offset: offset, + slivers: slivers, + cacheExtent: cacheExtent, + center: center, + anchor: anchor, + paintOrder: paintOrder, + clipBehavior: clipBehavior, + ); + } + + @override + Widget build(BuildContext context) { + final List slivers = buildSlivers(context); + final AxisDirection axisDirection = getDirection(context); + + final bool effectivePrimary = + primary ?? + controller == null && + PrimaryScrollController.shouldInherit(context, scrollDirection); + + final ScrollController? scrollController = effectivePrimary + ? PrimaryScrollController.maybeOf(context) + : controller; + + final scrollable = Scrollable( + dragStartBehavior: dragStartBehavior, + axisDirection: axisDirection, + controller: scrollController, + physics: physics, + scrollBehavior: scrollBehavior, + semanticChildCount: semanticChildCount, + restorationId: restorationId, + hitTestBehavior: hitTestBehavior, + viewportBuilder: (BuildContext context, ViewportOffset offset) { + return buildViewport(context, offset, axisDirection, slivers); + }, + clipBehavior: clipBehavior, + ); + + final Widget scrollableResult = effectivePrimary && scrollController != null + // Further descendant ScrollViews will not inherit the same PrimaryScrollController + ? PrimaryScrollController.none(child: scrollable) + : scrollable; + + final ScrollViewKeyboardDismissBehavior effectiveKeyboardDismissBehavior = + keyboardDismissBehavior ?? + scrollBehavior?.getKeyboardDismissBehavior(context) ?? + ScrollConfiguration.of(context).getKeyboardDismissBehavior(context); + + if (effectiveKeyboardDismissBehavior == + ScrollViewKeyboardDismissBehavior.onDrag) { + return NotificationListener( + child: scrollableResult, + onNotification: (ScrollUpdateNotification notification) { + final FocusScopeNode currentScope = FocusScope.of(context); + if (notification.dragDetails != null && + !currentScope.hasPrimaryFocus && + currentScope.hasFocus) { + FocusManager.instance.primaryFocus?.unfocus(); + } + return false; + }, + ); + } else { + return scrollableResult; + } + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('scrollDirection', scrollDirection)) + ..add( + FlagProperty( + 'reverse', + value: reverse, + ifTrue: 'reversed', + showName: true, + ), + ) + ..add( + DiagnosticsProperty( + 'controller', + controller, + showName: false, + defaultValue: null, + ), + ) + ..add( + FlagProperty( + 'primary', + value: primary, + ifTrue: 'using primary controller', + showName: true, + ), + ) + ..add( + DiagnosticsProperty( + 'physics', + physics, + showName: false, + defaultValue: null, + ), + ) + ..add( + FlagProperty( + 'shrinkWrap', + value: shrinkWrap, + ifTrue: 'shrink-wrapping', + showName: true, + ), + ); + } +} + +/// A [ScrollView] that creates custom scroll effects using [slivers]. +/// +/// A [ExtendedCustomScrollView] lets you supply [slivers] directly to create various +/// scrolling effects, such as lists, grids, and expanding headers. For example, +/// to create a scroll view that contains an expanding app bar followed by a +/// list and a grid, use a list of three slivers: [SliverAppBar], [SliverList], +/// and [SliverGrid]. +/// +/// [Widget]s in these [slivers] must produce [RenderSliver] objects. +/// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// +/// {@animation 400 376 https://flutter.github.io/assets-for-api-docs/assets/widgets/custom_scroll_view.mp4} +/// +/// {@tool snippet} +/// +/// This sample code shows a scroll view that contains a flexible pinned app +/// bar, a grid, and an infinite list. +/// +/// ```dart +/// CustomScrollView( +/// slivers: [ +/// const SliverAppBar( +/// pinned: true, +/// expandedHeight: 250.0, +/// flexibleSpace: FlexibleSpaceBar( +/// title: Text('Demo'), +/// ), +/// ), +/// SliverGrid( +/// gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( +/// maxCrossAxisExtent: 200.0, +/// mainAxisSpacing: 10.0, +/// crossAxisSpacing: 10.0, +/// childAspectRatio: 4.0, +/// ), +/// delegate: SliverChildBuilderDelegate( +/// (BuildContext context, int index) { +/// return Container( +/// alignment: Alignment.center, +/// color: Colors.teal[100 * (index % 9)], +/// child: Text('Grid Item $index'), +/// ); +/// }, +/// childCount: 20, +/// ), +/// ), +/// SliverFixedExtentList( +/// itemExtent: 50.0, +/// delegate: SliverChildBuilderDelegate( +/// (BuildContext context, int index) { +/// return Container( +/// alignment: Alignment.center, +/// color: Colors.lightBlue[100 * (index % 9)], +/// child: Text('List Item $index'), +/// ); +/// }, +/// ), +/// ), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool dartpad} +/// By default, if items are inserted at the "top" of a scrolling container like +/// [ExtendedListView] or [ExtendedCustomScrollView], the top item and all of the items below it +/// are scrolled downwards. In some applications, it's preferable to have the +/// top of the list just grow upwards, without changing the scroll position. +/// This example demonstrates how to do that with a [ExtendedCustomScrollView] with +/// two [SliverList] children, and the [ExtendedCustomScrollView.center] set to the key +/// of the bottom SliverList. The top one SliverList will grow upwards, and the +/// bottom SliverList will grow downwards. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/custom_scroll_view.1.dart ** +/// {@end-tool} +/// +/// ## Accessibility +/// +/// A [ExtendedCustomScrollView] can allow Talkback/VoiceOver to make announcements +/// to the user when the scroll state changes. For example, on Android an +/// announcement might be read as "showing items 1 to 10 of 23". To produce +/// this announcement, the scroll view needs three pieces of information: +/// +/// * The first visible child index. +/// * The total number of children. +/// * The total number of visible children. +/// +/// The last value can be computed exactly by the framework, however the first +/// two must be provided. Most of the higher-level scrollable widgets provide +/// this information automatically. For example, [ExtendedListView] provides each child +/// widget with a semantic index automatically and sets the semantic child +/// count to the length of the list. +/// +/// To determine visible indexes, the scroll view needs a way to associate the +/// generated semantics of each scrollable item with a semantic index. This can +/// be done by wrapping the child widgets in an [IndexedSemantics]. +/// +/// This semantic index is not necessarily the same as the index of the widget in +/// the scrollable, because some widgets may not contribute semantic +/// information. Consider a [ListView.separated]: every other widget is a +/// divider with no semantic information. In this case, only odd numbered +/// widgets have a semantic index (equal to the index ~/ 2). Furthermore, the +/// total number of children in this example would be half the number of +/// widgets. (The [ListView.separated] constructor handles this +/// automatically; this is only used here as an example.) +/// +/// The total number of visible children can be provided by the constructor +/// parameter `semanticChildCount`. This should always be the same as the +/// number of widgets wrapped in [IndexedSemantics]. +/// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// +/// See also: +/// +/// * [SliverList], which is a sliver that displays linear list of children. +/// * [SliverFixedExtentList], which is a more efficient sliver that displays +/// linear list of children that have the same extent along the scroll axis. +/// * [SliverGrid], which is a sliver that displays a 2D array of children. +/// * [SliverPadding], which is a sliver that adds blank space around another +/// sliver. +/// * [SliverAppBar], which is a sliver that displays a header that can expand +/// and float as the scroll view scrolls. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +/// * [IndexedSemantics], which allows annotating child lists with an index +/// for scroll announcements. + +// ignore: camel_case_types +typedef customScrollView = ExtendedCustomScrollView; + +class ExtendedCustomScrollView extends ScrollView { + /// Creates a [ScrollView] that creates custom scroll effects using slivers. + /// + /// See the [ScrollView] constructor for more details on these arguments. + const ExtendedCustomScrollView({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.scrollBehavior, + super.shrinkWrap, + super.center, + super.anchor, + super.cacheExtent, + super.paintOrder, + this.slivers = const [], + super.semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }); + + /// The slivers to place inside the viewport. + /// + /// ## What is a sliver? + /// + /// > _**sliver** (noun): a small, thin piece of something._ + /// + /// A _sliver_ is a widget backed by a [RenderSliver] subclass, i.e. one that + /// implements the constraint/geometry protocol that uses [SliverConstraints] + /// and [SliverGeometry]. + /// + /// This is as distinct from those widgets that are backed by [RenderBox] + /// subclasses, which use [BoxConstraints] and [Size] respectively, and are + /// known as box widgets. (Widgets like [Container], [Row], and [SizedBox] are + /// box widgets.) + /// + /// While boxes are much more straightforward (implementing a simple + /// two-dimensional Cartesian layout system), slivers are much more powerful, + /// and are optimized for one-axis scrolling environments. + /// + /// Slivers are hosted in viewports, also known as scroll views, most notably + /// [ExtendedCustomScrollView]. + /// + /// ## Examples of slivers + /// + /// The Flutter framework has many built-in sliver widgets, and custom widgets + /// can be created in the same manner. By convention, sliver widgets always + /// start with the prefix `Sliver` and are always used in properties called + /// `sliver` or `slivers` (as opposed to `child` and `children` which are used + /// for box widgets). + /// + /// Examples of widgets unique to the sliver world include: + /// + /// * [SliverList], a lazily-loading list of variably-sized box widgets. + /// * [SliverFixedExtentList], a lazily-loading list of box widgets that are + /// all forced to the same height. + /// * [SliverPrototypeExtentList], a lazily-loading list of box widgets that + /// are all forced to the same height as a given prototype widget. + /// * [SliverGrid], a lazily-loading grid of box widgets. + /// * [SliverAnimatedList] and [SliverAnimatedGrid], animated variants of + /// [SliverList] and [SliverGrid]. + /// * [SliverFillRemaining], a widget that fills all remaining space in a + /// scroll view, and lays a box widget out inside that space. + /// * [SliverFillViewport], a widget that lays a list of boxes out, each + /// being sized to fit the whole viewport. + /// * [SliverPersistentHeader], a sliver that implements pinned and floating + /// headers, e.g. used to implement [SliverAppBar]. + /// * [SliverToBoxAdapter], a sliver that wraps a box widget. + /// + /// Examples of sliver variants of common box widgets include: + /// + /// * [SliverOpacity], [SliverAnimatedOpacity], and [SliverFadeTransition], + /// sliver versions of [Opacity], [AnimatedOpacity], and [FadeTransition]. + /// * [SliverIgnorePointer], a sliver version of [IgnorePointer]. + /// * [SliverLayoutBuilder], a sliver version of [LayoutBuilder]. + /// * [SliverOffstage], a sliver version of [Offstage]. + /// * [SliverPadding], a sliver version of [Padding]. + /// * [SliverReorderableList], a sliver version of [ReorderableList] + /// * [SliverSafeArea], a sliver version of [SafeArea]. + /// * [SliverVisibility], a sliver version of [Visibility]. + /// + /// ## Benefits of slivers over boxes + /// + /// The sliver protocol ([SliverConstraints] and [SliverGeometry]) enables + /// _scroll effects_, such as floating app bars, widgets that expand and + /// shrink during scroll, section headers that are pinned only while the + /// section's children are visible, etc. + /// + /// {@youtube 560 315 https://www.youtube.com/watch?v=Mz3kHQxBjGg} + /// + /// ## Mixing slivers and boxes + /// + /// In general, slivers always wrap box widgets to actually render anything + /// (for example, there is no sliver equivalent of [Text] or [Container]); + /// the sliver part of the equation is mostly about how these boxes should + /// be laid out in a viewport (i.e. when scrolling). + /// + /// Typically, the simplest way to combine boxes into a sliver environment is + /// to use a [SliverList] (maybe using a [ExtendedListView], which is a convenient + /// combination of a [ExtendedCustomScrollView] and a [SliverList]). In rare cases, + /// e.g. if a single [Divider] widget is needed between two [SliverGrid]s, + /// a [SliverToBoxAdapter] can be used to wrap the box widgets. + /// + /// ## Performance considerations + /// + /// Because the purpose of scroll views is to, well, scroll, it is common + /// for scroll views to contain more contents than are rendered on the screen + /// at any particular time. + /// + /// To improve the performance of scroll views, the content can be rendered in + /// _lazy_ widgets, notably [SliverList] and [SliverGrid] (and their variants, + /// such as [SliverFixedExtentList] and [SliverAnimatedGrid]). These widgets + /// ensure that only the portion of their child lists that are actually + /// visible get built, laid out, and painted. + /// + /// The [ExtendedListView] and [ExtendedGridView] widgets provide a convenient way to combine + /// a [ExtendedCustomScrollView] and a [SliverList] or [SliverGrid] (respectively). + final List slivers; + + @override + List buildSlivers(BuildContext context) => slivers; +} + +/// A [ScrollView] that uses a single child layout model. +/// +/// {@template flutter.widgets.BoxScroll.scrollBehaviour} +/// [ScrollView]s are often decorated with [Scrollbar]s and overscroll indicators, +/// which are managed by the inherited [ScrollBehavior]. Placing a +/// [ScrollConfiguration] above a ScrollView can modify these behaviors for that +/// ScrollView, or can be managed app-wide by providing a ScrollBehavior to +/// [MaterialApp.scrollBehavior] or [CupertinoApp.scrollBehavior]. +/// {@endtemplate} +/// +/// See also: +/// +/// * [ExtendedListView], which is a [BoxScrollView] that uses a linear layout model. +/// * [ExtendedGridView], which is a [BoxScrollView] that uses a 2D layout model. +/// * [ExtendedCustomScrollView], which can combine multiple child layout models into a +/// single scroll view. +abstract class BoxScrollView extends ScrollView { + /// Creates a [ScrollView] uses a single child layout model. + /// + /// If the [primary] argument is true, the [controller] must be null. + const BoxScrollView({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + this.padding, + super.cacheExtent, + super.semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }); + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + @override + List buildSlivers(BuildContext context) { + Widget sliver = buildChildLayout(context); + EdgeInsetsGeometry? effectivePadding = padding; + if (padding == null) { + final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context); + if (mediaQuery != null) { + // Automatically pad sliver with padding from MediaQuery. + final EdgeInsets mediaQueryHorizontalPadding = mediaQuery.padding + .copyWith( + top: 0.0, + bottom: 0.0, + ); + final EdgeInsets mediaQueryVerticalPadding = mediaQuery.padding + .copyWith( + left: 0.0, + right: 0.0, + ); + // Consume the main axis padding with SliverPadding. + effectivePadding = scrollDirection == Axis.vertical + ? mediaQueryVerticalPadding + : mediaQueryHorizontalPadding; + // Leave behind the cross axis padding. + sliver = MediaQuery( + data: mediaQuery.copyWith( + padding: scrollDirection == Axis.vertical + ? mediaQueryHorizontalPadding + : mediaQueryVerticalPadding, + ), + child: sliver, + ); + } + } + + if (effectivePadding != null) { + sliver = SliverPadding(padding: effectivePadding, sliver: sliver); + } + return [sliver]; + } + + /// Subclasses should override this method to build the layout model. + @protected + Widget buildChildLayout(BuildContext context); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DiagnosticsProperty( + 'padding', + padding, + defaultValue: null, + ), + ); + } +} + +/// A scrollable list of widgets arranged linearly. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=KJpkjHGiI5A} +/// +/// [ExtendedListView] is the most commonly used scrolling widget. It displays its +/// children one after another in the scroll direction. In the cross axis, the +/// children are required to fill the [ExtendedListView]. +/// +/// If non-null, the [itemExtent] forces the children to have the given extent +/// in the scroll direction. +/// +/// If non-null, the [prototypeItem] forces the children to have the same extent +/// as the given widget in the scroll direction. +/// +/// Specifying an [itemExtent] or an [prototypeItem] is more efficient than +/// letting the children determine their own extent because the scrolling +/// machinery can make use of the foreknowledge of the children's extent to save +/// work, for example when the scroll position changes drastically. +/// +/// You can't specify both [itemExtent] and [prototypeItem], only one or none of +/// them. +/// +/// There are four options for constructing a [ExtendedListView]: +/// +/// 1. The default constructor takes an explicit [List] of children. This +/// constructor is appropriate for list views with a small number of +/// children because constructing the [List] requires doing work for every +/// child that could possibly be displayed in the list view instead of just +/// those children that are actually visible. +/// +/// 2. The [ListView.builder] constructor takes an [IndexedWidgetBuilder], which +/// builds the children on demand. This constructor is appropriate for list views +/// with a large (or infinite) number of children because the builder is called +/// only for those children that are actually visible. +/// +/// 3. The [ListView.separated] constructor takes two [IndexedWidgetBuilder]s: +/// `itemBuilder` builds child items on demand, and `separatorBuilder` +/// similarly builds separator children which appear in between the child items. +/// This constructor is appropriate for list views with a fixed number of children. +/// +/// 4. The [ListView.custom] constructor takes a [SliverChildDelegate], which provides +/// the ability to customize additional aspects of the child model. For example, +/// a [SliverChildDelegate] can control the algorithm used to estimate the +/// size of children that are not actually visible. +/// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// +/// By default, [ExtendedListView] will automatically pad the list's scrollable +/// extremities to avoid partial obstructions indicated by [MediaQuery]'s +/// padding. To avoid this behavior, override with a zero [padding] property. +/// +/// {@tool snippet} +/// This example uses the default constructor for [ExtendedListView] which takes an +/// explicit [List] of children. This [ExtendedListView]'s children are made up +/// of [Container]s with [Text]. +/// +/// ![A ListView of 3 amber colored containers with sample text.](https://flutter.github.io/assets-for-api-docs/assets/widgets/list_view.png) +/// +/// ```dart +/// ListView( +/// padding: const EdgeInsets.all(8), +/// children: [ +/// Container( +/// height: 50, +/// color: Colors.amber[600], +/// child: const Center(child: Text('Entry A')), +/// ), +/// Container( +/// height: 50, +/// color: Colors.amber[500], +/// child: const Center(child: Text('Entry B')), +/// ), +/// Container( +/// height: 50, +/// color: Colors.amber[100], +/// child: const Center(child: Text('Entry C')), +/// ), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// This example mirrors the previous one, creating the same list using the +/// [ListView.builder] constructor. Using the [IndexedWidgetBuilder], children +/// are built lazily and can be infinite in number. +/// +/// ![A ListView of 3 amber colored containers with sample text.](https://flutter.github.io/assets-for-api-docs/assets/widgets/list_view_builder.png) +/// +/// ```dart +/// final List entries = ['A', 'B', 'C']; +/// final List colorCodes = [600, 500, 100]; +/// +/// Widget build(BuildContext context) { +/// return ListView.builder( +/// padding: const EdgeInsets.all(8), +/// itemCount: entries.length, +/// itemBuilder: (BuildContext context, int index) { +/// return Container( +/// height: 50, +/// color: Colors.amber[colorCodes[index]], +/// child: Center(child: Text('Entry ${entries[index]}')), +/// ); +/// } +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// This example continues to build from our the previous ones, creating a +/// similar list using [ListView.separated]. Here, a [Divider] is used as a +/// separator. +/// +/// ![A ListView of 3 amber colored containers with sample text and a Divider +/// between each of them.](https://flutter.github.io/assets-for-api-docs/assets/widgets/list_view_separated.png) +/// +/// ```dart +/// final List entries = ['A', 'B', 'C']; +/// final List colorCodes = [600, 500, 100]; +/// +/// Widget build(BuildContext context) { +/// return ListView.separated( +/// padding: const EdgeInsets.all(8), +/// itemCount: entries.length, +/// itemBuilder: (BuildContext context, int index) { +/// return Container( +/// height: 50, +/// color: Colors.amber[colorCodes[index]], +/// child: Center(child: Text('Entry ${entries[index]}')), +/// ); +/// }, +/// separatorBuilder: (BuildContext context, int index) => const Divider(), +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// ## Child elements' lifecycle +/// +/// ### Creation +/// +/// While laying out the list, visible children's elements, states and render +/// objects will be created lazily based on existing widgets (such as when using +/// the default constructor) or lazily provided ones (such as when using the +/// [ListView.builder] constructor). +/// +/// ### Destruction +/// +/// When a child is scrolled out of view, the associated element subtree, +/// states and render objects are destroyed. A new child at the same position +/// in the list will be lazily recreated along with new elements, states and +/// render objects when it is scrolled back. +/// +/// ### Destruction mitigation +/// +/// In order to preserve state as child elements are scrolled in and out of +/// view, the following options are possible: +/// +/// * Moving the ownership of non-trivial UI-state-driving business logic +/// out of the list child subtree. For instance, if a list contains posts +/// with their number of upvotes coming from a cached network response, store +/// the list of posts and upvote number in a data model outside the list. Let +/// the list child UI subtree be easily recreate-able from the +/// source-of-truth model object. Use [StatefulWidget]s in the child +/// widget subtree to store instantaneous UI state only. +/// +/// * Letting [KeepAlive] be the root widget of the list child widget subtree +/// that needs to be preserved. The [KeepAlive] widget marks the child +/// subtree's top render object child for keepalive. When the associated top +/// render object is scrolled out of view, the list keeps the child's render +/// object (and by extension, its associated elements and states) in a cache +/// list instead of destroying them. When scrolled back into view, the render +/// object is repainted as-is (if it wasn't marked dirty in the interim). +/// +/// This only works if `addAutomaticKeepAlives` and `addRepaintBoundaries` +/// are false since those parameters cause the [ListView] to wrap each child +/// widget subtree with other widgets. +/// +/// * Using [AutomaticKeepAlive] widgets (inserted by default when +/// `addAutomaticKeepAlives` is true). [AutomaticKeepAlive] allows descendant +/// widgets to control whether the subtree is actually kept alive or not. +/// This behavior is in contrast with [KeepAlive], which will unconditionally keep +/// the subtree alive. +/// +/// As an example, the [EditableText] widget signals its list child element +/// subtree to stay alive while its text field has input focus. If it doesn't +/// have focus and no other descendants signaled for keepalive via a +/// [KeepAliveNotification], the list child element subtree will be destroyed +/// when scrolled away. +/// +/// [AutomaticKeepAlive] descendants typically signal it to be kept alive +/// by using the [AutomaticKeepAliveClientMixin], then implementing the +/// [AutomaticKeepAliveClientMixin.wantKeepAlive] getter and calling +/// [AutomaticKeepAliveClientMixin.updateKeepAlive]. +/// +/// ## Transitioning to [ExtendedCustomScrollView] +/// +/// A [ExtendedListView] is basically a [ExtendedCustomScrollView] with a single [SliverList] in +/// its [ExtendedCustomScrollView.slivers] property. +/// +/// If [ExtendedListView] is no longer sufficient, for example because the scroll view +/// is to have both a list and a grid, or because the list is to be combined +/// with a [SliverAppBar], etc, it is straight-forward to port code from using +/// [ExtendedListView] to using [ExtendedCustomScrollView] directly. +/// +/// The [key], [scrollDirection], [reverse], [controller], [primary], [physics], +/// and [shrinkWrap] properties on [ExtendedListView] map directly to the identically +/// named properties on [ExtendedCustomScrollView]. +/// +/// The [ExtendedCustomScrollView.slivers] property should be a list containing either: +/// * a [SliverList] if both [itemExtent] and [prototypeItem] were null; +/// * a [SliverFixedExtentList] if [itemExtent] was not null; or +/// * a [SliverPrototypeExtentList] if [prototypeItem] was not null. +/// +/// The [childrenDelegate] property on [ExtendedListView] corresponds to the +/// [SliverList.delegate] (or [SliverFixedExtentList.delegate]) property. The +/// [ExtendedListView] constructor's `children` argument corresponds to the +/// [childrenDelegate] being a [SliverChildListDelegate] with that same +/// argument. The [ListView.builder] constructor's `itemBuilder` and +/// `itemCount` arguments correspond to the [childrenDelegate] being a +/// [SliverChildBuilderDelegate] with the equivalent arguments. +/// +/// The [padding] property corresponds to having a [SliverPadding] in the +/// [ExtendedCustomScrollView.slivers] property instead of the list itself, and having +/// the [SliverList] instead be a child of the [SliverPadding]. +/// +/// [ExtendedCustomScrollView]s don't automatically avoid obstructions from [MediaQuery] +/// like [ExtendedListView]s do. To reproduce the behavior, wrap the slivers in +/// [SliverSafeArea]s. +/// +/// Once code has been ported to use [ExtendedCustomScrollView], other slivers, such as +/// [SliverGrid] or [SliverAppBar], can be put in the [ExtendedCustomScrollView.slivers] +/// list. +/// +/// {@tool snippet} +/// +/// Here are two brief snippets showing a [ExtendedListView] and its equivalent using +/// [CustomScrollView]: +/// +/// ```dart +/// ListView( +/// padding: const EdgeInsets.all(20.0), +/// children: const [ +/// Text("I'm dedicating every day to you"), +/// Text('Domestic life was never quite my style'), +/// Text('When you smile, you knock me out, I fall apart'), +/// Text('And I thought I was so smart'), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// {@tool snippet} +/// +/// ```dart +/// CustomScrollView( +/// slivers: [ +/// SliverPadding( +/// padding: const EdgeInsets.all(20.0), +/// sliver: SliverList( +/// delegate: SliverChildListDelegate( +/// [ +/// const Text("I'm dedicating every day to you"), +/// const Text('Domestic life was never quite my style'), +/// const Text('When you smile, you knock me out, I fall apart'), +/// const Text('And I thought I was so smart'), +/// ], +/// ), +/// ), +/// ), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// ## Special handling for an empty list +/// +/// A common design pattern is to have a custom UI for an empty list. The best +/// way to achieve this in Flutter is just conditionally replacing the +/// [ExtendedListView] at build time with whatever widgets you need to show for the +/// empty list state: +/// +/// {@tool snippet} +/// +/// Example of simple empty list interface: +/// +/// ```dart +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: AppBar(title: const Text('Empty List Test')), +/// body: itemCount > 0 +/// ? ListView.builder( +/// itemCount: itemCount, +/// itemBuilder: (BuildContext context, int index) { +/// return ListTile( +/// title: Text('Item ${index + 1}'), +/// ); +/// }, +/// ) +/// : const Center(child: Text('No items')), +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// ## Selection of list items +/// +/// [ExtendedListView] has no built-in notion of a selected item or items. For a small +/// example of how a caller might wire up basic item selection, see +/// [ListTile.selected]. +/// +/// {@tool dartpad} +/// This example shows a custom implementation of [ListTile] selection in a [ExtendedListView] or [ExtendedGridView]. +/// Long press any [ListTile] to enable selection mode. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** +/// {@end-tool} +/// +/// {@macro flutter.widgets.BoxScroll.scrollBehaviour} +/// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// +/// See also: +/// +/// * [SingleChildScrollView], which is a scrollable widget that has a single +/// child. +/// * [PageView], which is a scrolling list of child widgets that are each the +/// size of the viewport. +/// * [ExtendedGridView], which is a scrollable, 2D array of widgets. +/// * [ExtendedCustomScrollView], which is a scrollable widget that creates custom +/// scroll effects using slivers. +/// * [ListBody], which arranges its children in a similar manner, but without +/// scrolling. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +/// * The [catalog of layout widgets](https://docs.flutter.dev/ui/widgets/layout). +/// * Cookbook: [Use lists](https://docs.flutter.dev/cookbook/lists/basic-list) +/// * Cookbook: [Work with long lists](https://docs.flutter.dev/cookbook/lists/long-lists) +/// * Cookbook: [Create a horizontal list](https://docs.flutter.dev/cookbook/lists/horizontal-list) +/// * Cookbook: [Create lists with different types of items](https://docs.flutter.dev/cookbook/lists/mixed-list) +/// * Cookbook: [Implement swipe to dismiss](https://docs.flutter.dev/cookbook/gestures/dismissible) +// ignore: camel_case_types +typedef listView = ExtendedListView; + +class ExtendedListView extends BoxScrollView { + /// Creates a scrollable, linear array of widgets from an explicit [List]. + /// + /// This constructor is appropriate for list views with a small number of + /// children because constructing the [List] requires doing work for every + /// child that could possibly be displayed in the list view instead of just + /// those children that are actually visible. + /// + /// Like other widgets in the framework, this widget expects that + /// the [children] list will not be mutated after it has been passed in here. + /// See the documentation at [SliverChildListDelegate.children] for more details. + /// + /// It is usually more efficient to create children on demand using + /// [ListView.builder] because it will create the widget children lazily as necessary. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildListDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildListDelegate.addSemanticIndexes] property. None + /// may be null. + ExtendedListView({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + this.itemExtent, + this.itemExtentBuilder, + this.prototypeItem, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + List children = const [], + int? semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : assert( + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ), + childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? children.length); + + /// Creates a scrollable, linear array of widgets that are created on demand. + /// + /// This constructor is appropriate for list views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Providing a non-null `itemCount` improves the ability of the [ExtendedListView] to + /// estimate the maximum scroll extent. + /// + /// The `itemBuilder` callback will be called only with indices greater than + /// or equal to zero and less than `itemCount`. + /// + /// {@template flutter.widgets.ListView.builder.itemBuilder} + /// It is legal for `itemBuilder` to return `null`. If it does, the scroll view + /// will stop calling `itemBuilder`, even if it has yet to reach `itemCount`. + /// By returning `null`, the [ScrollPosition.maxScrollExtent] will not be accurate + /// unless the user has reached the end of the [ScrollView]. This can also cause the + /// [Scrollbar] to grow as the user scrolls. + /// + /// For more accurate [ScrollMetrics], consider specifying `itemCount`. + /// {@endtemplate} + /// + /// The `itemBuilder` should always create the widget instances when called. + /// Avoid using a builder that returns a previously-constructed widget; if the + /// list view's children are created in advance, or all at once when the + /// [ExtendedListView] itself is created, it is more efficient to use the [ExtendedListView] + /// constructor. Even more efficient, however, is to create the instances on + /// demand using this constructor's `itemBuilder` callback. + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. None may be + /// null. + ExtendedListView.builder({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + this.itemExtent, + this.itemExtentBuilder, + this.prototypeItem, + required NullableIndexedWidgetBuilder itemBuilder, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + int? semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : assert(itemCount == null || itemCount >= 0), + assert(semanticChildCount == null || semanticChildCount <= itemCount!), + assert( + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ), + childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? itemCount); + + /// Creates a fixed-length scrollable linear array of list "items" separated + /// by list item "separators". + /// + /// This constructor is appropriate for list views with a large number of + /// item and separator children because the builders are only called for + /// the children that are actually visible. + /// + /// The `itemBuilder` callback will be called with indices greater than + /// or equal to zero and less than `itemCount`. + /// + /// Separators only appear between list items: separator 0 appears after item + /// 0 and the last separator appears before the last item. + /// + /// The `separatorBuilder` callback will be called with indices greater than + /// or equal to zero and less than `itemCount - 1`. + /// + /// The `itemBuilder` and `separatorBuilder` callbacks should always + /// actually create widget instances when called. Avoid using a builder that + /// returns a previously-constructed widget; if the list view's children are + /// created in advance, or all at once when the [ExtendedListView] itself is created, + /// it is more efficient to use the [ExtendedListView] constructor. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// {@template flutter.widgets.ListView.separated.findItemIndexCallback} + /// The [findItemIndexCallback] returns item indices (excluding separators), + /// unlike the deprecated [findChildIndexCallback] which returns child indices + /// (including both items and separators). + /// + /// For example, in a list with 3 items and 2 separators: + /// * Item indices: 0, 1, 2 + /// * Child indices: 0 (item), 1 (separator), 2 (item), 3 (separator), 4 (item) + /// + /// This callback should be implemented if the order of items may change at a + /// later time. If null, reordering items may result in state-loss as widgets + /// may not map to their existing [RenderObject]s. + /// {@endtemplate} + /// + /// {@tool snippet} + /// + /// This example shows how to create [ExtendedListView] whose [ListTile] list items + /// are separated by [Divider]s. + /// + /// ```dart + /// ListView.separated( + /// itemCount: 25, + /// separatorBuilder: (BuildContext context, int index) => const Divider(), + /// itemBuilder: (BuildContext context, int index) { + /// return ListTile( + /// title: Text('item $index'), + /// ); + /// }, + /// ) + /// ``` + /// {@end-tool} + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. None may be + /// null. + ExtendedListView.separated({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required NullableIndexedWidgetBuilder itemBuilder, + @Deprecated( + 'Use findItemIndexCallback instead. ' + 'findChildIndexCallback returns child indices (which include separators), ' + 'while findItemIndexCallback returns item indices (which do not). ' + 'If you were multiplying results by 2 to account for separators, ' + 'you can remove that workaround when migrating to findItemIndexCallback. ' + 'This feature was deprecated after v3.37.0-1.0.pre.', + ) + ChildIndexGetter? findChildIndexCallback, + ChildIndexGetter? findItemIndexCallback, + required IndexedWidgetBuilder separatorBuilder, + required int itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : assert(itemCount >= 0), + assert( + findItemIndexCallback == null || findChildIndexCallback == null, + 'Cannot provide both findItemIndexCallback and findChildIndexCallback. ' + 'Use findItemIndexCallback as findChildIndexCallback is deprecated.', + ), + itemExtent = null, + itemExtentBuilder = null, + prototypeItem = null, + childrenDelegate = SliverChildBuilderDelegate( + (BuildContext context, int index) { + final int itemIndex = index ~/ 2; + if (index.isEven) { + return itemBuilder(context, itemIndex); + } + return separatorBuilder(context, itemIndex); + }, + findChildIndexCallback: findItemIndexCallback != null + ? (Key key) { + final int? itemIndex = findItemIndexCallback(key); + return itemIndex == null ? null : itemIndex * 2; + } + : findChildIndexCallback, + childCount: _computeActualChildCount(itemCount), + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + semanticIndexCallback: (Widget widget, int index) { + return index.isEven ? index ~/ 2 : null; + }, + ), + super(semanticChildCount: itemCount); + + /// Creates a scrollable, linear array of widgets with a custom child model. + /// + /// For example, a custom child model can control the algorithm used to + /// estimate the size of children that are not actually visible. + /// + /// {@tool dartpad} + /// This example shows a [ExtendedListView] that uses a custom [SliverChildBuilderDelegate] to support child + /// reordering. + /// + /// ** See code in examples/api/lib/widgets/scroll_view/list_view.1.dart ** + /// {@end-tool} + const ExtendedListView.custom({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + this.itemExtent, + this.prototypeItem, + this.itemExtentBuilder, + required this.childrenDelegate, + super.cacheExtent, + super.semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : assert( + (itemExtent == null && prototypeItem == null) || + (itemExtent == null && itemExtentBuilder == null) || + (prototypeItem == null && itemExtentBuilder == null), + 'You can only pass one of itemExtent, prototypeItem and itemExtentBuilder.', + ); + + /// {@template flutter.widgets.list_view.itemExtent} + /// If non-null, forces the children to have the given extent in the scroll + /// direction. + /// + /// Specifying an [itemExtent] is more efficient than letting the children + /// determine their own extent because the scrolling machinery can make use of + /// the foreknowledge of the children's extent to save work, for example when + /// the scroll position changes drastically. + /// + /// See also: + /// + /// * [SliverFixedExtentList], the sliver used internally when this property + /// is provided. It constrains its box children to have a specific given + /// extent along the main axis. + /// * The [prototypeItem] property, which allows forcing the children's + /// extent to be the same as the given widget. + /// * The [itemExtentBuilder] property, which allows forcing the children's + /// extent to be the value returned by the callback. + /// {@endtemplate} + final double? itemExtent; + + /// {@template flutter.widgets.list_view.itemExtentBuilder} + /// If non-null, forces the children to have the corresponding extent returned + /// by the builder. + /// + /// Specifying an [itemExtentBuilder] is more efficient than letting the children + /// determine their own extent because the scrolling machinery can make use of + /// the foreknowledge of the children's extent to save work, for example when + /// the scroll position changes drastically. + /// + /// This will be called multiple times during the layout phase of a frame to find + /// the items that should be loaded by the lazy loading process. + /// + /// Should return null if asked to build an item extent with a greater index than + /// exists. + /// + /// Unlike [itemExtent] or [prototypeItem], this allows children to have + /// different extents. + /// + /// See also: + /// + /// * [SliverVariedExtentList], the sliver used internally when this property + /// is provided. It constrains its box children to have a specific given + /// extent along the main axis. + /// * The [itemExtent] property, which allows forcing the children's extent + /// to a given value. + /// * The [prototypeItem] property, which allows forcing the children's + /// extent to be the same as the given widget. + /// {@endtemplate} + final ItemExtentBuilder? itemExtentBuilder; + + /// {@template flutter.widgets.list_view.prototypeItem} + /// If non-null, forces the children to have the same extent as the given + /// widget in the scroll direction. + /// + /// Specifying an [prototypeItem] is more efficient than letting the children + /// determine their own extent because the scrolling machinery can make use of + /// the foreknowledge of the children's extent to save work, for example when + /// the scroll position changes drastically. + /// + /// See also: + /// + /// * [SliverPrototypeExtentList], the sliver used internally when this + /// property is provided. It constrains its box children to have the same + /// extent as a prototype item along the main axis. + /// * The [itemExtent] property, which allows forcing the children's extent + /// to a given value. + /// * The [itemExtentBuilder] property, which allows forcing the children's + /// extent to be the value returned by the callback. + /// {@endtemplate} + final Widget? prototypeItem; + + /// A delegate that provides the children for the [ExtendedListView]. + /// + /// The [ListView.custom] constructor lets you specify this delegate + /// explicitly. The [ExtendedListView] and [ListView.builder] constructors create a + /// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder], + /// respectively. + final SliverChildDelegate childrenDelegate; + + @override + Widget buildChildLayout(BuildContext context) { + if (itemExtent != null) { + return SliverFixedExtentList( + delegate: childrenDelegate, + itemExtent: itemExtent!, + ); + } else if (itemExtentBuilder != null) { + return SliverVariedExtentList( + delegate: childrenDelegate, + itemExtentBuilder: itemExtentBuilder!, + ); + } else if (prototypeItem != null) { + return SliverPrototypeExtentList( + delegate: childrenDelegate, + prototypeItem: prototypeItem!, + ); + } + return SliverList(delegate: childrenDelegate); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add( + DoubleProperty('itemExtent', itemExtent, defaultValue: null), + ); + } + + // Helper method to compute the actual child count for the separated constructor. + static int _computeActualChildCount(int itemCount) { + return math.max(0, itemCount * 2 - 1); + } +} + +/// A scrollable, 2D array of widgets. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=bLOtZDTm4H8} +/// +/// The main axis direction of a grid is the direction in which it scrolls (the +/// [scrollDirection]). +/// +/// The most commonly used grid layouts are [GridView.count], which creates a +/// layout with a fixed number of tiles in the cross axis, and +/// [GridView.extent], which creates a layout with tiles that have a maximum +/// cross-axis extent. A custom [SliverGridDelegate] can produce an arbitrary 2D +/// arrangement of children, including arrangements that are unaligned or +/// overlapping. +/// +/// To create a grid with a large (or infinite) number of children, use the +/// [GridView.builder] constructor with either a +/// [SliverGridDelegateWithFixedCrossAxisCount] or a +/// [SliverGridDelegateWithMaxCrossAxisExtent] for the [gridDelegate]. +/// +/// To use a custom [SliverChildDelegate], use [GridView.custom]. +/// +/// To create a linear array of children, use a [ExtendedListView]. +/// +/// To control the initial scroll offset of the scroll view, provide a +/// [controller] with its [ScrollController.initialScrollOffset] property set. +/// +/// ## Transitioning to [ExtendedCustomScrollView] +/// +/// A [ExtendedGridView] is basically a [ExtendedCustomScrollView] with a single [SliverGrid] in +/// its [ExtendedCustomScrollView.slivers] property. +/// +/// If [ExtendedGridView] is no longer sufficient, for example because the scroll view +/// is to have both a grid and a list, or because the grid is to be combined +/// with a [SliverAppBar], etc, it is straight-forward to port code from using +/// [ExtendedGridView] to using [ExtendedCustomScrollView] directly. +/// +/// The [key], [scrollDirection], [reverse], [controller], [primary], [physics], +/// and [shrinkWrap] properties on [ExtendedGridView] map directly to the identically +/// named properties on [ExtendedCustomScrollView]. +/// +/// The [ExtendedCustomScrollView.slivers] property should be a list containing just a +/// [SliverGrid]. +/// +/// The [childrenDelegate] property on [ExtendedGridView] corresponds to the +/// [SliverGrid.delegate] property, and the [gridDelegate] property on the +/// [ExtendedGridView] corresponds to the [SliverGrid.gridDelegate] property. +/// +/// The [ExtendedGridView], [GridView.count], and [GridView.extent] +/// constructors' `children` arguments correspond to the [childrenDelegate] +/// being a [SliverChildListDelegate] with that same argument. The +/// [GridView.builder] constructor's `itemBuilder` and `childCount` arguments +/// correspond to the [childrenDelegate] being a [SliverChildBuilderDelegate] +/// with the matching arguments. +/// +/// The [GridView.count] and [GridView.extent] constructors create +/// custom grid delegates, and have equivalently named constructors on +/// [SliverGrid] to ease the transition: [SliverGrid.count] and +/// [SliverGrid.extent] respectively. +/// +/// The [padding] property corresponds to having a [SliverPadding] in the +/// [ExtendedCustomScrollView.slivers] property instead of the grid itself, and having +/// the [SliverGrid] instead be a child of the [SliverPadding]. +/// +/// Once code has been ported to use [ExtendedCustomScrollView], other slivers, such as +/// [SliverList] or [SliverAppBar], can be put in the [ExtendedCustomScrollView.slivers] +/// list. +/// +/// {@macro flutter.widgets.ScrollView.PageStorage} +/// +/// ## Examples +/// +/// {@tool snippet} +/// This example demonstrates how to create a [ExtendedGridView] with two columns. The +/// children are spaced apart using the `crossAxisSpacing` and `mainAxisSpacing` +/// properties. +/// +/// ![The GridView displays six children with different background colors arranged in two columns](https://flutter.github.io/assets-for-api-docs/assets/widgets/grid_view.png) +/// +/// ```dart +/// GridView.count( +/// primary: false, +/// padding: const EdgeInsets.all(20), +/// crossAxisSpacing: 10, +/// mainAxisSpacing: 10, +/// crossAxisCount: 2, +/// children: [ +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[100], +/// child: const Text("He'd have you all unravel at the"), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[200], +/// child: const Text('Heed not the rabble'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[300], +/// child: const Text('Sound of screams but the'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[400], +/// child: const Text('Who scream'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[500], +/// child: const Text('Revolution is coming...'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.teal[600], +/// child: const Text('Revolution, they...'), +/// ), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool snippet} +/// This example shows how to create the same grid as the previous example +/// using a [ExtendedCustomScrollView] and a [SliverGrid]. +/// +/// ![The CustomScrollView contains a SliverGrid that displays six children with different background colors arranged in two columns](https://flutter.github.io/assets-for-api-docs/assets/widgets/grid_view_custom_scroll.png) +/// +/// ```dart +/// CustomScrollView( +/// primary: false, +/// slivers: [ +/// SliverPadding( +/// padding: const EdgeInsets.all(20), +/// sliver: SliverGrid.count( +/// crossAxisSpacing: 10, +/// mainAxisSpacing: 10, +/// crossAxisCount: 2, +/// children: [ +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[100], +/// child: const Text("He'd have you all unravel at the"), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[200], +/// child: const Text('Heed not the rabble'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[300], +/// child: const Text('Sound of screams but the'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[400], +/// child: const Text('Who scream'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[500], +/// child: const Text('Revolution is coming...'), +/// ), +/// Container( +/// padding: const EdgeInsets.all(8), +/// color: Colors.green[600], +/// child: const Text('Revolution, they...'), +/// ), +/// ], +/// ), +/// ), +/// ], +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows a custom implementation of selection in list and grid views. +/// Use the button in the top right (possibly hidden under the DEBUG banner) to toggle between +/// [ExtendedListView] and [ExtendedGridView]. +/// Long press any [ListTile] or [GridTile] to enable selection mode. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/list_view.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows a custom [SliverGridDelegate]. +/// +/// ** See code in examples/api/lib/widgets/scroll_view/grid_view.0.dart ** +/// {@end-tool} +/// +/// ## Troubleshooting +/// +/// ### Padding +/// +/// By default, [ExtendedGridView] will automatically pad the limits of the +/// grid's scrollable to avoid partial obstructions indicated by +/// [MediaQuery]'s padding. To avoid this behavior, override with a +/// zero [padding] property. +/// +/// {@tool snippet} +/// The following example demonstrates how to override the default top padding +/// using [MediaQuery.removePadding]. +/// +/// ```dart +/// Widget myWidget(BuildContext context) { +/// return MediaQuery.removePadding( +/// context: context, +/// removeTop: true, +/// child: GridView.builder( +/// gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( +/// crossAxisCount: 3, +/// ), +/// itemCount: 300, +/// itemBuilder: (BuildContext context, int index) { +/// return Card( +/// color: Colors.amber, +/// child: Center(child: Text('$index')), +/// ); +/// } +/// ), +/// ); +/// } +/// ``` +/// {@end-tool} +/// +/// See also: +/// +/// * [SingleChildScrollView], which is a scrollable widget that has a single +/// child. +/// * [ExtendedListView], which is scrollable, linear list of widgets. +/// * [PageView], which is a scrolling list of child widgets that are each the +/// size of the viewport. +/// * [ExtendedCustomScrollView], which is a scrollable widget that creates custom +/// scroll effects using slivers. +/// * [SliverGridDelegateWithFixedCrossAxisCount], which creates a layout with +/// a fixed number of tiles in the cross axis. +/// * [SliverGridDelegateWithMaxCrossAxisExtent], which creates a layout with +/// tiles that have a maximum cross-axis extent. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). +// ignore: camel_case_types +typedef gridView = ExtendedGridView; + +class ExtendedGridView extends BoxScrollView { + /// Creates a scrollable, 2D array of widgets with a custom + /// [SliverGridDelegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + ExtendedGridView({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required this.gridDelegate, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + List children = const [], + int? semanticChildCount, + super.dragStartBehavior, + super.clipBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.hitTestBehavior, + }) : childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? children.length); + + /// Creates a scrollable, 2D array of widgets that are created on demand. + /// + /// This constructor is appropriate for grid views with a large (or infinite) + /// number of children because the builder is called only for those children + /// that are actually visible. + /// + /// Providing a non-null `itemCount` improves the ability of the [ExtendedGridView] to + /// estimate the maximum scroll extent. + /// + /// `itemBuilder` will be called only with indices greater than or equal to + /// zero and less than `itemCount`. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// The [gridDelegate] argument is required. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + ExtendedGridView.builder({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required this.gridDelegate, + required NullableIndexedWidgetBuilder itemBuilder, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + int? semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : childrenDelegate = SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? itemCount); + + /// Creates a scrollable, 2D array of widgets with both a custom + /// [SliverGridDelegate] and a custom [SliverChildDelegate]. + /// + /// To use an [IndexedWidgetBuilder] callback to build children, either use + /// a [SliverChildBuilderDelegate] or use the [GridView.builder] constructor. + const ExtendedGridView.custom({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required this.gridDelegate, + required this.childrenDelegate, + super.cacheExtent, + super.semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }); + + /// Creates a scrollable, 2D array of widgets with a fixed number of tiles in + /// the cross axis. + /// + /// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + /// + /// See also: + /// + /// * [SliverGrid.count], the equivalent constructor for [SliverGrid]. + ExtendedGridView.count({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required int crossAxisCount, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + double childAspectRatio = 1.0, + double? mainAxisExtent, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + List children = const [], + int? semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : gridDelegate = SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + mainAxisExtent: mainAxisExtent, + ), + childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? children.length); + + /// Creates a scrollable, 2D array of widgets with tiles that each have a + /// maximum cross-axis extent. + /// + /// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate]. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildListDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildListDelegate.addRepaintBoundaries] property. Both must not be + /// null. + /// + /// See also: + /// + /// * [SliverGrid.extent], the equivalent constructor for [SliverGrid]. + ExtendedGridView.extent({ + super.key, + super.scrollDirection, + super.reverse, + super.controller, + super.primary, + super.physics, + super.shrinkWrap, + super.padding, + required double maxCrossAxisExtent, + double mainAxisSpacing = 0.0, + double crossAxisSpacing = 0.0, + double childAspectRatio = 1.0, + double? mainAxisExtent, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + super.cacheExtent, + List children = const [], + int? semanticChildCount, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.restorationId, + super.clipBehavior, + super.hitTestBehavior, + }) : gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: maxCrossAxisExtent, + mainAxisSpacing: mainAxisSpacing, + crossAxisSpacing: crossAxisSpacing, + childAspectRatio: childAspectRatio, + mainAxisExtent: mainAxisExtent, + ), + childrenDelegate = SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + ), + super(semanticChildCount: semanticChildCount ?? children.length); + + /// A delegate that controls the layout of the children within the [ExtendedGridView]. + /// + /// The [ExtendedGridView], [GridView.builder], and [GridView.custom] constructors let you specify this + /// delegate explicitly. The other constructors create a [gridDelegate] + /// implicitly. + final SliverGridDelegate gridDelegate; + + /// A delegate that provides the children for the [ExtendedGridView]. + /// + /// The [GridView.custom] constructor lets you specify this delegate + /// explicitly. The other constructors create a [childrenDelegate] that wraps + /// the given child list. + final SliverChildDelegate childrenDelegate; + + @override + Widget buildChildLayout(BuildContext context) { + return SliverGrid(delegate: childrenDelegate, gridDelegate: gridDelegate); + } +} diff --git a/lib/common/widgets/flutter/scroll_view/scrollable.dart b/lib/common/widgets/flutter/scroll_view/scrollable.dart new file mode 100644 index 000000000..fbc3e7818 --- /dev/null +++ b/lib/common/widgets/flutter/scroll_view/scrollable.dart @@ -0,0 +1,1870 @@ +// Copyright 2014 The Flutter 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' as math; + +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scrollable_helpers.dart'; +import 'package:PiliPlus/common/widgets/gesture/vertical_drag_gesture_recognizer.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' + hide Scrollable, ScrollableState, EdgeDraggingAutoScroller; + +export 'package:flutter/physics.dart' show Tolerance; + +// The return type of _performEnsureVisible. +// +// The list of futures represents each pending ScrollPosition call to +// ensureVisible. The returned ScrollableState's context is used to find the +// next potential ancestor Scrollable. +typedef _EnsureVisibleResults = (List>, ScrollableState); + +/// A widget that manages scrolling in one dimension and informs the [Viewport] +/// through which the content is viewed. +/// +/// [Scrollable] implements the interaction model for a scrollable widget, +/// including gesture recognition, but does not have an opinion about how the +/// viewport, which actually displays the children, is constructed. +/// +/// It's rare to construct a [Scrollable] directly. Instead, consider [ListView] +/// or [GridView], which combine scrolling, viewporting, and a layout model. To +/// combine layout models (or to use a custom layout mode), consider using +/// [CustomScrollView]. +/// +/// The static [Scrollable.of] and [Scrollable.ensureVisible] functions are +/// often used to interact with the [Scrollable] widget inside a [ListView] or +/// a [GridView]. +/// +/// To further customize scrolling behavior with a [Scrollable]: +/// +/// 1. You can provide a [viewportBuilder] to customize the child model. For +/// example, [SingleChildScrollView] uses a viewport that displays a single +/// box child whereas [CustomScrollView] uses a [Viewport] or a +/// [ShrinkWrappingViewport], both of which display a list of slivers. +/// +/// 2. You can provide a custom [ScrollController] that creates a custom +/// [ScrollPosition] subclass. For example, [PageView] uses a +/// [PageController], which creates a page-oriented scroll position subclass +/// that keeps the same page visible when the [Scrollable] resizes. +/// +/// ## Persisting the scroll position during a session +/// +/// Scrollables attempt to persist their scroll position using [PageStorage]. +/// This can be disabled by setting [ScrollController.keepScrollOffset] to false +/// on the [controller]. If it is enabled, using a [PageStorageKey] for the +/// [key] of this widget (or one of its ancestors, e.g. a [ScrollView]) is +/// recommended to help disambiguate different [Scrollable]s from each other. +/// +/// See also: +/// +/// * [ListView], which is a commonly used [ScrollView] that displays a +/// scrolling, linear list of child widgets. +/// * [PageView], which is a scrolling list of child widgets that are each the +/// size of the viewport. +/// * [GridView], which is a [ScrollView] that displays a scrolling, 2D array +/// of child widgets. +/// * [CustomScrollView], which is a [ScrollView] that creates custom scroll +/// effects using slivers. +/// * [SingleChildScrollView], which is a scrollable widget that has a single +/// child. +/// * [ScrollNotification] and [NotificationListener], which can be used to watch +/// the scroll position without using a [ScrollController]. +class Scrollable extends StatefulWidget { + /// Creates a widget that scrolls. + const Scrollable({ + super.key, + this.axisDirection = AxisDirection.down, + this.controller, + this.physics, + required this.viewportBuilder, + this.incrementCalculator, + this.excludeFromSemantics = false, + this.semanticChildCount, + this.dragStartBehavior = DragStartBehavior.start, + this.restorationId, + this.scrollBehavior, + this.clipBehavior = Clip.hardEdge, + this.hitTestBehavior = HitTestBehavior.opaque, + }) : assert(semanticChildCount == null || semanticChildCount >= 0); + + /// {@template flutter.widgets.Scrollable.axisDirection} + /// The direction in which this widget scrolls. + /// + /// For example, if the [Scrollable.axisDirection] is [AxisDirection.down], + /// increasing the scroll position will cause content below the bottom of the + /// viewport to become visible through the viewport. Similarly, if the + /// axisDirection is [AxisDirection.right], increasing the scroll position + /// will cause content beyond the right edge of the viewport to become visible + /// through the viewport. + /// + /// Defaults to [AxisDirection.down]. + /// {@endtemplate} + final AxisDirection axisDirection; + + /// {@template flutter.widgets.Scrollable.controller} + /// An object that can be used to control the position to which this widget is + /// scrolled. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// + /// If null, a [ScrollController] will be created internally by [Scrollable] + /// in order to create and manage the [ScrollPosition]. + /// + /// See also: + /// + /// * [Scrollable.ensureVisible], which animates the scroll position to + /// reveal a given [BuildContext]. + /// {@endtemplate} + final ScrollController? controller; + + /// {@template flutter.widgets.Scrollable.physics} + /// How the widgets should respond to user input. + /// + /// For example, determines how the widget continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions via the physics provided from + /// the ambient [ScrollConfiguration]. + /// + /// If an explicit [ScrollBehavior] is provided to + /// [Scrollable.scrollBehavior], the [ScrollPhysics] provided by that behavior + /// will take precedence after [Scrollable.physics]. + /// + /// The physics can be changed dynamically, but new physics will only take + /// effect if the _class_ of the provided object changes. Merely constructing + /// a new instance with a different configuration is insufficient to cause the + /// physics to be reapplied. (This is because the final object used is + /// generated dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// + /// See also: + /// + /// * [AlwaysScrollableScrollPhysics], which can be used to indicate that the + /// scrollable should react to scroll requests (and possible overscroll) + /// even if the scrollable's contents fit without scrolling being necessary. + /// {@endtemplate} + final ScrollPhysics? physics; + + /// Builds the viewport through which the scrollable content is displayed. + /// + /// A typical viewport uses the given [ViewportOffset] to determine which part + /// of its content is actually visible through the viewport. + /// + /// See also: + /// + /// * [Viewport], which is a viewport that displays a list of slivers. + /// * [ShrinkWrappingViewport], which is a viewport that displays a list of + /// slivers and sizes itself based on the size of the slivers. + final ViewportBuilder viewportBuilder; + + /// {@template flutter.widgets.Scrollable.incrementCalculator} + /// An optional function that will be called to calculate the distance to + /// scroll when the scrollable is asked to scroll via the keyboard using a + /// [ScrollAction]. + /// + /// If not supplied, the [Scrollable] will scroll a default amount when a + /// keyboard navigation key is pressed (e.g. pageUp/pageDown, control-upArrow, + /// etc.), or otherwise invoked by a [ScrollAction]. + /// + /// If [incrementCalculator] is null, the default for + /// [ScrollIncrementType.page] is 80% of the size of the scroll window, and + /// for [ScrollIncrementType.line], 50 logical pixels. + /// {@endtemplate} + final ScrollIncrementCalculator? incrementCalculator; + + /// {@template flutter.widgets.scrollable.excludeFromSemantics} + /// Whether the scroll actions introduced by this [Scrollable] are exposed + /// in the semantics tree. + /// + /// Text fields with an overflow are usually scrollable to make sure that the + /// user can get to the beginning/end of the entered text. However, these + /// scrolling actions are generally not exposed to the semantics layer. + /// {@endtemplate} + /// + /// See also: + /// + /// * [GestureDetector.excludeFromSemantics], which is used to accomplish the + /// exclusion. + final bool excludeFromSemantics; + + /// {@template flutter.widgets.scrollable.hitTestBehavior} + /// Defines the behavior of gesture detector used in this [Scrollable]. + /// + /// This defaults to [HitTestBehavior.opaque] which means it prevents targets + /// behind this [Scrollable] from receiving events. + /// {@endtemplate} + /// + /// See also: + /// + /// * [HitTestBehavior], for an explanation on different behaviors. + final HitTestBehavior hitTestBehavior; + + /// The number of children that will contribute semantic information. + /// + /// The value will be null if the number of children is unknown or unbounded. + /// + /// Some subtypes of [ScrollView] can infer this value automatically. For + /// example [ListView] will use the number of widgets in the child list, + /// while the [ListView.separated] constructor will use half that amount. + /// + /// For [CustomScrollView] and other types which do not receive a builder + /// or list of widgets, the child count must be explicitly provided. + /// + /// See also: + /// + /// * [CustomScrollView], for an explanation of scroll semantics. + /// * [SemanticsConfiguration.scrollChildCount], the corresponding semantics property. + final int? semanticChildCount; + + // TODO(jslavitz): Set the DragStartBehavior default to be start across all widgets. + /// {@template flutter.widgets.scrollable.dragStartBehavior} + /// Determines the way that drag start behavior is handled. + /// + /// If set to [DragStartBehavior.start], scrolling drag behavior will + /// begin at the position where the drag gesture won the arena. If set to + /// [DragStartBehavior.down] it will begin at the position where a down + /// event is first detected. + /// + /// In general, setting this to [DragStartBehavior.start] will make drag + /// animation smoother and setting it to [DragStartBehavior.down] will make + /// drag behavior feel slightly more reactive. + /// + /// By default, the drag start behavior is [DragStartBehavior.start]. + /// + /// See also: + /// + /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for + /// the different behaviors. + /// + /// {@endtemplate} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scrollable.restorationId} + /// Restoration ID to save and restore the scroll offset of the scrollable. + /// + /// If a restoration id is provided, the scrollable will persist its current + /// scroll offset and restore it during state restoration. + /// + /// The scroll offset is persisted in a [RestorationBucket] claimed from + /// the surrounding [RestorationScope] using the provided restoration ID. + /// + /// See also: + /// + /// * [RestorationManager], which explains how state restoration works in + /// Flutter. + /// {@endtemplate} + final String? restorationId; + + /// {@template flutter.widgets.scrollable.scrollBehavior} + /// A [ScrollBehavior] that will be applied to this widget individually. + /// + /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and + /// modified to alter the viewport decoration, like [Scrollbar]s. + /// + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + /// {@endtemplate} + final ScrollBehavior? scrollBehavior; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + /// + /// This is passed to decorators in [ScrollableDetails], and does not directly affect + /// clipping of the [Scrollable]. This reflects the same [Clip] that is provided + /// to [ScrollView.clipBehavior] and is supplied to the [Viewport]. + final Clip clipBehavior; + + /// The axis along which the scroll view scrolls. + /// + /// Determined by the [axisDirection]. + Axis get axis => axisDirectionToAxis(axisDirection); + + @override + ScrollableState createState() => ScrollableState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(EnumProperty('axisDirection', axisDirection)) + ..add(DiagnosticsProperty('physics', physics)) + ..add(StringProperty('restorationId', restorationId)); + } + + /// The state from the closest instance of this class that encloses the given + /// context, or null if none is found. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ScrollableState? scrollable = Scrollable.maybeOf(context); + /// ``` + /// + /// Calling this method will create a dependency on the [ScrollableState] + /// that is returned, if there is one. This is typically the closest + /// [Scrollable], but may be a more distant ancestor if [axis] is used to + /// target a specific [Scrollable]. + /// + /// Using the optional [Axis] is useful when Scrollables are nested and the + /// target [Scrollable] is not the closest instance. When [axis] is provided, + /// the nearest enclosing [ScrollableState] in that [Axis] is returned, or + /// null if there is none. + /// + /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This + /// means that if the `context` is that of a [Scrollable], it will _not_ find + /// _that_ [Scrollable]. + /// + /// See also: + /// + /// * [Scrollable.of], which is similar to this method, but asserts + /// if no [Scrollable] ancestor is found. + static ScrollableState? maybeOf(BuildContext context, {Axis? axis}) { + // This is the context that will need to establish the dependency. + final originalContext = context; + InheritedElement? element = context + .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); + while (element != null) { + final ScrollableState scrollable = + (element.widget as _ScrollableScope).scrollable; + if (axis == null || + axisDirectionToAxis(scrollable.axisDirection) == axis) { + // Establish the dependency on the correct context. + originalContext.dependOnInheritedElement(element); + return scrollable; + } + context = scrollable.context; + element = context + .getElementForInheritedWidgetOfExactType<_ScrollableScope>(); + } + return null; + } + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ScrollableState scrollable = Scrollable.of(context); + /// ``` + /// + /// Calling this method will create a dependency on the [ScrollableState] + /// that is returned, if there is one. This is typically the closest + /// [Scrollable], but may be a more distant ancestor if [axis] is used to + /// target a specific [Scrollable]. + /// + /// Using the optional [Axis] is useful when Scrollables are nested and the + /// target [Scrollable] is not the closest instance. When [axis] is provided, + /// the nearest enclosing [ScrollableState] in that [Axis] is returned. + /// + /// This finds the nearest _ancestor_ [Scrollable] of the `context`. This + /// means that if the `context` is that of a [Scrollable], it will _not_ find + /// _that_ [Scrollable]. + /// + /// If no [Scrollable] ancestor is found, then this method will assert in + /// debug mode, and throw an exception in release mode. + /// + /// See also: + /// + /// * [Scrollable.maybeOf], which is similar to this method, but returns null + /// if no [Scrollable] ancestor is found. + static ScrollableState of(BuildContext context, {Axis? axis}) { + final ScrollableState? scrollableState = maybeOf(context, axis: axis); + assert(() { + if (scrollableState == null) { + throw FlutterError.fromParts([ + ErrorSummary( + 'Scrollable.of() was called with a context that does not contain a ' + 'Scrollable widget.', + ), + ErrorDescription( + 'No Scrollable widget ancestor could be found ' + '${axis == null ? '' : 'for the provided Axis: $axis '}' + 'starting from the context that was passed to Scrollable.of(). This ' + 'can happen because you are using a widget that looks for a Scrollable ' + 'ancestor, but no such ancestor exists.\n' + 'The context used was:\n' + ' $context', + ), + if (axis != null) + ErrorHint( + 'When specifying an axis, this method will only look for a Scrollable ' + 'that matches the given Axis.', + ), + ]); + } + return true; + }()); + return scrollableState!; + } + + /// Provides a heuristic to determine if expensive frame-bound tasks should be + /// deferred for the [context] at a specific point in time. + /// + /// Calling this method does _not_ create a dependency on any other widget. + /// This also means that the value returned is only good for the point in time + /// when it is called, and callers will not get updated if the value changes. + /// + /// The heuristic used is determined by the [physics] of this [Scrollable] + /// via [ScrollPhysics.recommendDeferredLoading]. That method is called with + /// the current [ScrollPosition.activity]'s [ScrollActivity.velocity]. + /// + /// The optional [Axis] allows targeting of a specific [Scrollable] of that + /// axis, useful when Scrollables are nested. When [axis] is provided, + /// [ScrollPosition.recommendDeferredLoading] is called for the nearest + /// [Scrollable] in that [Axis]. + /// + /// If there is no [Scrollable] in the widget tree above the [context], this + /// method returns false. + static bool recommendDeferredLoadingForContext( + BuildContext context, { + Axis? axis, + }) { + _ScrollableScope? widget = context + .getInheritedWidgetOfExactType<_ScrollableScope>(); + while (widget != null) { + if (axis == null || + axisDirectionToAxis(widget.scrollable.axisDirection) == axis) { + return widget.position.recommendDeferredLoading(context); + } + context = widget.scrollable.context; + widget = context.getInheritedWidgetOfExactType<_ScrollableScope>(); + } + return false; + } + + /// Scrolls all scrollables that enclose the given context so as to make the + /// given context visible. + /// + /// If a [Scrollable] enclosing the provided [BuildContext] is a + /// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure + /// the target is made visible. + static Future ensureVisible( + BuildContext context, { + double alignment = 0.0, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + ScrollPositionAlignmentPolicy alignmentPolicy = + ScrollPositionAlignmentPolicy.explicit, + }) { + final futures = >[]; + + // The targetRenderObject is used to record the first target renderObject. + // If there are multiple scrollable widgets nested, the targetRenderObject + // is made to be as visible as possible to improve the user experience. If + // the targetRenderObject is already visible, then let the outer + // renderObject be as visible as possible. + // + // Also see https://github.com/flutter/flutter/issues/65100 + RenderObject? targetRenderObject; + ScrollableState? scrollable = Scrollable.maybeOf(context); + while (scrollable != null) { + final List> newFutures; + (newFutures, scrollable) = scrollable._performEnsureVisible( + context.findRenderObject()!, + alignment: alignment, + duration: duration, + curve: curve, + alignmentPolicy: alignmentPolicy, + targetRenderObject: targetRenderObject, + ); + futures.addAll(newFutures); + + targetRenderObject ??= context.findRenderObject(); + context = scrollable.context; + scrollable = Scrollable.maybeOf(context); + } + + if (futures.isEmpty || duration == Duration.zero) { + return Future.value(); + } + if (futures.length == 1) { + return futures.single; + } + return Future.wait(futures).then((List _) => null); + } +} + +// Enable Scrollable.of() to work as if ScrollableState was an inherited widget. +// ScrollableState.build() always rebuilds its _ScrollableScope. +class _ScrollableScope extends InheritedWidget { + const _ScrollableScope({ + required this.scrollable, + required this.position, + required super.child, + }); + + final ScrollableState scrollable; + final ScrollPosition position; + + @override + bool updateShouldNotify(_ScrollableScope old) { + return position != old.position; + } +} + +/// State object for a [Scrollable] widget. +/// +/// To manipulate a [Scrollable] widget's scroll position, use the object +/// obtained from the [position] property. +/// +/// To be informed of when a [Scrollable] widget is scrolling, use a +/// [NotificationListener] to listen for [ScrollNotification] notifications. +/// +/// This class is not intended to be subclassed. To specialize the behavior of a +/// [Scrollable], provide it with a [ScrollPhysics]. +class ScrollableState extends State + with TickerProviderStateMixin, RestorationMixin + implements ScrollContext { + // GETTERS + + /// The manager for this [Scrollable] widget's viewport position. + /// + /// To control what kind of [ScrollPosition] is created for a [Scrollable], + /// provide it with custom [ScrollController] that creates the appropriate + /// [ScrollPosition] in its [ScrollController.createScrollPosition] method. + ScrollPosition get position => _position!; + ScrollPosition? _position; + + /// The resolved [ScrollPhysics] of the [ScrollableState]. + ScrollPhysics? get resolvedPhysics => _physics; + ScrollPhysics? _physics; + + /// An [Offset] that represents the absolute distance from the origin, or 0, + /// of the [ScrollPosition] expressed in the associated [Axis]. + /// + /// Used by [EdgeDraggingAutoScroller] to progress the position forward when a + /// drag gesture reaches the edge of the [Viewport]. + Offset get deltaToScrollOrigin => switch (axisDirection) { + AxisDirection.up => Offset(0, -position.pixels), + AxisDirection.down => Offset(0, position.pixels), + AxisDirection.left => Offset(-position.pixels, 0), + AxisDirection.right => Offset(position.pixels, 0), + }; + + ScrollController get _effectiveScrollController => + widget.controller ?? _fallbackScrollController!; + + @override + AxisDirection get axisDirection => widget.axisDirection; + + @override + TickerProvider get vsync => this; + + @override + double get devicePixelRatio => _devicePixelRatio; + late double _devicePixelRatio; + + @override + BuildContext? get notificationContext => _gestureDetectorKey.currentContext; + + @override + BuildContext get storageContext => context; + + @override + String? get restorationId => widget.restorationId; + final _RestorableScrollOffset _persistedScrollOffset = + _RestorableScrollOffset(); + + late ScrollBehavior _configuration; + ScrollController? _fallbackScrollController; + DeviceGestureSettings? _mediaQueryGestureSettings; + + // Only call this from places that will definitely trigger a rebuild. + void _updatePosition() { + _configuration = widget.scrollBehavior ?? ScrollConfiguration.of(context); + final ScrollPhysics? physicsFromWidget = + widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); + _physics = _configuration.getScrollPhysics(context); + _physics = physicsFromWidget?.applyTo(_physics) ?? _physics; + + final ScrollPosition? oldPosition = _position; + if (oldPosition != null) { + _effectiveScrollController.detach(oldPosition); + // It's important that we not dispose the old position until after the + // viewport has had a chance to unregister its listeners from the old + // position. So, schedule a microtask to do it. + scheduleMicrotask(oldPosition.dispose); + } + + _position = _effectiveScrollController.createScrollPosition( + _physics!, + this, + oldPosition, + ); + assert(_position != null); + _effectiveScrollController.attach(position); + } + + @protected + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_persistedScrollOffset, 'offset'); + assert(_position != null); + if (_persistedScrollOffset.value != null) { + position.restoreOffset( + _persistedScrollOffset.value!, + initialRestore: initialRestore, + ); + } + } + + @protected + @override + void saveOffset(double offset) { + assert(debugIsSerializableForRestoration(offset)); + _persistedScrollOffset.value = offset; + // [saveOffset] is called after a scrolling ends and it is usually not + // followed by a frame. Therefore, manually flush restoration data. + ServicesBinding.instance.restorationManager.flushData(); + } + + @protected + @override + void initState() { + if (widget.controller == null) { + _fallbackScrollController = ScrollController(); + } + super.initState(); + } + + @protected + @override + void didChangeDependencies() { + _mediaQueryGestureSettings = MediaQuery.maybeGestureSettingsOf(context); + _devicePixelRatio = + MediaQuery.maybeDevicePixelRatioOf(context) ?? + View.of(context).devicePixelRatio; + _updatePosition(); + super.didChangeDependencies(); + } + + bool _shouldUpdatePosition(Scrollable oldWidget) { + if ((widget.scrollBehavior == null) != (oldWidget.scrollBehavior == null)) { + return true; + } + if (widget.scrollBehavior != null && + oldWidget.scrollBehavior != null && + widget.scrollBehavior!.shouldNotify(oldWidget.scrollBehavior!)) { + return true; + } + ScrollPhysics? newPhysics = + widget.physics ?? widget.scrollBehavior?.getScrollPhysics(context); + ScrollPhysics? oldPhysics = + oldWidget.physics ?? + oldWidget.scrollBehavior?.getScrollPhysics(context); + do { + if (newPhysics?.runtimeType != oldPhysics?.runtimeType) { + return true; + } + newPhysics = newPhysics?.parent; + oldPhysics = oldPhysics?.parent; + } while (newPhysics != null || oldPhysics != null); + + return widget.controller?.runtimeType != oldWidget.controller?.runtimeType; + } + + @protected + @override + void didUpdateWidget(Scrollable oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.controller != oldWidget.controller) { + if (oldWidget.controller == null) { + // The old controller was null, meaning the fallback cannot be null. + // Dispose of the fallback. + assert(_fallbackScrollController != null); + assert(widget.controller != null); + _fallbackScrollController!.detach(position); + _fallbackScrollController!.dispose(); + _fallbackScrollController = null; + } else { + // The old controller was not null, detach. + oldWidget.controller?.detach(position); + if (widget.controller == null) { + // If the new controller is null, we need to set up the fallback + // ScrollController. + _fallbackScrollController = ScrollController(); + } + } + // Attach the updated effective scroll controller. + _effectiveScrollController.attach(position); + } + + if (_shouldUpdatePosition(oldWidget)) { + _updatePosition(); + } + } + + @protected + @override + void dispose() { + if (widget.controller != null) { + widget.controller!.detach(position); + } else { + _fallbackScrollController?.detach(position); + _fallbackScrollController?.dispose(); + } + + position.dispose(); + _persistedScrollOffset.dispose(); + super.dispose(); + } + + // SEMANTICS + + final GlobalKey _scrollSemanticsKey = GlobalKey(); + + @override + @protected + void setSemanticsActions(Set actions) { + if (_gestureDetectorKey.currentState != null) { + _gestureDetectorKey.currentState!.replaceSemanticsActions(actions); + } + } + + // GESTURE RECOGNITION AND POINTER IGNORING + + final GlobalKey _gestureDetectorKey = + GlobalKey(); + final GlobalKey _ignorePointerKey = GlobalKey(); + + // This field is set during layout, and then reused until the next time it is set. + Map _gestureRecognizers = + const {}; + bool _shouldIgnorePointer = false; + + bool? _lastCanDrag; + Axis? _lastAxisDirection; + + @override + @protected + void setCanDrag(bool value) { + if (value == _lastCanDrag && + (!value || widget.axis == _lastAxisDirection)) { + return; + } + if (!value) { + _gestureRecognizers = const {}; + // Cancel the active hold/drag (if any) because the gesture recognizers + // will soon be disposed by our RawGestureDetector, and we won't be + // receiving pointer up events to cancel the hold/drag. + _handleDragCancel(); + } else { + switch (widget.axis) { + case Axis.vertical: + _gestureRecognizers = { + CustomVerticalDragGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + CustomVerticalDragGestureRecognizer + >( + () => CustomVerticalDragGestureRecognizer( + supportedDevices: _configuration.dragDevices, + ), + (CustomVerticalDragGestureRecognizer instance) { + instance + ..isDyAllowed = PlatformUtils.isMobile + ? isDyAllowed + : null + ..onDown = _handleDragDown + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..onCancel = _handleDragCancel + ..minFlingDistance = _physics?.minFlingDistance + ..minFlingVelocity = _physics?.minFlingVelocity + ..maxFlingVelocity = _physics?.maxFlingVelocity + ..velocityTrackerBuilder = _configuration + .velocityTrackerBuilder(context) + ..dragStartBehavior = widget.dragStartBehavior + ..multitouchDragStrategy = _configuration + .getMultitouchDragStrategy(context) + ..gestureSettings = _mediaQueryGestureSettings + ..supportedDevices = _configuration.dragDevices; + }, + ), + }; + case Axis.horizontal: + _gestureRecognizers = { + HorizontalDragGestureRecognizer: + GestureRecognizerFactoryWithHandlers< + HorizontalDragGestureRecognizer + >( + () => HorizontalDragGestureRecognizer( + supportedDevices: _configuration.dragDevices, + ), + (HorizontalDragGestureRecognizer instance) { + instance + ..onDown = _handleDragDown + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..onCancel = _handleDragCancel + ..minFlingDistance = _physics?.minFlingDistance + ..minFlingVelocity = _physics?.minFlingVelocity + ..maxFlingVelocity = _physics?.maxFlingVelocity + ..velocityTrackerBuilder = _configuration + .velocityTrackerBuilder(context) + ..dragStartBehavior = widget.dragStartBehavior + ..multitouchDragStrategy = _configuration + .getMultitouchDragStrategy(context) + ..gestureSettings = _mediaQueryGestureSettings + ..supportedDevices = _configuration.dragDevices; + }, + ), + }; + } + } + _lastCanDrag = value; + _lastAxisDirection = widget.axis; + if (_gestureDetectorKey.currentState != null) { + _gestureDetectorKey.currentState!.replaceGestureRecognizers( + _gestureRecognizers, + ); + } + } + + @override + @protected + void setIgnorePointer(bool value) { + if (_shouldIgnorePointer == value) { + return; + } + _shouldIgnorePointer = value; + if (_ignorePointerKey.currentContext != null) { + final renderBox = + _ignorePointerKey.currentContext!.findRenderObject()! + as RenderIgnorePointer; + renderBox.ignoring = _shouldIgnorePointer; + } + } + + // TOUCH HANDLERS + + Drag? _drag; + ScrollHoldController? _hold; + + bool isDyAllowed(double dy) { + return position.viewportDimension - dy > 30; + } + + void _handleDragDown(DragDownDetails details) { + assert(_drag == null); + assert(_hold == null); + _hold = position.hold(_disposeHold); + } + + void _handleDragStart(DragStartDetails details) { + // It's possible for _hold to become null between _handleDragDown and + // _handleDragStart, for example if some user code calls jumpTo or otherwise + // triggers a new activity to begin. + assert(_drag == null); + _drag = position.drag(details, _disposeDrag); + assert(_drag != null); + // _hold might be non-null if the scroll position is currently animating. + if (_hold != null) { + _disposeHold(); + } + } + + void _handleDragUpdate(DragUpdateDetails details) { + // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); + _drag?.update(details); + } + + void _handleDragEnd(DragEndDetails details) { + // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); + _drag?.end(details); + assert(_drag == null); + } + + void _handleDragCancel() { + if (_gestureDetectorKey.currentContext == null) { + // The cancel was caused by the GestureDetector getting disposed, which + // means we will get disposed momentarily as well and shouldn't do + // any work. + return; + } + // _hold might be null if the drag started. + // _drag might be null if the drag activity ended and called _disposeDrag. + assert(_hold == null || _drag == null); + _hold?.cancel(); + _drag?.cancel(); + assert(_hold == null); + assert(_drag == null); + } + + void _disposeHold() { + _hold = null; + } + + void _disposeDrag() { + _drag = null; + } + + // SCROLL WHEEL + + // Returns the offset that should result from applying [event] to the current + // position, taking min/max scroll extent into account. + double _targetScrollOffsetForPointerScroll(double delta) { + return math.min( + math.max(position.pixels + delta, position.minScrollExtent), + position.maxScrollExtent, + ); + } + + // Returns the delta that should result from applying [event] with axis, + // direction, and any modifiers specified by the ScrollBehavior taken into + // account. + double _pointerSignalEventDelta(PointerScrollEvent event) { + final Set pressed = + HardwareKeyboard.instance.logicalKeysPressed; + final bool flipAxes = + pressed.any(_configuration.pointerAxisModifiers.contains) && + // Axes are only flipped for physical mouse wheel input. + // On some platforms, like web, trackpad input is handled through pointer + // signals, but should not be included in this axis modifying behavior. + // This is because on a trackpad, all directional axes are available to + // the user, while mouse scroll wheels typically are restricted to one + // axis. + event.kind == PointerDeviceKind.mouse; + + final Axis axis = flipAxes ? flipAxis(widget.axis) : widget.axis; + final double delta = switch (axis) { + Axis.horizontal => event.scrollDelta.dx, + Axis.vertical => event.scrollDelta.dy, + }; + + return axisDirectionIsReversed(widget.axisDirection) ? -delta : delta; + } + + void _receivedPointerSignal(PointerSignalEvent event) { + if (event is PointerScrollEvent && _position != null) { + if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) { + // The handler won't use the `event`, so allow the platform to trigger + // any default native actions. + event.respond(allowPlatformDefault: true); + return; + } + final double delta = _pointerSignalEventDelta(event); + final double targetScrollOffset = _targetScrollOffsetForPointerScroll( + delta, + ); + // Only express interest in the event if it would actually result in a scroll. + if (delta != 0.0 && targetScrollOffset != position.pixels) { + GestureBinding.instance.pointerSignalResolver.register( + event, + _handlePointerScroll, + ); + return; + } + // The `event` won't result in a scroll, so allow the platform to trigger + // any default native actions. + event.respond(allowPlatformDefault: true); + } else if (event is PointerScrollInertiaCancelEvent) { + position.pointerScroll(0); + // Don't use the pointer signal resolver, all hit-tested scrollables should stop. + } + } + + void _handlePointerScroll(PointerEvent event) { + assert(event is PointerScrollEvent); + final double delta = _pointerSignalEventDelta(event as PointerScrollEvent); + final double targetScrollOffset = _targetScrollOffsetForPointerScroll( + delta, + ); + if (delta != 0.0 && targetScrollOffset != position.pixels) { + position.pointerScroll(delta); + } + } + + bool _handleScrollMetricsNotification( + ScrollMetricsNotification notification, + ) { + if (notification.depth == 0) { + final RenderObject? scrollSemanticsRenderObject = _scrollSemanticsKey + .currentContext + ?.findRenderObject(); + if (scrollSemanticsRenderObject != null) { + scrollSemanticsRenderObject.markNeedsSemanticsUpdate(); + } + } + return false; + } + + Widget _buildChrome(BuildContext context, Widget child) { + final details = ScrollableDetails( + direction: widget.axisDirection, + controller: _effectiveScrollController, + decorationClipBehavior: widget.clipBehavior, + ); + + return _configuration.buildScrollbar( + context, + _configuration.buildOverscrollIndicator(context, child, details), + details, + ); + } + + // DESCRIPTION + + @protected + @override + Widget build(BuildContext context) { + assert(_position != null); + // _ScrollableScope must be placed above the BuildContext returned by notificationContext + // so that we can get this ScrollableState by doing the following: + // + // ScrollNotification notification; + // Scrollable.of(notification.context) + // + // Since notificationContext is pointing to _gestureDetectorKey.context, _ScrollableScope + // must be placed above the widget using it: RawGestureDetector + Widget result = _ScrollableScope( + scrollable: this, + position: position, + child: Listener( + onPointerSignal: _receivedPointerSignal, + child: RawGestureDetector( + key: _gestureDetectorKey, + gestures: _gestureRecognizers, + behavior: widget.hitTestBehavior, + excludeFromSemantics: widget.excludeFromSemantics, + child: Semantics( + explicitChildNodes: !widget.excludeFromSemantics, + child: IgnorePointer( + key: _ignorePointerKey, + ignoring: _shouldIgnorePointer, + child: widget.viewportBuilder(context, position), + ), + ), + ), + ), + ); + + if (!widget.excludeFromSemantics) { + result = NotificationListener( + onNotification: _handleScrollMetricsNotification, + child: _ScrollSemantics( + key: _scrollSemanticsKey, + position: position, + allowImplicitScrolling: _physics!.allowImplicitScrolling, + axis: widget.axis, + semanticChildCount: widget.semanticChildCount, + child: result, + ), + ); + } + + result = _buildChrome(context, result); + + // Selection is only enabled when there is a parent registrar. + final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context); + if (registrar != null) { + result = _ScrollableSelectionHandler( + state: this, + position: position, + registrar: registrar, + child: result, + ); + } + + return result; + } + + // Returns the Future from calling ensureVisible for the ScrollPosition, as + // as well as this ScrollableState instance so its context can be used to + // check for other ancestor Scrollables in executing ensureVisible. + _EnsureVisibleResults _performEnsureVisible( + RenderObject object, { + double alignment = 0.0, + Duration duration = Duration.zero, + Curve curve = Curves.ease, + ScrollPositionAlignmentPolicy alignmentPolicy = + ScrollPositionAlignmentPolicy.explicit, + RenderObject? targetRenderObject, + }) { + final Future ensureVisibleFuture = position.ensureVisible( + object, + alignment: alignment, + duration: duration, + curve: curve, + alignmentPolicy: alignmentPolicy, + targetRenderObject: targetRenderObject, + ); + return (>[ensureVisibleFuture], this); + } + + @protected + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('position', _position)) + ..add( + DiagnosticsProperty('effective physics', _physics), + ); + } +} + +/// A widget to handle selection for a scrollable. +/// +/// This widget registers itself to the [registrar] and uses +/// [SelectionContainer] to collect selectables from its subtree. +class _ScrollableSelectionHandler extends StatefulWidget { + const _ScrollableSelectionHandler({ + required this.state, + required this.position, + required this.registrar, + required this.child, + }); + + final ScrollableState state; + final ScrollPosition position; + final Widget child; + final SelectionRegistrar registrar; + + @override + _ScrollableSelectionHandlerState createState() => + _ScrollableSelectionHandlerState(); +} + +class _ScrollableSelectionHandlerState + extends State<_ScrollableSelectionHandler> { + late _ScrollableSelectionContainerDelegate _selectionDelegate; + + @override + void initState() { + super.initState(); + _selectionDelegate = _ScrollableSelectionContainerDelegate( + state: widget.state, + position: widget.position, + ); + } + + @override + void didUpdateWidget(_ScrollableSelectionHandler oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.position != widget.position) { + _selectionDelegate.position = widget.position; + } + } + + @override + void dispose() { + _selectionDelegate.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SelectionContainer( + registrar: widget.registrar, + delegate: _selectionDelegate, + child: widget.child, + ); + } +} + +/// This updater handles the case where the selectables change frequently, and +/// it optimizes toward scrolling updates. +/// +/// It keeps track of the drag start offset relative to scroll origin for every +/// selectable. The records are used to determine whether the selection is up to +/// date with the scroll position when it sends the drag update event to a +/// selectable. +class _ScrollableSelectionContainerDelegate + extends MultiSelectableSelectionContainerDelegate { + _ScrollableSelectionContainerDelegate({ + required this.state, + required ScrollPosition position, + }) : _position = position, + _autoScroller = EdgeDraggingAutoScroller( + state, + velocityScalar: _kDefaultSelectToScrollVelocityScalar, + ) { + _position.addListener(_scheduleLayoutChange); + } + + // Pointer drag is a single point, it should not have a size. + static const double _kDefaultDragTargetSize = 0; + + // An eye-balled value for a smooth scrolling speed. + static const double _kDefaultSelectToScrollVelocityScalar = 30; + + final ScrollableState state; + final EdgeDraggingAutoScroller _autoScroller; + bool _scheduledLayoutChange = false; + Offset? _currentDragStartRelatedToOrigin; + Offset? _currentDragEndRelatedToOrigin; + + // The scrollable only auto scrolls if the selection starts in the scrollable. + bool _selectionStartsInScrollable = false; + + ScrollPosition get position => _position; + ScrollPosition _position; + set position(ScrollPosition other) { + if (other == _position) { + return; + } + _position.removeListener(_scheduleLayoutChange); + _position = other; + _position.addListener(_scheduleLayoutChange); + } + + // The layout will only be updated a frame later than position changes. + // Schedule PostFrameCallback to capture the accurate layout. + void _scheduleLayoutChange() { + if (_scheduledLayoutChange) { + return; + } + _scheduledLayoutChange = true; + SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { + if (!_scheduledLayoutChange) { + return; + } + _scheduledLayoutChange = false; + layoutDidChange(); + }, debugLabel: 'ScrollableSelectionContainer.layoutDidChange'); + } + + /// Stores the scroll offset when a scrollable receives the last + /// [SelectionEdgeUpdateEvent]. + /// + /// The stored scroll offset may be null if a scrollable never receives a + /// [SelectionEdgeUpdateEvent]. + /// + /// When a new [SelectionEdgeUpdateEvent] is dispatched to a selectable, this + /// updater checks the current scroll offset against the one stored in these + /// records. If the scroll offset is different, it synthesizes an opposite + /// [SelectionEdgeUpdateEvent] and dispatches the event before dispatching the + /// new event. + /// + /// For example, if a selectable receives an end [SelectionEdgeUpdateEvent] + /// and its scroll offset in the records is different from the current value, + /// it synthesizes a start [SelectionEdgeUpdateEvent] and dispatches it before + /// dispatching the original end [SelectionEdgeUpdateEvent]. + final Map _selectableStartEdgeUpdateRecords = + {}; + final Map _selectableEndEdgeUpdateRecords = + {}; + + @override + void didChangeSelectables() { + final Set selectableSet = selectables.toSet(); + _selectableStartEdgeUpdateRecords.removeWhere( + (Selectable key, double value) => !selectableSet.contains(key), + ); + _selectableEndEdgeUpdateRecords.removeWhere( + (Selectable key, double value) => !selectableSet.contains(key), + ); + super.didChangeSelectables(); + } + + @override + SelectionResult handleClearSelection(ClearSelectionEvent event) { + _selectableStartEdgeUpdateRecords.clear(); + _selectableEndEdgeUpdateRecords.clear(); + _currentDragStartRelatedToOrigin = null; + _currentDragEndRelatedToOrigin = null; + _selectionStartsInScrollable = false; + return super.handleClearSelection(event); + } + + @override + SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) { + if (_currentDragEndRelatedToOrigin == null && + _currentDragStartRelatedToOrigin == null) { + assert(!_selectionStartsInScrollable); + _selectionStartsInScrollable = _globalPositionInScrollable( + event.globalPosition, + ); + } + final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); + if (event.type == SelectionEventType.endEdgeUpdate) { + _currentDragEndRelatedToOrigin = _inferPositionRelatedToOrigin( + event.globalPosition, + ); + final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( + -deltaToOrigin.dx, + -deltaToOrigin.dy, + ); + event = SelectionEdgeUpdateEvent.forEnd( + globalPosition: endOffset, + granularity: event.granularity, + ); + } else { + _currentDragStartRelatedToOrigin = _inferPositionRelatedToOrigin( + event.globalPosition, + ); + final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( + -deltaToOrigin.dx, + -deltaToOrigin.dy, + ); + event = SelectionEdgeUpdateEvent.forStart( + globalPosition: startOffset, + granularity: event.granularity, + ); + } + final SelectionResult result = super.handleSelectionEdgeUpdate(event); + + // Result may be pending if one of the selectable child is also a scrollable. + // In that case, the parent scrollable needs to wait for the child to finish + // scrolling. + if (result == SelectionResult.pending) { + _autoScroller.stopAutoScroll(); + return result; + } + if (_selectionStartsInScrollable) { + _autoScroller.startAutoScrollIfNecessary(_dragTargetFromEvent(event)); + if (_autoScroller.scrolling) { + return SelectionResult.pending; + } + } + return result; + } + + Offset _inferPositionRelatedToOrigin(Offset globalPosition) { + final box = state.context.findRenderObject()! as RenderBox; + final Offset localPosition = box.globalToLocal(globalPosition); + if (!_selectionStartsInScrollable) { + // If the selection starts outside of the scrollable, selecting across the + // scrollable boundary will act as selecting the entire content in the + // scrollable. This logic move the offset to the 0.0 or infinity to cover + // the entire content if the input position is outside of the scrollable. + if (localPosition.dy < 0 || localPosition.dx < 0) { + return box.localToGlobal(Offset.zero); + } + if (localPosition.dy > box.size.height || + localPosition.dx > box.size.width) { + return Offset.infinite; + } + } + final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); + return box.localToGlobal( + localPosition.translate(deltaToOrigin.dx, deltaToOrigin.dy), + ); + } + + /// Infers the [_currentDragStartRelatedToOrigin] and + /// [_currentDragEndRelatedToOrigin] from the geometry. + /// + /// This method is called after a select word and select all event where the + /// selection is triggered by none drag events. The + /// [_currentDragStartRelatedToOrigin] and [_currentDragEndRelatedToOrigin] + /// are essential to handle future [SelectionEdgeUpdateEvent]s. + void _updateDragLocationsFromGeometries({ + bool forceUpdateStart = true, + bool forceUpdateEnd = true, + }) { + final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); + final box = state.context.findRenderObject()! as RenderBox; + final Matrix4 transform = box.getTransformTo(null); + if (currentSelectionStartIndex != -1 && + (_currentDragStartRelatedToOrigin == null || forceUpdateStart)) { + final SelectionGeometry geometry = + selectables[currentSelectionStartIndex].value; + assert(geometry.hasSelection); + final SelectionPoint start = geometry.startSelectionPoint!; + final Matrix4 childTransform = selectables[currentSelectionStartIndex] + .getTransformTo(box); + final Offset localDragStart = MatrixUtils.transformPoint( + childTransform, + start.localPosition + Offset(0, -start.lineHeight / 2), + ); + _currentDragStartRelatedToOrigin = MatrixUtils.transformPoint( + transform, + localDragStart + deltaToOrigin, + ); + } + if (currentSelectionEndIndex != -1 && + (_currentDragEndRelatedToOrigin == null || forceUpdateEnd)) { + final SelectionGeometry geometry = + selectables[currentSelectionEndIndex].value; + assert(geometry.hasSelection); + final SelectionPoint end = geometry.endSelectionPoint!; + final Matrix4 childTransform = selectables[currentSelectionEndIndex] + .getTransformTo(box); + final Offset localDragEnd = MatrixUtils.transformPoint( + childTransform, + end.localPosition + Offset(0, -end.lineHeight / 2), + ); + _currentDragEndRelatedToOrigin = MatrixUtils.transformPoint( + transform, + localDragEnd + deltaToOrigin, + ); + } + } + + @override + SelectionResult handleSelectAll(SelectAllSelectionEvent event) { + assert(!_selectionStartsInScrollable); + final SelectionResult result = super.handleSelectAll(event); + assert( + (currentSelectionStartIndex == -1) == (currentSelectionEndIndex == -1), + ); + if (currentSelectionStartIndex != -1) { + _updateDragLocationsFromGeometries(); + } + return result; + } + + @override + SelectionResult handleSelectWord(SelectWordSelectionEvent event) { + _selectionStartsInScrollable = _globalPositionInScrollable( + event.globalPosition, + ); + final SelectionResult result = super.handleSelectWord(event); + _updateDragLocationsFromGeometries(); + return result; + } + + @override + SelectionResult handleGranularlyExtendSelection( + GranularlyExtendSelectionEvent event, + ) { + final SelectionResult result = super.handleGranularlyExtendSelection(event); + // The selection geometry may not have the accurate offset for the edges + // that are outside of the viewport whose transform may not be valid. Only + // the edge this event is updating is sure to be accurate. + _updateDragLocationsFromGeometries( + forceUpdateStart: !event.isEnd, + forceUpdateEnd: event.isEnd, + ); + if (_selectionStartsInScrollable) { + _jumpToEdge(event.isEnd); + } + return result; + } + + @override + SelectionResult handleDirectionallyExtendSelection( + DirectionallyExtendSelectionEvent event, + ) { + final SelectionResult result = super.handleDirectionallyExtendSelection( + event, + ); + // The selection geometry may not have the accurate offset for the edges + // that are outside of the viewport whose transform may not be valid. Only + // the edge this event is updating is sure to be accurate. + _updateDragLocationsFromGeometries( + forceUpdateStart: !event.isEnd, + forceUpdateEnd: event.isEnd, + ); + if (_selectionStartsInScrollable) { + _jumpToEdge(event.isEnd); + } + return result; + } + + void _jumpToEdge(bool isExtent) { + final Selectable selectable; + final double? lineHeight; + final SelectionPoint? edge; + if (isExtent) { + selectable = selectables[currentSelectionEndIndex]; + edge = selectable.value.endSelectionPoint; + lineHeight = selectable.value.endSelectionPoint!.lineHeight; + } else { + selectable = selectables[currentSelectionStartIndex]; + edge = selectable.value.startSelectionPoint; + lineHeight = selectable.value.startSelectionPoint?.lineHeight; + } + if (lineHeight == null || edge == null) { + return; + } + final scrollableBox = state.context.findRenderObject()! as RenderBox; + final Matrix4 transform = selectable.getTransformTo(scrollableBox); + final Offset edgeOffsetInScrollableCoordinates = MatrixUtils.transformPoint( + transform, + edge.localPosition, + ); + final scrollableRect = Rect.fromLTRB( + 0, + 0, + scrollableBox.size.width, + scrollableBox.size.height, + ); + switch (state.axisDirection) { + case AxisDirection.up: + final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; + final double edgeTop = + edgeOffsetInScrollableCoordinates.dy - lineHeight; + if (edgeBottom >= scrollableRect.bottom && + edgeTop <= scrollableRect.top) { + return; + } + if (edgeBottom > scrollableRect.bottom) { + position.jumpTo(position.pixels + scrollableRect.bottom - edgeBottom); + return; + } + if (edgeTop < scrollableRect.top) { + position.jumpTo(position.pixels + scrollableRect.top - edgeTop); + } + return; + case AxisDirection.right: + final double edge = edgeOffsetInScrollableCoordinates.dx; + if (edge >= scrollableRect.right && edge <= scrollableRect.left) { + return; + } + if (edge > scrollableRect.right) { + position.jumpTo(position.pixels + edge - scrollableRect.right); + return; + } + if (edge < scrollableRect.left) { + position.jumpTo(position.pixels + edge - scrollableRect.left); + } + return; + case AxisDirection.down: + final double edgeBottom = edgeOffsetInScrollableCoordinates.dy; + final double edgeTop = + edgeOffsetInScrollableCoordinates.dy - lineHeight; + if (edgeBottom >= scrollableRect.bottom && + edgeTop <= scrollableRect.top) { + return; + } + if (edgeBottom > scrollableRect.bottom) { + position.jumpTo(position.pixels + edgeBottom - scrollableRect.bottom); + return; + } + if (edgeTop < scrollableRect.top) { + position.jumpTo(position.pixels + edgeTop - scrollableRect.top); + } + return; + case AxisDirection.left: + final double edge = edgeOffsetInScrollableCoordinates.dx; + if (edge >= scrollableRect.right && edge <= scrollableRect.left) { + return; + } + if (edge > scrollableRect.right) { + position.jumpTo(position.pixels + scrollableRect.right - edge); + return; + } + if (edge < scrollableRect.left) { + position.jumpTo(position.pixels + scrollableRect.left - edge); + } + return; + } + } + + bool _globalPositionInScrollable(Offset globalPosition) { + final box = state.context.findRenderObject()! as RenderBox; + final Offset localPosition = box.globalToLocal(globalPosition); + final rect = Rect.fromLTWH(0, 0, box.size.width, box.size.height); + return rect.contains(localPosition); + } + + Rect _dragTargetFromEvent(SelectionEdgeUpdateEvent event) { + return Rect.fromCenter( + center: event.globalPosition, + width: _kDefaultDragTargetSize, + height: _kDefaultDragTargetSize, + ); + } + + @override + SelectionResult dispatchSelectionEventToChild( + Selectable selectable, + SelectionEvent event, + ) { + switch (event.type) { + case SelectionEventType.startEdgeUpdate: + _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; + ensureChildUpdated(selectable); + case SelectionEventType.endEdgeUpdate: + _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; + ensureChildUpdated(selectable); + case SelectionEventType.granularlyExtendSelection: + case SelectionEventType.directionallyExtendSelection: + ensureChildUpdated(selectable); + _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; + _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; + case SelectionEventType.clear: + _selectableEndEdgeUpdateRecords.remove(selectable); + _selectableStartEdgeUpdateRecords.remove(selectable); + case SelectionEventType.selectAll: + case SelectionEventType.selectWord: + case SelectionEventType.selectParagraph: + _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; + _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; + } + return super.dispatchSelectionEventToChild(selectable, event); + } + + @override + void ensureChildUpdated(Selectable selectable) { + final double newRecord = state.position.pixels; + final double? previousStartRecord = + _selectableStartEdgeUpdateRecords[selectable]; + if (_currentDragStartRelatedToOrigin != null && + (previousStartRecord == null || + (newRecord - previousStartRecord).abs() > + precisionErrorTolerance)) { + // Make sure the selectable has up to date events. + final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); + final Offset startOffset = _currentDragStartRelatedToOrigin!.translate( + -deltaToOrigin.dx, + -deltaToOrigin.dy, + ); + selectable.dispatchSelectionEvent( + SelectionEdgeUpdateEvent.forStart(globalPosition: startOffset), + ); + // Make sure we track that we have synthesized a start event for this selectable, + // so we don't synthesize events unnecessarily. + _selectableStartEdgeUpdateRecords[selectable] = state.position.pixels; + } + final double? previousEndRecord = + _selectableEndEdgeUpdateRecords[selectable]; + if (_currentDragEndRelatedToOrigin != null && + (previousEndRecord == null || + (newRecord - previousEndRecord).abs() > precisionErrorTolerance)) { + // Make sure the selectable has up to date events. + final Offset deltaToOrigin = _getDeltaToScrollOrigin(state); + final Offset endOffset = _currentDragEndRelatedToOrigin!.translate( + -deltaToOrigin.dx, + -deltaToOrigin.dy, + ); + selectable.dispatchSelectionEvent( + SelectionEdgeUpdateEvent.forEnd(globalPosition: endOffset), + ); + // Make sure we track that we have synthesized an end event for this selectable, + // so we don't synthesize events unnecessarily. + _selectableEndEdgeUpdateRecords[selectable] = state.position.pixels; + } + } + + @override + void dispose() { + _selectableStartEdgeUpdateRecords.clear(); + _selectableEndEdgeUpdateRecords.clear(); + _scheduledLayoutChange = false; + _autoScroller.stopAutoScroll(); + super.dispose(); + } +} + +Offset _getDeltaToScrollOrigin(ScrollableState scrollableState) { + return switch (scrollableState.axisDirection) { + AxisDirection.up => Offset(0, -scrollableState.position.pixels), + AxisDirection.down => Offset(0, scrollableState.position.pixels), + AxisDirection.left => Offset(-scrollableState.position.pixels, 0), + AxisDirection.right => Offset(scrollableState.position.pixels, 0), + }; +} + +/// With [_ScrollSemantics] certain child [SemanticsNode]s can be +/// excluded from the scrollable area for semantics purposes. +/// +/// Nodes, that are to be excluded, have to be tagged with +/// [RenderViewport.excludeFromScrolling] and the [RenderAbstractViewport] in +/// use has to add the [RenderViewport.useTwoPaneSemantics] tag to its +/// [SemanticsConfiguration] by overriding +/// [RenderObject.describeSemanticsConfiguration]. +/// +/// If the tag [RenderViewport.useTwoPaneSemantics] is present on the viewport, +/// two semantics nodes will be used to represent the [Scrollable]: The outer +/// node will contain all children, that are excluded from scrolling. The inner +/// node, which is annotated with the scrolling actions, will house the +/// scrollable children. +class _ScrollSemantics extends SingleChildRenderObjectWidget { + const _ScrollSemantics({ + super.key, + required this.position, + required this.allowImplicitScrolling, + required this.axis, + required this.semanticChildCount, + super.child, + }) : assert(semanticChildCount == null || semanticChildCount >= 0); + + final ScrollPosition position; + final bool allowImplicitScrolling; + final int? semanticChildCount; + final Axis axis; + + @override + _RenderScrollSemantics createRenderObject(BuildContext context) { + return _RenderScrollSemantics( + position: position, + allowImplicitScrolling: allowImplicitScrolling, + semanticChildCount: semanticChildCount, + axis: axis, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderScrollSemantics renderObject, + ) { + renderObject + ..allowImplicitScrolling = allowImplicitScrolling + ..axis = axis + ..position = position + ..semanticChildCount = semanticChildCount; + } +} + +class _RenderScrollSemantics extends RenderProxyBox { + _RenderScrollSemantics({ + required ScrollPosition position, + required bool allowImplicitScrolling, + required this.axis, + required int? semanticChildCount, + RenderBox? child, + }) : _position = position, + _allowImplicitScrolling = allowImplicitScrolling, + _semanticChildCount = semanticChildCount, + super(child) { + position.addListener(markNeedsSemanticsUpdate); + } + + /// Whether this render object is excluded from the semantic tree. + ScrollPosition get position => _position; + ScrollPosition _position; + set position(ScrollPosition value) { + if (value == _position) { + return; + } + _position.removeListener(markNeedsSemanticsUpdate); + _position = value; + _position.addListener(markNeedsSemanticsUpdate); + markNeedsSemanticsUpdate(); + } + + /// Whether this node can be scrolled implicitly. + bool get allowImplicitScrolling => _allowImplicitScrolling; + bool _allowImplicitScrolling; + set allowImplicitScrolling(bool value) { + if (value == _allowImplicitScrolling) { + return; + } + _allowImplicitScrolling = value; + markNeedsSemanticsUpdate(); + } + + Axis axis; + + int? get semanticChildCount => _semanticChildCount; + int? _semanticChildCount; + set semanticChildCount(int? value) { + if (value == semanticChildCount) { + return; + } + _semanticChildCount = value; + markNeedsSemanticsUpdate(); + } + + void _onScrollToOffset(Offset targetOffset) { + final double offset = switch (axis) { + Axis.horizontal => targetOffset.dx, + Axis.vertical => targetOffset.dy, + }; + _position.jumpTo(offset); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.isSemanticBoundary = true; + if (position.haveDimensions) { + config + ..hasImplicitScrolling = allowImplicitScrolling + ..scrollPosition = _position.pixels + ..scrollExtentMax = _position.maxScrollExtent + ..scrollExtentMin = _position.minScrollExtent + ..scrollChildCount = semanticChildCount; + if (position.maxScrollExtent > position.minScrollExtent && + allowImplicitScrolling) { + config.onScrollToOffset = _onScrollToOffset; + } + } + } + + SemanticsNode? _innerNode; + + @override + void assembleSemanticsNode( + SemanticsNode node, + SemanticsConfiguration config, + Iterable children, + ) { + if (children.isEmpty || + !children.first.isTagged(RenderViewport.useTwoPaneSemantics)) { + _innerNode = null; + super.assembleSemanticsNode(node, config, children); + return; + } + + (_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect; + + int? firstVisibleIndex; + final excluded = [_innerNode!]; + final included = []; + for (final child in children) { + assert(child.isTagged(RenderViewport.useTwoPaneSemantics)); + if (child.isTagged(RenderViewport.excludeFromScrolling)) { + excluded.add(child); + } else { + if (!child.flagsCollection.isHidden) { + firstVisibleIndex ??= child.indexInParent; + } + included.add(child); + } + } + config.scrollIndex = firstVisibleIndex; + node.updateWith(config: null, childrenInInversePaintOrder: excluded); + _innerNode!.updateWith( + config: config, + childrenInInversePaintOrder: included, + ); + } + + @override + void clearSemantics() { + super.clearSemantics(); + _innerNode = null; + } +} + +// Not using a RestorableDouble because we want to allow null values and override +// [enabled]. +class _RestorableScrollOffset extends RestorableValue { + @override + double? createDefaultValue() => null; + + @override + void didUpdateValue(double? oldValue) { + notifyListeners(); + } + + @override + double fromPrimitives(Object? data) { + return data! as double; + } + + @override + Object? toPrimitives() { + return value; + } + + @override + bool get enabled => value != null; +} diff --git a/lib/common/widgets/flutter/scroll_view/scrollable_helpers.dart b/lib/common/widgets/flutter/scroll_view/scrollable_helpers.dart new file mode 100644 index 000000000..856d19834 --- /dev/null +++ b/lib/common/widgets/flutter/scroll_view/scrollable_helpers.dart @@ -0,0 +1,208 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// @docImport 'package:flutter/material.dart'; +/// +/// @docImport 'overscroll_indicator.dart'; +/// @docImport 'viewport.dart'; + +// ignore_for_file: dangling_library_doc_comments + +import 'dart:math' as math; + +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scrollable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' + hide EdgeDraggingAutoScroller, Scrollable, ScrollableState; + +/// An auto scroller that scrolls the [scrollable] if a drag gesture drags close +/// to its edge. +/// +/// The scroll velocity is controlled by the [velocityScalar]: +/// +/// velocity = (distance of overscroll) * [velocityScalar]. +class EdgeDraggingAutoScroller { + /// Creates a auto scroller that scrolls the [scrollable]. + EdgeDraggingAutoScroller( + this.scrollable, { + this.onScrollViewScrolled, + required this.velocityScalar, + }); + + /// The [Scrollable] this auto scroller is scrolling. + final ScrollableState scrollable; + + /// Called when a scroll view is scrolled. + /// + /// The scroll view may be scrolled multiple times in a row until the drag + /// target no longer triggers the auto scroll. This callback will be called + /// in between each scroll. + final VoidCallback? onScrollViewScrolled; + + /// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar} + /// The velocity scalar per pixel over scroll. + /// + /// It represents how the velocity scale with the over scroll distance. The + /// auto-scroll velocity = (distance of overscroll) * velocityScalar. + /// {@endtemplate} + final double velocityScalar; + + late Rect _dragTargetRelatedToScrollOrigin; + + /// Whether the auto scroll is in progress. + bool get scrolling => _scrolling; + bool _scrolling = false; + + double _offsetExtent(Offset offset, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => offset.dx, + Axis.vertical => offset.dy, + }; + } + + double _sizeExtent(Size size, Axis scrollDirection) { + return switch (scrollDirection) { + Axis.horizontal => size.width, + Axis.vertical => size.height, + }; + } + + AxisDirection get _axisDirection => scrollable.axisDirection; + Axis get _scrollDirection => axisDirectionToAxis(_axisDirection); + + /// Starts the auto scroll if the [dragTarget] is close to the edge. + /// + /// The scroll starts to scroll the [scrollable] if the target rect is close + /// to the edge of the [scrollable]; otherwise, it remains stationary. + /// + /// If the scrollable is already scrolling, calling this method updates the + /// previous dragTarget to the new value and continues scrolling if necessary. + void startAutoScrollIfNecessary(Rect dragTarget) { + final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; + _dragTargetRelatedToScrollOrigin = dragTarget.translate( + deltaToOrigin.dx, + deltaToOrigin.dy, + ); + if (_scrolling) { + // The change will be picked up in the next scroll. + return; + } + assert(!_scrolling); + _scroll(); + } + + /// Stop any ongoing auto scrolling. + void stopAutoScroll() { + _scrolling = false; + } + + Future _scroll() async { + final scrollRenderBox = scrollable.context.findRenderObject()! as RenderBox; + final Matrix4 transform = scrollRenderBox.getTransformTo(null); + final Rect globalRect = MatrixUtils.transformRect( + transform, + Rect.fromLTRB( + 0, + 0, + scrollRenderBox.size.width, + scrollRenderBox.size.height, + ), + ); + final Rect transformedDragTarget = MatrixUtils.transformRect( + transform, + _dragTargetRelatedToScrollOrigin, + ); + + assert( + (globalRect.size.width + precisionErrorTolerance) >= + transformedDragTarget.size.width && + (globalRect.size.height + precisionErrorTolerance) >= + transformedDragTarget.size.height, + 'Drag target size is larger than scrollable size, which may cause bouncing', + ); + _scrolling = true; + double? newOffset; + const overDragMax = 20.0; + + final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; + final Offset viewportOrigin = globalRect.topLeft.translate( + deltaToOrigin.dx, + deltaToOrigin.dy, + ); + final double viewportStart = _offsetExtent( + viewportOrigin, + _scrollDirection, + ); + final double viewportEnd = + viewportStart + _sizeExtent(globalRect.size, _scrollDirection); + + final double proxyStart = _offsetExtent( + _dragTargetRelatedToScrollOrigin.topLeft, + _scrollDirection, + ); + final double proxyEnd = _offsetExtent( + _dragTargetRelatedToScrollOrigin.bottomRight, + _scrollDirection, + ); + switch (_axisDirection) { + case AxisDirection.up: + case AxisDirection.left: + if (proxyEnd > viewportEnd && + scrollable.position.pixels > scrollable.position.minScrollExtent) { + final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax); + newOffset = math.max( + scrollable.position.minScrollExtent, + scrollable.position.pixels - overDrag, + ); + } else if (proxyStart < viewportStart && + scrollable.position.pixels < scrollable.position.maxScrollExtent) { + final double overDrag = math.min( + viewportStart - proxyStart, + overDragMax, + ); + newOffset = math.min( + scrollable.position.maxScrollExtent, + scrollable.position.pixels + overDrag, + ); + } + case AxisDirection.right: + case AxisDirection.down: + if (proxyStart < viewportStart && + scrollable.position.pixels > scrollable.position.minScrollExtent) { + final double overDrag = math.min( + viewportStart - proxyStart, + overDragMax, + ); + newOffset = math.max( + scrollable.position.minScrollExtent, + scrollable.position.pixels - overDrag, + ); + } else if (proxyEnd > viewportEnd && + scrollable.position.pixels < scrollable.position.maxScrollExtent) { + final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax); + newOffset = math.min( + scrollable.position.maxScrollExtent, + scrollable.position.pixels + overDrag, + ); + } + } + + if (newOffset == null || + (newOffset - scrollable.position.pixels).abs() < 1.0) { + // Drag should not trigger scroll. + _scrolling = false; + return; + } + final duration = Duration(milliseconds: (1000 / velocityScalar).round()); + await scrollable.position.animateTo( + newOffset, + duration: duration, + curve: Curves.linear, + ); + onScrollViewScrolled?.call(); + if (_scrolling) { + await _scroll(); + } + } +} diff --git a/lib/common/widgets/gesture/vertical_drag_gesture_recognizer.dart b/lib/common/widgets/gesture/vertical_drag_gesture_recognizer.dart new file mode 100644 index 000000000..20c1e6ee4 --- /dev/null +++ b/lib/common/widgets/gesture/vertical_drag_gesture_recognizer.dart @@ -0,0 +1,39 @@ +import 'package:flutter/gestures.dart' + show VerticalDragGestureRecognizer, PointerEvent, RecognizerCallback; + +typedef IsDyAllowed = bool Function(double dy); + +class CustomVerticalDragGestureRecognizer + extends VerticalDragGestureRecognizer { + CustomVerticalDragGestureRecognizer({ + super.debugOwner, + super.supportedDevices, + super.allowedButtonsFilter, + }); + + IsDyAllowed? isDyAllowed; + + bool _isDyAllowed = false; + + @override + bool isPointerAllowed(PointerEvent event) { + _isDyAllowed = isDyAllowed?.call(event.localPosition.dy) ?? true; + return super.isPointerAllowed(event); + } + + @override + T? invokeCallback( + String name, + RecognizerCallback callback, { + String Function()? debugReport, + }) { + if (!_isDyAllowed) return null; + return super.invokeCallback(name, callback, debugReport: debugReport); + } + + @override + void dispose() { + isDyAllowed = null; + super.dispose(); + } +} diff --git a/lib/common/widgets/loading_widget/loading_widget.dart b/lib/common/widgets/loading_widget/loading_widget.dart index d1df73e60..932bf9f87 100644 --- a/lib/common/widgets/loading_widget/loading_widget.dart +++ b/lib/common/widgets/loading_widget/loading_widget.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/m3e_loading_indicator.dart'; import 'package:flutter/material.dart'; @@ -8,13 +9,13 @@ const Widget linearLoading = SliverToBoxAdapter( child: LinearProgressIndicator(), ); -const Widget scrollableError = CustomScrollView(slivers: [HttpError()]); +const Widget scrollableError = customScrollView(slivers: [HttpError()]); Widget scrollErrorWidget({ String? errMsg, VoidCallback? onReload, ScrollController? controller, -}) => CustomScrollView( +}) => customScrollView( controller: controller, slivers: [ HttpError( diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 29ee5b55e..7d2edf6e1 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -8,6 +8,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/export_import.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -90,7 +91,7 @@ class _AboutPageState extends State { return Scaffold( appBar: showAppBar ? AppBar(title: const Text('关于')) : null, resizeToAvoidBottomInset: false, - body: ListView( + body: listView( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 5d92d25f1..936c424ef 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; @@ -83,7 +84,7 @@ class _ArticlePageState extends CommonDynPageState { if (isPortrait) { return Padding( padding: EdgeInsets.symmetric(horizontal: padding), - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -112,7 +113,7 @@ class _ArticlePageState extends CommonDynPageState { children: [ Expanded( flex: flex, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -144,7 +145,7 @@ class _ArticlePageState extends CommonDynPageState { resizeToAvoidBottomInset: false, body: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/article_list/view.dart b/lib/pages/article_list/view.dart index 77c2d863e..90b8d9342 100644 --- a/lib/pages/article_list/view.dart +++ b/lib/pages/article_list/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/constants.dart'; @@ -38,7 +39,7 @@ class _ArticleListPageState extends State with GridMixin { color: theme.colorScheme.surface, child: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ Obx(() => _buildHeader(theme, _controller.list.value)), diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index de5ffc288..92acfb053 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; @@ -202,7 +203,7 @@ class _AudioPageState extends State { builder: (context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - Widget child = CustomScrollView( + Widget child = customScrollView( controller: scrollController, physics: _controller.reachStart ? null diff --git a/lib/pages/blacklist/view.dart b/lib/pages/blacklist/view.dart index 584e07bc2..a36ad9b7f 100644 --- a/lib/pages/blacklist/view.dart +++ b/lib/pages/blacklist/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -45,7 +46,7 @@ class _BlackListPageState extends State { ), body: refreshIndicator( onRefresh: _blackListController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _blackListController.scrollController, slivers: [ diff --git a/lib/pages/bubble/view.dart b/lib/pages/bubble/view.dart index 159ece5ca..95fb88b35 100644 --- a/lib/pages/bubble/view.dart +++ b/lib/pages/bubble/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; @@ -54,7 +55,7 @@ class _BubblePageState extends State final padding = MediaQuery.viewPaddingOf(context); Widget child = refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _controller.scrollController, slivers: [ diff --git a/lib/pages/common/search/common_search_page.dart b/lib/pages/common/search/common_search_page.dart index 3855836e5..6e7e029b6 100644 --- a/lib/pages/common/search/common_search_page.dart +++ b/lib/pages/common/search/common_search_page.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -38,7 +39,7 @@ abstract class CommonSearchPageState Widget _build(bool multiSelect) { return Scaffold( appBar: _buildBar(multiSelect), - body: CustomScrollView( + body: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, slivers: [ diff --git a/lib/pages/danmaku_block/view.dart b/lib/pages/danmaku_block/view.dart index 6e4e349de..e9d54b770 100644 --- a/lib/pages/danmaku_block/view.dart +++ b/lib/pages/danmaku_block/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; @@ -84,7 +85,7 @@ class _DanmakuBlockPageState extends State { if (list.isEmpty) { return scrollableError; } - return ListView.builder( + return ExtendedListView.builder( itemCount: list.length, padding: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 100, diff --git a/lib/pages/dlna/view.dart b/lib/pages/dlna/view.dart index 39ed53944..d5ba978dd 100644 --- a/lib/pages/dlna/view.dart +++ b/lib/pages/dlna/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; @@ -83,7 +84,7 @@ class _DLNAPageState extends State { const SizedBox(width: 6), ], ), - body: CustomScrollView( + body: customScrollView( slivers: [ if (_isSearching) linearLoading, ViewSliverSafeArea(sliver: _buildBody(colorScheme)), diff --git a/lib/pages/download/detail/view.dart b/lib/pages/download/detail/view.dart index be1312dda..a19520d93 100644 --- a/lib/pages/download/detail/view.dart +++ b/lib/pages/download/detail/view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; @@ -143,7 +144,7 @@ class _DownloadDetailPageState extends State ], ), ), - body: CustomScrollView( + body: customScrollView( slivers: [ ViewSliverSafeArea( sliver: Obx(() { diff --git a/lib/pages/download/downloading/view.dart b/lib/pages/download/downloading/view.dart index 001da6ea9..c55aa2dee 100644 --- a/lib/pages/download/downloading/view.dart +++ b/lib/pages/download/downloading/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; @@ -63,7 +64,7 @@ class _DownloadingPageState extends State ], ), ), - body: CustomScrollView( + body: customScrollView( slivers: [ ViewSliverSafeArea( sliver: Obx(() { diff --git a/lib/pages/download/view.dart b/lib/pages/download/view.dart index b743e2040..9d2e238ea 100644 --- a/lib/pages/download/view.dart +++ b/lib/pages/download/view.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -122,7 +123,7 @@ class _DownloadPageState extends State { ), body: Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), - child: CustomScrollView( + child: customScrollView( slivers: [ Obx(() { final entry = diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index 8a53c6ca6..a5021275a 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/assets.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -41,7 +42,7 @@ class _UpPanelState extends State { final upData = controller.upState.value.data; final List upList = upData.upList; final List? liveList = upData.liveUsers?.items; - return CustomScrollView( + return customScrollView( scrollDirection: isTop ? Axis.horizontal : Axis.vertical, physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, diff --git a/lib/pages/dynamics_create_vote/view.dart b/lib/pages/dynamics_create_vote/view.dart index f7610a54d..1e476bc6b 100644 --- a/lib/pages/dynamics_create_vote/view.dart +++ b/lib/pages/dynamics_create_vote/view.dart @@ -1,6 +1,7 @@ import 'dart:io' show File; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/time_picker.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart'; @@ -57,7 +58,7 @@ class _CreateVotePageState extends State { appBar: AppBar( title: Text('${_controller.voteId != null ? '' : '发起'}投票'), ), - body: ListView( + body: listView( padding: EdgeInsets.only( left: padding.left + 16, right: padding.right + 16, diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index f6080dbea..42d8fbbfb 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/constants.dart'; @@ -248,7 +249,7 @@ class _DynamicDetailPageState extends CommonDynPageState { if (isPortrait) { child = Padding( padding: EdgeInsets.symmetric(horizontal: padding), - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -275,7 +276,7 @@ class _DynamicDetailPageState extends CommonDynPageState { children: [ Expanded( flex: flex, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -307,7 +308,7 @@ class _DynamicDetailPageState extends CommonDynPageState { resizeToAvoidBottomInset: false, body: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart index 3bd8f679a..a1666fc67 100644 --- a/lib/pages/dynamics_mention/view.dart +++ b/lib/pages/dynamics_mention/view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart' as topic_sheet; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; @@ -184,7 +185,7 @@ class _DynMentionPanelState } return false; }, - child: CustomScrollView( + child: customScrollView( controller: widget.scrollController, slivers: [ Obx( diff --git a/lib/pages/dynamics_select_topic/view.dart b/lib/pages/dynamics_select_topic/view.dart index 1d9bd60c1..6443a3978 100644 --- a/lib/pages/dynamics_select_topic/view.dart +++ b/lib/pages/dynamics_select_topic/view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart' as topic_sheet; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/topic_item.dart'; @@ -191,7 +192,7 @@ class _SelectTopicPanelState Loading() => m3eLoading, Success?>(:final response) => response != null && response.isNotEmpty - ? ListView.builder( + ? ExtendedListView.builder( padding: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 100, ), diff --git a/lib/pages/dynamics_tab/view.dart b/lib/pages/dynamics_tab/view.dart index c14c2e08b..d63b0a00b 100644 --- a/lib/pages/dynamics_tab/view.dart +++ b/lib/pages/dynamics_tab/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart'; @@ -70,7 +71,7 @@ class _DynamicsTabPageState extends State dynamicsController.queryFollowUp(); return controller.onRefresh(); }, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, slivers: [ diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 544046c85..e42fa51d1 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; @@ -66,7 +67,7 @@ class _DynTopicPageState extends State with DynMixin { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/dynamics_topic_rcmd/view.dart b/lib/pages/dynamics_topic_rcmd/view.dart index f80c09806..fe3ad6119 100644 --- a/lib/pages/dynamics_topic_rcmd/view.dart +++ b/lib/pages/dynamics_topic_rcmd/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; @@ -26,7 +27,7 @@ class _DynTopicRcmdPageState extends State { appBar: AppBar(title: const Text('话题')), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ ViewSliverSafeArea( diff --git a/lib/pages/episode_panel/view.dart b/lib/pages/episode_panel/view.dart index ee082b0a3..8a6adfd83 100644 --- a/lib/pages/episode_panel/view.dart +++ b/lib/pages/episode_panel/view.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; @@ -280,7 +281,7 @@ class _EpisodePanelState extends State ) { final isCurrTab = tabIndex == widget.initialTabIndex; return KeepAliveWrapper( - child: CustomScrollView( + child: customScrollView( reverse: _isReversed[tabIndex], physics: const AlwaysScrollableScrollPhysics(), controller: _itemScrollController[tabIndex], diff --git a/lib/pages/fav/article/view.dart b/lib/pages/fav/article/view.dart index f2b74a017..dacc27df7 100644 --- a/lib/pages/fav/article/view.dart +++ b/lib/pages/fav/article/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_article/item.dart'; @@ -30,7 +31,7 @@ class _FavArticlePageState extends State super.build(context); return refreshIndicator( onRefresh: _favArticleController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _favArticleController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav/cheese/view.dart b/lib/pages/fav/cheese/view.dart index c41616520..aef8bbf6a 100644 --- a/lib/pages/fav/cheese/view.dart +++ b/lib/pages/fav/cheese/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_cheese/item.dart'; @@ -29,7 +30,7 @@ class _FavCheesePageState extends State final ThemeData theme = Theme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav/note/child_view.dart b/lib/pages/fav/note/child_view.dart index 4c9efa84e..0318f6a7c 100644 --- a/lib/pages/fav/note/child_view.dart +++ b/lib/pages/fav/note/child_view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_note/list.dart'; @@ -43,7 +44,7 @@ class _FavNoteChildPageState extends State children: [ refreshIndicator( onRefresh: _favNoteController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _favNoteController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav/pgc/child_view.dart b/lib/pages/fav/pgc/child_view.dart index 0f0f43e65..6b18f4693 100644 --- a/lib/pages/fav/pgc/child_view.dart +++ b/lib/pages/fav/pgc/child_view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/fav_pgc_item.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_pgc/list.dart'; @@ -49,7 +50,7 @@ class _FavPgcChildPageState extends State children: [ refreshIndicator( onRefresh: _favPgcController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _favPgcController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav/topic/view.dart b/lib/pages/fav/topic/view.dart index 69f75c3c6..7b3718e33 100644 --- a/lib/pages/fav/topic/view.dart +++ b/lib/pages/fav/topic/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart' show m3eLoading; @@ -33,7 +34,7 @@ class _FavTopicPageState extends State final ThemeData theme = Theme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav/video/view.dart b/lib/pages/fav/video/view.dart index f37ff43ca..75a66938b 100644 --- a/lib/pages/fav/video/view.dart +++ b/lib/pages/fav/video/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; @@ -28,7 +29,7 @@ class _FavVideoPageState extends State super.build(context); return refreshIndicator( onRefresh: _favController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _favController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index a083e49cc..d4cea32a2 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/fav.dart'; @@ -100,7 +101,7 @@ class _FavDetailPageState extends State with GridMixin { ), body: refreshIndicator( onRefresh: _favDetailController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _favDetailController.scrollController, slivers: [ diff --git a/lib/pages/fav_panel/view.dart b/lib/pages/fav_panel/view.dart index bb2fc822f..818113e89 100644 --- a/lib/pages/fav_panel/view.dart +++ b/lib/pages/fav_panel/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; @@ -42,7 +43,7 @@ class _FavPanelState extends State { late final list = widget.ctr.favFolderData.value.list!; return switch (loadingState) { Loading() => m3eLoading, - Success() => ListView.builder( + Success() => ExtendedListView.builder( controller: widget.scrollController, itemCount: list.length, itemBuilder: (context, index) { diff --git a/lib/pages/follow/child/child_view.dart b/lib/pages/follow/child/child_view.dart index 649bb6d8d..48bbf5693 100644 --- a/lib/pages/follow/child/child_view.dart +++ b/lib/pages/follow/child/child_view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/follow_order_type.dart'; @@ -58,7 +59,7 @@ class _FollowChildPageState extends State padding: EdgeInsets.only(left: padding.left, right: padding.right), child: refreshIndicator( onRefresh: _followController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _followController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/follow_type/view.dart b/lib/pages/follow_type/view.dart index 1fa199ae6..42b8f7ecf 100644 --- a/lib/pages/follow_type/view.dart +++ b/lib/pages/follow_type/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -24,7 +25,7 @@ abstract class FollowTypePageState extends State { appBar: appBar, body: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), // controller: controller.scrollController, slivers: [ diff --git a/lib/pages/group_panel/view.dart b/lib/pages/group_panel/view.dart index 921e41e25..7e547ca23 100644 --- a/lib/pages/group_panel/view.dart +++ b/lib/pages/group_panel/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; @@ -69,7 +70,7 @@ class _GroupPanelState extends State { Widget get _buildBody { return switch (loadingState) { Loading() => m3eLoading, - Success(:final response) => ListView.builder( + Success(:final response) => ExtendedListView.builder( controller: widget.scrollController, itemCount: response.length, itemBuilder: (context, index) { diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index d8ba6540c..55c6e4645 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -62,7 +63,7 @@ class _HistoryPageState extends State final padding = MediaQuery.viewPaddingOf(context); Widget child = refreshIndicator( onRefresh: _historyController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _historyController.scrollController, slivers: [ diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 472dad6b0..b1cfcefdc 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; @@ -60,7 +61,7 @@ class _HotPageState extends State super.build(context); return refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, slivers: [ diff --git a/lib/pages/later/child_view.dart b/lib/pages/later/child_view.dart index 02b84140e..83f05a9c5 100644 --- a/lib/pages/later/child_view.dart +++ b/lib/pages/later/child_view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/later_view_type.dart'; @@ -44,7 +45,7 @@ class _LaterViewChildPageState extends State super.build(context); return refreshIndicator( onRefresh: _laterController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: _laterController.scrollController, slivers: [ diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 3fe46b972..e9ed00a94 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; @@ -55,7 +56,7 @@ class _LivePageState extends State decoration: const BoxDecoration(borderRadius: Style.mdRadius), child: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/live_area_detail/child/view.dart b/lib/pages/live_area_detail/child/view.dart index 04d84bda6..7d6337298 100644 --- a/lib/pages/live_area_detail/child/view.dart +++ b/lib/pages/live_area_detail/child/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -47,7 +48,7 @@ class _LiveAreaChildPageState extends State final ThemeData theme = Theme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/live_follow/view.dart b/lib/pages/live_follow/view.dart index 0ea623ebb..ea3b4ea5d 100644 --- a/lib/pages/live_follow/view.dart +++ b/lib/pages/live_follow/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/live/live_follow/item.dart'; @@ -35,7 +36,7 @@ class _LiveFollowPageState extends State { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/live_room/contribution_rank/view.dart b/lib/pages/live_room/contribution_rank/view.dart index b29e6d144..d9108adc9 100644 --- a/lib/pages/live_room/contribution_rank/view.dart +++ b/lib/pages/live_room/contribution_rank/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; @@ -131,7 +132,7 @@ class _ContributionRankTypeState extends State<_ContributionRankType> type: .transparency, child: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/live_search/child/view.dart b/lib/pages/live_search/child/view.dart index 2c4447022..d74e05cdb 100644 --- a/lib/pages/live_search/child/view.dart +++ b/lib/pages/live_search/child/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/live/live_search_type.dart'; @@ -37,7 +38,7 @@ class _LiveSearchChildPageState extends State double padding = widget.searchType == LiveSearchType.room ? 12 : 0; return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/log_table/view.dart b/lib/pages/log_table/view.dart index f1d0658d3..2fbdac72d 100644 --- a/lib/pages/log_table/view.dart +++ b/lib/pages/log_table/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -22,7 +23,7 @@ class _LogPageState extends State> { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar(title: Text(_controller.title)), - body: CustomScrollView( + body: customScrollView( slivers: [ SliverPadding( padding: EdgeInsets.only( diff --git a/lib/pages/login_devices/view.dart b/lib/pages/login_devices/view.dart index 1c130f119..41354d04c 100644 --- a/lib/pages/login_devices/view.dart +++ b/lib/pages/login_devices/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -27,7 +28,7 @@ class LoginDevicesPageState extends State { appBar: AppBar(title: const Text('登录设备')), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ ViewSliverSafeArea( diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart index 40b305c10..113ad83ed 100644 --- a/lib/pages/main_reply/view.dart +++ b/lib/pages/main_reply/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; @@ -77,7 +78,7 @@ class _MainReplyPageState extends State left: padding.left, right: padding.right, ), - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ buildReplyHeader(colorScheme), diff --git a/lib/pages/match_info/view.dart b/lib/pages/match_info/view.dart index c903a4456..9b550cf4e 100644 --- a/lib/pages/match_info/view.dart +++ b/lib/pages/match_info/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -47,7 +48,7 @@ class _MatchInfoPageState extends CommonDynPageState { body: ViewSafeArea( child: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/member/widget/medal_wall.dart b/lib/pages/member/widget/medal_wall.dart index 3a0fcfafc..659800afa 100644 --- a/lib/pages/member/widget/medal_wall.dart +++ b/lib/pages/member/widget/medal_wall.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/assets.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/models_new/live/live_medal_wall/data.dart'; @@ -22,7 +23,7 @@ class MedalWall extends StatelessWidget { title: const Text('粉丝勋章墙'), contentPadding: const .symmetric(vertical: 16), constraints: const BoxConstraints.tightFor(width: 380), - content: CustomScrollView( + content: customScrollView( shrinkWrap: true, slivers: [ SliverToBoxAdapter( diff --git a/lib/pages/member_article/view.dart b/lib/pages/member_article/view.dart index ef04dac70..1175332aa 100644 --- a/lib/pages/member_article/view.dart +++ b/lib/pages/member_article/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_article/item.dart'; @@ -43,7 +44,7 @@ class _MemberArticleState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_audio/view.dart b/lib/pages/member_audio/view.dart index 78ce6f192..8990f6f89 100644 --- a/lib/pages/member_audio/view.dart +++ b/lib/pages/member_audio/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; @@ -44,7 +45,7 @@ class _MemberAudioState extends State final colorScheme = ColorScheme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_cheese/view.dart b/lib/pages/member_cheese/view.dart index 543924f9a..150662444 100644 --- a/lib/pages/member_cheese/view.dart +++ b/lib/pages/member_cheese/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_cheese/item.dart'; @@ -40,7 +41,7 @@ class _MemberCheeseState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_coin_arc/view.dart b/lib/pages/member_coin_arc/view.dart index 43de1ecab..059ad9670 100644 --- a/lib/pages/member_coin_arc/view.dart +++ b/lib/pages/member_coin_arc/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/member/coin_like_arc/item.dart'; @@ -51,7 +52,7 @@ class _MemberCoinArcPageState extends State { ), body: refreshIndicator( onRefresh: _ctr.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_comic/view.dart b/lib/pages/member_comic/view.dart index d1a177b0b..5e7c995db 100644 --- a/lib/pages/member_comic/view.dart +++ b/lib/pages/member_comic/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; @@ -40,7 +41,7 @@ class _MemberComicState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_dynamics/view.dart b/lib/pages/member_dynamics/view.dart index a24619ddd..3f32a5d12 100644 --- a/lib/pages/member_dynamics/view.dart +++ b/lib/pages/member_dynamics/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; @@ -61,7 +62,7 @@ class _MemberDynamicsPageState extends State Widget _buildBody(EdgeInsets padding) => refreshIndicator( onRefresh: _memberDynamicController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_favorite/view.dart b/lib/pages/member_favorite/view.dart index 599f33eae..9e418f44a 100644 --- a/lib/pages/member_favorite/view.dart +++ b/lib/pages/member_favorite/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -46,7 +47,7 @@ class _MemberFavoriteState extends State final theme = Theme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: _FavScrollPhysics(controller: _controller), slivers: [ SliverPadding( diff --git a/lib/pages/member_guard/view.dart b/lib/pages/member_guard/view.dart index f3c49db11..992f55c4b 100644 --- a/lib/pages/member_guard/view.dart +++ b/lib/pages/member_guard/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; @@ -60,7 +61,7 @@ class _MemberGuardState extends State { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( slivers: [ ViewSliverSafeArea( sliver: Obx(() => _buildBody(_controller.loadingState.value)), diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index d8c6e3acc..067fe756b 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/more_btn.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space/data.dart'; @@ -76,7 +77,7 @@ class _MemberHomeState extends State Loading() => m3eLoading, Success(response: final res) => res != null - ? CustomScrollView( + ? customScrollView( slivers: [ if (res.archive?.item?.isNotEmpty == true) ...[ _header( diff --git a/lib/pages/member_like_arc/view.dart b/lib/pages/member_like_arc/view.dart index 48ae86c2c..4d6aa0f4a 100644 --- a/lib/pages/member_like_arc/view.dart +++ b/lib/pages/member_like_arc/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/member/coin_like_arc/item.dart'; @@ -51,7 +52,7 @@ class _MemberLikeArcPageState extends State { ), body: refreshIndicator( onRefresh: _ctr.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index bbc67d92b..b414b7950 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/space_opus.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_opus/item.dart'; @@ -53,7 +54,7 @@ class _MemberOpusState extends State children: [ refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_pgc/view.dart b/lib/pages/member_pgc/view.dart index 1280415b7..7bab72de6 100644 --- a/lib/pages/member_pgc/view.dart +++ b/lib/pages/member_pgc/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; @@ -47,7 +48,7 @@ class _MemberBangumiState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_profile/view.dart b/lib/pages/member_profile/view.dart index fa4082b48..527a8b42c 100644 --- a/lib/pages/member_profile/view.dart +++ b/lib/pages/member_profile/view.dart @@ -1,6 +1,7 @@ import 'dart:io' show File; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/constants.dart'; @@ -128,7 +129,7 @@ class _EditProfilePageState extends State { return switch (loadingState) { Loading() => m3eLoading, - Success(:final response) => ListView( + Success(:final response) => listView( padding: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 25, ), diff --git a/lib/pages/member_search/child/view.dart b/lib/pages/member_search/child/view.dart index 1a4cf5bf6..375ae6c98 100644 --- a/lib/pages/member_search/child/view.dart +++ b/lib/pages/member_search/child/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -36,7 +37,7 @@ class _MemberSearchChildPageState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/member_season_series/view.dart b/lib/pages/member_season_series/view.dart index 9e3439fae..a2fa693b3 100644 --- a/lib/pages/member_season_series/view.dart +++ b/lib/pages/member_season_series/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -44,7 +45,7 @@ class _SeasonSeriesPageState extends State @override Widget build(BuildContext context) { super.build(context); - return CustomScrollView( + return customScrollView( slivers: [ SliverPadding( padding: EdgeInsets.only( diff --git a/lib/pages/member_shop/view.dart b/lib/pages/member_shop/view.dart index 8d8120f93..74a06950e 100644 --- a/lib/pages/member_shop/view.dart +++ b/lib/pages/member_shop/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/space_opus.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_shop/item.dart'; @@ -45,7 +46,7 @@ class _MemberShopState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/member_upower_rank/view.dart b/lib/pages/member_upower_rank/view.dart index a10f2e2fd..09c744c4a 100644 --- a/lib/pages/member_upower_rank/view.dart +++ b/lib/pages/member_upower_rank/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -72,7 +73,7 @@ class _UpowerRankPageState extends State final padding = MediaQuery.viewPaddingOf(context); final child = refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 1ec37c9d5..b7f87d68a 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; @@ -99,7 +100,7 @@ class _MemberVideoState extends State } } }, - child: CustomScrollView( + child: customScrollView( physics: ReloadScrollPhysics(controller: _controller), slivers: [ SliverPadding( diff --git a/lib/pages/member_video_web/base/view.dart b/lib/pages/member_video_web/base/view.dart index a476c194e..ebd1fd197 100644 --- a/lib/pages/member_video_web/base/view.dart +++ b/lib/pages/member_video_web/base/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; @@ -63,7 +64,7 @@ abstract class BaseVideoWebState< ), body: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: ReloadScrollPhysics(controller: controller), slivers: [ SliverPadding( diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index e133eb72a..81fac2845 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/nav_bar_config.dart'; @@ -78,7 +79,7 @@ class _MediaPageState extends CommonPageState child: refreshIndicator( onRefresh: controller.onRefresh, child: onBuild( - ListView( + listView( padding: const .only(bottom: 100), physics: const AlwaysScrollableScrollPhysics(), children: [ diff --git a/lib/pages/msg_feed_top/at_me/view.dart b/lib/pages/msg_feed_top/at_me/view.dart index 554888ee6..7f2d3701a 100644 --- a/lib/pages/msg_feed_top/at_me/view.dart +++ b/lib/pages/msg_feed_top/at_me/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pbenum.dart' @@ -52,7 +53,7 @@ class _AtMePageState extends State { ), body: refreshIndicator( onRefresh: _atMeController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/msg_feed_top/like_detail/view.dart b/lib/pages/msg_feed_top/like_detail/view.dart index c4f55fc2a..20e24c89e 100644 --- a/lib/pages/msg_feed_top/like_detail/view.dart +++ b/lib/pages/msg_feed_top/like_detail/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -34,7 +35,7 @@ class _LikeDetailPageState extends State { appBar: AppBar(title: const Text('点赞详情')), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index c5164002c..b20bd2d33 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; @@ -53,7 +54,7 @@ class _LikeMePageState extends State { ), body: refreshIndicator( onRefresh: _likeMeController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart index ed941d6db..63a2781cb 100644 --- a/lib/pages/msg_feed_top/reply_me/view.dart +++ b/lib/pages/msg_feed_top/reply_me/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pbenum.dart' @@ -52,7 +53,7 @@ class _ReplyMePageState extends State { ), body: refreshIndicator( onRefresh: _replyMeController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/msg_feed_top/sys_msg/view.dart b/lib/pages/msg_feed_top/sys_msg/view.dart index e33e34a42..cee244a10 100644 --- a/lib/pages/msg_feed_top/sys_msg/view.dart +++ b/lib/pages/msg_feed_top/sys_msg/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/msg_feed_sys_msg_.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -37,7 +38,7 @@ class _SysMsgPageState extends State { appBar: AppBar(title: const Text('系统通知')), body: refreshIndicator( onRefresh: _sysMsgController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/music/video/view.dart b/lib/pages/music/video/view.dart index 2d6bad63a..dff82ca41 100644 --- a/lib/pages/music/video/view.dart +++ b/lib/pages/music/video/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -33,7 +34,7 @@ class _MusicRecommendPageState extends State color: theme.colorScheme.surface, child: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ _buildAppBar(theme, padding), diff --git a/lib/pages/music/view.dart b/lib/pages/music/view.dart index e637c6bfe..2fd35d84c 100644 --- a/lib/pages/music/view.dart +++ b/lib/pages/music/view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; @@ -114,7 +115,7 @@ class _MusicDetailPageState extends CommonDynPageState { if (isPortrait) { child = Padding( padding: EdgeInsets.symmetric(horizontal: padding), - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -140,7 +141,7 @@ class _MusicDetailPageState extends CommonDynPageState { children: [ Expanded( flex: flex, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ @@ -173,7 +174,7 @@ class _MusicDetailPageState extends CommonDynPageState { resizeToAvoidBottomInset: false, body: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/my_reply/view.dart b/lib/pages/my_reply/view.dart index a10553dc7..4c789e6dc 100644 --- a/lib/pages/my_reply/view.dart +++ b/lib/pages/my_reply/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/export_import.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -76,7 +77,7 @@ class _MyReplyState extends State with DynMixin { const SizedBox(width: 6), ], ), - body: CustomScrollView( + body: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ _replies.isNotEmpty diff --git a/lib/pages/pgc/view.dart b/lib/pages/pgc/view.dart index d56e5fc4f..dba8806cc 100644 --- a/lib/pages/pgc/view.dart +++ b/lib/pages/pgc/view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; @@ -57,7 +58,7 @@ class _PgcPageState extends State with AutomaticKeepAliveClientMixin { final ThemeData theme = Theme.of(context); return refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/pgc_index/view.dart b/lib/pages/pgc_index/view.dart index 4da07b810..69b02f1c3 100644 --- a/lib/pages/pgc_index/view.dart +++ b/lib/pages/pgc_index/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/self_sized_horizontal_list.dart'; @@ -67,7 +68,7 @@ class _PgcIndexPageState extends State if (count == 0) return const SizedBox.shrink(); return Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), - child: CustomScrollView( + child: customScrollView( controller: _ctr.scrollController, slivers: [ if (widget.indexType != null) diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index 7660e3c61..405ef561e 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -67,7 +68,7 @@ class _PgcReviewChildPageState extends State final theme = Theme.of(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/popular_precious/view.dart b/lib/pages/popular_precious/view.dart index aee184759..6fa32d903 100644 --- a/lib/pages/popular_precious/view.dart +++ b/lib/pages/popular_precious/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; @@ -29,7 +30,7 @@ class _PopularPreciousPageState extends State appBar: AppBar(title: const Text('入站必刷')), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ ViewSliverSafeArea( diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart index a36b5a13a..c4e34e7c0 100644 --- a/lib/pages/popular_series/view.dart +++ b/lib/pages/popular_series/view.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; @@ -41,7 +42,7 @@ class _PopularSeriesPageState extends State with GridMixin { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: ReloadScrollPhysics(controller: _controller), slivers: [ ViewSliverSafeArea( diff --git a/lib/pages/rank/zone/view.dart b/lib/pages/rank/zone/view.dart index f5ae671ff..9b59d4094 100644 --- a/lib/pages/rank/zone/view.dart +++ b/lib/pages/rank/zone/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -40,7 +41,7 @@ class _ZonePageState extends State super.build(context); return refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index cae5cd4b1..74321a7de 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_v.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -34,7 +35,7 @@ class _RcmdPageState extends State decoration: const BoxDecoration(borderRadius: Style.mdRadius), child: refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 7f7500dee..b98022140 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:PiliPlus/common/widgets/dialog/export_import.dart'; import 'package:PiliPlus/common/widgets/disabled_icon.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver_wrap.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -61,7 +62,7 @@ class _SearchPageState extends State { appBar: _buildAppBar, body: Padding( padding: .only(left: padding.left, right: padding.right), - child: CustomScrollView( + child: customScrollView( slivers: [ if (_searchController.searchSuggestion) _buildSearchSuggest(), if (isPortrait) ...[ diff --git a/lib/pages/search_panel/view.dart b/lib/pages/search_panel/view.dart index 707294efe..8360024c9 100644 --- a/lib/pages/search_panel/view.dart +++ b/lib/pages/search_panel/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/search/search_type.dart'; @@ -44,7 +45,7 @@ abstract class CommonSearchPanelState< final theme = Theme.of(context); return refreshIndicator( onRefresh: controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/search_trending/view.dart b/lib/pages/search_trending/view.dart index d55756671..2b3f31328 100644 --- a/lib/pages/search_trending/view.dart +++ b/lib/pages/search_trending/view.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -118,7 +119,7 @@ class _SearchTrendingPageState extends State { width: width, child: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index 0c6343e0c..91701f5ad 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ class _ExtraSettingState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: showAppBar ? AppBar(title: const Text('其它设置')) : null, - body: ListView.builder( + body: ExtendedListView.builder( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/setting/pages/color_select.dart b/lib/pages/setting/pages/color_select.dart index 3bb7476bf..cbfbd49eb 100644 --- a/lib/pages/setting/pages/color_select.dart +++ b/lib/pages/setting/pages/color_select.dart @@ -1,6 +1,7 @@ import 'dart:io' show Platform; import 'package:PiliPlus/common/widgets/color_palette.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/main.dart' show MyApp; import 'package:PiliPlus/models/common/nav_bar_config.dart'; import 'package:PiliPlus/models/common/theme/theme_color_type.dart'; @@ -67,7 +68,7 @@ class _ColorSelectPageState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar(title: const Text('选择应用主题')), - body: ListView( + body: listView( children: [ ListTile( onTap: () async { diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart index 040b26621..703a9fc0d 100644 --- a/lib/pages/setting/pages/logs.dart +++ b/lib/pages/setting/pages/logs.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/utils/date_utils.dart'; @@ -163,7 +164,7 @@ class _LogsPageState extends State { left: padding.left + 12, right: padding.right + 12, ), - child: CustomScrollView( + child: customScrollView( slivers: [ if (latestLog != null) SliverToBoxAdapter( diff --git a/lib/pages/setting/pages/play_speed_set.dart b/lib/pages/setting/pages/play_speed_set.dart index aa67061b3..602b3ebcb 100644 --- a/lib/pages/setting/pages/play_speed_set.dart +++ b/lib/pages/setting/pages/play_speed_set.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; @@ -200,7 +201,7 @@ class _PlaySpeedPageState extends State { ], ), body: ViewSafeArea( - child: ListView( + child: listView( children: [ Padding( padding: const EdgeInsets.only( diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index 6c97855a5..4d2273180 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/setting/models/play_settings.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ class _PlaySettingState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: showAppBar ? AppBar(title: const Text('播放器设置')) : null, - body: ListView.builder( + body: ExtendedListView.builder( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/setting/recommend_setting.dart b/lib/pages/setting/recommend_setting.dart index d77315fe3..01ae3100d 100644 --- a/lib/pages/setting/recommend_setting.dart +++ b/lib/pages/setting/recommend_setting.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; import 'package:flutter/material.dart' hide ListTile; @@ -22,7 +23,7 @@ class _RecommendSettingState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: widget.showAppBar ? AppBar(title: const Text('推荐流设置')) : null, - body: ListView( + body: listView( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index 1b13350d7..b53619458 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/setting/models/style_settings.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ class _StyleSettingState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: showAppBar ? AppBar(title: const Text('外观设置')) : null, - body: ListView.builder( + body: ExtendedListView.builder( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/setting/video_setting.dart b/lib/pages/setting/video_setting.dart index 495720aa6..ba32d3c85 100644 --- a/lib/pages/setting/video_setting.dart +++ b/lib/pages/setting/video_setting.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/setting/models/video_settings.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ class _VideoSettingState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: showAppBar ? AppBar(title: const Text('音视频设置')) : null, - body: ListView.builder( + body: ExtendedListView.builder( padding: EdgeInsets.only( left: showAppBar ? padding.left : 0, right: showAppBar ? padding.right : 0, diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index fb94fa9c5..4fb8c7eda 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/login.dart'; import 'package:PiliPlus/models/common/setting_type.dart'; @@ -170,7 +171,7 @@ class _SettingPageState extends State { TextStyle subTitleStyle = theme.textTheme.labelMedium!.copyWith( color: theme.colorScheme.outline, ); - return ListView( + return listView( padding: EdgeInsets.only(bottom: padding.bottom + 100), children: [ _buildSearchItem(theme), diff --git a/lib/pages/settings_search/view.dart b/lib/pages/settings_search/view.dart index ba69b26b5..bb88ccb70 100644 --- a/lib/pages/settings_search/view.dart +++ b/lib/pages/settings_search/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/pages/search/controller.dart' show DebounceStreamState; @@ -88,7 +89,7 @@ class _SettingsSearchPageState ), ), ), - body: CustomScrollView( + body: customScrollView( slivers: [ ViewSliverSafeArea( sliver: Obx( diff --git a/lib/pages/space_setting/view.dart b/lib/pages/space_setting/view.dart index 4cfb6555d..f314c124f 100644 --- a/lib/pages/space_setting/view.dart +++ b/lib/pages/space_setting/view.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space_setting/privacy.dart'; @@ -54,7 +55,7 @@ class _SpaceSettingPageState extends State { color: theme.colorScheme.outline.withValues(alpha: 0.1), ), ); - return CustomScrollView( + return customScrollView( slivers: [ dividerL, SliverList.separated( diff --git a/lib/pages/sponsor_block/view.dart b/lib/pages/sponsor_block/view.dart index b372b532b..b0dfa8624 100644 --- a/lib/pages/sponsor_block/view.dart +++ b/lib/pages/sponsor_block/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/init.dart'; @@ -485,7 +486,7 @@ class _SponsorBlockPageState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar(title: const Text('空降助手')), - body: CustomScrollView( + body: customScrollView( slivers: [ dividerL, SliverToBoxAdapter(child: _serverStatusItem(theme, titleStyle)), diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart index 6d9ef4d63..b00e5fe2a 100644 --- a/lib/pages/subscription/view.dart +++ b/lib/pages/subscription/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -26,7 +27,7 @@ class _SubPageState extends State with GridMixin { appBar: AppBar(title: const Text('我的订阅')), body: refreshIndicator( onRefresh: _subController.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ ViewSliverSafeArea( diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart index 2e76c4df0..14ef0d7b0 100644 --- a/lib/pages/subscription_detail/view.dart +++ b/lib/pages/subscription_detail/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -54,7 +55,7 @@ class _SubDetailPageState extends State with GridMixin { color: theme.colorScheme.surface, child: refreshIndicator( onRefresh: _subDetailController.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _subDetailController.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/video/ai_conclusion/view.dart b/lib/pages/video/ai_conclusion/view.dart index f3ae11c13..5ee5801ec 100644 --- a/lib/pages/video/ai_conclusion/view.dart +++ b/lib/pages/video/ai_conclusion/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/text.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart'; @@ -26,7 +27,7 @@ class AiConclusionPanel extends CommonSlidePage { Key? key, bool tap = true, }) { - return CustomScrollView( + return customScrollView( key: key, shrinkWrap: !tap, physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/pages/video/download_panel/view.dart b/lib/pages/video/download_panel/view.dart index 9f14590b5..e9a73aa46 100644 --- a/lib/pages/video/download_panel/view.dart +++ b/lib/pages/video/download_panel/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; @@ -175,7 +176,7 @@ class _DownloadPanelState extends State { return Expanded( child: Material( type: MaterialType.transparency, - child: CustomScrollView( + child: customScrollView( controller: widget.scrollController, slivers: [ SliverPadding( diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index c8619e1e2..11093803d 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/flutter/selectable_text/text.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; @@ -117,7 +118,7 @@ class _IntroDetailState extends State final TextStyle textStyle = TextStyle( color: theme.colorScheme.onSurfaceVariant, ); - return ListView( + return listView( controller: _controller, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only( diff --git a/lib/pages/video/medialist/view.dart b/lib/pages/video/medialist/view.dart index d322d7316..cc16b99c8 100644 --- a/lib/pages/video/medialist/view.dart +++ b/lib/pages/video/medialist/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -120,7 +121,7 @@ class _MediaListPanelState extends State Widget _buildList(ThemeData theme) { final showDelBtn = widget.onDelete != null && widget.mediaList.length > 1; - return CustomScrollView( + return customScrollView( controller: _controller, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart index 8816a62e3..3d9cc9c53 100644 --- a/lib/pages/video/member/view.dart +++ b/lib/pages/video/member/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; @@ -89,7 +90,7 @@ class _HorizontalMemberPageState extends State { Expanded( child: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index f8ac82f27..7f2fd231c 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -109,7 +110,7 @@ class _NoteListPageState extends State Widget buildList(ThemeData theme) { Widget child = refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( key: _key, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index b2b6c6153..94b1bae34 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -252,7 +253,7 @@ class _PostPanelState extends State return const HttpError(isSliver: false); } final bottom = MediaQuery.viewPaddingOf(context).bottom; - Widget child = ListView.builder( + Widget child = ExtendedListView.builder( key: _key, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only(bottom: 88 + bottom), diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index ad86d5cd2..6d180cd97 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -82,7 +83,7 @@ class _VideoReplyPanelState extends State child: Stack( clipBehavior: Clip.none, children: [ - CustomScrollView( + customScrollView( controller: widget.isNested ? null : _videoReplyController.scrollController, diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index efa174847..8024e6d3b 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; @@ -186,7 +187,7 @@ class _VideoReplyReplyPanelState extends State final child = refreshIndicator( onRefresh: _controller.onRefresh, isClampingScrollPhysics: widget.isNested, - child: CustomScrollView( + child: customScrollView( key: ValueKey(scrollController.hashCode), controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/pages/video/reply_search_item/child/view.dart b/lib/pages/video/reply_search_item/child/view.dart index d73cecedc..89db44907 100644 --- a/lib/pages/video/reply_search_item/child/view.dart +++ b/lib/pages/video/reply_search_item/child/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show SearchItem; @@ -33,7 +34,7 @@ class _ReplySearchChildPageState extends State super.build(context); return refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( controller: _controller.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index 68018e228..fcc8dfe46 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -179,7 +180,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { ), ), ), - child: ListView( + child: listView( physics: const ClampingScrollPhysics(), padding: .only( top: 12, diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 24e2fd73c..c6f2af720 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; @@ -1017,7 +1018,7 @@ class _VideoDetailPageVState extends State localIntroPanel() else if (showIntro) KeepAliveWrapper( - child: CustomScrollView( + child: customScrollView( key: const PageStorageKey(CommonIntroController), controller: videoDetailController.effectiveIntroScrollCtr, @@ -1676,7 +1677,7 @@ class _VideoDetailPageVState extends State Widget localIntroPanel({ bool needCtr = true, }) { - return CustomScrollView( + return customScrollView( controller: needCtr ? videoDetailController.effectiveIntroScrollCtr : null, @@ -1708,7 +1709,7 @@ class _VideoDetailPageVState extends State return localIntroPanel(needCtr: needCtr); } Widget introPanel() { - Widget child = CustomScrollView( + Widget child = customScrollView( key: const PageStorageKey(CommonIntroController), controller: needCtr ? videoDetailController.effectiveIntroScrollCtr diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 26d54a555..2049e6ee2 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; @@ -91,7 +92,7 @@ class _ViewPointsPageState extends State @override Widget buildList(ThemeData theme) { - final child = ListView.builder( + final child = ExtendedListView.builder( key: _key, physics: const AlwaysScrollableScrollPhysics(), padding: EdgeInsets.only( diff --git a/lib/pages/webdav/view.dart b/lib/pages/webdav/view.dart index f71cf6c37..49ecfa029 100644 --- a/lib/pages/webdav/view.dart +++ b/lib/pages/webdav/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/pages/webdav/webdav.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; @@ -43,7 +44,7 @@ class _WebDavSettingPageState extends State { body: Stack( clipBehavior: Clip.none, children: [ - ListView( + listView( padding: padding.copyWith( top: 20, left: 20 + (showAppBar ? padding.left : 0), diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 04e8fa987..3694827aa 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/whisper_item.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -94,7 +95,7 @@ class _WhisperPageState extends State { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ _buildTopItems(theme, padding), diff --git a/lib/pages/whisper_link_setting/view.dart b/lib/pages/whisper_link_setting/view.dart index a85f2a550..876501de5 100644 --- a/lib/pages/whisper_link_setting/view.dart +++ b/lib/pages/whisper_link_setting/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/msg/im_user_infos/datum.dart'; @@ -49,7 +50,7 @@ class _WhisperLinkSettingPageState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar(title: const Text('聊天设置')), - body: ListView( + body: listView( padding: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 100, ), diff --git a/lib/pages/whisper_secondary/view.dart b/lib/pages/whisper_secondary/view.dart index cabab1358..d4e7fb342 100644 --- a/lib/pages/whisper_secondary/view.dart +++ b/lib/pages/whisper_secondary/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/whisper_item.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -73,7 +74,7 @@ class _WhisperSecPageState extends State { ), body: refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( + child: customScrollView( physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( diff --git a/lib/pages/whisper_settings/view.dart b/lib/pages/whisper_settings/view.dart index 070a983f6..f6629387a 100644 --- a/lib/pages/whisper_settings/view.dart +++ b/lib/pages/whisper_settings/view.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/common/widgets/flutter/scroll_view/scroll_view.dart' + show ExtendedListView; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' show IMSettingType, Setting; @@ -165,7 +167,7 @@ class _WhisperSettingsPageState extends State { Success>(:final response) => Builder( builder: (context) { final keys = response.keys.toList()..sort(); - return ListView.separated( + return ExtendedListView.separated( padding: EdgeInsets.only( bottom: MediaQuery.viewPaddingOf(context).bottom + 100, ),