Shaked animation flutter - dart

I need shaking animation like this video
I'm newbie to Flutter. I would appreciate a solution or a link to the tutorial.

I think there can be better solution. But this one works fine, maybe it'll help
class TestAnimWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestAnimWidgetState();
}
class _TestAnimWidgetState extends State<TestAnimWidget> with SingleTickerProviderStateMixin {
final TextEditingController textController = TextEditingController();
AnimationController controller;
#override
void initState() {
controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
final Animation<double> offsetAnimation =
Tween(begin: 0.0, end: 24.0).chain(CurveTween(curve: Curves.elasticIn)).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
}
});
return Scaffold(
appBar: AppBar(),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedBuilder(
animation: offsetAnimation,
builder: (buildContext, child) {
if (offsetAnimation.value < 0.0) print('${offsetAnimation.value + 8.0}');
return Container(
margin: EdgeInsets.symmetric(horizontal: 24.0),
padding: EdgeInsets.only(left: offsetAnimation.value + 24.0, right: 24.0 - offsetAnimation.value),
child: Center(child: TextField(controller: textController, )),
);
}),
RaisedButton(onPressed: () {
if (textController.value.text.isEmpty) controller.forward(from: 0.0);
},
child: Text('Enter'),)
],
),
);
}
}

Related

Flutter - Expand bottomNavigationBar by swiping or pressing the floatingActionButton

I have a main widget called DashboardWidget. Inside it, I have a Scaffold with BottomNavigationBar and a FloatingActionButton:
Now, I want to make a widget that would be dragged from the bottom by:
Swiping up with the finger.
Pressing on FloatingActionButton.
In other words, I want to expand the BottomNavigationBar.
Here's a design concept in case I was unclear.
The problem is, I'm not sure where to start to implement that. I've thought about removing the BottomNavigationBar and create a custom widget that can be expanded, but I'm not sure if it's possible either.
Output:
I used a different approach and did it without AnimationController, GlobalKey etc, the logic code is very short (_handleClick).
I only used 4 variables, simple and short!
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static double _minHeight = 80, _maxHeight = 600;
Offset _offset = Offset(0, _minHeight);
bool _isOpen = false;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xFFF6F6F6),
appBar: AppBar(backgroundColor: Color(0xFFF6F6F6), elevation: 0),
body: Stack(
alignment: Alignment.bottomCenter,
children: <Widget>[
Align(
alignment: Alignment.topLeft,
child: FlatButton(
onPressed: _handleClick,
splashColor: Colors.transparent,
textColor: Colors.grey,
child: Text(_isOpen ? "Back" : ""),
),
),
Align(child: FlutterLogo(size: 300)),
GestureDetector(
onPanUpdate: (details) {
_offset = Offset(0, _offset.dy - details.delta.dy);
if (_offset.dy < _HomePageState._minHeight) {
_offset = Offset(0, _HomePageState._minHeight);
_isOpen = false;
} else if (_offset.dy > _HomePageState._maxHeight) {
_offset = Offset(0, _HomePageState._maxHeight);
_isOpen = true;
}
setState(() {});
},
child: AnimatedContainer(
duration: Duration.zero,
curve: Curves.easeOut,
height: _offset.dy,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30),
topRight: Radius.circular(30),
),
boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.5), spreadRadius: 5, blurRadius: 10)]),
child: Text("This is my Bottom sheet"),
),
),
Positioned(
bottom: 2 * _HomePageState._minHeight - _offset.dy - 28, // 56 is the height of FAB so we use here half of it.
child: FloatingActionButton(
child: Icon(_isOpen ? Icons.keyboard_arrow_down : Icons.add),
onPressed: _handleClick,
),
),
],
),
);
}
// first it opens the sheet and when called again it closes.
void _handleClick() {
_isOpen = !_isOpen;
Timer.periodic(Duration(milliseconds: 5), (timer) {
if (_isOpen) {
double value = _offset.dy + 10; // we increment the height of the Container by 10 every 5ms
_offset = Offset(0, value);
if (_offset.dy > _maxHeight) {
_offset = Offset(0, _maxHeight); // makes sure it does't go above maxHeight
timer.cancel();
}
} else {
double value = _offset.dy - 10; // we decrement the height by 10 here
_offset = Offset(0, value);
if (_offset.dy < _minHeight) {
_offset = Offset(0, _minHeight); // makes sure it doesn't go beyond minHeight
timer.cancel();
}
}
setState(() {});
});
}
}
You can use the BottomSheet class.
Here is a Medium-tutorial for using that, here is a youtube-tutorial using it and here is the documentation for the class.
The only difference from the tutorials is that you have to add an extra call method for showBottomSheet from your FloatingActionButton when it is touched.
Bonus: here is the Material Design page on how to use it.
You can check this code, it is a complete example of how to start implementing this kind of UI, take it with a grain of salt.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Orination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
bool _isOpen;
double _dragStart;
double _hieght;
double _maxHight;
double _currentPosition;
GlobalKey _cardKey;
AnimationController _controller;
Animation<double> _cardAnimation;
#override
void initState() {
_isOpen = false;
_hieght = 50.0;
_cardKey = GlobalKey();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 700));
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.addListener(() {
setState(() {
_hieght = _cardAnimation.value;
});
});
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
backgroundColor: Colors.transparent,
titleSpacing: 0.0,
title: _isOpen
? MaterialButton(
child: Text(
"Back",
style: TextStyle(color: Colors.red),
),
onPressed: () {
_isOpen = false;
_cardAnimation = Tween(begin: _hieght, end: 50.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
},
)
: Text(""),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.keyboard_arrow_up),
onPressed: () {
final RenderBox renderBoxCard = _cardKey.currentContext
.findRenderObject();
_maxHight = renderBoxCard.size.height;
_cardAnimation = Tween(begin: _hieght, end: _maxHight).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
_controller.forward(from: 0.0);
_isOpen = true;
}),
body: Stack(
key: _cardKey,
alignment: Alignment.bottomCenter,
children: <Widget>[
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black12,
),
GestureDetector(
onPanStart: _onPanStart,
onPanUpdate: _onPanUpdate,
onPanEnd: _onPanEnd,
child:Material(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
),
elevation: 60.0,
color: Colors.white,
// shadowColor: Colors.,
child: Container(
height: _hieght,
child: Center(
child: Text("Hello, You can drag up"),
),
),
),
),
],
),
);
}
void _onPanStart(DragStartDetails details) {
_dragStart = details.globalPosition.dy;
_currentPosition = _hieght;
}
void _onPanUpdate(DragUpdateDetails details) {
final RenderBox renderBoxCard = _cardKey.currentContext.findRenderObject();
_maxHight = renderBoxCard.size.height;
final hieght = _currentPosition - details.globalPosition.dy + _dragStart;
print(
"_currentPosition = $_currentPosition _hieght = $_hieght hieght = $hieght");
if (hieght <= _maxHight && hieght >= 50.0) {
setState(() {
_hieght = _currentPosition - details.globalPosition.dy + _dragStart;
});
}
}
void _onPanEnd(DragEndDetails details) {
_currentPosition = _hieght;
if (_hieght <= 60.0) {
setState(() {
_isOpen = false;
});
} else {
setState(() {
_isOpen = true;
});
}
}
}
Edit: I modified the code by using Material Widget instead of A container with shadow for better performance,If you have any issue, please let me know .

How to detect TabBar change in Flutter?

I need to detect TabBar when I swipe then print somethings on console, how I can do that? This is my code.
bottomNavigationBar: new Material(
color: Colors.blueAccent,
child: new TabBar(
onTap: (int index){ setState(() {
_onTap(index);
});},
indicatorColor: Colors.white,
controller: controller,
tabs: <Widget>[
new Tab(icon: new Icon(Icons.shopping_basket)),
new Tab(icon: new Icon(Icons.store)),
new Tab(icon: new Icon(Icons.local_offer)),
new Tab(icon: new Icon(Icons.assignment)),
new Tab(icon: new Icon(Icons.settings)),
],
)
),
You need to add a listener to your tab controller - maybe in initState.
controller.addListener((){
print('my index is'+ controller.index.toString());
});
Swipe functionality is not provided by onTap() function, for that TabController is used
class TabBarDemo extends StatefulWidget {
#override
_TabBarDemoState createState() => _TabBarDemoState();
}
class _TabBarDemoState extends State<TabBarDemo>
with SingleTickerProviderStateMixin {
TabController _controller;
int _selectedIndex = 0;
List<Widget> list = [
Tab(icon: Icon(Icons.card_travel)),
Tab(icon: Icon(Icons.add_shopping_cart)),
];
#override
void initState() {
// TODO: implement initState
super.initState();
// Create TabController for getting the index of current tab
_controller = TabController(length: list.length, vsync: this);
_controller.addListener(() {
setState(() {
_selectedIndex = _controller.index;
});
print("Selected Index: " + _controller.index.toString());
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
bottom: TabBar(
onTap: (index) {
// Should not used it as it only called when tab options are clicked,
// not when user swapped
},
controller: _controller,
tabs: list,
),
title: Text('Tabs Demo'),
),
body: TabBarView(
controller: _controller,
children: [
Center(
child: Text(
_selectedIndex.toString(),
style: TextStyle(fontSize: 40),
)),
Center(
child: Text(
_selectedIndex.toString(),
style: TextStyle(fontSize: 40),
)),
],
),
),
);
}
}
Github Repo:
https://github.com/jitsm555/Flutter-Problems/tree/master/tab_bar_tricks
Output:
Here is a full example. Use a TabController and add a callback using addListener.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
home: MyTabbedPage(),
);
}
}
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage> with SingleTickerProviderStateMixin {
var _context;
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: myTabs.length);
_tabController.addListener(_handleTabSelection);
}
void _handleTabSelection() {
if (_tabController.indexIsChanging) {
switch (_tabController.index) {
case 0:
Scaffold.of(_context).showSnackBar(SnackBar(
content: Text('Page 1 tapped.'),
duration: Duration(milliseconds: 500),
));
break;
case 1:
Scaffold.of(_context).showSnackBar(SnackBar(
content: Text('Page 2 tapped.'),
duration: Duration(milliseconds: 500),
));
break;
}
}
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: Builder(
builder: (context) {
_context = context;
return TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
);
},
),
);
}
}
You can create wrapper widget
class _DefaultTabControllerListener extends StatefulWidget {
const _DefaultTabControllerListener(
{Key? key, this.onTabSelected, required this.child})
: super(key: key);
final void Function(int index)? onTabSelected;
final Widget child;
#override
_DefaultTabControllerListenerState createState() =>
_DefaultTabControllerListenerState();
}
class _DefaultTabControllerListenerState
extends State<_DefaultTabControllerListener> {
late final void Function()? _listener;
TabController? _tabController;
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
final tabController = DefaultTabController.of(context)!;
_listener = () {
final onTabSelected = widget.onTabSelected;
if (onTabSelected != null) {
onTabSelected(tabController.index);
}
};
tabController.addListener(_listener!);
});
}
#override
void didChangeDependencies() {
_tabController = DefaultTabController.of(context);
super.didChangeDependencies();
}
#override
void dispose() {
if (_listener != null && _tabController != null) {
_tabController!.removeListener(_listener!);
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
And wrap TabBar with this widget
DefaultTabController(
child: _DefaultTabControllerListener(
onTabSelected: (index) {
// Handler
},
child: TabBar(.....
We can listen to tab scroll notification by using NotificationListener Widget
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: NotificationListener(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollUpdateNotification) {
_onStartScroll(scrollNotification.metrics);
}
},
child: _buildTabBarAndTabBarViews(),
);
}
_onStartScroll(ScrollMetrics metrics) {
print('hello world');
}
}
I ran into a similar issue and following is what I did to accomplish the same: -
import 'package:flutter/material.dart';
import '../widgets/basic_dialog.dart';
import 'sign_in_form.dart';
import 'sign_up_form.dart';
class LoginDialog extends StatefulWidget {
#override
_LoginDialogState createState() => _LoginDialogState();
}
class _LoginDialogState extends State<LoginDialog>
with SingleTickerProviderStateMixin {
int activeTab = 0;
TabController controller;
#override
void initState() {
super.initState();
controller = TabController(vsync: this, length: 2);
}
#override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
setState(() {
if (notification.metrics.pixels <= 100) {
controller.index = 0;
} else {
controller.index = 1;
}
});
return true;
},
child: BasicDialog(
child: Container(
height: controller.index == 0
? MediaQuery.of(context).size.height / 2.7
: MediaQuery.of(context).size.height / 1.8,
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TabBar(
controller: controller,
tabs: <Widget>[
Padding(
padding: const EdgeInsets.all(5.0),
child: Text('Sign In'),
),
Padding(
padding: const EdgeInsets.all(5.0),
child: Text('Sign up'),
),
],
),
Expanded(
child: TabBarView(
controller: controller,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: SignInForm(),
),
),
// If a container is not displayed during the tab switch to tab0, renderflex error is thrown because of the height change.
controller.index == 0
? Container()
: Padding(
padding: const EdgeInsets.all(8.0),
child: SingleChildScrollView(
child: SignUpForm(),
),
),
],
),
)
],
),
),
),
);
}
}
If you are using DefaultTabController and want to listen to updates in TabBar, you can expose the controller using the DefaultTabController.of method and then add a listener to it:
DefaultTabController(
length: 3,
child: Builder(
builder: (BuildContext context) {
final TabController controller = DefaultTabController.of(context)!;
controller.addListener(() {
if (!controller.indexIsChanging) {
print(controller.index);
// add code to be executed on TabBar change
}
});
return Scaffold(...
Here you have a full example:
class TabControllerDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 3,
child: Builder(builder: (BuildContext context) {
final TabController controller = DefaultTabController.of(context)!;
controller.addListener(() {
if (!controller.indexIsChanging) {
print(controller.index);
// add code to be executed on TabBar change
}
});
return Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(text: "Tab 0"),
Tab(text: "Tab 1"),
Tab(text: "Tab 2"),
],
),
title: const Text('Tabs Demo'),
),
body: const TabBarView(
children: [
Center(child: Text('View 0')),
Center(child: Text('View 1')),
Center(child: Text('View 2')),
],
),
);
})),
);
}
}
You can also check this DartPad LiveDemo.
Warp your tab in BottomNavigationBar . it will give you option onTap() where you can check which tab will clicked.
using this code you will also redirect to particular page when you tap on Tab
import 'package:flutter/material.dart';
class BottomBarList extends StatefulWidget {
#override
_BottomBarListState createState() => _BottomBarListState();
}
class _BottomBarListState extends State<BottomBarList> {
int bottomSelectedIndex = 0;
int _selectedIndex = 0;
List<Widget> _widgetOptions = <Widget>[
AllMovieList(),
MovieDescription(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(),
bottomNavigationBar: bottomBar(),
body:_widgetOptions.elementAt(_selectedIndex),
);
}
bottomBar() {
return BottomNavigationBar(
type: BottomNavigationBarType.shifting,
unselectedItemColor: Colors.grey,
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.tv),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.star),
title: Text('Business'),
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.black,
backgroundColor: Colors.orange,
onTap: _onItemTapped,
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
}
You can use the only scrollNotification (ScrollEndNotification) of the NotificationListener. It covers the tap and swipe actions.
class HandlingTabChanges extends State<JsonTestDetailFrame> with SingleTickerProviderStateMixin {
late final TabController _tabController;
final int _tabLength = 2;
#override
void initState() {
super.initState();
_tabController = TabController(length: _tabLength, vsync: this);
}
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabLength,
child: Scaffold(
appBar: AppBar(
title: Text("Some title"),
bottom: TabBar(
controller: _tabController,
tabs: [
Icon(Icons.settings),
Icon(Icons.list_alt),
],
),
),
body: NotificationListener(
onNotification: (scrollNotification) {
if (scrollNotification is ScrollEndNotification) _onTabChanged();
return false;
},
child: TabBarView(
controller: _tabController,
children: [
SomeWidget1(),
SomeWidget2(),
],
),
),
),
);
}
void _onTabChanged() {
switch (_tabController.index) {
case 0:
// handle 0 position
break;
case 1:
// handle 1 position
break;
}
}
}
You can disable swiping effect on TabBarView by adding:
physics: NeverScrollableScrollPhysics(),
and declaring one TabController and assigning that to your TabBar and TabBarView:
TabController _tabController;

Constraining Draggable area

I'm attempting to create a draggable slider-like widget (like a confirm slider). My question is if there is a way to constrain the draggable area?
import 'package:flutter/material.dart';
import 'confirmation_slider.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
body: new ListView(
children: <Widget>[
new Container(
margin: EdgeInsets.only(
top: 50.0
),
),
new Container(
margin: EdgeInsets.only(
left: 50.0,
right: 50.0
),
child: new Draggable(
axis: Axis.horizontal,
child: new FlutterLogo(size: 50.0),
feedback: new FlutterLogo(size: 50.0),
),
height: 50.0,
color: Colors.green
),
],
),
),
);
}
}
I imagined that the container class would constrain the draggable area, but it doesn't appear to do that.
No. That's not the goal of Draggable widget. Instead, use a GestureDetector to detect drag. Then combine it with something like Align to move your content around
Here's a fully working slider based on your current code.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Slider(),
),
),
);
}
}
class Slider extends StatefulWidget {
final ValueChanged<double> valueChanged;
Slider({this.valueChanged});
#override
SliderState createState() {
return new SliderState();
}
}
class SliderState extends State<Slider> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
#override
void initState() {
valueListener.addListener(notifyParent);
super.initState();
}
void notifyParent() {
if (widget.valueChanged != null) {
widget.valueChanged(valueListener.value);
}
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
height: 50.0,
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Builder(
builder: (context) {
final handle = GestureDetector(
onHorizontalDragUpdate: (details) {
valueListener.value = (valueListener.value +
details.delta.dx / context.size.width)
.clamp(.0, 1.0);
},
child: FlutterLogo(size: 50.0),
);
return AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return Align(
alignment: Alignment(valueListener.value * 2 - 1, .5),
child: child,
);
},
child: handle,
);
},
),
);
}
}
As at 2022 here's a replica of #Remi's answer above, with minor tweaks to handle revisions to flutter/dart since 2018 (e.g. handling null-safety)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Slider(),
),
),
);
}
}
class Slider extends StatefulWidget {
final ValueChanged<double>? valueChanged;
const Slider({this.valueChanged});
#override
SliderState createState() {
return SliderState();
}
}
class SliderState extends State<Slider> {
ValueNotifier<double> valueListener = ValueNotifier(.0);
#override
void initState() {
valueListener.addListener(notifyParent);
super.initState();
}
void notifyParent() {
if (widget.valueChanged != null) {
widget.valueChanged!(valueListener.value);
}
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
height: 50.0,
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Builder(
builder: (context) {
final handle = GestureDetector(
onHorizontalDragUpdate: (details) {
valueListener.value = (valueListener.value + details.delta.dx / context.size!.width).clamp(.0, 1.0);
},
child: const FlutterLogo(size: 50.0),
);
return AnimatedBuilder(
animation: valueListener,
builder: (context, child) {
return Align(
alignment: Alignment(valueListener.value * 2 - 1, .5),
child: child,
);
},
child: handle,
);
},
),
);
}
}

How can I expose a StatefulWidget's method?

Say I want to build a StatefulWidget named MySlideWidget that provides a public instance method: animate().
When I press a button on parent of MySlideWidget, then I can call MySlideWidget's animate() method to trigger an internal SlideTransition of MySlideWidget.
The usage would look like this:
class MySlideWidgetDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
MySlideWidget mySlideWidget = new MySlideWidget();
return new Scaffold(
body: mySlideWidget,
floatingActionButton: new FloatingActionButton(
onPressed: () {
mySlideWidget.animate();
},
tooltip: 'Start',
child: new Icon(Icons.add),
));
}
}
What I wondering is how to encapsulate the implementations of AnimationController and _controller.forward() inside MySlideWidget, so user of MySlideWidget can simply call animate().
Is this possible? Or what is the idea way to do encapsulation in Flutter?
You should use AnimationController and pass it to SlideTransition. Then call controller.forward() when needed.
Here is sample:
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _controller;
Animation<FractionalOffset> _slideTransitionPosition;
#override
initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: const Duration(milliseconds: 2000),
);
_slideTransitionPosition = new FractionalOffsetTween(
begin: const FractionalOffset(0.0, -1.0),
end: const FractionalOffset(0.0, 10.0),
).animate(new CurvedAnimation(
parent: _controller,
curve: Curves.fastOutSlowIn,
));
}
void _onPress() {
_controller.forward();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(
child: new SlideTransition(
position: _slideTransitionPosition,
child: new Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _onPress,
tooltip: 'Start',
child: new Icon(Icons.add),
),
);
}
}
Also you can find examples in demos

Flutter - Effect of flip card

I'm trying to make a flip card, what would be the best way to get the effect
I would use an AnimatedBuilder or AnimatedWidget to animate the values of a Transform widget. ScaleTransition almost does this for you, but it scales both directions, and you only want one.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePageState createState() => new MyHomePageState();
}
class MyCustomCard extends StatelessWidget {
MyCustomCard({ this.colors });
final MaterialColor colors;
Widget build(BuildContext context) {
return new Container(
alignment: FractionalOffset.center,
height: 144.0,
width: 360.0,
decoration: new BoxDecoration(
color: colors.shade50,
border: new Border.all(color: new Color(0xFF9E9E9E)),
),
child: new FlutterLogo(size: 100.0, colors: colors),
);
}
}
class MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
AnimationController _controller;
Animation<double> _frontScale;
Animation<double> _backScale;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_frontScale = new Tween(
begin: 1.0,
end: 0.0,
).animate(new CurvedAnimation(
parent: _controller,
curve: new Interval(0.0, 0.5, curve: Curves.easeIn),
));
_backScale = new CurvedAnimation(
parent: _controller,
curve: new Interval(0.5, 1.0, curve: Curves.easeOut),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
return new Scaffold(
appBar: new AppBar(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.flip_to_back),
onPressed: () {
setState(() {
if (_controller.isCompleted || _controller.velocity > 0)
_controller.reverse();
else
_controller.forward();
});
},
),
body: new Center(
child: new Stack(
children: <Widget>[
new AnimatedBuilder(
child: new MyCustomCard(colors: Colors.orange),
animation: _backScale,
builder: (BuildContext context, Widget child) {
final Matrix4 transform = new Matrix4.identity()
..scale(1.0, _backScale.value, 1.0);
return new Transform(
transform: transform,
alignment: FractionalOffset.center,
child: child,
);
},
),
new AnimatedBuilder(
child: new MyCustomCard(colors: Colors.blue),
animation: _frontScale,
builder: (BuildContext context, Widget child) {
final Matrix4 transform = new Matrix4.identity()
..scale(1.0, _frontScale.value, 1.0);
return new Transform(
transform: transform,
alignment: FractionalOffset.center,
child: child,
);
},
),
],
),
),
);
}
}
I used simple approach, rotated it on X axis. Here is the full code.
void main() => runApp(MaterialApp(home: HomePage()));
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _controller;
bool _flag = true;
Color _color = Colors.blue;
#override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: Duration(seconds: 1), value: 1);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.crop_rotate),
onPressed: () async {
if (_flag) {
await _controller.reverse();
setState(() {
_color = Colors.orange;
});
await _controller.forward();
} else {
await _controller.reverse();
setState(() {
_color = Colors.blue;
});
await _controller.forward();
}
_flag = !_flag;
},
),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform(
transform: Matrix4.rotationX((1 - _controller.value) * math.pi / 2),
alignment: Alignment.center,
child: Container(
height: 100,
margin: EdgeInsets.symmetric(horizontal: 20),
padding: EdgeInsets.symmetric(vertical: 12),
alignment: Alignment.center,
decoration: BoxDecoration(color: _color.withOpacity(0.2), border: Border.all(color: Colors.grey)),
child: FlutterLogo(colors: _color, size: double.maxFinite),
),
);
},
),
),
);
}
}
You can use the flip_card Flutter package. It lets you define a front and back widget and can be flipped horizontally or vertically.

Resources