Last active
January 20, 2020 17:17
-
-
Save leonardarnold/49c6933a12c444c4f7bed00d5b0526f7 to your computer and use it in GitHub Desktop.
Leonard Arnold Contact
This file contains 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:math'; | |
enum ThemeOptions { darkTheme, lightTheme } | |
void main() => runApp(ThemeChanger( | |
child: MyApp(), | |
)); | |
class MyApp extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
theme: ThemeChanger.of(context), | |
home: ProfileScreen(), | |
); | |
} | |
} | |
class ThemeChanger extends StatefulWidget { | |
final Widget child; | |
const ThemeChanger({ | |
Key key, | |
@required this.child, | |
}) : super(key: key); | |
@override | |
ThemeChangerState createState() => ThemeChangerState(); | |
static ThemeData of(BuildContext context) { | |
// deprecated warning but otherwise it would be incompatible with DartPad | |
// dependOnInheritedWidgetOfExactType yet | |
ThemeInheritedWidget inherited = | |
//ignore: deprecated_member_use | |
context.inheritFromWidgetOfExactType(ThemeInheritedWidget); | |
return inherited.appState.theme; | |
} | |
static ThemeChangerState instanceOf(BuildContext context) { | |
ThemeInheritedWidget inherited = | |
//ignore: deprecated_member_use | |
context.inheritFromWidgetOfExactType(ThemeInheritedWidget); | |
return inherited.appState; | |
} | |
} | |
class ThemeChangerState extends State<ThemeChanger> { | |
ThemeData _theme; | |
ThemeOptions themeOptions; | |
bool isDark; | |
ThemeData get theme => _theme; | |
@override | |
void initState() { | |
super.initState(); | |
_changeTheme(ThemeOptions.darkTheme); | |
} | |
void changeTheme(bool darkMode) { | |
this.isDark = darkMode; | |
if (darkMode) { | |
_changeTheme(ThemeOptions.darkTheme); | |
} else { | |
_changeTheme(ThemeOptions.lightTheme); | |
} | |
} | |
void _changeTheme(ThemeOptions themeOptions) { | |
setState(() { | |
this.themeOptions = themeOptions; | |
switch (themeOptions) { | |
case ThemeOptions.lightTheme: | |
isDark = false; | |
_theme = ThemeData.light(); | |
break; | |
case ThemeOptions.darkTheme: | |
isDark = true; | |
_theme = ThemeData( | |
primaryColor: Colors.red, | |
primaryColorDark: Colors.red[900], | |
primaryColorLight: Colors.red[300], | |
brightness: Brightness.dark, | |
toggleableActiveColor: Colors.red); | |
break; | |
} | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return ThemeInheritedWidget(appState: this, child: widget.child); | |
} | |
} | |
class ThemeInheritedWidget extends InheritedWidget { | |
final ThemeChangerState appState; | |
ThemeInheritedWidget( | |
{@required this.appState, Key key, @required Widget child}) | |
: super(key: key, child: child); | |
@override | |
bool updateShouldNotify(ThemeInheritedWidget oldWidget) { | |
return true; | |
} | |
} | |
class ProfileScreen extends StatefulWidget { | |
@override | |
_ProfileScreenState createState() => _ProfileScreenState(); | |
} | |
class _ProfileScreenState extends State<ProfileScreen> | |
with TickerProviderStateMixin { | |
AnimationController _animController; | |
AnimationController _rotateController; | |
Animation<double> _translateCard; | |
Animation<double> _rotateCard; | |
bool flipped = false; | |
Size size; | |
final _negativeMiddleMargin = -24; | |
final _textSpacer = const SizedBox( | |
height: 8, | |
); | |
void _flipCard() { | |
if (!_rotateController.isAnimating) { | |
if (!flipped) { | |
_rotateController.forward(from: 0); | |
} else { | |
_rotateController.reverse(from: pi); | |
} | |
} | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_animController = | |
AnimationController(vsync: this, duration: Duration(milliseconds: 500)); | |
_rotateController = | |
AnimationController(vsync: this, duration: Duration(milliseconds: 500)); | |
_translateCard = Tween(begin: 1.0, end: 0.0) | |
.animate(CurvedAnimation(parent: _animController, curve: Curves.easeIn)) | |
..addListener(() => setState(() {})); | |
_rotateCard = Tween(begin: 0.0, end: pi).animate( | |
CurvedAnimation(parent: _rotateController, curve: Curves.bounceIn)) | |
..addListener(() => setState(() { | |
if (_rotateCard.value != 0) { | |
flipped = (_rotateCard.value >= pi / 2) ? true : false; | |
} | |
})); | |
_animController.forward(); | |
} | |
changeDarkMode({@required BuildContext context, bool value}) { | |
if (value == null) { | |
value = !ThemeChanger.instanceOf(context).isDark; | |
} | |
ThemeChanger.instanceOf(context).changeTheme(value); | |
} | |
@override | |
Widget build(BuildContext context) { | |
size = MediaQuery.of(context).size; | |
return Scaffold( | |
body: SafeArea( | |
child: Stack( | |
children: <Widget>[ | |
//image | |
ClipPath( | |
clipper: ProfileBackgroundClipper(), | |
child: Container( | |
decoration: BoxDecoration( | |
color: Theme.of(context).primaryColorLight, | |
image: DecorationImage( | |
alignment: Alignment.topCenter, | |
image: NetworkImage( | |
"https://arnold.work/wp-content/uploads/2020/01/leonard_portrait.jpg"), | |
fit: BoxFit.cover, | |
), | |
), | |
), | |
), | |
//card | |
Positioned( | |
top: size.height / 2 + _negativeMiddleMargin, | |
width: size.width, | |
height: size.height / 8 * 3, | |
child: _getFreelanceCard()), | |
//top button card | |
Positioned( | |
top: size.height / 2 + 2 * _negativeMiddleMargin, | |
left: 64, | |
child: Opacity( | |
opacity: 1 - _translateCard.value, | |
child: Transform.translate( | |
offset: Offset(_translateCard.value * size.width, 0.0), | |
child: RaisedButton.icon( | |
elevation: 8, | |
shape: RoundedRectangleBorder( | |
borderRadius: new BorderRadius.circular(18.0), | |
), | |
icon: CircleAvatar( | |
child: Text("LA"), | |
backgroundColor: Theme.of(context).primaryColorDark, | |
foregroundColor: Theme.of(context).primaryColorLight, | |
radius: 14, | |
), | |
label: Text( | |
"Leonard Arnold", | |
), | |
color: Theme.of(context).primaryColor, | |
onPressed: _flipCard), | |
), | |
), | |
), | |
], | |
))); | |
} | |
Widget _getFreelanceCard() { | |
var content = Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 32.0), | |
child: Opacity( | |
opacity: 1 - _translateCard.value, | |
child: Transform.translate( | |
offset: Offset(0.0, _translateCard.value * size.height / 2), | |
child: Card( | |
elevation: 8, | |
child: LayoutBuilder(builder: (context, constraint) { | |
return SingleChildScrollView( | |
child: ConstrainedBox( | |
constraints: BoxConstraints(minHeight: constraint.maxHeight), | |
child: IntrinsicHeight( | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 32.0), | |
child: (!flipped) | |
? _getFreelanceCardContent() | |
: _getFreelanceCardContentFlipped(), | |
), | |
), | |
), | |
); | |
}), | |
), | |
), | |
), | |
); | |
//Flutter for web has problems on transformed text, even if it's transformed | |
//back and forth equally. SO it is necessary to put out the transformer | |
//to avoid blurry text on web(/dartpad) | |
return (_rotateCard.isCompleted) | |
? content | |
: Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.0001) | |
..rotateX(0) | |
..rotateY(_rotateCard.value), | |
alignment: FractionalOffset.center, | |
child: content); | |
} | |
Widget _getFreelanceCardContentFlipped() { | |
Widget content = Center( | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.center, | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
FlutterLogo( | |
size: 96, | |
), | |
SizedBox( | |
height: 8, | |
), | |
Text( | |
'Flutter for Android, iOS and Web (beta)', | |
textAlign: TextAlign.center, | |
), | |
], | |
)); | |
//Flutter for web has problems on transformed text, even if it's transformed | |
//back and forth equally. SO it is necessary to put out the transformer | |
//to avoid blurry text on web(/dartpad) | |
return (_rotateCard.isCompleted) | |
? content | |
: Transform( | |
transform: Matrix4.identity() | |
..setEntry(3, 2, 0.0001) | |
..rotateX(0) | |
..rotateY(_rotateCard.value), | |
alignment: FractionalOffset.center, | |
child: content); | |
} | |
Widget _getFreelanceCardContent() { | |
// text is still blurred on web here | |
// https://github.com/flutter/flutter/issues/32274 | |
return Column( | |
mainAxisSize: MainAxisSize.max, | |
mainAxisAlignment: MainAxisAlignment.start, | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: <Widget>[ | |
_textSpacer, | |
_textSpacer, | |
_textSpacer, | |
Text( | |
"Flutter Freelancer", | |
style: Theme.of(context).textTheme.headline, | |
), | |
_textSpacer, | |
Text("leonard@arnold.work"), | |
_textSpacer, | |
Text("Flutter, Android, iOS, Spanish, English, German"), | |
_textSpacer, | |
Expanded( | |
child: Container(), | |
), | |
RoundedOutlineButton( | |
icon: Icon( | |
Icons.play_arrow, | |
), | |
label: Text( | |
"Sunrise Animation", | |
), | |
onPressed: () { | |
Navigator.of(context).push(MaterialPageRoute(builder: (context) { | |
return SunriseScreen( | |
isDark: ThemeChanger.instanceOf(context).isDark); | |
})); | |
}, | |
), | |
Row( | |
children: <Widget>[ | |
Expanded( | |
child: Container(), | |
), | |
Card( | |
elevation: 0, | |
child: InkWell( | |
onTap: () => changeDarkMode(context: context), | |
child: Padding( | |
padding: const EdgeInsets.all(8.0), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.end, | |
mainAxisSize: MainAxisSize.min, | |
children: <Widget>[ | |
Text("Dark Mode"), | |
Switch( | |
value: ThemeChanger.instanceOf(context).isDark, | |
onChanged: (v) => | |
changeDarkMode(context: context, value: v), | |
), | |
], | |
), | |
), | |
), | |
), | |
], | |
), | |
], | |
); | |
} | |
} | |
class RoundedOutlineButton extends StatelessWidget { | |
final Widget label; | |
final Widget icon; | |
final Function onPressed; | |
RoundedOutlineButton( | |
{@required this.label, @required this.icon, @required this.onPressed}); | |
@override | |
Widget build(BuildContext context) { | |
Widget icon; | |
if (this.icon is Icon) { | |
icon = | |
Icon((this.icon as Icon).icon, color: Theme.of(context).primaryColor); | |
} | |
return RaisedButton.icon( | |
elevation: 0, | |
color: Theme.of(context).cardColor, | |
shape: RoundedRectangleBorder( | |
borderRadius: BorderRadius.circular(18.0), | |
side: BorderSide(color: Theme.of(context).primaryColor)), | |
icon: icon ?? this.icon, | |
label: label ?? this.label, | |
onPressed: this.onPressed, | |
); | |
} | |
} | |
class ProfileBackgroundClipper extends CustomClipper<Path> { | |
@override | |
Path getClip(Size size) { | |
final path = Path(); | |
path.lineTo(0, size.height / 2); | |
path.lineTo(size.width, size.height / 8 * 5); | |
path.lineTo(size.width, 0); | |
path.close(); | |
return path; | |
} | |
@override | |
bool shouldReclip(ProfileBackgroundClipper oldClipper) => false; | |
} | |
class SunriseScreen extends StatefulWidget { | |
final bool isDark; | |
SunriseScreen({Key key, @required this.isDark}) : super(key: key); | |
@override | |
_SunriseScreenState createState() => _SunriseScreenState(); | |
} | |
class _SunriseScreenState extends State<SunriseScreen> | |
with TickerProviderStateMixin { | |
AnimationController _rotationController; | |
AnimationController _bounceController; | |
Animation<double> _innerCircleAnimation; | |
Animation<double> _outerCircleAnimation; | |
Animation<Color> _colorAnimation; | |
Animation<Color> _backgroundColorAnimation; | |
int _sunriseCount = 20; | |
List<Color> _actualColors; | |
final List<Color> _darkColors = [ | |
Colors.amber[700], | |
Colors.amber[900], | |
Colors.red[500], | |
Colors.red[900], | |
]; | |
final List<Color> _lightColors = [ | |
Colors.red, | |
Colors.red[200], | |
Colors.blue, | |
Colors.blue[200], | |
]; | |
@override | |
void initState() { | |
super.initState(); | |
_actualColors = (widget.isDark) ? _darkColors : _lightColors; | |
_rotationController = AnimationController( | |
duration: const Duration(milliseconds: 3000), vsync: this); | |
_rotationController.value = 1; | |
_bounceController = AnimationController( | |
duration: const Duration(milliseconds: 1000), vsync: this); | |
_rotationController.value = 1; | |
_innerCircleAnimation = Tween(begin: 1.0, end: 1.6).animate( | |
CurvedAnimation(parent: _bounceController, curve: Curves.bounceIn)) | |
..addListener(() => setState(() {})); | |
_outerCircleAnimation = Tween(begin: 0.0, end: 1.0).animate( | |
CurvedAnimation(parent: _rotationController, curve: Curves.easeInOut)); | |
_colorAnimation = ColorTween(begin: _actualColors[0], end: _actualColors[2]) | |
.animate(CurvedAnimation( | |
parent: _rotationController, curve: Curves.easeInOut)) | |
..addListener(() => setState(() {})); | |
_backgroundColorAnimation = | |
ColorTween(begin: _actualColors[1], end: _actualColors[3]).animate( | |
CurvedAnimation( | |
parent: _rotationController, curve: Curves.easeInOut)) | |
..addListener(() => setState(() {})); | |
_bounceController.repeat(reverse: true); | |
} | |
_startAnimation() { | |
if (!_rotationController.isAnimating) { | |
_bounceController.stop(canceled: false); | |
_bounceController.reverse(); | |
if (_rotationController.value == 0) | |
_rotationController.forward(from: 0); | |
else | |
_rotationController.animateBack(0); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Container( | |
color: _backgroundColorAnimation.value, | |
child: Stack( | |
children: <Widget>[ | |
RotationTransition( | |
turns: _outerCircleAnimation, | |
child: CustomPaint( | |
foregroundPainter: SunrisePainter( | |
color: _colorAnimation.value, sunriseCount: _sunriseCount), | |
child: SizedBox.expand(), | |
), | |
), | |
AppBar( | |
backgroundColor: Colors.transparent, | |
elevation: 0, | |
), | |
Center( | |
child: Transform.scale( | |
scale: _innerCircleAnimation.value, | |
child: Container( | |
width: 64, | |
height: 64, | |
child: RawMaterialButton( | |
fillColor: (widget.isDark) ? Colors.black : Colors.white, | |
shape: CircleBorder(), | |
elevation: 0, | |
child: Icon( | |
Icons.play_arrow, | |
color: _colorAnimation.value, | |
size: 32, | |
), | |
onPressed: () => _startAnimation(), | |
), | |
), | |
)), | |
], | |
), | |
), | |
); | |
} | |
@override | |
void dispose() { | |
_rotationController.dispose(); | |
_bounceController.dispose(); | |
super.dispose(); | |
} | |
} | |
class SunrisePainter extends CustomPainter { | |
final sunriseCount; | |
final color; | |
SunrisePainter({this.sunriseCount = 20, this.color = Colors.blue}); | |
@override | |
void paint(Canvas canvas, Size size) { | |
//hypotenuse of width/2 and height/2 is the (minimum) radius | |
final circleRadius = sqrt(pow(size.width / 2, 2) + pow(size.height / 2, 2)); | |
//circumference: pi*diameter = pi * 2 * radius | |
final sunriseWidth = pi * circleRadius * 2 / sunriseCount; | |
final angle = 2 * pi / sunriseCount; | |
var paint = Paint(); | |
paint.color = color; | |
paint.style = PaintingStyle.fill; | |
canvas.translate(size.width / 2, size.height / 2); | |
for (var i = 0; i < sunriseCount; i++) { | |
if (i % 2 == 0) { | |
var path = Path(); | |
path.lineTo(circleRadius, 0); | |
path.lineTo(circleRadius, sunriseWidth); | |
path.lineTo(0, 0); | |
canvas.drawPath(path, paint); | |
} | |
canvas.rotate(angle); | |
} | |
} | |
@override | |
bool shouldRepaint(CustomPainter oldDelegate) { | |
return false; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment