mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +08:00
update flutter widgets
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -21,9 +21,6 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
|
||||
abstract class _ParentInkResponseState {
|
||||
void markChildInkResponsePressed(
|
||||
_ParentInkResponseState childState,
|
||||
@@ -144,6 +141,7 @@ class InkResponse extends StatelessWidget {
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressUp,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapDown,
|
||||
@@ -197,6 +195,19 @@ class InkResponse extends StatelessWidget {
|
||||
/// Called when the user long-presses on this part of the material.
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
|
||||
/// Called when the user lifts their finger after a long press on the button.
|
||||
///
|
||||
/// This callback is triggered at the end of a long press gesture, specifically
|
||||
/// after the user holds a long press and then releases it. It does not include
|
||||
/// position details.
|
||||
///
|
||||
/// Common use cases include performing an action only after the long press completes,
|
||||
/// such as displaying a context menu or confirming a held gesture.
|
||||
///
|
||||
/// See also:
|
||||
/// * [onLongPress], which is triggered when the long press gesture is first recognized.
|
||||
final GestureLongPressUpCallback? onLongPressUp;
|
||||
|
||||
/// Called when the user taps this part of the material with a secondary button.
|
||||
///
|
||||
/// See also:
|
||||
@@ -237,7 +248,7 @@ class InkResponse extends StatelessWidget {
|
||||
/// become highlighted and false if this part of the material has stopped
|
||||
/// being highlighted.
|
||||
///
|
||||
/// If all of [onTap], [onDoubleTap], and [onLongPress] become null while a
|
||||
/// If all of [onTap], [onDoubleTap], [onLongPress], and [onLongPressUp] become null while a
|
||||
/// gesture is ongoing, then [onTapCancel] will be fired and
|
||||
/// [onHighlightChanged] will be fired with the value false _during the
|
||||
/// build_. This means, for instance, that in that scenario [State.setState]
|
||||
@@ -493,6 +504,7 @@ class InkResponse extends StatelessWidget {
|
||||
onTapCancel: onTapCancel,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onLongPress: onLongPress,
|
||||
onLongPressUp: onLongPressUp,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
onSecondaryTapUp: onSecondaryTapUp,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
@@ -550,6 +562,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
this.onTapCancel,
|
||||
this.onDoubleTap,
|
||||
this.onLongPress,
|
||||
this.onLongPressUp,
|
||||
this.onSecondaryTap,
|
||||
this.onSecondaryTapUp,
|
||||
this.onSecondaryTapDown,
|
||||
@@ -588,6 +601,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
final GestureTapCallback? onTapCancel;
|
||||
final GestureTapCallback? onDoubleTap;
|
||||
final GestureLongPressCallback? onLongPress;
|
||||
final GestureLongPressUpCallback? onLongPressUp;
|
||||
final GestureTapCallback? onSecondaryTap;
|
||||
final GestureTapUpCallback? onSecondaryTapUp;
|
||||
final GestureTapDownCallback? onSecondaryTapDown;
|
||||
@@ -628,6 +642,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||
if (onTap != null) 'tap',
|
||||
if (onDoubleTap != null) 'double tap',
|
||||
if (onLongPress != null) 'long press',
|
||||
if (onLongPressUp != null) 'long press up',
|
||||
if (onTapDown != null) 'tap down',
|
||||
if (onTapUp != null) 'tap up',
|
||||
if (onTapCancel != null) 'tap cancel',
|
||||
@@ -1099,6 +1114,12 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
}
|
||||
}
|
||||
|
||||
void handleLongPressUp() {
|
||||
_currentSplash?.confirm();
|
||||
_currentSplash = null;
|
||||
widget.onLongPressUp?.call();
|
||||
}
|
||||
|
||||
void handleSecondaryTap() {
|
||||
_currentSplash?.confirm();
|
||||
_currentSplash = null;
|
||||
@@ -1140,6 +1161,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
return widget.onTap != null ||
|
||||
widget.onDoubleTap != null ||
|
||||
widget.onLongPress != null ||
|
||||
widget.onLongPressUp != null ||
|
||||
widget.onTapUp != null ||
|
||||
widget.onTapDown != null;
|
||||
}
|
||||
@@ -1276,6 +1298,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||
onLongPress: widget.onLongPress != null
|
||||
? handleLongPress
|
||||
: null,
|
||||
onLongPressUp: widget.onLongPressUp != null
|
||||
? handleLongPressUp
|
||||
: null,
|
||||
onSecondaryTapDown: _secondaryEnabled
|
||||
? handleSecondaryTapDown
|
||||
: null,
|
||||
@@ -1387,6 +1412,7 @@ class InkWell extends InkResponse {
|
||||
super.onTap,
|
||||
super.onDoubleTap,
|
||||
super.onLongPress,
|
||||
super.onLongPressUp,
|
||||
super.onTapDown,
|
||||
super.onTapUp,
|
||||
super.onTapCancel,
|
||||
|
||||
@@ -914,10 +914,7 @@ class ListTile extends StatelessWidget {
|
||||
WidgetState.disabled,
|
||||
};
|
||||
final MouseCursor effectiveMouseCursor =
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(
|
||||
mouseCursor,
|
||||
mouseStates,
|
||||
) ??
|
||||
WidgetStateProperty.resolveAs<MouseCursor?>(mouseCursor, mouseStates) ??
|
||||
tileTheme.mouseCursor?.resolve(mouseStates) ??
|
||||
WidgetStateMouseCursor.clickable.resolve(mouseStates);
|
||||
|
||||
@@ -1330,12 +1327,7 @@ class _RenderListTile extends RenderBox
|
||||
@override
|
||||
Iterable<RenderBox> get children {
|
||||
final RenderBox? title = childForSlot(_ListTileSlot.title);
|
||||
return <RenderBox>[
|
||||
?leading,
|
||||
?title,
|
||||
?subtitle,
|
||||
?trailing,
|
||||
];
|
||||
return <RenderBox>[?leading, ?title, ?subtitle, ?trailing];
|
||||
}
|
||||
|
||||
bool get isDense => _isDense;
|
||||
|
||||
@@ -19,13 +19,14 @@ library;
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/page/scrollable_helpers.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart' hide Scrollable, ScrollableState;
|
||||
import 'package:flutter/material.dart'
|
||||
hide Scrollable, ScrollableState, EdgeDraggingAutoScroller;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
export 'package:flutter/physics.dart' show Tolerance;
|
||||
|
||||
@@ -933,7 +934,7 @@ class CustomScrollableState extends State<CustomScrollable>
|
||||
if (_animController.value * _maxWidth +
|
||||
(_isRTL ? (_maxWidth - dx) : dx) >=
|
||||
100) {
|
||||
Get.back();
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
_animController.reverse();
|
||||
}
|
||||
@@ -1937,7 +1938,7 @@ class _RenderScrollSemantics extends RenderProxyBox {
|
||||
if (child.isTagged(RenderViewport.excludeFromScrolling)) {
|
||||
excluded.add(child);
|
||||
} else {
|
||||
if (!child.hasFlag(SemanticsFlag.isHidden)) {
|
||||
if (!child.flagsCollection.isHidden) {
|
||||
firstVisibleIndex ??= child.indexInParent;
|
||||
}
|
||||
included.add(child);
|
||||
@@ -1982,222 +1983,3 @@ class _RestorableScrollOffset extends RestorableValue<double?> {
|
||||
@override
|
||||
bool get enabled => value != null;
|
||||
}
|
||||
|
||||
// 2D SCROLLING
|
||||
|
||||
/// Specifies how to configure the [DragGestureRecognizer]s of a
|
||||
/// [TwoDimensionalScrollable].
|
||||
// TODO(Piinks): Add sample code, https://github.com/flutter/flutter/issues/126298
|
||||
enum DiagonalDragBehavior {
|
||||
/// This behavior will not allow for any diagonal scrolling.
|
||||
///
|
||||
/// Drag gestures in one direction or the other will lock the input axis until
|
||||
/// the gesture is released.
|
||||
none,
|
||||
|
||||
/// This behavior will only allow diagonal scrolling on a weighted
|
||||
/// scale per gesture event.
|
||||
///
|
||||
/// This means that after initially evaluating the drag gesture, the weighted
|
||||
/// evaluation (based on [kTouchSlop]) stands until the gesture is released.
|
||||
weightedEvent,
|
||||
|
||||
/// This behavior will only allow diagonal scrolling on a weighted
|
||||
/// scale that is evaluated throughout a gesture event.
|
||||
///
|
||||
/// This means that during each update to the drag gesture, the scrolling
|
||||
/// axis will be allowed to scroll diagonally if it exceeds the
|
||||
/// [kTouchSlop].
|
||||
weightedContinuous,
|
||||
|
||||
/// This behavior allows free movement in any and all directions when
|
||||
/// dragging.
|
||||
free,
|
||||
}
|
||||
|
||||
/// 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 [CustomScrollable] this auto scroller is scrolling.
|
||||
final CustomScrollableState 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<void> _scroll() async {
|
||||
final RenderBox scrollRenderBox =
|
||||
scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
scrollRenderBox.getTransformTo(null),
|
||||
Rect.fromLTWH(
|
||||
0,
|
||||
0,
|
||||
scrollRenderBox.size.width,
|
||||
scrollRenderBox.size.height,
|
||||
),
|
||||
);
|
||||
assert(
|
||||
globalRect.size.width >= _dragTargetRelatedToScrollOrigin.size.width &&
|
||||
globalRect.size.height >=
|
||||
_dragTargetRelatedToScrollOrigin.size.height,
|
||||
'Drag target size is larger than scrollable size, which may cause bouncing',
|
||||
);
|
||||
_scrolling = true;
|
||||
double? newOffset;
|
||||
const double 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 = Duration(
|
||||
milliseconds: (1000 / velocityScalar).round(),
|
||||
);
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
onScrollViewScrolled?.call();
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
210
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
210
lib/common/widgets/flutter/page/scrollable_helpers.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
// 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/page/scrollable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 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 [CustomScrollable] this auto scroller is scrolling.
|
||||
final CustomScrollableState 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<void> _scroll() async {
|
||||
final RenderBox scrollRenderBox =
|
||||
scrollable.context.findRenderObject()! as RenderBox;
|
||||
final Matrix4 transform = scrollRenderBox.getTransformTo(null);
|
||||
final Rect globalRect = MatrixUtils.transformRect(
|
||||
transform,
|
||||
Rect.fromLTWH(
|
||||
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 double 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 = Duration(
|
||||
milliseconds: (1000 / velocityScalar).round(),
|
||||
);
|
||||
await scrollable.position.animateTo(
|
||||
newOffset,
|
||||
duration: duration,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
onScrollViewScrolled?.call();
|
||||
if (_scrolling) {
|
||||
await _scroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
769
lib/common/widgets/flutter/refresh_indicator.dart
Normal file
769
lib/common/widgets/flutter/refresh_indicator.dart
Normal file
@@ -0,0 +1,769 @@
|
||||
// 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.
|
||||
|
||||
// ignore_for_file: uri_does_not_exist_in_doc_import
|
||||
|
||||
/// @docImport 'color_scheme.dart';
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/material.dart' hide RefreshIndicator;
|
||||
|
||||
double displacement = Pref.refreshDisplacement;
|
||||
|
||||
// The over-scroll distance that moves the indicator to its maximum
|
||||
// displacement, as a percentage of the scrollable's container extent.
|
||||
double kDragContainerExtentPercentage = Pref.refreshDragPercentage;
|
||||
|
||||
// How much the scroll's drag gesture can overshoot the RefreshIndicator's
|
||||
// displacement; max displacement = _kDragSizeFactorLimit * displacement.
|
||||
const double _kDragSizeFactorLimit = 1.5;
|
||||
|
||||
// When the scroll ends, the duration of the refresh indicator's animation
|
||||
// to the RefreshIndicator's displacement.
|
||||
const Duration _kIndicatorSnapDuration = Duration(milliseconds: 150);
|
||||
|
||||
// The duration of the ScaleTransition that starts when the refresh action
|
||||
// has completed.
|
||||
const Duration _kIndicatorScaleDuration = Duration(milliseconds: 200);
|
||||
|
||||
/// The signature for a function that's called when the user has dragged a
|
||||
/// [RefreshIndicator] far enough to demonstrate that they want the app to
|
||||
/// refresh. The returned [Future] must complete when the refresh operation is
|
||||
/// finished.
|
||||
///
|
||||
/// Used by [RefreshIndicator.onRefresh].
|
||||
typedef RefreshCallback = Future<void> Function();
|
||||
|
||||
/// Indicates current status of Material `RefreshIndicator`.
|
||||
enum RefreshIndicatorStatus {
|
||||
/// Pointer is down.
|
||||
drag,
|
||||
|
||||
/// Dragged far enough that an up event will run the onRefresh callback.
|
||||
armed,
|
||||
|
||||
/// Animating to the indicator's final "displacement".
|
||||
snap,
|
||||
|
||||
/// Running the refresh callback.
|
||||
refresh,
|
||||
|
||||
/// Animating the indicator's fade-out after refreshing.
|
||||
done,
|
||||
|
||||
/// Animating the indicator's fade-out after not arming.
|
||||
canceled,
|
||||
}
|
||||
|
||||
/// Used to configure how [RefreshIndicator] can be triggered.
|
||||
enum RefreshIndicatorTriggerMode {
|
||||
/// The indicator can be triggered regardless of the scroll position
|
||||
/// of the [Scrollable] when the drag starts.
|
||||
anywhere,
|
||||
|
||||
/// The indicator can only be triggered if the [Scrollable] is at the edge
|
||||
/// when the drag starts.
|
||||
onEdge,
|
||||
}
|
||||
|
||||
enum _IndicatorType { material, adaptive, noSpinner }
|
||||
|
||||
/// A widget that supports the Material "swipe to refresh" idiom.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=ORApMlzwMdM}
|
||||
///
|
||||
/// When the child's [Scrollable] descendant overscrolls, an animated circular
|
||||
/// progress indicator is faded into view. When the scroll ends, if the
|
||||
/// indicator has been dragged far enough for it to become completely opaque,
|
||||
/// the [onRefresh] callback is called. The callback is expected to update the
|
||||
/// scrollable's contents and then complete the [Future] it returns. The refresh
|
||||
/// indicator disappears after the callback's [Future] has completed.
|
||||
///
|
||||
/// The trigger mode is configured by [RefreshIndicator.triggerMode].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how [RefreshIndicator] can be triggered in different ways.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to trigger [RefreshIndicator] in a nested scroll view using
|
||||
/// the [notificationPredicate] property.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to use [RefreshIndicator] without the spinner.
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/refresh_indicator/refresh_indicator.2.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// ## Troubleshooting
|
||||
///
|
||||
/// ### Refresh indicator does not show up
|
||||
///
|
||||
/// The [RefreshIndicator] will appear if its scrollable descendant can be
|
||||
/// overscrolled, i.e. if the scrollable's content is bigger than its viewport.
|
||||
/// To ensure that the [RefreshIndicator] will always appear, even if the
|
||||
/// scrollable's content fits within its viewport, set the scrollable's
|
||||
/// [Scrollable.physics] property to [AlwaysScrollableScrollPhysics]:
|
||||
///
|
||||
/// ```dart
|
||||
/// ListView(
|
||||
/// physics: const AlwaysScrollableScrollPhysics(),
|
||||
/// // ...
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// A [RefreshIndicator] can only be used with a vertical scroll view.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://material.io/design/platform-guidance/android-swipe-to-refresh.html>
|
||||
/// * [RefreshIndicatorState], can be used to programmatically show the refresh indicator.
|
||||
/// * [RefreshProgressIndicator], widget used by [RefreshIndicator] to show
|
||||
/// the inner circular progress spinner during refreshes.
|
||||
/// * [CupertinoSliverRefreshControl], an iOS equivalent of the pull-to-refresh pattern.
|
||||
/// Must be used as a sliver inside a [CustomScrollView] instead of wrapping
|
||||
/// around a [ScrollView] because it's a part of the scrollable instead of
|
||||
/// being overlaid on top of it.
|
||||
class RefreshIndicator extends StatefulWidget {
|
||||
/// Creates a refresh indicator.
|
||||
///
|
||||
/// The [onRefresh], [child], and [notificationPredicate] arguments must be
|
||||
/// non-null. The default
|
||||
/// [displacement] is 40.0 logical pixels.
|
||||
///
|
||||
/// The [semanticsLabel] is used to specify an accessibility label for this widget.
|
||||
/// If it is null, it will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel].
|
||||
/// An empty string may be passed to avoid having anything read by screen reading software.
|
||||
/// The [semanticsValue] may be used to specify progress on the widget.
|
||||
const RefreshIndicator({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.material,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates an adaptive [RefreshIndicator] based on whether the target
|
||||
/// platform is iOS or macOS, following Material design's
|
||||
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
|
||||
///
|
||||
/// When the descendant overscrolls, a different spinning progress indicator
|
||||
/// is shown depending on platform. On iOS and macOS,
|
||||
/// [CupertinoActivityIndicator] is shown, but on all other platforms,
|
||||
/// [CircularProgressIndicator] appears.
|
||||
///
|
||||
/// If a [CupertinoActivityIndicator] is shown, the following parameters are ignored:
|
||||
/// [backgroundColor], [semanticsLabel], [semanticsValue], [strokeWidth].
|
||||
///
|
||||
/// The target platform is based on the current [Theme]: [ThemeData.platform].
|
||||
///
|
||||
/// Notably the scrollable widget itself will have slightly different behavior
|
||||
/// from [CupertinoSliverRefreshControl], due to a difference in structure.
|
||||
const RefreshIndicator.adaptive({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.displacement = 40.0,
|
||||
this.edgeOffset = 0.0,
|
||||
required this.onRefresh,
|
||||
this.color,
|
||||
this.backgroundColor,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.strokeWidth = RefreshProgressIndicator.defaultStrokeWidth,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.adaptive,
|
||||
onStatusChange = null,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// Creates a [RefreshIndicator] with no spinner and calls `onRefresh` when
|
||||
/// successfully armed by a drag event.
|
||||
///
|
||||
/// Events can be optionally listened by using the `onStatusChange` callback.
|
||||
const RefreshIndicator.noSpinner({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.onRefresh,
|
||||
this.onStatusChange,
|
||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.triggerMode = RefreshIndicatorTriggerMode.onEdge,
|
||||
this.elevation = 2.0,
|
||||
}) : _indicatorType = _IndicatorType.noSpinner,
|
||||
// The following parameters aren't used because [_IndicatorType.noSpinner] is being used,
|
||||
// which involves showing no spinner, hence the following parameters are useless since
|
||||
// their only use is to change the spinner's appearance.
|
||||
displacement = 0.0,
|
||||
edgeOffset = 0.0,
|
||||
color = null,
|
||||
backgroundColor = null,
|
||||
strokeWidth = 0.0,
|
||||
assert(elevation >= 0.0);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// The refresh indicator will be stacked on top of this child. The indicator
|
||||
/// will appear when child's Scrollable descendant is over-scrolled.
|
||||
///
|
||||
/// Typically a [ListView] or [CustomScrollView].
|
||||
final Widget child;
|
||||
|
||||
/// The distance from the child's top or bottom [edgeOffset] where
|
||||
/// the refresh indicator will settle. During the drag that exposes the refresh
|
||||
/// indicator, its actual displacement may significantly exceed this value.
|
||||
///
|
||||
/// In most cases, [displacement] distance starts counting from the parent's
|
||||
/// edges. However, if [edgeOffset] is larger than zero then the [displacement]
|
||||
/// value is calculated from that offset instead of the parent's edge.
|
||||
final double displacement;
|
||||
|
||||
/// The offset where [RefreshProgressIndicator] starts to appear on drag start.
|
||||
///
|
||||
/// Depending whether the indicator is showing on the top or bottom, the value
|
||||
/// of this variable controls how far from the parent's edge the progress
|
||||
/// indicator starts to appear. This may come in handy when, for example, the
|
||||
/// UI contains a top [Widget] which covers the parent's edge where the progress
|
||||
/// indicator would otherwise appear.
|
||||
///
|
||||
/// By default, the edge offset is set to 0.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [displacement], can be used to change the distance from the edge that
|
||||
/// the indicator settles.
|
||||
final double edgeOffset;
|
||||
|
||||
/// A function that's called when the user has dragged the refresh indicator
|
||||
/// far enough to demonstrate that they want the app to refresh. The returned
|
||||
/// [Future] must complete when the refresh operation is finished.
|
||||
final RefreshCallback onRefresh;
|
||||
|
||||
/// Called to get the current status of the [RefreshIndicator] to update the UI as needed.
|
||||
/// This is an optional parameter, used to fine tune app cases.
|
||||
final ValueChanged<RefreshIndicatorStatus?>? onStatusChange;
|
||||
|
||||
/// The progress indicator's foreground color. The current theme's
|
||||
/// [ColorScheme.primary] by default.
|
||||
final Color? color;
|
||||
|
||||
/// The progress indicator's background color. The current theme's
|
||||
/// [ThemeData.canvasColor] by default.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// A check that specifies whether a [ScrollNotification] should be
|
||||
/// handled by this widget.
|
||||
///
|
||||
/// By default, checks whether `notification.depth == 0`. Set it to something
|
||||
/// else for more complicated layouts.
|
||||
final ScrollNotificationPredicate notificationPredicate;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsLabel}
|
||||
///
|
||||
/// This will be defaulted to [MaterialLocalizations.refreshIndicatorSemanticLabel]
|
||||
/// if it is null.
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// {@macro flutter.progress_indicator.ProgressIndicator.semanticsValue}
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Defines [strokeWidth] for `RefreshIndicator`.
|
||||
///
|
||||
/// By default, the value of [strokeWidth] is 2.0 pixels.
|
||||
final double strokeWidth;
|
||||
|
||||
final _IndicatorType _indicatorType;
|
||||
|
||||
/// Defines how this [RefreshIndicator] can be triggered when users overscroll.
|
||||
///
|
||||
/// The [RefreshIndicator] can be pulled out in two cases,
|
||||
/// 1, Keep dragging if the scrollable widget at the edge with zero scroll position
|
||||
/// when the drag starts.
|
||||
/// 2, Keep dragging after overscroll occurs if the scrollable widget has
|
||||
/// a non-zero scroll position when the drag starts.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.anywhere], both of the cases above can be triggered.
|
||||
///
|
||||
/// If this is [RefreshIndicatorTriggerMode.onEdge], only case 1 can be triggered.
|
||||
///
|
||||
/// Defaults to [RefreshIndicatorTriggerMode.onEdge].
|
||||
final RefreshIndicatorTriggerMode triggerMode;
|
||||
|
||||
/// Defines the elevation of the underlying [RefreshIndicator].
|
||||
///
|
||||
/// Defaults to 2.0.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
RefreshIndicatorState createState() => RefreshIndicatorState();
|
||||
}
|
||||
|
||||
/// Contains the state for a [RefreshIndicator]. This class can be used to
|
||||
/// programmatically show the refresh indicator, see the [show] method.
|
||||
class RefreshIndicatorState extends State<RefreshIndicator>
|
||||
with TickerProviderStateMixin<RefreshIndicator> {
|
||||
late AnimationController _positionController;
|
||||
late AnimationController _scaleController;
|
||||
late Animation<double> _positionFactor;
|
||||
late Animation<double> _scaleFactor;
|
||||
late Animation<double> _value;
|
||||
late Animation<Color?> _valueColor;
|
||||
|
||||
RefreshIndicatorStatus? _status;
|
||||
late Future<void> _pendingRefreshFuture;
|
||||
bool? _isIndicatorAtTop;
|
||||
double? _dragOffset;
|
||||
late Color _effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
|
||||
static final Animatable<double> _threeQuarterTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 0.75,
|
||||
);
|
||||
|
||||
static final Animatable<double> _kDragSizeFactorLimitTween = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: _kDragSizeFactorLimit,
|
||||
);
|
||||
|
||||
static final Animatable<double> _oneToZeroTween = Tween<double>(
|
||||
begin: 1.0,
|
||||
end: 0.0,
|
||||
);
|
||||
|
||||
@protected
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_positionController = AnimationController(vsync: this);
|
||||
_positionFactor = _positionController.drive(_kDragSizeFactorLimitTween);
|
||||
|
||||
// The "value" of the circular progress indicator during a drag.
|
||||
_value = _positionController.drive(_threeQuarterTween);
|
||||
|
||||
_scaleController = AnimationController(vsync: this);
|
||||
_scaleFactor = _scaleController.drive(_oneToZeroTween);
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_setupColorTween();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void didUpdateWidget(covariant RefreshIndicator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.color != widget.color) {
|
||||
_setupColorTween();
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
void dispose() {
|
||||
_positionController.dispose();
|
||||
_scaleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setupColorTween() {
|
||||
// Reset the current value color.
|
||||
_effectiveValueColor =
|
||||
widget.color ?? Theme.of(context).colorScheme.primary;
|
||||
final Color color = _effectiveValueColor;
|
||||
if (color.a == 0) {
|
||||
// Set an always stopped animation instead of a driven tween.
|
||||
_valueColor = AlwaysStoppedAnimation<Color>(color);
|
||||
} else {
|
||||
// Respect the alpha of the given color.
|
||||
_valueColor = _positionController.drive(
|
||||
ColorTween(
|
||||
begin: color.withValues(alpha: 0),
|
||||
end: color,
|
||||
).chain(
|
||||
CurveTween(curve: const Interval(0.0, 1.0 / _kDragSizeFactorLimit)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldStart(ScrollNotification notification) {
|
||||
// If the notification.dragDetails is null, this scroll is not triggered by
|
||||
// user dragging. It may be a result of ScrollController.jumpTo or ballistic scroll.
|
||||
// In this case, we don't want to trigger the refresh indicator.
|
||||
return ((notification is ScrollStartNotification &&
|
||||
notification.dragDetails != null) ||
|
||||
(notification is ScrollUpdateNotification &&
|
||||
notification.dragDetails != null &&
|
||||
widget.triggerMode == RefreshIndicatorTriggerMode.anywhere)) &&
|
||||
((notification.metrics.axisDirection == AxisDirection.up &&
|
||||
notification.metrics.extentAfter == 0.0) ||
|
||||
(notification.metrics.axisDirection == AxisDirection.down &&
|
||||
notification.metrics.extentBefore == 0.0)) &&
|
||||
_status == null &&
|
||||
_start(notification.metrics.axisDirection);
|
||||
}
|
||||
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (!widget.notificationPredicate(notification)) {
|
||||
return false;
|
||||
}
|
||||
if (_shouldStart(notification)) {
|
||||
setState(() {
|
||||
_status = RefreshIndicatorStatus.drag;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
final bool? indicatorAtTopNow =
|
||||
switch (notification.metrics.axisDirection) {
|
||||
AxisDirection.down || AxisDirection.up => true,
|
||||
AxisDirection.left || AxisDirection.right => null,
|
||||
};
|
||||
if (indicatorAtTopNow != _isIndicatorAtTop) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
}
|
||||
} else if (notification is ScrollUpdateNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.scrollDelta!;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.scrollDelta!;
|
||||
}
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.armed &&
|
||||
notification.dragDetails == null) {
|
||||
// On iOS start the refresh when the Scrollable bounces back from the
|
||||
// overscroll (ScrollNotification indicating this don't have dragDetails
|
||||
// because the scroll activity is not directly triggered by a drag).
|
||||
_show();
|
||||
}
|
||||
} else if (notification is OverscrollNotification) {
|
||||
if (_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed) {
|
||||
if (notification.metrics.axisDirection == AxisDirection.down) {
|
||||
_dragOffset = _dragOffset! - notification.overscroll;
|
||||
} else if (notification.metrics.axisDirection == AxisDirection.up) {
|
||||
_dragOffset = _dragOffset! + notification.overscroll;
|
||||
}
|
||||
_checkDragOffset(notification.metrics.viewportDimension);
|
||||
}
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
switch (_status) {
|
||||
case RefreshIndicatorStatus.armed:
|
||||
if (_positionController.value < 1.0) {
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
} else {
|
||||
_show();
|
||||
}
|
||||
case RefreshIndicatorStatus.drag:
|
||||
_dismiss(RefreshIndicatorStatus.canceled);
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
case RefreshIndicatorStatus.done:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
case null:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _handleIndicatorNotification(
|
||||
OverscrollIndicatorNotification notification,
|
||||
) {
|
||||
if (notification.depth != 0 || !notification.leading) {
|
||||
return false;
|
||||
}
|
||||
if (_status == RefreshIndicatorStatus.drag) {
|
||||
notification.disallowIndicator();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _start(AxisDirection direction) {
|
||||
assert(_status == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
assert(_dragOffset == null);
|
||||
switch (direction) {
|
||||
case AxisDirection.down:
|
||||
case AxisDirection.up:
|
||||
_isIndicatorAtTop = true;
|
||||
case AxisDirection.left:
|
||||
case AxisDirection.right:
|
||||
_isIndicatorAtTop = null;
|
||||
// we do not support horizontal scroll views.
|
||||
return false;
|
||||
}
|
||||
_dragOffset = 0.0;
|
||||
_scaleController.value = 0.0;
|
||||
_positionController.value = 0.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void _checkDragOffset(double containerExtent) {
|
||||
assert(
|
||||
_status == RefreshIndicatorStatus.drag ||
|
||||
_status == RefreshIndicatorStatus.armed,
|
||||
);
|
||||
double newValue =
|
||||
_dragOffset! / (containerExtent * kDragContainerExtentPercentage);
|
||||
if (_status == RefreshIndicatorStatus.armed) {
|
||||
newValue = math.max(newValue, 1.0 / _kDragSizeFactorLimit);
|
||||
}
|
||||
_positionController.value = clampDouble(
|
||||
newValue,
|
||||
0.0,
|
||||
1.0,
|
||||
); // This triggers various rebuilds.
|
||||
if (_status == RefreshIndicatorStatus.drag &&
|
||||
_valueColor.value!.a == _effectiveValueColor.a) {
|
||||
_status = RefreshIndicatorStatus.armed;
|
||||
widget.onStatusChange?.call(_status);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop showing the refresh indicator.
|
||||
Future<void> _dismiss(RefreshIndicatorStatus newMode) async {
|
||||
await Future<void>.value();
|
||||
// This can only be called from _show() when refreshing and
|
||||
// _handleScrollNotification in response to a ScrollEndNotification or
|
||||
// direction change.
|
||||
assert(
|
||||
newMode == RefreshIndicatorStatus.canceled ||
|
||||
newMode == RefreshIndicatorStatus.done,
|
||||
);
|
||||
setState(() {
|
||||
_status = newMode;
|
||||
widget.onStatusChange?.call(_status);
|
||||
});
|
||||
switch (_status!) {
|
||||
case RefreshIndicatorStatus.done:
|
||||
await _scaleController.animateTo(
|
||||
1.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.canceled:
|
||||
await _positionController.animateTo(
|
||||
0.0,
|
||||
duration: _kIndicatorScaleDuration,
|
||||
);
|
||||
case RefreshIndicatorStatus.armed:
|
||||
case RefreshIndicatorStatus.drag:
|
||||
case RefreshIndicatorStatus.refresh:
|
||||
case RefreshIndicatorStatus.snap:
|
||||
assert(false);
|
||||
}
|
||||
if (mounted && _status == newMode) {
|
||||
_dragOffset = null;
|
||||
_isIndicatorAtTop = null;
|
||||
setState(() {
|
||||
_status = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _show() {
|
||||
assert(_status != RefreshIndicatorStatus.refresh);
|
||||
assert(_status != RefreshIndicatorStatus.snap);
|
||||
final Completer<void> completer = Completer<void>();
|
||||
_pendingRefreshFuture = completer.future;
|
||||
_status = RefreshIndicatorStatus.snap;
|
||||
widget.onStatusChange?.call(_status);
|
||||
_positionController
|
||||
.animateTo(
|
||||
1.0 / _kDragSizeFactorLimit,
|
||||
duration: _kIndicatorSnapDuration,
|
||||
)
|
||||
.whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.snap) {
|
||||
setState(() {
|
||||
// Show the indeterminate progress indicator.
|
||||
_status = RefreshIndicatorStatus.refresh;
|
||||
});
|
||||
|
||||
widget.onRefresh().whenComplete(() {
|
||||
if (mounted && _status == RefreshIndicatorStatus.refresh) {
|
||||
completer.complete();
|
||||
_dismiss(RefreshIndicatorStatus.done);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Show the refresh indicator and run the refresh callback as if it had
|
||||
/// been started interactively. If this method is called while the refresh
|
||||
/// callback is running, it quietly does nothing.
|
||||
///
|
||||
/// Creating the [RefreshIndicator] with a [GlobalKey<RefreshIndicatorState>]
|
||||
/// makes it possible to refer to the [RefreshIndicatorState].
|
||||
///
|
||||
/// The future returned from this method completes when the
|
||||
/// [RefreshIndicator.onRefresh] callback's future completes.
|
||||
///
|
||||
/// If you await the future returned by this function from a [State], you
|
||||
/// should check that the state is still [mounted] before calling [setState].
|
||||
///
|
||||
/// When initiated in this manner, the refresh indicator is independent of any
|
||||
/// actual scroll view. It defaults to showing the indicator at the top. To
|
||||
/// show it at the bottom, set `atTop` to false.
|
||||
Future<void> show({bool atTop = true}) {
|
||||
if (_status != RefreshIndicatorStatus.refresh &&
|
||||
_status != RefreshIndicatorStatus.snap) {
|
||||
if (_status == null) {
|
||||
_start(atTop ? AxisDirection.down : AxisDirection.up);
|
||||
}
|
||||
_show();
|
||||
}
|
||||
return _pendingRefreshFuture;
|
||||
}
|
||||
|
||||
@protected
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterialLocalizations(context));
|
||||
final Widget child = NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: NotificationListener<OverscrollIndicatorNotification>(
|
||||
onNotification: _handleIndicatorNotification,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
assert(() {
|
||||
if (_status == null) {
|
||||
assert(_dragOffset == null);
|
||||
assert(_isIndicatorAtTop == null);
|
||||
} else {
|
||||
assert(_dragOffset != null);
|
||||
assert(_isIndicatorAtTop != null);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
final bool showIndeterminateIndicator =
|
||||
_status == RefreshIndicatorStatus.refresh ||
|
||||
_status == RefreshIndicatorStatus.done;
|
||||
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: <Widget>[
|
||||
child,
|
||||
if (_status != null)
|
||||
Positioned(
|
||||
top: _isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
bottom: !_isIndicatorAtTop! ? widget.edgeOffset : null,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
child: SizeTransition(
|
||||
axisAlignment: _isIndicatorAtTop! ? 1.0 : -1.0,
|
||||
sizeFactor: _positionFactor, // This is what brings it down.
|
||||
child: Padding(
|
||||
padding: _isIndicatorAtTop!
|
||||
? EdgeInsets.only(top: widget.displacement)
|
||||
: EdgeInsets.only(bottom: widget.displacement),
|
||||
child: Align(
|
||||
alignment: _isIndicatorAtTop!
|
||||
? Alignment.topCenter
|
||||
: Alignment.bottomCenter,
|
||||
child: ScaleTransition(
|
||||
scale: _scaleFactor,
|
||||
child: AnimatedBuilder(
|
||||
animation: _positionController,
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
final Widget materialIndicator =
|
||||
RefreshProgressIndicator(
|
||||
semanticsLabel:
|
||||
widget.semanticsLabel ??
|
||||
MaterialLocalizations.of(
|
||||
context,
|
||||
).refreshIndicatorSemanticLabel,
|
||||
semanticsValue: widget.semanticsValue,
|
||||
value: showIndeterminateIndicator
|
||||
? null
|
||||
: _value.value,
|
||||
valueColor: _valueColor,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
elevation: widget.elevation,
|
||||
);
|
||||
|
||||
final Widget cupertinoIndicator =
|
||||
CupertinoActivityIndicator(
|
||||
color: widget.color,
|
||||
);
|
||||
|
||||
switch (widget._indicatorType) {
|
||||
case _IndicatorType.material:
|
||||
return materialIndicator;
|
||||
|
||||
case _IndicatorType.adaptive:
|
||||
final ThemeData theme = Theme.of(context);
|
||||
switch (theme.platform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return materialIndicator;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return cupertinoIndicator;
|
||||
}
|
||||
|
||||
case _IndicatorType.noSpinner:
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget refreshIndicator({
|
||||
required RefreshCallback onRefresh,
|
||||
required Widget child,
|
||||
}) {
|
||||
return RefreshIndicator(
|
||||
displacement: displacement,
|
||||
onRefresh: onRefresh,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
370
lib/common/widgets/flutter/tabs.dart
Normal file
370
lib/common/widgets/flutter/tabs.dart
Normal file
@@ -0,0 +1,370 @@
|
||||
// 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:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/foundation.dart' show clampDouble;
|
||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
||||
import 'package:flutter/material.dart' hide TabBarView;
|
||||
|
||||
/// A page view that displays the widget which corresponds to the currently
|
||||
/// selected tab.
|
||||
///
|
||||
/// This widget is typically used in conjunction with a [TabBar].
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=POtoEH-5l40}
|
||||
///
|
||||
/// If a [TabController] is not provided, then there must be a [DefaultTabController]
|
||||
/// ancestor.
|
||||
///
|
||||
/// The tab controller's [TabController.length] must equal the length of the
|
||||
/// [children] list and the length of the [TabBar.tabs] list.
|
||||
///
|
||||
/// To see a sample implementation, visit the [TabController] documentation.
|
||||
class CustomTabBarView extends StatefulWidget {
|
||||
/// Creates a page view with one child per tab.
|
||||
///
|
||||
/// The length of [children] must be the same as the [controller]'s length.
|
||||
const CustomTabBarView({
|
||||
super.key,
|
||||
required this.children,
|
||||
this.controller,
|
||||
this.physics,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.viewportFraction = 1.0,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
});
|
||||
|
||||
/// This widget's selection and animation state.
|
||||
///
|
||||
/// If [TabController] is not provided, then the value of [DefaultTabController.of]
|
||||
/// will be used.
|
||||
final TabController? controller;
|
||||
|
||||
/// One widget per tab.
|
||||
///
|
||||
/// Its length must match the length of the [TabBar.tabs]
|
||||
/// list, as well as the [controller]'s [TabController.length].
|
||||
final List<Widget> children;
|
||||
|
||||
/// How the page view should respond to user input.
|
||||
///
|
||||
/// For example, determines how the page view continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
/// {@macro flutter.widgets.pageview.viewportFraction}
|
||||
final double viewportFraction;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
final Axis scrollDirection;
|
||||
|
||||
@override
|
||||
State<CustomTabBarView> createState() => _CustomTabBarViewState();
|
||||
}
|
||||
|
||||
class _CustomTabBarViewState extends State<CustomTabBarView> {
|
||||
TabController? _controller;
|
||||
PageController? _pageController;
|
||||
late List<Widget> _childrenWithKey;
|
||||
int? _currentIndex;
|
||||
int _warpUnderwayCount = 0;
|
||||
int _scrollUnderwayCount = 0;
|
||||
bool _debugHasScheduledValidChildrenCountCheck = false;
|
||||
|
||||
// If the TabBarView is rebuilt with a new tab controller, the caller should
|
||||
// dispose the old one. In that case the old controller's animation will be
|
||||
// null and should not be accessed.
|
||||
bool get _controllerIsValid => _controller?.animation != null;
|
||||
|
||||
void _updateTabController() {
|
||||
final TabController? newController =
|
||||
widget.controller ?? DefaultTabController.maybeOf(context);
|
||||
assert(() {
|
||||
if (newController == null) {
|
||||
throw FlutterError(
|
||||
'No TabController for ${widget.runtimeType}.\n'
|
||||
'When creating a ${widget.runtimeType}, you must either provide an explicit '
|
||||
'TabController using the "controller" property, or you must ensure that there '
|
||||
'is a DefaultTabController above the ${widget.runtimeType}.\n'
|
||||
'In this case, there was neither an explicit controller nor a default controller.',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
if (newController == _controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = newController;
|
||||
if (_controller != null) {
|
||||
_controller!.animation!.addListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
}
|
||||
|
||||
void _jumpToPage(int page) {
|
||||
_warpUnderwayCount += 1;
|
||||
_pageController!.jumpToPage(page);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
Future<void> _animateToPage(
|
||||
int page, {
|
||||
required Duration duration,
|
||||
required Curve curve,
|
||||
}) async {
|
||||
_warpUnderwayCount += 1;
|
||||
await _pageController!.animateToPage(
|
||||
page,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
_warpUnderwayCount -= 1;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_updateChildren();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
if (_pageController == null) {
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
} else {
|
||||
_pageController!.jumpToPage(_currentIndex!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CustomTabBarView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
_updateTabController();
|
||||
_currentIndex = _controller!.index;
|
||||
_jumpToPage(_currentIndex!);
|
||||
}
|
||||
if (widget.viewportFraction != oldWidget.viewportFraction) {
|
||||
_pageController?.dispose();
|
||||
_pageController = PageController(
|
||||
initialPage: _currentIndex!,
|
||||
viewportFraction: widget.viewportFraction,
|
||||
);
|
||||
}
|
||||
// While a warp is under way, we stop updating the tab page contents.
|
||||
// This is tracked in https://github.com/flutter/flutter/issues/31269.
|
||||
if (widget.children != oldWidget.children && _warpUnderwayCount == 0) {
|
||||
_updateChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_controllerIsValid) {
|
||||
_controller!.animation!.removeListener(_handleTabControllerAnimationTick);
|
||||
}
|
||||
_controller = null;
|
||||
_pageController?.dispose();
|
||||
// We don't own the _controller Animation, so it's not disposed here.
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _updateChildren() {
|
||||
_childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(
|
||||
widget.children.map<Widget>((Widget child) {
|
||||
return Semantics(role: SemanticsRole.tabPanel, child: child);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleTabControllerAnimationTick() {
|
||||
if (_scrollUnderwayCount > 0 || !_controller!.indexIsChanging) {
|
||||
return;
|
||||
} // This widget is driving the controller's animation.
|
||||
|
||||
if (_controller!.index != _currentIndex) {
|
||||
_currentIndex = _controller!.index;
|
||||
_warpToCurrentIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void _warpToCurrentIndex() {
|
||||
if (!mounted || _pageController!.page == _currentIndex!.toDouble()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bool adjacentDestination =
|
||||
(_currentIndex! - _controller!.previousIndex).abs() == 1;
|
||||
if (adjacentDestination) {
|
||||
_warpToAdjacentTab(_controller!.animationDuration);
|
||||
} else {
|
||||
_warpToNonAdjacentTab(_controller!.animationDuration);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _warpToAdjacentTab(Duration duration) async {
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
setState(_updateChildren);
|
||||
}
|
||||
return Future<void>.value();
|
||||
}
|
||||
|
||||
Future<void> _warpToNonAdjacentTab(Duration duration) async {
|
||||
final int previousIndex = _controller!.previousIndex;
|
||||
assert((_currentIndex! - previousIndex).abs() > 1);
|
||||
|
||||
// initialPage defines which page is shown when starting the animation.
|
||||
// This page is adjacent to the destination page.
|
||||
final int initialPage = _currentIndex! > previousIndex
|
||||
? _currentIndex! - 1
|
||||
: _currentIndex! + 1;
|
||||
|
||||
setState(() {
|
||||
// Needed for `RenderSliverMultiBoxAdaptor.move` and kept alive children.
|
||||
// For motivation, see https://github.com/flutter/flutter/pull/29188 and
|
||||
// https://github.com/flutter/flutter/issues/27010#issuecomment-486475152.
|
||||
_childrenWithKey = List<Widget>.of(_childrenWithKey, growable: false);
|
||||
final Widget temp = _childrenWithKey[initialPage];
|
||||
_childrenWithKey[initialPage] = _childrenWithKey[previousIndex];
|
||||
_childrenWithKey[previousIndex] = temp;
|
||||
});
|
||||
|
||||
// Make a first jump to the adjacent page.
|
||||
_jumpToPage(initialPage);
|
||||
|
||||
// Jump or animate to the destination page.
|
||||
if (duration == Duration.zero) {
|
||||
_jumpToPage(_currentIndex!);
|
||||
} else {
|
||||
await _animateToPage(
|
||||
_currentIndex!,
|
||||
duration: duration,
|
||||
curve: Curves.ease,
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(_updateChildren);
|
||||
}
|
||||
}
|
||||
|
||||
void _syncControllerOffset() {
|
||||
_controller!.offset = clampDouble(
|
||||
_pageController!.page! - _controller!.index,
|
||||
-1.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
|
||||
// Called when the PageView scrolls
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (_warpUnderwayCount > 0 || _scrollUnderwayCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (notification.depth != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_controllerIsValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_scrollUnderwayCount += 1;
|
||||
final double page = _pageController!.page!;
|
||||
if (notification is ScrollUpdateNotification &&
|
||||
!_controller!.indexIsChanging) {
|
||||
final bool pageChanged = (page - _controller!.index).abs() > 1.0;
|
||||
if (pageChanged) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
}
|
||||
_syncControllerOffset();
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
_controller!.index = page.round();
|
||||
_currentIndex = _controller!.index;
|
||||
if (!_controller!.indexIsChanging) {
|
||||
_syncControllerOffset();
|
||||
}
|
||||
}
|
||||
_scrollUnderwayCount -= 1;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _debugScheduleCheckHasValidChildrenCount() {
|
||||
if (_debugHasScheduledValidChildrenCountCheck) {
|
||||
return true;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
|
||||
_debugHasScheduledValidChildrenCountCheck = false;
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
assert(() {
|
||||
if (_controller!.length != widget.children.length) {
|
||||
throw FlutterError(
|
||||
"Controller's length property (${_controller!.length}) does not match the "
|
||||
"number of children (${widget.children.length}) present in TabBarView's children property.",
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}, debugLabel: 'TabBarView.validChildrenCountCheck');
|
||||
_debugHasScheduledValidChildrenCountCheck = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(_debugScheduleCheckHasValidChildrenCount());
|
||||
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: _handleScrollNotification,
|
||||
child: PageView(
|
||||
scrollDirection: widget.scrollDirection,
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
clipBehavior: widget.clipBehavior,
|
||||
controller: _pageController,
|
||||
physics: widget.physics == null
|
||||
? const PageScrollPhysics().applyTo(const ClampingScrollPhysics())
|
||||
: const PageScrollPhysics().applyTo(widget.physics),
|
||||
children: _childrenWithKey,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
/// @docImport 'editable.dart';
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui'
|
||||
as ui
|
||||
@@ -46,6 +45,15 @@ typedef _TextBoundaryAtPositionInText =
|
||||
|
||||
const String _kEllipsis = '\u2026';
|
||||
|
||||
class _UnspecifiedTextScaler extends TextScaler {
|
||||
const _UnspecifiedTextScaler();
|
||||
@override
|
||||
Never get textScaleFactor => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Never scale(double fontSize) => throw UnimplementedError();
|
||||
}
|
||||
|
||||
/// A render object that displays a paragraph of text.
|
||||
class RenderParagraph extends RenderBox
|
||||
with
|
||||
@@ -68,7 +76,7 @@ class RenderParagraph extends RenderBox
|
||||
'This feature was deprecated after v3.12.0-2.0.pre.',
|
||||
)
|
||||
double textScaleFactor = 1.0,
|
||||
TextScaler textScaler = TextScaler.noScaling,
|
||||
TextScaler textScaler = const _UnspecifiedTextScaler(),
|
||||
int? maxLines,
|
||||
Locale? locale,
|
||||
StrutStyle? strutStyle,
|
||||
@@ -80,26 +88,22 @@ class RenderParagraph extends RenderBox
|
||||
required Color primary,
|
||||
VoidCallback? onShowMore,
|
||||
}) : assert(text.debugAssertIsValid()),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
assert(
|
||||
maxLines == null ||
|
||||
(maxLines > 0 &&
|
||||
overflow != TextOverflow.ellipsis &&
|
||||
overflow != TextOverflow.fade),
|
||||
),
|
||||
assert(
|
||||
identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0,
|
||||
identical(textScaler, const _UnspecifiedTextScaler()) ||
|
||||
textScaleFactor == 1.0,
|
||||
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
|
||||
),
|
||||
_primary = primary,
|
||||
_onShowMore = onShowMore,
|
||||
_softWrap = softWrap,
|
||||
_overflow = overflow,
|
||||
_selectionColor = selectionColor,
|
||||
_onShowMore = onShowMore,
|
||||
_textPainter = TextPainter(
|
||||
text: text,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
textScaler: textScaler == TextScaler.noScaling
|
||||
textScaler: textScaler == const _UnspecifiedTextScaler()
|
||||
? TextScaler.linear(textScaleFactor)
|
||||
: textScaler,
|
||||
maxLines: maxLines,
|
||||
@@ -842,6 +846,11 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
}
|
||||
|
||||
assert(() {
|
||||
_textPainter.debugPaintTextLayoutBoxes = debugPaintTextLayoutBoxes;
|
||||
return true;
|
||||
}());
|
||||
|
||||
_textPainter.paint(context.canvas, offset);
|
||||
|
||||
paintInlineChildren(context, offset);
|
||||
@@ -1013,8 +1022,9 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
|
||||
if (needsAssembleSemanticsNode) {
|
||||
config.explicitChildNodes = true;
|
||||
config.isSemanticBoundary = true;
|
||||
config
|
||||
..explicitChildNodes = true
|
||||
..isSemanticBoundary = true;
|
||||
} else if (needsChildConfigurationsDelegate) {
|
||||
config.childConfigurationsDelegate =
|
||||
_childSemanticsConfigurationsDelegate;
|
||||
@@ -1043,8 +1053,9 @@ class RenderParagraph extends RenderBox
|
||||
AttributedString(buffer.toString(), attributes: attributes),
|
||||
];
|
||||
}
|
||||
config.attributedLabel = _cachedAttributedLabels![0];
|
||||
config.textDirection = textDirection;
|
||||
config
|
||||
..attributedLabel = _cachedAttributedLabels![0]
|
||||
..textDirection = textDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1126,7 +1137,7 @@ class RenderParagraph extends RenderBox
|
||||
// can be re-used when [assembleSemanticsNode] is called again. This ensures
|
||||
// stable ids for the [SemanticsNode]s of [TextSpan]s across
|
||||
// [assembleSemanticsNode] invocations.
|
||||
LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
|
||||
Map<Key, SemanticsNode>? _cachedChildNodes;
|
||||
|
||||
@override
|
||||
void assembleSemanticsNode(
|
||||
@@ -1143,8 +1154,7 @@ class RenderParagraph extends RenderBox
|
||||
int placeholderIndex = 0;
|
||||
int childIndex = 0;
|
||||
RenderBox? child = firstChild;
|
||||
final LinkedHashMap<Key, SemanticsNode> newChildCache =
|
||||
LinkedHashMap<Key, SemanticsNode>();
|
||||
final Map<Key, SemanticsNode> newChildCache = <Key, SemanticsNode>{};
|
||||
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
|
||||
for (final InlineSpanSemanticsInformation info
|
||||
in _cachedCombinedSemanticsInfos!) {
|
||||
@@ -1214,8 +1224,9 @@ class RenderParagraph extends RenderBox
|
||||
onDoubleTap: final VoidCallback? handler,
|
||||
):
|
||||
if (handler != null) {
|
||||
configuration.onTap = handler;
|
||||
configuration.isLink = true;
|
||||
configuration
|
||||
..onTap = handler
|
||||
..isLink = true;
|
||||
}
|
||||
case LongPressGestureRecognizer(
|
||||
onLongPress: final GestureLongPressCallback? onLongPress,
|
||||
@@ -1285,29 +1296,30 @@ class RenderParagraph extends RenderBox
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(EnumProperty<TextOverflow>('overflow', overflow));
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties
|
||||
..add(EnumProperty<TextAlign>('textAlign', textAlign))
|
||||
..add(EnumProperty<TextDirection>('textDirection', textDirection))
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(EnumProperty<TextOverflow>('overflow', overflow))
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1768,8 +1780,7 @@ class _SelectableFragment
|
||||
final TextPosition? existingSelectionEnd = _textSelectionEnd;
|
||||
|
||||
_setSelectionPosition(null, isEnd: isEnd);
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
final Matrix4 transform = paragraph.getTransformTo(null)..invert();
|
||||
final Offset localPosition = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
globalPosition,
|
||||
@@ -1842,8 +1853,7 @@ class _SelectableFragment
|
||||
required bool isEnd,
|
||||
}) {
|
||||
_setSelectionPosition(null, isEnd: isEnd);
|
||||
final Matrix4 transform = paragraph.getTransformTo(null);
|
||||
transform.invert();
|
||||
final Matrix4 transform = paragraph.getTransformTo(null)..invert();
|
||||
final Offset localPosition = MatrixUtils.transformPoint(
|
||||
transform,
|
||||
globalPosition,
|
||||
@@ -2348,8 +2358,8 @@ class _SelectableFragment
|
||||
existingSelectionEnd,
|
||||
);
|
||||
}
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null);
|
||||
originTransform.invert();
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null)
|
||||
..invert();
|
||||
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
originTransform,
|
||||
globalPosition,
|
||||
@@ -2653,8 +2663,8 @@ class _SelectableFragment
|
||||
existingSelectionEnd,
|
||||
);
|
||||
}
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null);
|
||||
originTransform.invert();
|
||||
final Matrix4 originTransform = originParagraph.getTransformTo(null)
|
||||
..invert();
|
||||
final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
originTransform,
|
||||
globalPosition,
|
||||
@@ -3116,8 +3126,7 @@ class _SelectableFragment
|
||||
RenderObject? current = paragraph;
|
||||
while (current != null) {
|
||||
if (current is RenderParagraph) {
|
||||
final Matrix4 currentTransform = current.getTransformTo(null);
|
||||
currentTransform.invert();
|
||||
final Matrix4 currentTransform = current.getTransformTo(null)..invert();
|
||||
final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint(
|
||||
currentTransform,
|
||||
globalPosition,
|
||||
@@ -3809,13 +3818,14 @@ class _SelectableFragment
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
DiagnosticsProperty<String>(
|
||||
'textInsideRange',
|
||||
range.textInside(fullText),
|
||||
),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<TextRange>('range', range));
|
||||
properties.add(DiagnosticsProperty<String>('fullText', fullText));
|
||||
properties
|
||||
..add(
|
||||
DiagnosticsProperty<String>(
|
||||
'textInsideRange',
|
||||
range.textInside(fullText),
|
||||
),
|
||||
)
|
||||
..add(DiagnosticsProperty<TextRange>('range', range))
|
||||
..add(DiagnosticsProperty<String>('fullText', fullText));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import 'dart:ui' as ui;
|
||||
// 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:ui' as ui show TextHeightBehavior;
|
||||
|
||||
import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -280,68 +284,69 @@ class RichText extends MultiChildRenderObjectWidget {
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
);
|
||||
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
|
||||
properties.add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
);
|
||||
properties.add(StringProperty('text', text.toPlainText()));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties
|
||||
..add(
|
||||
EnumProperty<TextAlign>(
|
||||
'textAlign',
|
||||
textAlign,
|
||||
defaultValue: TextAlign.start,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextDirection>(
|
||||
'textDirection',
|
||||
textDirection,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
FlagProperty(
|
||||
'softWrap',
|
||||
value: softWrap,
|
||||
ifTrue: 'wrapping at box width',
|
||||
ifFalse: 'no wrapping except at line break characters',
|
||||
showName: true,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
EnumProperty<TextOverflow>(
|
||||
'overflow',
|
||||
overflow,
|
||||
defaultValue: TextOverflow.clip,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextScaler>(
|
||||
'textScaler',
|
||||
textScaler,
|
||||
defaultValue: TextScaler.noScaling,
|
||||
),
|
||||
)
|
||||
..add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'))
|
||||
..add(
|
||||
EnumProperty<TextWidthBasis>(
|
||||
'textWidthBasis',
|
||||
textWidthBasis,
|
||||
defaultValue: TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
..add(StringProperty('text', text.toPlainText()))
|
||||
..add(
|
||||
DiagnosticsProperty<Locale>('locale', locale, defaultValue: null),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<StrutStyle>(
|
||||
'strutStyle',
|
||||
strutStyle,
|
||||
defaultValue: null,
|
||||
),
|
||||
)
|
||||
..add(
|
||||
DiagnosticsProperty<TextHeightBehavior>(
|
||||
'textHeightBehavior',
|
||||
textHeightBehavior,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// Container(
|
||||
/// width: 100,
|
||||
/// decoration: BoxDecoration(border: Border.all()),
|
||||
/// child: Text(overflow: TextOverflow.ellipsis, 'Hello $_name, how are you?'))
|
||||
/// child: const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.ellipsis,
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
@@ -60,10 +64,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// Text(
|
||||
/// const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.fade,
|
||||
/// maxLines: 1,
|
||||
/// 'Hello $_name, how are you?')
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Here soft wrapping is enabled and the [Text] widget tries to wrap the words
|
||||
@@ -74,10 +79,11 @@ import 'package:flutter/rendering.dart' hide RenderParagraph;
|
||||
/// 
|
||||
///
|
||||
/// ```dart
|
||||
/// Text(
|
||||
/// const Text(
|
||||
/// 'Hello, how are you?',
|
||||
/// overflow: TextOverflow.fade,
|
||||
/// softWrap: false,
|
||||
/// 'Hello $_name, how are you?')
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget
|
||||
@@ -410,6 +416,7 @@ class Text extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
locale: locale,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
primary: primary,
|
||||
@@ -442,6 +449,7 @@ class Text extends StatelessWidget {
|
||||
text: TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
text: data,
|
||||
locale: locale,
|
||||
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
|
||||
),
|
||||
onShowMore: onShowMore,
|
||||
@@ -1105,7 +1113,7 @@ class _SelectableTextContainerDelegate
|
||||
bool forwardSelection =
|
||||
currentSelectionEndIndex >= currentSelectionStartIndex;
|
||||
if (currentSelectionEndIndex == currentSelectionStartIndex) {
|
||||
// Determining selection direction is innacurate if currentSelectionStartIndex == currentSelectionEndIndex.
|
||||
// Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
|
||||
// Use the range from the selectable within the selection as the source of truth for selection direction.
|
||||
final SelectedContentRange rangeAtSelectableInSelection =
|
||||
selectables[currentSelectionStartIndex].getSelection()!;
|
||||
|
||||
@@ -209,7 +209,7 @@ class CupertinoAdaptiveTextSelectionToolbar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there aren't any buttons to build, build an empty toolbar.
|
||||
if ((children?.isEmpty ?? false) || (buttonItems?.isEmpty ?? false)) {
|
||||
if ((children ?? buttonItems)?.isEmpty ?? true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user