Last active
December 5, 2019 09:40
-
-
Save gladimdim/ff3928add11eb9a8cf060ba4a12d7152 to your computer and use it in GitHub Desktop.
Revolver scroll animation for the PageView Flutter widget.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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