Skip to content

Instantly share code, notes, and snippets.

@lwlizhe
Last active May 24, 2024 11:44
Show Gist options
  • Save lwlizhe/558ee91b691a7d9e6873f16d9abccf78 to your computer and use it in GitHub Desktop.
Save lwlizhe/558ee91b691a7d9e6873f16d9abccf78 to your computer and use it in GitHub Desktop.
FLutter Nested Primary PageView
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart' show Drag, DragStartBehavior;
import 'package:flutter/foundation.dart' show precisionErrorTolerance;
class PrimaryPageController extends ScrollController {
PrimaryPageController({
this.initialPage = 0,
this.keepPage = true,
this.viewportFraction = 1.0,
this.coordinator,
}) : assert(initialPage != null),
assert(keepPage != null),
assert(viewportFraction != null),
assert(viewportFraction > 0.0);
int initialPage;
final bool keepPage;
final double viewportFraction;
PrimaryPageCoordinator coordinator;
List<PrimaryPageController> childPageController = [];
double get page {
assert(
positions.isNotEmpty,
'PrimaryPageController.page cannot be accessed before a NestedPrimaryPageView is built with it.',
);
assert(
positions.length == 1,
'The page property cannot be read when multiple PageViews are attached to '
'the same PrimaryPageController.',
);
final PrimaryPagePosition position = this.position;
return position.page;
}
Future<void> animateToPage(
int page, {
@required Duration duration,
@required Curve curve,
}) {
final PrimaryPagePosition position = this.position;
return position.animateTo(
position.getPixelsFromPage(page.toDouble()),
duration: duration,
curve: curve,
);
}
void jumpToPage(int page) {
final PrimaryPagePosition position = this.position;
position.jumpTo(position.getPixelsFromPage(page.toDouble()));
}
Future<void> nextPage({@required Duration duration, @required Curve curve}) {
return animateToPage(page.round() + 1, duration: duration, curve: curve);
}
Future<void> previousPage(
{@required Duration duration, @required Curve curve}) {
return animateToPage(page.round() - 1, duration: duration, curve: curve);
}
@override
ScrollPosition createScrollPosition(ScrollPhysics physics,
ScrollContext context, ScrollPosition oldPosition) {
PrimaryPagePosition result = PrimaryPagePosition(
physics: physics,
context: context,
initialPage: initialPage,
keepPage: keepPage,
viewportFraction: viewportFraction,
oldPosition: oldPosition,
)..coordinator = coordinator;
return result;
}
@override
void attach(ScrollPosition position) {
super.attach(position);
final PrimaryPagePosition pagePosition = position;
pagePosition.viewportFraction = viewportFraction;
if (position is PrimaryPagePosition) {
position.coordinator = coordinator;
}
}
@override
void detach(ScrollPosition position) {
super.detach(position);
}
}
class PrimaryPagePosition extends ScrollPosition
implements PageMetrics, ScrollActivityDelegate {
PrimaryPagePosition({
ScrollPhysics physics,
ScrollContext context,
this.initialPage = 0,
bool keepPage = true,
double viewportFraction = 1.0,
double initialPixels = 0.0,
ScrollPosition oldPosition,
}) : assert(initialPage != null),
assert(keepPage != null),
assert(viewportFraction != null),
assert(viewportFraction > 0.0),
_viewportFraction = viewportFraction,
_pageToUseOnStartup = initialPage.toDouble(),
super(
physics: physics,
context: context,
keepScrollOffset: keepPage,
oldPosition: oldPosition,
) {
if (activity == null) goIdle();
assert(activity != null);
}
final int initialPage;
double _pageToUseOnStartup;
@override
double get viewportFraction => _viewportFraction;
double _viewportFraction;
PrimaryPageCoordinator coordinator;
set viewportFraction(double value) {
if (_viewportFraction == value) return;
final double oldPage = page;
_viewportFraction = value;
if (oldPage != null) forcePixels(getPixelsFromPage(oldPage));
}
double get _initialPageOffset =>
math.max(0, viewportDimension * (viewportFraction - 1) / 2);
ScrollActivity get scrollActivity => activity;
double getPageFromPixels(double pixels, double viewportDimension) {
final double actual = math.max(0.0, pixels - _initialPageOffset) /
math.max(1.0, viewportDimension * viewportFraction);
final double round = actual.roundToDouble();
if ((actual - round).abs() < precisionErrorTolerance) {
return round;
}
return actual;
}
double getPixelsFromPage(double page) {
return page * viewportDimension * viewportFraction + _initialPageOffset;
}
@override
double get page {
assert(
pixels == null || (minScrollExtent != null && maxScrollExtent != null),
'Page value is only available after content dimensions are established.',
);
return pixels == null
? null
: getPageFromPixels(
pixels.clamp(minScrollExtent, maxScrollExtent), viewportDimension);
}
@override
void saveScrollOffset() {
PageStorage.of(context.storageContext)?.writeState(
context.storageContext, getPageFromPixels(pixels, viewportDimension));
}
@override
void restoreScrollOffset() {
if (pixels == null) {
final double value = PageStorage.of(context.storageContext)
?.readState(context.storageContext);
if (value != null) _pageToUseOnStartup = value;
}
}
@override
bool applyViewportDimension(double viewportDimension) {
final double oldViewportDimensions = this.viewportDimension;
final bool result = super.applyViewportDimension(viewportDimension);
final double oldPixels = pixels;
final double page = (oldPixels == null || oldViewportDimensions == 0.0)
? _pageToUseOnStartup
: getPageFromPixels(oldPixels, this.viewportDimension);
final double newPixels = getPixelsFromPage(page);
if (newPixels != oldPixels) {
correctPixels(newPixels);
return false;
}
return result;
}
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
final double newMinScrollExtent = minScrollExtent + _initialPageOffset;
return super.applyContentDimensions(
newMinScrollExtent,
math.max(newMinScrollExtent, maxScrollExtent - _initialPageOffset),
);
}
@override
PageMetrics copyWith({
double minScrollExtent,
double maxScrollExtent,
double pixels,
double viewportDimension,
AxisDirection axisDirection,
double viewportFraction,
}) {
return PageMetrics(
minScrollExtent: minScrollExtent ?? this.minScrollExtent,
maxScrollExtent: maxScrollExtent ?? this.maxScrollExtent,
pixels: pixels ?? this.pixels,
viewportDimension: viewportDimension ?? this.viewportDimension,
axisDirection: axisDirection ?? this.axisDirection,
viewportFraction: viewportFraction ?? this.viewportFraction,
);
}
/// Velocity from a previous activity temporarily held by [hold] to potentially
/// transfer to a next activity.
double _heldPreviousVelocity = 0.0;
@override
AxisDirection get axisDirection => context.axisDirection;
@override
double setPixels(double newPixels) {
print("current position:" + newPixels.toString());
return super.setPixels(newPixels);
}
@override
void absorb(ScrollPosition other) {
super.absorb(other);
if (other is! ScrollPositionWithSingleContext) {
goIdle();
return;
}
activity.updateDelegate(this);
final PrimaryPagePosition typedOther = other;
_userScrollDirection = typedOther._userScrollDirection;
assert(_currentDrag == null);
if (typedOther._currentDrag != null) {
_currentDrag = typedOther._currentDrag;
_currentDrag.updateDelegate(this);
typedOther._currentDrag = null;
}
}
@override
void applyNewDimensions() {
super.applyNewDimensions();
context.setCanDrag(physics.shouldAcceptUserOffset(this));
}
@override
void beginActivity(ScrollActivity newActivity) {
_heldPreviousVelocity = 0.0;
if (newActivity == null) return;
super.beginActivity(newActivity);
_currentDrag?.dispose();
_currentDrag = null;
if (!activity.isScrolling) updateUserScrollDirection(ScrollDirection.idle);
}
@override
void applyUserOffset(double delta) {
updateUserScrollDirection(
delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}
@override
void goIdle() {
beginActivity(IdleScrollActivity(this));
}
/// Start a physics-driven simulation that settles the [pixels] position,
/// starting at a particular velocity.
///
/// This method defers to [ScrollPhysics.createBallisticSimulation], which
/// typically provides a bounce simulation when the current position is out of
/// bounds and a friction simulation when the position is in bounds but has a
/// non-zero velocity.
///
/// The velocity should be in logical pixels per second.
@override
void goBallistic(double velocity) {
assert(pixels != null);
final Simulation simulation =
physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
@override
ScrollDirection get userScrollDirection => _userScrollDirection;
ScrollDirection _userScrollDirection = ScrollDirection.idle;
/// Set [userScrollDirection] to the given value.
///
/// If this changes the value, then a [UserScrollNotification] is dispatched.
@protected
@visibleForTesting
void updateUserScrollDirection(ScrollDirection value) {
assert(value != null);
if (userScrollDirection == value) return;
_userScrollDirection = value;
didUpdateScrollDirection(value);
}
@override
Future<void> animateTo(
double to, {
@required Duration duration,
@required Curve curve,
}) {
if (nearEqual(to, pixels, physics.tolerance.distance)) {
// Skip the animation, go straight to the position as we are already close.
jumpTo(to);
return Future<void>.value();
}
final DrivenScrollActivity activity = DrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: context.vsync,
);
beginActivity(activity);
return activity.done;
}
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
notifyListeners();
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
goBallistic(0.0);
}
@Deprecated(
'This will lead to bugs.') // ignore: flutter_deprecation_syntax, https://github.com/flutter/flutter/issues/44609
@override
void jumpToWithoutSettling(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
notifyListeners();
didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
didEndScroll();
}
}
@override
ScrollHoldController hold(VoidCallback holdCancelCallback) {
if (coordinator != null && coordinator.isOuterControllerEnable()) {
return coordinator.hold(holdCancelCallback);
} else {
final double previousVelocity = activity.velocity;
final HoldScrollActivity holdActivity = HoldScrollActivity(
delegate: this,
onHoldCanceled: holdCancelCallback,
);
beginActivity(holdActivity);
_heldPreviousVelocity = previousVelocity;
return holdActivity;
}
}
ScrollDragController _currentDrag;
@override
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
if (coordinator != null && coordinator.isOuterControllerEnable()) {
return coordinator.drag(details, dragCancelCallback);
} else {
final ScrollDragController drag = ScrollDragController(
delegate: this.coordinator ?? this,
details: details,
onDragCanceled: dragCancelCallback,
carriedVelocity: physics.carriedMomentum(_heldPreviousVelocity),
motionStartDistanceThreshold: physics.dragStartDistanceMotionThreshold,
);
beginActivity(DragScrollActivity(this, drag));
assert(_currentDrag == null);
_currentDrag = drag;
return drag;
}
}
@override
void dispose() {
_currentDrag?.dispose();
_currentDrag = null;
super.dispose();
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${context.runtimeType}');
description.add('$physics');
description.add('$activity');
description.add('$userScrollDirection');
}
// Returns the amount of delta that was not used.
//
// Positive delta means going down (exposing stuff above), negative delta
// going up (exposing stuff below).
double applyClampedDragUpdate(double delta) {
assert(delta != 0.0);
// If we are going towards the maxScrollExtent (negative scroll offset),
// then the furthest we can be in the minScrollExtent direction is negative
// infinity. For example, if we are already overscrolled, then scrolling to
// reduce the overscroll should not disallow the overscroll.
//
// If we are going towards the minScrollExtent (positive scroll offset),
// then the furthest we can be in the minScrollExtent direction is wherever
// we are now, if we are already overscrolled (in which case pixels is less
// than the minScrollExtent), or the minScrollExtent if we are not.
//
// In other words, we cannot, via applyClampedDragUpdate, _enter_ an
// overscroll situation.
//
// An overscroll situation might be nonetheless entered via several means.
// One is if the physics allow it, via applyFullDragUpdate (see below). An
// overscroll situation can also be forced, e.g. if the scroll position is
// artificially set using the scroll controller.
final double min =
delta < 0.0 ? -double.infinity : math.min(minScrollExtent, pixels);
// The logic for max is equivalent but on the other side.
final double max =
delta > 0.0 ? double.infinity : math.max(maxScrollExtent, pixels);
final double oldPixels = pixels;
final double newPixels = (pixels - delta).clamp(min, max);
final double clampedDelta = newPixels - pixels;
if (clampedDelta == 0.0) return delta;
final double overscroll = physics.applyBoundaryConditions(this, newPixels);
final double actualNewPixels = newPixels - overscroll;
final double offset = actualNewPixels - oldPixels;
if (offset != 0.0) {
forcePixels(actualNewPixels);
didUpdateScrollPositionBy(offset);
}
return delta + offset;
}
// Returns the overscroll.
double applyFullDragUpdate(double delta) {
assert(delta != 0.0);
final double oldPixels = pixels;
// Apply friction:
final double newPixels =
pixels - physics.applyPhysicsToUserOffset(this, delta);
if (oldPixels == newPixels)
return 0.0; // delta must have been so small we dropped it during floating point addition
// Check for overscroll:
final double overscroll = physics.applyBoundaryConditions(this, newPixels);
final double actualNewPixels = newPixels - overscroll;
if (actualNewPixels != oldPixels) {
forcePixels(actualNewPixels);
didUpdateScrollPositionBy(actualNewPixels - oldPixels);
}
if (overscroll != 0.0) {
didOverscrollBy(overscroll);
return overscroll;
}
return 0.0;
}
}
class PrimaryPageCoordinator
implements ScrollActivityDelegate, ScrollHoldController {
PrimaryPageController _outerController;
PrimaryPageController _selfController;
ScrollDragController _currentDrag;
bool isOperateBody = false;
PrimaryPageCoordinator(PrimaryPageController selfController,
PrimaryPageController parentController) {
_selfController = selfController;
_selfController.coordinator = this;
_outerController = parentController;
_outerController.coordinator = this;
if (!_outerController.childPageController.contains(_selfController)) {
_outerController.childPageController.add(_selfController);
}
}
PrimaryPageController getOuterController() {
return _outerController;
}
bool isOuterControllerEnable() {
return _outerController != null && _outerController.hasClients;
}
PrimaryPageController getInnerController() {
return _selfController;
}
bool isInnerControllerEnable() {
return _selfController != null && _selfController.hasClients;
}
@override
void applyUserOffset(double delta) {
updateUserScrollDirection(
delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
// PrimaryPagePosition innerPosition =
// (getInnerController().position as PrimaryPagePosition);
PrimaryPagePosition outPosition = isOuterControllerEnable()
? (getOuterController().position as PrimaryPagePosition)
: null;
// /// 如果不在头尾,且内部page可滑动
// if ((outPosition.pixels > outPosition?.minScrollExtent &&
// outPosition.pixels < outPosition?.maxScrollExtent) &&
// (delta < 0
// ? innerPosition.pixels < innerPosition.maxScrollExtent
// : innerPosition.pixels > innerPosition.minScrollExtent)) {
// isOperateBody = true;
// innerPosition.applyUserOffset(delta);
// return;
// }
/// 如果快速滑动,没结束掉动画那种,那么在这里判断下是否需要传给子Page
if ((outPosition.pixels >= outPosition?.minScrollExtent &&
outPosition.pixels <= outPosition?.maxScrollExtent)) {
// if (outPosition.pixels % outPosition.viewportDimension != 0) {
// outPosition.applyUserOffset(delta);
// return;
// }
if (getOuterController().childPageController.length >
outPosition.page.round()) {
PrimaryPageController currentChildPageController =
getOuterController().childPageController[outPosition.page.round()];
PrimaryPagePosition currentInnerPosition =
currentChildPageController.position;
if (delta < 0
? currentInnerPosition.pixels < currentInnerPosition.maxScrollExtent
: currentInnerPosition.pixels >
currentInnerPosition.minScrollExtent) {
isOperateBody = true;
outPosition.goBallistic(0.0);
currentInnerPosition.applyUserOffset(delta);
return;
}
}
}
/// 都不符合,那才通过外部滑动
isOperateBody = false;
outPosition.applyUserOffset(delta);
}
@override
AxisDirection get axisDirection => _outerController.position.axisDirection;
@override
void cancel() {
goBallistic(0.0);
}
@override
void goBallistic(double velocity) {
PrimaryPagePosition outPosition = isOuterControllerEnable()
? (getOuterController().position as PrimaryPagePosition)
: null;
if (getOuterController().childPageController.length >
outPosition.page.round()) {
PrimaryPageController currentChildPageController =
getOuterController().childPageController[outPosition.page.round()];
PrimaryPagePosition currentInnerPosition =
currentChildPageController.position;
// if (isOperateBody) {
if (!isOperateBody &&
(outPosition != null) &&
(outPosition.pixels > outPosition.minScrollExtent &&
outPosition.pixels < outPosition.maxScrollExtent)) {
outPosition.goBallistic(velocity);
currentInnerPosition.goBallistic(0.0);
_currentDrag?.dispose();
_currentDrag = null;
return;
}
if (isOperateBody && velocity > 0) {
if (currentInnerPosition.pixels < currentInnerPosition.maxScrollExtent &&
currentInnerPosition.pixels > currentInnerPosition.minScrollExtent) {
outPosition.goBallistic(0.0);
currentInnerPosition.goBallistic(velocity);
} else {
outPosition?.goBallistic(velocity);
currentInnerPosition.goBallistic(0.0);
}
} else {
if (currentInnerPosition.pixels < currentInnerPosition.maxScrollExtent) {
outPosition.goBallistic(0.0);
currentInnerPosition.goBallistic(velocity);
} else {
outPosition?.goBallistic(velocity);
currentInnerPosition.goBallistic(0.0);
}
}
}
_currentDrag?.dispose();
_currentDrag = null;
}
@override
void goIdle() {
beginActivity(IdleScrollActivity(this), IdleScrollActivity(this));
}
@override
double setPixels(double pixels) {
return 0.0;
}
ScrollHoldController hold(VoidCallback holdCancelCallback) {
beginActivity(
HoldScrollActivity(delegate: this, onHoldCanceled: holdCancelCallback),
HoldScrollActivity(delegate: this, onHoldCanceled: holdCancelCallback));
return this;
}
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback) {
final ScrollDragController drag = ScrollDragController(
delegate: this,
details: details,
onDragCanceled: dragCancelCallback,
);
beginActivity(
DragScrollActivity(this, drag), DragScrollActivity(this, drag));
assert(_currentDrag == null);
_currentDrag = drag;
return drag;
}
void beginActivity(
ScrollActivity newOuterActivity, ScrollActivity newInnerActivity) {
getInnerController().position.beginActivity(newInnerActivity);
if (isOuterControllerEnable()) {
getOuterController().position.beginActivity(newOuterActivity);
}
_currentDrag?.dispose();
_currentDrag = null;
if ((newOuterActivity == null || !newOuterActivity.isScrolling) &&
(newInnerActivity == null || !newInnerActivity.isScrolling)) {
updateUserScrollDirection(ScrollDirection.idle);
}
}
ScrollDirection get userScrollDirection => _userScrollDirection;
ScrollDirection _userScrollDirection = ScrollDirection.idle;
void updateUserScrollDirection(ScrollDirection value) {
assert(value != null);
if (userScrollDirection == value) return;
_userScrollDirection = value;
getOuterController().position.didUpdateScrollDirection(value);
if (isOuterControllerEnable()) {
getInnerController().position.didUpdateScrollDirection(value);
}
}
}
const PageScrollPhysics _kPagePhysics = PageScrollPhysics();
class PrimaryPageView extends StatefulWidget {
/// Creates a scrollable list that works page by page from an explicit [List]
/// of widgets.
///
/// This constructor is appropriate for page views with a small number of
/// children because constructing the [List] requires doing work for every
/// child that could possibly be displayed in the page view, instead of just
/// those children that are actually visible.
PrimaryPageView({
Key key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PrimaryPageController controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
this.primary = false,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
}) : childrenDelegate = SliverChildListDelegate(children),
super(key: key) {
if (controller == null) {
this.controller = PrimaryPageController();
} else {
this.controller = controller;
}
}
/// Creates a scrollable list that works page by page using widgets that are
/// created on demand.
///delta
/// This constructor is appropriate for page 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] lets the [PrimaryPageView] compute the maximum
/// scroll extent.
///
/// [itemBuilder] will be called only with indices greater than or equal to
/// zero and less than [itemCount].
///
/// [PrimaryPageView.builder] by default does not support child reordering. If
/// you are planning to change child order at a later time, consider using
/// [PrimaryPageView] or [PrimaryPageView.custom].
PrimaryPageView.builder({
Key key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PrimaryPageController controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
this.primary = false,
this.dragStartBehavior = DragStartBehavior.start,
}) : childrenDelegate =
SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
super(key: key) {
if (controller == null) {
this.controller = PrimaryPageController();
} else {
this.controller = controller;
}
}
PrimaryPageView.custom({
Key key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PrimaryPageController controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
@required this.childrenDelegate,
this.primary = false,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(childrenDelegate != null),
super(key: key) {
if (controller == null) {
this.controller = PrimaryPageController();
} else {
this.controller = controller;
}
}
/// The axis along which the page view scrolls.
///
/// Defaults to [Axis.horizontal].
final Axis scrollDirection;
/// Whether the page view scrolls in the reading direction.
///
/// For example, if the reading direction is left-to-right and
/// [scrollDirection] is [Axis.horizontal], then the page 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 page view
/// scrolls from top to bottom when [reverse] is false and from bottom to top
/// when [reverse] is true.
///
/// Defaults to false.
final bool reverse;
/// An object that can be used to control the position to which this page
/// view is scrolled.
PrimaryPageController controller;
/// 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;
/// Set to false to disable page snapping, useful for custom scroll behavior.
final bool pageSnapping;
/// Called whenever the page in the center of the viewport changes.
final ValueChanged<int> onPageChanged;
/// A delegate that provides the children for the [PrimaryPageView].
///
/// The [PrimaryPageView.custom] constructor lets you specify this delegate
/// explicitly. The [PrimaryPageView] and [PrimaryPageView.builder] constructors create a
/// [childrenDelegate] that wraps the given [List] and [IndexedWidgetBuilder],
/// respectively.
final SliverChildDelegate childrenDelegate;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
final bool primary;
@override
_PageViewState createState() => _PageViewState();
}
class _PageViewState extends State<PrimaryPageView> {
int _lastReportedPage = 0;
@override
void initState() {
super.initState();
_lastReportedPage = widget.controller.initialPage;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
AxisDirection _getDirection(BuildContext context) {
switch (widget.scrollDirection) {
case Axis.horizontal:
assert(debugCheckHasDirectionality(context));
final TextDirection textDirection = Directionality.of(context);
final AxisDirection axisDirection =
textDirectionToAxisDirection(textDirection);
return widget.reverse
? flipAxisDirection(axisDirection)
: axisDirection;
case Axis.vertical:
return widget.reverse ? AxisDirection.up : AxisDirection.down;
}
return null;
}
@override
Widget build(BuildContext context) {
final AxisDirection axisDirection = _getDirection(context);
final ScrollPhysics physics = widget.pageSnapping
? _kPagePhysics.applyTo(widget.physics)
: widget.physics;
final ScrollController scrollController = widget.primary
? PrimaryScrollController.of(context)
: widget.controller;
if (widget.primary && scrollController is PrimaryPageController) {
scrollController.initialPage = _lastReportedPage;
PrimaryPageCoordinator(widget.controller, scrollController);
}
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
widget.onPageChanged != null &&
notification is ScrollUpdateNotification) {
final PageMetrics metrics = notification.metrics;
final int currentPage = metrics.page.round();
if (currentPage != _lastReportedPage) {
_lastReportedPage = currentPage;
widget.onPageChanged(currentPage);
}
}
return false;
},
child: Scrollable(
/// magic code,魔法代码勿动……这个key很重要
/// 如果PrimaryPageView下面还有多个同级PrimaryPageView,
/// 那么竟然会导致父PrimaryPageView识别为子PrimaryPageView
/// 进而在didUpdateWidget中解绑父PrimaryPageView中controller绑定的position
/// 并将其赋予给子PrimaryPageView的controller
/// 这样就导致父PrimaryPageView就这么神奇的丢失了自己的position……
/// 进而无法触发任何父PrimaryPageView的滑动事件
/// 不知道是我的问题还是flutter的问题
/// 不过既然知道原因了
/// 用key打个补丁,加上个身份证就好了……有空研究下这个神奇的问题
key: Key(widget.controller.toString()),
dragStartBehavior: widget.dragStartBehavior,
axisDirection: axisDirection,
controller: widget.controller,
physics: physics,
viewportBuilder: (BuildContext context, ViewportOffset position) {
return Viewport(
cacheExtent: 0.0,
axisDirection: axisDirection,
offset: position,
slivers: <Widget>[
PrimaryScrollController(
controller: widget.controller,
child: SliverFillViewport(
viewportFraction: widget.controller.viewportFraction,
delegate: widget.childrenDelegate,
)),
],
);
},
),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description
.add(EnumProperty<Axis>('scrollDirection', widget.scrollDirection));
description.add(
FlagProperty('reverse', value: widget.reverse, ifTrue: 'reversed'));
description.add(DiagnosticsProperty<PrimaryPageController>(
'controller', widget.controller,
showName: false));
description.add(DiagnosticsProperty<ScrollPhysics>(
'physics', widget.physics,
showName: false));
description.add(FlagProperty('pageSnapping',
value: widget.pageSnapping, ifFalse: 'snapping disabled'));
}
}
@lwlizhe
Copy link
Author

lwlizhe commented Mar 16, 2020

How to use it?

For example:

class TestWidget extends StatefulWidget {
  @override
  _TestWidgetState createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  bool isAbsorbing = false;

  @override
  Widget build(BuildContext context) {
    final List<Tab> tabs = [
      Tab(
        text: "推荐1",
      ),
      Tab(
        text: "关注",
      )
    ];

    double maxWidth = MediaQuery.of(context).size.width;
    double maxHeight = MediaQuery.of(context).size.height;

    return Scaffold(
      body: PrimaryPageView(
        controller: PrimaryPageController(initialPage: 1),
        children: <Widget>[
          Container(
            height: maxHeight,
            width: maxWidth,
            alignment: Alignment.center,
            child: Column(
              children: <Widget>[
                Expanded(
                  child: TestInnerPage(),
                ),
                Padding(
                  padding: EdgeInsets.all(10),
                  child: Text("马老师的千层饼——第二层"),
                )
              ],
            ),
          ),
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.red,
            alignment: Alignment.center,
//            child: MainPageLeftMenuUserDetailPage(),
            child: Test2InnerPage(),
//            child: Text("我是抖音的右边独立存在的菜单页"),
          )
        ],
      ),
    );
  }
}

class TestInnerPage extends StatefulWidget {
  @override
  _TestInnerPageState createState() => _TestInnerPageState();
}

class _TestInnerPageState extends State<TestInnerPage>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);

    double maxWidth = MediaQuery.of(context).size.width;
    double maxHeight = MediaQuery.of(context).size.height;

    return Container(
      child: PrimaryPageView(
        primary: true,
        controller: PrimaryPageController(initialPage: 2),
        children: <Widget>[
          TestInside1InnerPage(),
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.red,
            alignment: Alignment.center,
            child: Text("你以为我是第一层\n<-----------"),
          ),
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.yellow,
            alignment: Alignment.center,
            child: Text("你以为我是第一层\n<-----------"),
          ),
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.blue,
            alignment: Alignment.center,
            child: Text("你以为我是第一层\n<-----------"),
          )
        ],
      ),
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

class Test2InnerPage extends StatefulWidget {
  @override
  _Test2InnerPageState createState() => _Test2InnerPageState();
}

class _Test2InnerPageState extends State<Test2InnerPage>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);

    double maxWidth = MediaQuery.of(context).size.width;
    double maxHeight = MediaQuery.of(context).size.height;

    return Container(
      child: PrimaryPageView(
        primary: true,
        controller: PrimaryPageController(initialPage: 1),
        children: <Widget>[
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.yellow,
            alignment: Alignment.center,
            child: Text("你只看到了第二层\n<-----------"),
          ),
          Container(
            height: maxHeight,
            width: maxWidth,
            color: Colors.blue,
            alignment: Alignment.center,
            child: Text("你只看到了第二层\n<-----------"),
          )
        ],
      ),
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

class TestInside1InnerPage extends StatefulWidget {
  @override
  _TestInside1InnerPageState createState() => _TestInside1InnerPageState();
}

class _TestInside1InnerPageState extends State<TestInside1InnerPage>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);

    double maxWidth = MediaQuery.of(context).size.width;
    double maxHeight = MediaQuery.of(context).size.height;

    return Container(
      height: maxHeight,
      width: maxWidth,
      alignment: Alignment.center,
      child: Column(
        children: <Widget>[
          Expanded(
            child: Container(
              child: PrimaryPageView(
                primary: true,
                controller: PrimaryPageController(initialPage: 2),
                children: <Widget>[
                  Container(
                    height: maxHeight,
                    width: maxWidth,
                    color: Colors.red,
                    alignment: Alignment.center,
                    child: Text("但其实我是第五层\n(*^_^*)"),
                  ),
                  Container(
                    height: maxHeight,
                    width: maxWidth,
                    color: Colors.yellow,
                    alignment: Alignment.center,
                    child: Text("但其实我是第五层\n(*^_^*)"),
                  ),
                  Container(
                    height: maxHeight,
                    width: maxWidth,
                    color: Colors.blue,
                    alignment: Alignment.center,
                    child: Text("但其实我是第五层\n(*^_^*)"),
                  )
                ],
              ),
            ),
          ),
          Padding(
            padding: EdgeInsets.all(10),
            child: Text("马老师的千层饼——第五层"),
          )
        ],
      ),
    );
  }

  @override
  // TODO: implement wantKeepAlive
  bool get wantKeepAlive => true;
}

@ylytddd
Copy link

ylytddd commented Jul 1, 2020

可以试试下面的demo. 测试1.12.13和1.17.3中间的PrimaryPageView会丢失子元素

class PrimaryPageViewPage extends StatefulWidget {
  @override
  _PrimaryPageViewPageState createState() => _PrimaryPageViewPageState();
}

class _PrimaryPageViewPageState extends State<PrimaryPageViewPage> {

  PrimaryPageController primaryPageController = PrimaryPageController();
  PrimaryPageController primaryPageController1 = PrimaryPageController();
  PrimaryPageController primaryPageController2 = PrimaryPageController();
  PrimaryPageController primaryPageController3 = PrimaryPageController();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PrimaryPageView(
        controller: primaryPageController,
        physics: ClampingScrollPhysics(),
        children: <Widget>[
          ChildPage(
            controller: primaryPageController1,
            key: ValueKey<int>(1),
            children: <Widget>[
              _buildChild('page11', Colors.blueAccent),
              _buildChild('page12', Colors.redAccent),
              _buildChild('page13', Colors.black45),
            ],
          ),

          ChildPage(
            controller: primaryPageController2,
            key: ValueKey<int>(2),
            children: <Widget>[
              _buildChild('page21', Colors.yellowAccent),
              _buildChild('page22', Colors.greenAccent),
              _buildChild('page23', Colors.black45),
            ],
          ),

          ChildPage(
            controller: primaryPageController3,
            key: ValueKey<int>(3),
            children: <Widget>[
              _buildChild('page31', Colors.pinkAccent),
              _buildChild('page32', Colors.cyanAccent),
              _buildChild('page33', Colors.black45),
            ],
          ),

        ],
      ),
    );
  }
}

class ChildPage extends StatefulWidget {

  final List<Widget> children;
  final PrimaryPageController controller;
  ChildPage({this.children, Key key, this.controller}):super(key: key);

  @override
  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  @override
  Widget build(BuildContext context) {
    return PrimaryPageView(
      primary: true,
      controller: widget.controller,
      physics: ClampingScrollPhysics(),
      children: widget.children,
    );
  }
}

Widget _buildChild(String text, Color color) {
  return Container(
    color: color,
    alignment: Alignment.center,
    child: Text(text),
  );
}

@lwlizhe
Copy link
Author

lwlizhe commented Jul 3, 2020

可以试试下面的demo. 测试1.12.13和1.17.3中间的PrimaryPageView会丢失子元素

class PrimaryPageViewPage extends StatefulWidget {
  @override
  _PrimaryPageViewPageState createState() => _PrimaryPageViewPageState();
}

class _PrimaryPageViewPageState extends State<PrimaryPageViewPage> {

  PrimaryPageController primaryPageController = PrimaryPageController();
  PrimaryPageController primaryPageController1 = PrimaryPageController();
  PrimaryPageController primaryPageController2 = PrimaryPageController();
  PrimaryPageController primaryPageController3 = PrimaryPageController();

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: PrimaryPageView(
        controller: primaryPageController,
        physics: ClampingScrollPhysics(),
        children: <Widget>[
          ChildPage(
            controller: primaryPageController1,
            key: ValueKey<int>(1),
            children: <Widget>[
              _buildChild('page11', Colors.blueAccent),
              _buildChild('page12', Colors.redAccent),
              _buildChild('page13', Colors.black45),
            ],
          ),

          ChildPage(
            controller: primaryPageController2,
            key: ValueKey<int>(2),
            children: <Widget>[
              _buildChild('page21', Colors.yellowAccent),
              _buildChild('page22', Colors.greenAccent),
              _buildChild('page23', Colors.black45),
            ],
          ),

          ChildPage(
            controller: primaryPageController3,
            key: ValueKey<int>(3),
            children: <Widget>[
              _buildChild('page31', Colors.pinkAccent),
              _buildChild('page32', Colors.cyanAccent),
              _buildChild('page33', Colors.black45),
            ],
          ),

        ],
      ),
    );
  }
}

class ChildPage extends StatefulWidget {

  final List<Widget> children;
  final PrimaryPageController controller;
  ChildPage({this.children, Key key, this.controller}):super(key: key);

  @override
  _ChildPageState createState() => _ChildPageState();
}

class _ChildPageState extends State<ChildPage> {
  @override
  Widget build(BuildContext context) {
    return PrimaryPageView(
      primary: true,
      controller: widget.controller,
      physics: ClampingScrollPhysics(),
      children: widget.children,
    );
  }
}

Widget _buildChild(String text, Color color) {
  return Container(
    color: color,
    alignment: Alignment.center,
    child: Text(text),
  );
}

上面那个我更新了下,简单的来说就是滑动完内部和外部都调用goBallistic(0.0)强行复位,试了下好像效果还行,你看看你觉得咋样

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment