I'm working a little bit with Flutter and I understand everything except Animations (i never liked working with animations).
I've tried to implement a Backdrop in my Flutter app using this Flutter Demo. Implementing the Backdrop is easy.
I stuck on implementing the navigation of the Backdrop which let it slide down and up by the hamburger button.
I have read the Animations in Flutter Tutorial. I understood the basics of animations (controller, animation etc.). But in this Backdrop example, it is a little bit different.
Can someone explain to me this case step by step? Thanks.
I managed to reach a solution combining the information of the following links. Please note that I’m not an expert in Flutter (neither animations). That being said, suggestions and corrections are really appreciated.
Backdrop implementation:
https://github.com/material-components/material-components-flutter-codelabs/blob/104-complete/mdc_100_series/lib/backdrop.dart
Animation:
https://medium.com/#vigneshprakash15/flutter-image-rotate-animation-6b6eaed7fb33
After creating your backdrop.dart file, go to the _BackdropTitle class and edit the part where you define the IconButton for the menu and close icons. You have to perform a rotation in the Opacity item that contains the Icon:
icon: Stack(children: <Widget>[
new Opacity(
opacity: new CurvedAnimation(
parent: new ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: new AnimatedBuilder(
animation: animationController,
child: new Container(
child: new Icon(Icons.close),
),
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value * -6.3,
child: _widget,
);
},
),
),
new Opacity(
opacity: new CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: new AnimatedBuilder(
animation: animationController,
child: new Container(
child: new Icon(Icons.menu),
),
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value * 6.3,
child: _widget,
);
},
),
),
],
I had to use a negative value in the angle of the close transformation in order to rotate it in the same direction of the menu animation.
And here is the full code:
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'menu.dart';
const double _kFlingVelocity = 2.0;
class Backdrop extends StatefulWidget {
final MenuCategory currentCategory;
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
const Backdrop({
#required this.currentCategory,
#required this.frontLayer,
#required this.backLayer,
#required this.frontTitle,
#required this.backTitle,
}) : assert(currentCategory != null),
assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null);
#override
_BackdropState createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
#override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 300),
value: 1.0,
vsync: this,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void didUpdateWidget(Backdrop old) {
super.didUpdateWidget(old);
if (widget.currentCategory != old.currentCategory) {
_toggleBackdropLayerVisibility();
} else if (!_frontLayerVisible) {
_controller.fling(velocity: _kFlingVelocity);
}
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48.0;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
Animation<RelativeRect> layerAnimation = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0.0, layerTop, 0.0, layerTop - layerSize.height),
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller.view);
return Stack(
key: _backdropKey,
children: <Widget>[
widget.backLayer,
PositionedTransition(
rect: layerAnimation,
child: _FrontLayer(
onTap: _toggleBackdropLayerVisibility,
child: widget.frontLayer,
),
),
],
);
}
#override
Widget build(BuildContext context) {
var appBar = AppBar(
brightness: Brightness.light,
elevation: 0.0,
titleSpacing: 0.0,
title: _BackdropTitle(
animationController: _controller,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
// TODO
},
)
],
);
return Scaffold(
appBar: appBar,
body: LayoutBuilder(builder: _buildStack)
);
}
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
_controller.fling(
velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
}
}
class _FrontLayer extends StatelessWidget {
const _FrontLayer({
Key key,
this.onTap,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final Widget child;
#override
Widget build(BuildContext context) {
return Material(
elevation: 16.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0)
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 40.0,
alignment: AlignmentDirectional.centerStart,
),
),
Expanded(
child: child,
),
],
),
);
}
}
class _BackdropTitle extends AnimatedWidget {
final AnimationController animationController;
final Function onPress;
final Widget frontTitle;
final Widget backTitle;
_BackdropTitle({
Key key,
this.onPress,
#required this.frontTitle,
#required this.backTitle,
#required this.animationController,
}) : assert(frontTitle != null),
assert(backTitle != null),
assert(animationController != null),
super(key: key, listenable: animationController.view);
#override
Widget build(BuildContext context) {
final Animation<double> animation = this.listenable;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: <Widget>[
// branded icon
SizedBox(
width: 72.0,
child: IconButton(
padding: EdgeInsets.only(right: 8.0),
onPressed: this.onPress,
icon: Stack(children: <Widget>[
new Opacity(
opacity: new CurvedAnimation(
parent: new ReverseAnimation(animation),
curve: const Interval(0.5, 1.0),
).value,
child: new AnimatedBuilder(
animation: animationController,
child: new Container(
child: new Icon(Icons.close),
),
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value * -6.3,
child: _widget,
);
},
),
),
new Opacity(
opacity: new CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
).value,
child: new AnimatedBuilder(
animation: animationController,
child: new Container(
child: new Icon(Icons.menu),
),
builder: (BuildContext context, Widget _widget) {
return new Transform.rotate(
angle: animationController.value * 6.3,
child: _widget,
);
},
),
),
]),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Stack(
children: <Widget>[
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(0.5, 0.0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: Interval(0.5, 1.0),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset(-0.25, 0.0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
)
]),
);
}
}
You can find a sample demo for backdrop in flutter gallery..which you will get while installing Flutter.
Related
I'm trying to use the Expandable Floating Action Button available at the link: https://docs.flutter.dev/cookbook/effects/expandable-fab but the internal buttons don't work, I press them and nothing happens. I'm using this widget inside a Stack. Below the Expandable Floating Action Button code is where it is used;
#immutable
class ExpandableFab extends StatefulWidget {
final bool? initialOpen;
final double distance;
final List<Widget> children;
final VoidCallback onTap;
const ExpandableFab({
super.key,
this.initialOpen,
required this.distance,
required this.children,
required this.onTap,
});
#override
_ExpandableFabState createState() => _ExpandableFabState();
}
class _ExpandableFabState extends State<ExpandableFab>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _expandAnimation;
bool _open = false;
#override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
#override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
..._buildExpandingActionButtons(),
_buildTapToOpenFab()
],
),
);
}
Widget _buildTapToCloseFab() {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Icon(Icons.close, color: Colors.deepPurple),
),
),
),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
final step = 90.0 / (count - 1);
for (var i = 0, angleInDegrees = 0.0;
i < count;
i++, angleInDegrees += step) {
children.add(
_ExpandingActionButton(
directionInDegrees: angleInDegrees,
maxDistance: widget.distance,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: ClipOval(
child: Material(
color: Colors.deepPurple, // Button color
child: InkWell(
//splashColor: Colors.white, // Splash color
onLongPress: _toggle,
onTap: widget.onTap,
onDoubleTap: () {},
child: const SizedBox(
width: 56,
height: 56,
child: Icon(Icons.add, color: Colors.white),
),
),
),
),
),
),
);
}
}
#immutable
class _ExpandingActionButton extends StatelessWidget {
const _ExpandingActionButton({
Key? key,
required this.directionInDegrees,
required this.maxDistance,
required this.progress,
required this.child,
}) : super(key: key);
final double directionInDegrees;
final double maxDistance;
final Animation<double> progress;
final Widget child;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress,
builder: (context, child) {
final offset = Offset.fromDirection(
directionInDegrees * (math.pi / 180.0),
progress.value * maxDistance,
);
return Positioned(
right: 4.0 + offset.dx,
bottom: 4.0 + offset.dy,
child: Transform.rotate(
angle: (1.0 - progress.value) * math.pi / 2,
child: child!,
),
);
},
child: FadeTransition(opacity: progress, child: child),
);
}
}
#immutable
class ActionButton extends StatelessWidget {
const ActionButton({
super.key,
this.onPressed,
required this.icon,
});
final VoidCallback? onPressed;
final Widget icon;
#override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
color: Colors.deepPurple,
elevation: 4.0,
child: IconTheme.merge(
data: theme.primaryIconTheme,
child: IconButton(onPressed: onPressed, icon: icon),
),
);
}
}
Expandable FLoating Action Button is used within this Stack:
Stack(
children: <Widget>[
// some widget
Positioned(
right: 30.0,
bottom: 140.0,
child: SizedBox(
width: 60.0,
height: 60.0,
child: ExpandableFab(
distance: 112.0,
children: <Widget>[
ActionButton(
icon: const Icon(Icons.delete),
onPressed: () {
debugPrint("test1");
},
),
ActionButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
debugPrint("test2");
},
),
ActionButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
debugPrint("test3");
},
)
],
onTap: () {
debugPrint("test4");
},
),
),
),
],
),
If you do want to use it in the stack, place the code for the ExpandedButton in the last position so that it gets reported with touch events.
Code sample:
Stack(
children: <Widget>[
// All your other widgets go up here
RandomWidget(),
RandomWidget2(),
........... //other widgets
// This comes last.
Positioned(
right: 30.0,
bottom: 140.0,
child: SizedBox(
width: 60.0,
height: 60.0,
child: ExpandableFab(
distance: 112.0,
children: <Widget>[
ActionButton(
icon: const Icon(Icons.delete),
onPressed: () {
debugPrint("test1");
},
),
ActionButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
debugPrint("test2");
},
),
ActionButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () {
debugPrint("test3");
},
)
],
onTap: () {
debugPrint("test4");
},
),
),
),
],
),
By the way, Why are you using this in a stack? You can pretty much use it as a FloatingActionButton by passing it to floatingActionButton parameter of the Scaffold.
i have setup banner ads in flutter and those are overlapping the bottom navigation bar
I want to display ads below that bottom navigation bar,
is there any way that i can add a margin below the bottom navigation bar ?
i have implemented ads in home.dart (mainpage)
import 'package:provider/provider.dart';
import '../../ui/widgets/bottom_nav_bar.dart';
import '../../core/utils/theme.dart';
import 'search_page.dart';
import 'category.dart';
import 'main_page.dart';
import 'settings.dart';
import 'package:flutter/material.dart';
import 'package:firebase_admob/firebase_admob.dart';
import 'for_you.dart';
const String AD_MOB_APP_ID = 'ca-app-pub-3940256099942544~3347511713';
const String AD_MOB_TEST_DEVICE = 'DEC3010B2445165B43EB949F5D97D0F8 - run ad then check device logs for value';
const String AD_MOB_AD_ID = 'ca-app-pub-3940256099942544/6300978111';
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
BannerAd _bannerAd;
static final MobileAdTargetingInfo targetingInfo = new MobileAdTargetingInfo(
testDevices: <String>[AD_MOB_TEST_DEVICE],
);
BannerAd createBannerAd() {
return new BannerAd(
adUnitId: AD_MOB_AD_ID,
size: AdSize.banner,
targetingInfo: targetingInfo,
);
}
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedIndex = 0;
final PageController _pageController = PageController();
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
_pageController.dispose();
}
#override
Widget build(BuildContext context) {
final stateData = Provider.of<ThemeNotifier>(context);
final ThemeData state = stateData.getTheme();
FirebaseAdMob.instance.initialize(appId: AD_MOB_APP_ID);
_bannerAd = createBannerAd()..load()..show();
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
centerTitle: false,
backgroundColor: state.primaryColor,
elevation: 0,
title: Text(
'RevWalls',
style: state.textTheme.headline,
),
actions: <Widget>[
IconButton(
icon: Icon(
Icons.search,
color: state.textTheme.body1.color,
),
onPressed: () => showSearch(
context: context, delegate: WallpaperSearch(themeData: state)),
)
],
),
body: Container(
color: state.primaryColor,
child: PageView(
controller: _pageController,
physics: BouncingScrollPhysics(),
onPageChanged: (index) {
setState(() {
_selectedIndex = index;
});
},
children: <Widget>[
MainBody(),
Category(),
ForYou(),
SettingsPage(),
],
),
),
bottomNavigationBar: BottomNavyBar(
selectedIndex: _selectedIndex,
unselectedColor: state.textTheme.body1.color,
onItemSelected: (index) {
_pageController.jumpToPage(index);
},
selectedColor: state.accentColor,
backgroundColor: state.primaryColor,
showElevation: false,
items: [
BottomNavyBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavyBarItem(
icon: Icon(Icons.category),
title: Text('Subreddits'),
),
BottomNavyBarItem(
icon: Icon(Icons.phone_android),
title: Text('Exact Fit'),
),
BottomNavyBarItem(
icon: Icon(Icons.settings),
title: Text('Settings'),
),
],
),
);
}
Widget oldBody(ThemeData state) {
return NestedScrollView(
headerSliverBuilder: (BuildContext context, bool boxIsScrolled) {
return <Widget>[
SliverAppBar(
backgroundColor: state.primaryColor,
elevation: 4,
title: Text(
'reWalls',
style: state.textTheme.headline,
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search, color: state.accentColor),
onPressed: () {
showSearch(
context: context,
delegate: WallpaperSearch(themeData: state));
},
)
],
floating: true,
pinned: _selectedIndex == 0 ? false : true,
snap: false,
centerTitle: false,
),
];
},
body: Container(
color: state.primaryColor,
child: PageView(
controller: _pageController,
onPageChanged: (index) {
setState(() {
_selectedIndex = index;
});
},
children: <Widget>[
MainBody(),
Category(),
ForYou(),
SettingsPage(),
],
),
),
);
}
}
and here is the bottom navigation bar -
library bottom_navy_bar;
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class BottomNavyBar extends StatelessWidget {
final int selectedIndex;
final double iconSize;
final Color backgroundColor, selectedColor, unselectedColor;
final bool showElevation;
final Duration animationDuration;
final List<BottomNavyBarItem> items;
final ValueChanged<int> onItemSelected;
BottomNavyBar(
{Key key,
this.selectedIndex = 0,
this.showElevation = true,
this.iconSize = 20,
this.backgroundColor,
this.selectedColor,
this.unselectedColor,
this.animationDuration = const Duration(milliseconds: 250),
#required this.items,
#required this.onItemSelected}) {
assert(items != null);
assert(items.length >= 2 && items.length <= 5);
assert(onItemSelected != null);
}
Widget _buildItem(BottomNavyBarItem item, bool isSelected) {
return AnimatedContainer(
width: isSelected ? 120 : 50,
height: double.maxFinite,
duration: animationDuration,
decoration: BoxDecoration(
color: isSelected ? selectedColor.withOpacity(0.2) : backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(100.0)),
),
alignment: Alignment.center,
child: ListView(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
scrollDirection: Axis.horizontal,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 8),
child: IconTheme(
data: IconThemeData(
size: iconSize,
color: isSelected ? selectedColor : unselectedColor),
child: item.icon,
),
),
isSelected
? DefaultTextStyle.merge(
style: TextStyle(
fontSize: 16,
color: selectedColor,
fontWeight: FontWeight.bold),
child: item.title,
)
: SizedBox.shrink()
],
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Container(
color: backgroundColor,
child: Container(
width: double.infinity,
height: 55,
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items.map((item) {
var index = items.indexOf(item);
return GestureDetector(
onTap: () {
onItemSelected(index);
},
child: _buildItem(item, selectedIndex == index),
);
}).toList(),
),
),
);
}
}
class BottomNavyBarItem {
final Icon icon;
final Text title;
BottomNavyBarItem({
#required this.icon,
#required this.title,
}) {
assert(icon != null);
assert(title != null);
}
}
Please help
adding builder like this will solve the problem
var paddingBottom = 60.0;
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
//
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'World General info',
//theme: ThemeData(primarySwatch: Colors.cyan,),
theme: ThemeData(
primaryColor: Colors.cyan,
accentColor: Colors.green,
textTheme: TextTheme(bodyText2: TextStyle(color: Colors.purple)),
),
home: MyHomePage(title: 'World General Info'),
builder: (BuildContext context, Widget child) {
return new Padding(
child: child,
padding: EdgeInsets.only(bottom: paddingBottom),
);
}
);
}
}
You can use the MediaQuery.of(context) for that.
Wrap the whole Code inside a Container of height: MediaQuery.of(context).size.height - 60 . (the height of ad)
Column(
children: [
Container(
height: MediaQuery.of(context).size.height - 60,
child: HomePage(),
),
BannerAd(),
]
);
Found answer myself
We can use this to set margin in a container with other things like height, width
This code will add margin to bottom of bottom nav bar, as per my need i want to show ads below navbar so this solves my problem
margin: const EdgeInsets.only(bottom: 50)
Add this to your scaffold
bottomNavigationBar: Container(
height: 50.0,
color: Colors.white,
),
Think Simplest bro ... wrap your column ( mainAxisSize : MainAxisSize.min )
Scaffold(
appBar: _appBar,
body: _body,
bottomNavigationBar: Column(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.amber,
height: 70,
child: Center(child: Text('Banner'))),
BottomNavigationBar(
items: const [
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add'),
BottomNavigationBarItem(icon: Icon(Icons.add), label: 'Add')
],
)
]))
I am learning flutter and I have come across this UI on dribble which I am trying to replicate for practice.
https://gph.is/g/4oLJ0k5
As you can see in the above gif, there is a card widget which expands to a new screen when you swipe up. And you can pop the screen by swiping down (or clicking on the back button). How do I implement this? I want it to look and feel exactly like shown in the gif.
I am pretty new to flutter so if you could provide a little more detail to your explanations it would be amazing.
Use container transform in animations.
https://pub.dev/packages/animations
hey I tried 2 animation but couldn't made exactly same.
I just used hero widget & ScaleTranstion widget
hope you like it.
see_video
CODE:
class AnimationDemo extends StatefulWidget {
#override
_AnimationDemoState createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> {
int changeIndex=0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animation Demo"),
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
stops: [
0.1,
0.4,
],
colors: [
Colors.blue, // changeIndex.isEven? Colors.yellow:Colors.blue,
Colors.indigo,//changeIndex.isEven ?Colors.red:Colors.indigo,
])),
// color: Colors.white,
alignment: Alignment.center,
padding: const EdgeInsets.all(10),
child: new Center(
child: new Container(
alignment: Alignment.center,
height: 300.0,
child: new ListView(
scrollDirection: Axis.horizontal,
children: new List.generate(10, (int index) {
setState(() {
changeIndex = index;
});
print("index:$index");
return new InkWell(
child: Hero(
tag: index,
child: Card(
color: Colors.white,
child: new Container(
alignment: Alignment.center,
width: 300.0,
height: 300.0,
child: new Text("$index"),
),
),
),
onTap: (){
Navigator.of(context)
.push(MaterialPageRoute<Null>(builder: (BuildContext context) {
return new SecondPage(index: index,);
}));
// Navigator.of(context).push(new SecondAnimationPage(index)); ANOTHER ANIMATION WITH SCALE TRANSITION WIDGET.
},
);
}),
),
),
),
)
);
}
}
class SecondPage extends StatelessWidget {
final int index ;
const SecondPage({Key key, this.index}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Hero Animations Example")),
body: Hero(
tag: index,
child: Container(
alignment: Alignment.center,
child: Card(
elevation: 5,
child: Text(index.toString()),
),
),
)
);
}
}
SECOND TRIED:
class SecondAnimationPage extends CupertinoPageRoute {
final int index;
SecondAnimationPage(this.index) : super(builder: (BuildContext context) => new ViewPage(index: index,));
#override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return new
ScaleTransition(
scale: animation,
child: new ViewPage(index: index,)
);
}
}
class ViewPage extends StatefulWidget {
final int index;
const ViewPage({Key key, this.index}) : super(key: key);
#override
_ViewPageState createState() => new _ViewPageState();
}
class _ViewPageState extends State<ViewPage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Transition Animation'),
),
body: new Center(
child: new Text(widget.index.toString()),
),
);
}
}
I have a flashcard app using flutter in android studio, I am very new at android studio and flutter. So every time the user flips, it shows the back side of the card. If the card is showing the back side while the user is pressing the next button, the card automatically shows the back side of the proceeding cards. What i wanted to do is only show the front side each time the user presses the next button.
I have 3 classes, a flip_card.dart [my animations], Flash.dart [buttons, widgets, front/back image lists, etc.], main.dart [main, only calls the class].
I do not know what to do, either call the animation when it is played to a function? or to call a local "bool isFront" inside the animation when it is played.
this is my whole flip_dart.dart
library flip_card;
import 'dart:math';
import 'package:flutter/material.dart';
import 'Flash.dart';
enum FlipDirection {
VERTICAL,
HORIZONTAL,
}
class AnimationCard extends StatelessWidget {
AnimationCard({this.child, this.animation, this.direction});
final Widget child;
final Animation<double> animation;
final FlipDirection direction;
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
var transform = Matrix4.identity();
transform.setEntry(3, 2, 0.001);
if (direction == FlipDirection.VERTICAL) {
transform.rotateX(animation.value);
} else {
transform.rotateY(animation.value);
}
return Transform(
transform: transform,
alignment: Alignment.center,
child: child,
);
},
child: child,
);
}
}
class FlipCard extends StatefulWidget {
final Widget front;
final Widget back;
final int speed = 500;
final FlipDirection direction;
const FlipCard(
{Key key,
#required this.front,
#required this.back,
this.direction = FlipDirection.HORIZONTAL})
: super(key: key);
#override
State<StatefulWidget> createState() {
return _FlipCardState();
}
}
class _FlipCardState extends State<FlipCard>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> _frontRotation;
Animation<double> _backRotation;
bool isFront = true;
#override
void initState() {
super.initState();
controller = AnimationController(
duration: Duration(milliseconds: widget.speed), vsync: this);
_frontRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween(begin: 0.0, end: pi / 2)
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
],
).animate(controller);
_backRotation = TweenSequence(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: ConstantTween<double>(pi / 2),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween(begin: -pi / 2, end: 0.0)
.chain(CurveTween(curve: Curves.linear)),
weight: 50.0,
),
],
).animate(controller);
}
_toggleCard() {
if (isFront) {
controller.forward();
// if(_nextImage()){
//
// }
} else {
controller.reverse();
}
isFront = !isFront;
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _toggleCard,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimationCard(
animation: _frontRotation,
child: widget.front,
direction: widget.direction,
),
AnimationCard(
animation: _backRotation,
child: widget.back,
direction: widget.direction,
),
],
),
);
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
}
And this is my Flash.dart
import 'flip_card.dart';
import 'package:flutter/material.dart';
class Flashcard extends StatefulWidget {
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<Flashcard> {
int photoIndex = 0; //startfirstpage
bool isFront = true;
var playerProgress = 0;
List<String> photos = [ //front
'assets/c1v1f.png',
'assets/c1v2f.png',
'assets/c1v3f.png',
'assets/c1v4f.png',
'assets/c1v5f.png',
'assets/c1v6f.png',
'assets/c1v7f.png',
'assets/c1v8f.png',
'assets/c1v9f.png',
'assets/c1v10f.png',
];
List<String> photos1 = [ //back
'assets/c1v1b.png',
'assets/c1v2b.png',
'assets/c1v3b.png',
'assets/c1v4b.png',
'assets/c1v5b.png',
'assets/c1v6b.png',
'assets/c1v7b.png',
'assets/c1v8b.png',
'assets/c1v9b.png',
'assets/c1v10b.png',
];
var bookmarkicon = "assets/bookmarkoff.png";
var bookmarkOff = "assets/bookmarkoff.png";
var bookmarkOn = "assets/bookmarkon.png";
void _previousImage() { // prev
setState(() {
photoIndex = photoIndex > 0 ? photoIndex - 1 : 0;
});
}
void _nextImage() {
// next
setState(() {
photoIndex = photoIndex < photos.length - 1 ? photoIndex + 1 : photoIndex;
playerProgress += 10;
print(playerProgress);
print(photoIndex);
// if(isFront){
// photoIndex--;
// }
Flashcard();
});
}
#override
Widget build(BuildContext context) {
return new Scaffold( //title_page
appBar: new AppBar(
title: new Text('Category 時'),
centerTitle: true,
),
body: Column( //content
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: Stack(
children: <Widget>[
Container(
child: FlipCard(
direction: FlipDirection.HORIZONTAL,
front: Material( //front_side
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
image: DecorationImage(
image: AssetImage(photos[photoIndex]), //images front
fit: BoxFit.cover)),
),
elevation: 20.0,
borderRadius: BorderRadius.all(Radius.circular(40.0)),
),
back: Material( //back_side
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
image: DecorationImage(
image: AssetImage(photos1[photoIndex]), // images back
fit: BoxFit.cover)),
),
elevation: 20.0,
borderRadius: BorderRadius.all(Radius.circular(40.0)),
),
),
height: 400.0,//flashcard
width: 370.0,
),
Positioned(
top: 380.0, //dots
left: 25.0,
right: 25.0,
child: SelectedPhoto(numberOfDots: photos.length,
photoIndex: photoIndex),
)
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('Prev', textScaleFactor: 1.3,),
onPressed: _previousImage,
elevation: 10.0,
color: Colors.redAccent,
textColor: Colors.white,
),
SizedBox(width: 10.0), //space
ButtonTheme( //bookmark
minWidth: 10,
height: 10,
child: RaisedButton(
child: Image.asset(bookmarkicon, scale: 3.5,),
color: Colors.transparent,
onPressed: (){
if(this.bookmarkicon=='assets/bookmarkoff.png'){
print(this.bookmarkicon +" is clicked");
setState(() {
bookmarkicon = bookmarkOn;
});
this.bookmarkicon = 'assets/bookmarkon.png';
}
else if(this.bookmarkicon=='assets/bookmarkon.png'){
print(this.bookmarkicon +" is clicked");
this.bookmarkicon = 'assets/bookmarkoff.png';
setState(() {
bookmarkicon = bookmarkOff;
});
}
//any implementation for bookmark will be here ;)
},
),
),
SizedBox(width: 10.0), //space
RaisedButton(
child: Text('Next', textScaleFactor: 1.3,),
onPressed:_nextImage,
elevation: 10.0,
color: Colors.redAccent,
textColor: Colors.white,
)
],
)
],
));
}
}
class SelectedPhoto extends StatelessWidget {
final int numberOfDots;
final int photoIndex;
SelectedPhoto({this.numberOfDots, this.photoIndex});
Widget _inactivePhoto() {
return new Container(
child: new Padding(
padding: const EdgeInsets.only(left: 3.0, right: 3.0),
child: Container(
height: 8.0,
width: 8.0,
decoration: BoxDecoration(
color: Colors.grey,
borderRadius: BorderRadius.circular(4.0)
),
),
)
);
}
Widget _activePhoto() {
return Container(
child: Padding(
padding: EdgeInsets.only(left: 3.0, right: 3.0),
child: Container(
height: 10.0,
width: 10.0,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5.0),
boxShadow: [
BoxShadow(
color: Colors.grey,
spreadRadius: 0.0,
blurRadius: 2.0
)
]
),
),
),
);
}
List<Widget> _buildDots() {
List<Widget> dots = [];
for(int i = 0; i< numberOfDots; ++i) {
dots.add(
i == photoIndex ? _activePhoto(): _inactivePhoto()
);
}
return dots;
}
#override
Widget build(BuildContext context) {
return new Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _buildDots(),
),
);
}
}
The code works, but when the user pressed next, it will display the next card even though it is the back side showing.
So here's what I added to my code to make sure every time the user decides to see the next card, it shows the front side of the next card, not the back side:
First I defined a global variable:
GlobalKey<FlipCardState> cardKey = GlobalKey<FlipCardState>();
then it's passed to the key property of flip_card package:
FlipCard(
key: cardKey,
Then I use the variable to check the status of card whether the front of card is shown or the back, if the back side is shown then it will be toggled to show the front side of the next card when clicking on the "next card button":
onPressed: () {
if(cardKey.currentState != null) { //null safety
if (!cardKey.currentState!.isFront) {
cardKey.currentState!.toggleCard();
}
}
},
Adding to the onPressed property of the "next card" button.
Never mind, i just made it into one .dart and made my animator controller into global, and accessed it to .reverse() each time the next button is pressed.
Currently I am trying to develop a BottomSheet that expands to a specific size. When that size is reached, the user should be able to drag the BottomSheet a little bit up. I have implmented the GestureDetector inside the BottomSheet, so that I am able to detect a vertical drag. The drag function is called, but unfortunately it is not changing the size of the BottomSheet.
This is my code:
//These values are outside of the classes
double initial;
double _kShoppingMenuHeight;
//My custom BottomSheet with rounded corner
Future<T> showRoundedBottomSheet<T> ({
#required BuildContext context,
#required Widget child,
double height
}) {
assert(context != null);
assert(child != null);
return showModalBottomSheet(
context: context,
builder: (BuildContext context){
return new Container(
height: (height != null
? height
: 400.0
),
color: Color(0xFF737373),
child: new Container(
decoration: new BoxDecoration(
color: Colors.white,
borderRadius: new BorderRadius.only(
topLeft: const Radius.circular(5.0),
topRight: const Radius.circular(5.0)
)
),
child: Builder(
builder: (BuildContext context){
return child;
},
)
),
);
}
);
}
//The function that opens the BottomSheet
// this is in another class
return showRoundedBottomSheet(
context: context,
height: _kShoppingMenuHeight,
//Make bottomsheet draggable and fixed at specific point
child: ShoppingMenu(
title: _title("Ihre Listen"),
items: items
)
);
//The stateful widget with the content
return GestureDetector(
onVerticalDragStart: (DragStartDetails details){
initial = details.globalPosition.dy;
},
onVerticalDragUpdate: (DragUpdateDetails details){
setState(() {
_kShoppingMenuHeight = MediaQuery.of(context).size.height / 2 - details.globalPosition.dy;
if(_kShoppingMenuHeight.isNegative) _kShoppingMenuHeight = _kShoppingMenuHeight * (-1);
});
},
onVerticalDragEnd: (DragEndDetails details){
},
child: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (overscroll){
overscroll.disallowGlow();
},
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: _kShoppingMenuHeight
),
child: ListView(
physics: NeverScrollableScrollPhysics(),
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 30.0, left: 10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(bottom: 10.0),
child: widget.title,
),
Column(
children: widget.items
)
],
),
),
Divider(),
GestureDetector(
child: ListTile(
leading: Icon(Icons.add, color: Colors.black54),
title: Text(
"Neue Liste erstellen"
),
),
onTap: (){
Navigator.pop(context, "neue Liste");
},
),
Divider(),
GestureDetector(
child: ListTile(
leading: Icon(OMIcons.smsFailed, color: Colors.black54),
title: Text(
"Feedback geben"
),
),
onTap: (){
Navigator.pop(context, "feedback");
},
)
],
),
),
),
);
This is a complete example of how you can drag around with your modal bottom sheet.
The idea is to wrap the content of the sheet by a stream builder ,and update the stream when drag occurs. let me know if you need further explanation.
import 'package:flutter/material.dart';
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('My App'),
),
body: MyWidget(),
),
);
}
}
StreamController<double> controller = StreamController.broadcast();
class MyWidget extends StatefulWidget{
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
double position;
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: RaisedButton(
child: Text('Show Buttom Sheet'),
onPressed: () {
showModalBottomSheet(context: context, builder: (context){
return StreamBuilder(
stream: controller.stream,
builder:(context,snapshot) => GestureDetector(
onVerticalDragUpdate: (DragUpdateDetails details){
position = MediaQuery.of(context).size.height- details.globalPosition.dy;
print('position dy = ${position}');
position.isNegative?Navigator.pop(context)
:controller.add(position);
},
behavior: HitTestBehavior.translucent,
child:
Container(
color: Colors.red,
height: snapshot.hasData ? snapshot.data:200.0,
width: double.infinity,
child: Text('Child'),
)),
);
});
}),
),
);
}
}
I think setState() call on the wrong widget.
setState() need to be called on the widget holding the Scaffold because bottom sheet belongs to the scaffold itself .
inherited widget may be the solution