Skip to content

Instantly share code, notes, and snippets.

@gladimdim
Last active December 5, 2019 09:40
Show Gist options
  • Select an option

  • Save gladimdim/ff3928add11eb9a8cf060ba4a12d7152 to your computer and use it in GitHub Desktop.

Select an option

Save gladimdim/ff3928add11eb9a8cf060ba4a12d7152 to your computer and use it in GitHub Desktop.
Revolver scroll animation for the PageView Flutter widget.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: TransformingPageView(scrollDirection: Axis.vertical,),
),
),
);
}
}
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text('Hello, World!', style: Theme.of(context).textTheme.display1);
}
}
class TransformingPageView extends StatefulWidget {
final Axis scrollDirection;
final List stories = ["Do", "Some", "Thing", "Good", "For", "Me"];
TransformingPageView({
this.scrollDirection = Axis.vertical,
});
@override
_TransformingPageViewState createState() => _TransformingPageViewState();
}
class _TransformingPageViewState extends State<TransformingPageView> {
double currentPage = 0.0;
PageController _pageController;
void _onScroll() {
setState(() {
currentPage = _pageController.page;
});
}
@override
void initState() {
super.initState();
_pageController = PageController(initialPage: 0, viewportFraction: 0.8)
..addListener(_onScroll);
}
@override
Widget build(BuildContext context) {
return PageView.builder(
scrollDirection: widget.scrollDirection,
controller: _pageController,
itemCount: widget.stories.length,
itemBuilder: (context, index) {
if (index == currentPage.floor()) {
return Transform.translate(
offset: Offset(100 * (currentPage - index.toDouble()), 0),
child: _buildStoryShell(
widget.stories[index], currentPage - index.toDouble(), context),
);
} else if (index >= currentPage.floor() + 1) {
return Transform.translate(
offset: Offset(100 * (index.toDouble() - currentPage), 0),
child: _buildStoryShell(
widget.stories[index], currentPage - index.toDouble(), context),
);
} else if (index <= currentPage.floor() - 1) {
return Transform.translate(
offset: Offset(100 * (currentPage - index.toDouble()), 0),
child: _buildStoryShell(
widget.stories[index], currentPage - index.toDouble(), context),
);
} else {
return _buildStoryShell(
widget.stories[index], currentPage - index.toDouble(), context);
}
},
);
}
Widget _buildStoryShell(
String story, double value, BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: BorderedContainer(
child: RevolverShell(
top: Container(
height: 75,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Text(
story,
style: TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.bold,
color: Theme.of(context).textTheme.title.color,
backgroundColor: Theme.of(context).primaryColor,
),
),
),
middle: Container(),
bottom: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 10,
child: SlideableButton(
onPress: () => {},
child: Container(
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Align(
alignment: Alignment.center,
child: Text(
"Show story details",
style: TextStyle(
color: Theme.of(context).textTheme.title.color,
fontSize: 22.0,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
Flexible(
flex: 1,
child: SizedBox(
height: 50.0,
child: Container(
color: Theme.of(context).backgroundColor,
)),
),
Flexible(
flex: 10,
child: SlideableButton(
onPress: () => {},
child: Container(
height: 50,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Align(
alignment: Alignment.center,
child: Text(
"Read the story",
style: TextStyle(
color: Theme.of(context).textTheme.title.color,
fontSize: 22.0,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
],
),
animationValue: value,
),
),
),
),
);
}
}
class SlideableButton extends StatefulWidget {
final Widget child;
final Function onPress;
final Direction direction;
final Duration duration;
SlideableButton({
@required this.child,
@required this.onPress,
this.direction = Direction.Right,
this.duration = const Duration(milliseconds: 200),
});
@override
_SlideableButtonState createState() => _SlideableButtonState();
}
class _SlideableButtonState extends State<SlideableButton>
with SingleTickerProviderStateMixin {
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: widget.duration,
)..addStatusListener((state) {
if (state == AnimationStatus.completed) {
widget.onPress();
controller.reverse();
}
});
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var width = MediaQuery.of(context).size.width * 2;
final end = widget.direction == Direction.Right ? width : -width;
final animation = Tween(begin: 0.0, end: end).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
));
return AnimatedBuilder(
animation: animation,
child: widget.child,
builder: (context, child) {
return Transform.translate(
offset: Offset(animation.value, 0.0),
child: InkWell(
child: child,
onTap: () {
controller.forward();
}),
);
},
);
}
}
enum Direction {
Left,
Right,
}
class BorderedContainer extends StatelessWidget {
final Widget child;
BorderedContainer({this.child});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).backgroundColor,
border: Border.all(
color: Theme.of(context).primaryColor,
width: 3.0,
),
),
child: child);
}
}
class RevolverShell extends StatefulWidget {
final Widget top;
final Widget bottom;
final Widget middle;
final double animationValue;
RevolverShell(
{this.top, this.bottom, this.middle, this.animationValue = 0.0});
@override
_RevolverShellState createState() => _RevolverShellState();
}
class _RevolverShellState extends State<RevolverShell> {
Widget _animatedWidget;
@override
Widget build(BuildContext context) {
if (widget.animationValue > 0.5 && widget.animationValue <= 1) {
_animatedWidget = Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Opacity(
opacity: 1 - widget.animationValue,
child: widget.middle,
),
Opacity(
opacity: widget.animationValue,
child: widget.top,
),
],
);
} else {
_animatedWidget = Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
widget.top,
widget.middle,
widget.bottom,
],
);
}
return AnimatedSwitcher(
duration: Duration(milliseconds: 100),
child: _animatedWidget,
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment