Open appleslize opened 10 months ago
Would be really appreciated!
I have added fade animation, should be a drop-in-replacement (also added prop to change gradient color, because, you know, white isn't always what you want). Feel free to PR and merge.
New optional parameters:
/// What color should the fade be.
///
/// Default is [Colors.white]
late final Color gradientColor;
/// Fade transition duration.
///
/// Default is 200ms
late final Duration? duration;
/// Fade transition curve.
///
/// Default is [Curves.easeInOutSine]
late final Curve curve;
import 'package:flutter/material.dart';
class AnimatedFadingEdgeScrollView extends StatefulWidget {
/// child widget
final Widget child;
/// What color should the fade be.
///
/// Default is [Colors.white]
late final Color gradientColor;
/// Fade transition duration.
///
/// Default is 200ms
late final Duration? duration;
/// Fade transition curve.
///
/// Default is [Curves.easeInOutSine]
late final Curve curve;
/// scroll controller of child widget
///
/// Look for more documentation at [ScrollView.scrollController]
final ScrollController scrollController;
/// Whether the scroll view scrolls in the reading direction.
///
/// Look for more documentation at [ScrollView.reverse]
final bool reverse;
/// The axis along which child view scrolls
///
/// Look for more documentation at [ScrollView.scrollDirection]
final Axis scrollDirection;
/// what part of screen on start half should be covered by fading edge gradient
/// [gradientFractionOnStart] must be 0 <= [gradientFractionOnStart] <= 1
/// 0 means no gradient,
/// 1 means gradients on start half of widget fully covers it
final double gradientFractionOnStart;
/// what part of screen on end half should be covered by fading edge gradient
/// [gradientFractionOnEnd] must be 0 <= [gradientFractionOnEnd] <= 1
/// 0 means no gradient,
/// 1 means gradients on start half of widget fully covers it
final double gradientFractionOnEnd;
AnimatedFadingEdgeScrollView._internal({
super.key,
required this.child,
required this.reverse,
required this.scrollController,
required this.scrollDirection,
required this.gradientFractionOnStart,
required this.gradientFractionOnEnd,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
assert(gradientFractionOnStart >= 0 && gradientFractionOnStart <= 1);
assert(gradientFractionOnEnd >= 0 && gradientFractionOnEnd <= 1);
this.gradientColor = gradientColor ?? Colors.white;
this.duration = duration ?? const Duration(milliseconds: 200);
this.curve = curve ?? Curves.easeInOutSine;
}
/// Constructor for creating [AnimatedFadingEdgeScrollView] with [ScrollView] as child
/// child must have [ScrollView.controller] set
factory AnimatedFadingEdgeScrollView.fromScrollView({
Key? key,
required ScrollView child,
double gradientFractionOnStart = 0.1,
double gradientFractionOnEnd = 0.1,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
final controller = child.controller;
if (controller == null) {
throw Exception("Child must have controller set");
}
return AnimatedFadingEdgeScrollView._internal(
key: key,
scrollController: controller,
scrollDirection: child.scrollDirection,
reverse: child.reverse,
gradientFractionOnStart: gradientFractionOnStart,
gradientFractionOnEnd: gradientFractionOnEnd,
gradientColor: gradientColor,
duration: duration,
curve: curve,
child: child,
);
}
/// Constructor for creating [AnimatedFadingEdgeScrollView] with [SingleChildScrollView] as child
/// child must have [SingleChildScrollView.controller] set
factory AnimatedFadingEdgeScrollView.fromSingleChildScrollView({
Key? key,
required SingleChildScrollView child,
double gradientFractionOnStart = 0.1,
double gradientFractionOnEnd = 0.1,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
final controller = child.controller;
if (controller == null) {
throw Exception("Child must have controller set");
}
return AnimatedFadingEdgeScrollView._internal(
key: key,
scrollController: controller,
scrollDirection: child.scrollDirection,
reverse: child.reverse,
gradientFractionOnStart: gradientFractionOnStart,
gradientFractionOnEnd: gradientFractionOnEnd,
gradientColor: gradientColor,
duration: duration,
curve: curve,
child: child,
);
}
/// Constructor for creating [AnimatedFadingEdgeScrollView] with [PageView] as child
/// child must have [PageView.controller] set
factory AnimatedFadingEdgeScrollView.fromPageView({
Key? key,
required PageView child,
double gradientFractionOnStart = 0.1,
double gradientFractionOnEnd = 0.1,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
final controller = child.controller;
//ignore: unnecessary_null_comparison
if (controller == null) {
throw Exception("Child must have controller set");
}
return AnimatedFadingEdgeScrollView._internal(
key: key,
scrollController: controller,
scrollDirection: child.scrollDirection,
reverse: child.reverse,
gradientFractionOnStart: gradientFractionOnStart,
gradientFractionOnEnd: gradientFractionOnEnd,
gradientColor: gradientColor,
duration: duration,
curve: curve,
child: child,
);
}
/// Constructor for creating [AnimatedFadingEdgeScrollView] with [AnimatedList] as child
/// child must have [AnimatedList.controller] set
factory AnimatedFadingEdgeScrollView.fromAnimatedList({
Key? key,
required AnimatedList child,
double gradientFractionOnStart = 0.1,
double gradientFractionOnEnd = 0.1,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
final controller = child.controller;
if (controller == null) {
throw Exception("Child must have controller set");
}
return AnimatedFadingEdgeScrollView._internal(
key: key,
scrollController: controller,
scrollDirection: child.scrollDirection,
reverse: child.reverse,
gradientFractionOnStart: gradientFractionOnStart,
gradientFractionOnEnd: gradientFractionOnEnd,
gradientColor: gradientColor,
duration: duration,
curve: curve,
child: child,
);
}
/// Constructor for creating [AnimatedFadingEdgeScrollView] with [ScrollView] as child
/// child must have [ScrollView.controller] set
factory AnimatedFadingEdgeScrollView.fromListWheelScrollView({
Key? key,
required ListWheelScrollView child,
double gradientFractionOnStart = 0.1,
double gradientFractionOnEnd = 0.1,
Color? gradientColor,
Duration? duration,
Curve? curve,
}) {
final controller = child.controller;
if (controller == null) {
throw Exception("Child must have controller set");
}
return AnimatedFadingEdgeScrollView._internal(
key: key,
scrollController: controller,
scrollDirection: Axis.vertical,
reverse: false,
gradientFractionOnStart: gradientFractionOnStart,
gradientFractionOnEnd: gradientFractionOnEnd,
gradientColor: gradientColor,
duration: duration,
curve: curve,
child: child,
);
}
@override
AnimatedFadingEdgeScrollViewState createState() => AnimatedFadingEdgeScrollViewState();
}
class AnimatedFadingEdgeScrollViewState extends State<AnimatedFadingEdgeScrollView> with TickerProviderStateMixin, WidgetsBindingObserver {
AnimationController? _topAnimationController;
AnimationController? _bottomAnimationController;
Animation<double>? _topFade;
Animation<double>? _bottomFade;
@override
void initState() {
super.initState();
_topAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_bottomAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
CurvedAnimation topCurve = CurvedAnimation(parent: _topAnimationController!, curve: Curves.easeInOutSine);
CurvedAnimation bottomCurve = CurvedAnimation(parent: _bottomAnimationController!, curve: Curves.easeInOutSine);
_topFade = Tween<double>(begin: 1.0, end: 0.0).animate(topCurve)
..addListener(() {
setState(() {});
});
_bottomFade = Tween<double>(begin: 1.0, end: 0.0).animate(bottomCurve)
..addListener(() {
setState(() {});
});
widget.scrollController.addListener(_updateFade);
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
// Add the shading or remove it when the screen resize (web/desktop) or mobile is rotated
_updateFade();
}
bool get _controllerIsReady => widget.scrollController.hasClients && widget.scrollController.positions.last.hasContentDimensions;
@override
void dispose() {
_topAnimationController?.dispose();
_bottomAnimationController?.dispose();
widget.scrollController.removeListener(_updateFade);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void _updateFade() {
if(!_controllerIsReady) {
return;
}
if(!_topAnimationController!.isAnimating) {
if (widget.scrollController.offset > 0 && _topFade!.value == 1.0) {
_topAnimationController!.forward();
} else if (widget.scrollController.offset <= 0 && _topFade!.value != 1.0) {
_topAnimationController!.reverse();
}
}
if(!_bottomAnimationController!.isAnimating) {
if (widget.scrollController.offset < widget.scrollController.position.maxScrollExtent - 0 && _bottomFade!.value == 1.0) {
_bottomAnimationController!.forward();
} else if (widget.scrollController.offset >= widget.scrollController.position.maxScrollExtent - 0 && _bottomFade!.value != 1.0) {
_bottomAnimationController!.reverse();
}
}
}
Gradient _createShaderGradient() => LinearGradient(
begin: _gradientStart,
end: _gradientEnd,
stops: [
0,
widget.gradientFractionOnStart * 0.5,
1 - widget.gradientFractionOnEnd * 0.5,
1,
],
colors: [
Colors.transparent.withOpacity(_topFade!.value),
widget.gradientColor,
widget.gradientColor,
Colors.transparent.withOpacity(_bottomFade!.value)
],
);
AlignmentGeometry get _gradientStart =>
widget.scrollDirection == Axis.vertical
? _verticalStart
: _horizontalStart;
AlignmentGeometry get _gradientEnd =>
widget.scrollDirection == Axis.vertical ? _verticalEnd : _horizontalEnd;
Alignment get _verticalStart =>
widget.reverse ? Alignment.bottomCenter : Alignment.topCenter;
Alignment get _verticalEnd =>
widget.reverse ? Alignment.topCenter : Alignment.bottomCenter;
AlignmentDirectional get _horizontalStart => widget.reverse
? AlignmentDirectional.centerEnd
: AlignmentDirectional.centerStart;
AlignmentDirectional get _horizontalEnd => widget.reverse
? AlignmentDirectional.centerStart
: AlignmentDirectional.centerEnd;
@override
Widget build(BuildContext context) {
return ShaderMask(
shaderCallback: (bounds) => _createShaderGradient().createShader(
bounds.shift(Offset(-bounds.left, -bounds.top)),
textDirection: Directionality.of(context),
),
blendMode: BlendMode.dstIn,
child: NotificationListener<ScrollMetricsNotification>(
child: widget.child,
onNotification: (_) {
_updateFade();
// Enable notification to still bubble up.
return false;
},
),
);
}
}
It would be nice if the fade at the bottom doesn't disappear so sudden. Maybe it could smoothly disappear when you are scrolling towards the end / start of the list?