247 lines
7.1 KiB
Dart
247 lines
7.1 KiB
Dart
import 'package:dartx/dartx.dart';
|
||
import 'package:dreampad/app/shared/shared.dart';
|
||
import 'package:flutter/gestures.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||
|
||
class SwipeNextPageContainer extends HookWidget {
|
||
/// 叠放[children]
|
||
///
|
||
/// [children]可用用[SwipeNextPageHandler]套[Widget]使之成为滑动的[handler]
|
||
const SwipeNextPageContainer({
|
||
super.key,
|
||
required this.children,
|
||
});
|
||
|
||
final List<Widget> children;
|
||
|
||
// 定义一个便捷方法,方便子树中的widget获取共享数据
|
||
static SwipePageHandlerReceiver? of(BuildContext context) {
|
||
return context.getInheritedWidgetOfExactType<SwipePageHandlerReceiver>();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(builder: (context, constraint) {
|
||
final height = constraint.maxHeight;
|
||
return HookBuilder(builder: (context) {
|
||
final currentIndex = useState(children.length - 1);
|
||
final controller = useAnimationController(
|
||
lowerBound: 0,
|
||
upperBound: height,
|
||
duration: const Duration(milliseconds: 500),
|
||
);
|
||
useEffect(() {
|
||
void onAnimationStateChanged(AnimationStatus status) {
|
||
if (status == AnimationStatus.completed) {
|
||
currentIndex.value = currentIndex.value - 1;
|
||
}
|
||
}
|
||
|
||
controller.addStatusListener(onAnimationStateChanged);
|
||
return () {
|
||
controller.removeStatusListener(onAnimationStateChanged);
|
||
};
|
||
}, [controller]);
|
||
useEffect(() {
|
||
controller.value = 0;
|
||
return null;
|
||
}, [currentIndex.value]);
|
||
return SwipePageHandlerReceiver(
|
||
controller: controller,
|
||
currentIndex: currentIndex,
|
||
child: AnimatedBuilder(
|
||
animation: controller,
|
||
builder: (context, _) =>
|
||
Stack(
|
||
fit: StackFit.expand,
|
||
children: children.mapIndexed((index, child) {
|
||
final Widget result;
|
||
if (index > currentIndex.value) {
|
||
result = const Positioned(child: SizedBox.shrink());
|
||
} else if (index < currentIndex.value) {
|
||
result = Positioned.fill(child: child);
|
||
} else {
|
||
final offset = controller.value;
|
||
|
||
result = Positioned(
|
||
left: 0,
|
||
right: 0,
|
||
top: 0 - offset,
|
||
bottom: 0 + offset,
|
||
child: child,
|
||
);
|
||
}
|
||
return SwipePageItemState(
|
||
index: index,
|
||
child: result,
|
||
);
|
||
}).toList(),
|
||
)),
|
||
);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
class SwipePageHandlerReceiver extends InheritedWidget {
|
||
const SwipePageHandlerReceiver({
|
||
super.key,
|
||
required super.child,
|
||
required this.controller,
|
||
required this.currentIndex,
|
||
});
|
||
|
||
// 定义一个便捷方法,方便子树中的widget获取共享数据
|
||
static SwipePageHandlerReceiver? of(BuildContext context) {
|
||
return context.getInheritedWidgetOfExactType<SwipePageHandlerReceiver>();
|
||
}
|
||
|
||
final AnimationController controller;
|
||
|
||
final ValueNotifier<int> currentIndex;
|
||
|
||
@override
|
||
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
class SwipePageItemState extends InheritedWidget {
|
||
const SwipePageItemState({
|
||
super.key,
|
||
required super.child,
|
||
required this.index,
|
||
});
|
||
|
||
// 定义一个便捷方法,方便子树中的widget获取共享数据
|
||
static SwipePageItemState? of(BuildContext context) {
|
||
return context.getInheritedWidgetOfExactType();
|
||
}
|
||
|
||
final int index;
|
||
|
||
@override
|
||
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
class SwipeNextPageHandler extends StatelessWidget {
|
||
const SwipeNextPageHandler({
|
||
super.key,
|
||
required this.child,
|
||
});
|
||
|
||
final Widget child;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final controller = SwipePageHandlerReceiver
|
||
.of(context)
|
||
?.controller;
|
||
if (controller == null) {
|
||
return child;
|
||
}
|
||
return Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: () {},
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onPanEnd: (details) {
|
||
var velocityY = details.velocity.pixelsPerSecond.dy;
|
||
if (velocityY.abs() > kMinFlingVelocity) {
|
||
if (velocityY < 0) {
|
||
controller.animateTo(
|
||
controller.upperBound,
|
||
curve: Curves.decelerate,
|
||
duration: Duration(
|
||
milliseconds:
|
||
(controller.upperBound - controller.value).abs() ~/
|
||
(velocityY.abs() / 1000).coerceAtLeast(1),
|
||
),
|
||
);
|
||
} else {
|
||
controller.animateBack(
|
||
controller.lowerBound,
|
||
curve: Curves.decelerate,
|
||
duration: Duration(
|
||
milliseconds:
|
||
(controller.value - controller.lowerBound).abs() ~/
|
||
(velocityY.abs() / 1000).coerceAtLeast(1),
|
||
),
|
||
);
|
||
}
|
||
} else {
|
||
if (controller.value > controller.upperBound * 2 / 3) {
|
||
controller.forward();
|
||
} else {
|
||
controller.reverse();
|
||
}
|
||
}
|
||
},
|
||
onPanCancel: () {
|
||
|
||
},
|
||
onPanUpdate: (details) {
|
||
controller.value -= details.delta.dy;
|
||
},
|
||
child: child,
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class SwipePageFadeOutContainer extends HookWidget {
|
||
const SwipePageFadeOutContainer({
|
||
super.key,
|
||
required this.child,
|
||
});
|
||
|
||
final Widget child;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return AnimatedBuilder(
|
||
animation: SwipeNextPageContainer.of(context)!.controller,
|
||
builder: (context, _) {
|
||
final controller = SwipeNextPageContainer.of(context)!.controller;
|
||
return Opacity(
|
||
opacity: 1 - controller.value / controller.upperBound,
|
||
child: child,
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class SwipePageCurrentVisible extends HookWidget {
|
||
const SwipePageCurrentVisible({
|
||
super.key,
|
||
required this.child,
|
||
});
|
||
|
||
final Widget child;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final currentIndexState = SwipeNextPageContainer.of(context)!.currentIndex;
|
||
final index = SwipePageItemState.of(context)!.index;
|
||
final isVisible =
|
||
useListenableSelector(
|
||
currentIndexState, () => currentIndexState.value == index);
|
||
return Visibility(
|
||
visible: isVisible,
|
||
child: AnimatedVisibilityWidget(
|
||
animationWidgetBuilder: AnimatedVisibilityWidget.fadeAnimationWidgetBuilder,
|
||
duration: const Duration(milliseconds: 800),
|
||
isVisible: true,
|
||
isInitAnimated: true,
|
||
child: child,
|
||
),
|
||
);
|
||
}
|
||
}
|