247 lines
7.2 KiB
Dart
247 lines
7.2 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: () {
|
|||
|
controller.animateTo(controller.lowerBound, curve: Curves.easeIn);
|
|||
|
},
|
|||
|
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,
|
|||
|
),
|
|||
|
);
|
|||
|
}
|
|||
|
}
|