Created
April 5, 2020 06:14
-
-
Save diegoveloper/b4094e2cdafe9787dde96a77d0b2500f to your computer and use it in GitHub Desktop.
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'; | |
import 'dart:ui'; | |
import 'dart:math' as math; | |
void main() { | |
runApp(MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData.dark(), | |
debugShowCheckedModeBanner: false, | |
home: Scaffold( | |
body: Center( | |
child: CubeTransitionSample(), | |
), | |
), | |
); | |
} | |
} | |
class CubeTransitionSample extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
final height = MediaQuery.of(context).size.height / 2; | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('PageView Cube Transition'), | |
), | |
body: Center( | |
child: SizedBox( | |
height: height, | |
child: CubePageView( | |
children: places | |
.map( | |
(item) => Stack( | |
fit: StackFit.expand, | |
children: [ | |
Image.network( | |
item.url, | |
height: height, | |
fit: BoxFit.cover, | |
), | |
Positioned( | |
left: 0, | |
bottom: 0, | |
child: Padding( | |
padding: const EdgeInsets.all(12.0), | |
child: Container( | |
decoration: BoxDecoration(boxShadow: [ | |
BoxShadow( | |
color: Colors.black45, | |
spreadRadius: 5, | |
blurRadius: 5, | |
), | |
]), | |
child: Text( | |
item.name, | |
style: Theme.of(context) | |
.textTheme | |
.headline4 | |
.apply(color: Colors.white), | |
), | |
), | |
), | |
), | |
], | |
), | |
) | |
.toList(), | |
), | |
), | |
), | |
); | |
} | |
} | |
/// Signature for a function that creates a widget for a given index in a [CubePageView] | |
/// | |
/// Used by [CubePageView.builder] and other APIs that use lazily-generated widgets. | |
/// | |
typedef CubeWidgetBuilder = CubeWidget Function( | |
BuildContext context, int index, double pageNotifier); | |
/// This Widget has the [PageView] widget inside. | |
/// It works in two modes : | |
/// 1 - Using the default constructor [CubePageView] passing the items in `children` property. | |
/// 2 - Using the factory constructor [CubePageView.builder] passing a `itemBuilder` and `itemCount` properties. | |
class CubePageView extends StatefulWidget { | |
/// Called whenever the page in the center of the viewport changes. | |
final ValueChanged<int> onPageChanged; | |
/// An object that can be used to control the position to which this page | |
/// view is scrolled. | |
final PageController controller; | |
/// Builder to customize your items | |
final CubeWidgetBuilder itemBuilder; | |
/// The number of items you have, this is only required if you use [CubePageView.builder] | |
final int itemCount; | |
/// Widgets you want to use inside the [CubePageView], this is only required if you use [CubePageView] constructor | |
final List<Widget> children; | |
/// Creates a scrollable list that works page by page from an explicit [List] | |
/// of widgets. | |
const CubePageView({ | |
Key key, | |
this.onPageChanged, | |
this.controller, | |
@required this.children, | |
}) : itemBuilder = null, | |
itemCount = null, | |
assert(children != null), | |
super(key: key); | |
/// Creates a scrollable list that works page by page using widgets that are | |
/// created on demand. | |
/// | |
/// This constructor is appropriate if you want to customize the behavior | |
/// | |
/// Providing a non-null [itemCount] lets the [CubePageView] compute the maximum | |
/// scroll extent. | |
/// | |
/// [itemBuilder] will be called only with indices greater than or equal to | |
/// zero and less than [itemCount]. | |
CubePageView.builder({ | |
Key key, | |
@required this.itemCount, | |
@required this.itemBuilder, | |
this.onPageChanged, | |
this.controller, | |
}) : this.children = null, | |
assert(itemCount != null), | |
assert(itemBuilder != null), | |
super(key: key); | |
@override | |
_CubePageViewState createState() => _CubePageViewState(); | |
} | |
class _CubePageViewState extends State<CubePageView> { | |
final _pageNotifier = ValueNotifier(0.0); | |
PageController _pageController; | |
void _listener() { | |
_pageNotifier.value = _pageController.page; | |
} | |
@override | |
void initState() { | |
_pageController = widget.controller ?? PageController(); | |
WidgetsBinding.instance.addPostFrameCallback((_) { | |
_pageController.addListener(_listener); | |
}); | |
super.initState(); | |
} | |
@override | |
void dispose() { | |
_pageController.removeListener(_listener); | |
_pageController.dispose(); | |
_pageNotifier.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
color: Colors.transparent, | |
child: Center( | |
child: ValueListenableBuilder<double>( | |
valueListenable: _pageNotifier, | |
builder: (_, value, child) => PageView.builder( | |
controller: _pageController, | |
onPageChanged: widget.onPageChanged, | |
physics: const ClampingScrollPhysics(), | |
itemCount: widget.itemCount ?? widget.children.length, | |
itemBuilder: (_, index) { | |
if (widget.itemBuilder != null) | |
return widget.itemBuilder(context, index, value); | |
return CubeWidget( | |
child: widget.children[index], | |
index: index, | |
pageNotifier: value, | |
); | |
}, | |
), | |
), | |
), | |
); | |
} | |
} | |
/// This widget has the logic to do the 3D cube transformation | |
/// It only should be used if you use [CubePageView.builder] | |
class CubeWidget extends StatelessWidget { | |
/// Index of the current item | |
final int index; | |
/// Page Notifier value, it comes from the [CubeWidgetBuilder] | |
final double pageNotifier; | |
/// Child you want to use inside the Cube | |
final Widget child; | |
const CubeWidget({ | |
Key key, | |
@required this.index, | |
@required this.pageNotifier, | |
@required this.child, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final isLeaving = (index - pageNotifier) <= 0; | |
final t = (index - pageNotifier); | |
final rotationY = lerpDouble(0, 90, t); | |
final opacity = lerpDouble(0, 1, t.abs()).clamp(0.0, 1.0); | |
final transform = Matrix4.identity(); | |
transform.setEntry(3, 2, 0.003); | |
transform.rotateY(-degToRad(rotationY)); | |
return Transform( | |
alignment: isLeaving ? Alignment.centerRight : Alignment.centerLeft, | |
transform: transform, | |
child: Stack( | |
children: [ | |
child, | |
Positioned.fill( | |
child: Opacity( | |
opacity: opacity, | |
child: Container( | |
child: Container( | |
color: Colors.black87, | |
), | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
num degToRad(num deg) => deg * (math.pi / 180.0); | |
class Place { | |
final String name; | |
final String url; | |
const Place({this.name, this.url}); | |
} | |
const places = [ | |
const Place( | |
name: 'Taj Mahal - India', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/bb/a2/fa/shutterstock-180918317.jpg'), | |
const Place( | |
name: 'Colosseum - Italy', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/55/2c/54/shutterstock-433413835.jpg'), | |
const Place( | |
name: 'Chichen Itza - Mexico', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/18/ec/64/shutterstock-356871482.jpg'), | |
const Place( | |
name: 'Machu Picchu - Peru', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/ba/e8/c1/shutterstock-168497345.jpg'), | |
const Place( | |
name: 'Christ the Redeemer - Brazil', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/a2/b7/90/shutterstock-1283692720-1.jpg'), | |
const Place( | |
name: 'Petra - Jordan', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/ba/36/57/shutterstock-1030695895.jpg'), | |
const Place( | |
name: 'Great Wall of China - China', | |
url: | |
'https://www.worldatlas.com/r/w728/upload/ce/fc/93/shutterstock-275490581.jpg'), | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment