Flutter PositionedTransitioned to below other Stack child - how to get height? - dart

I'm doing a backdrop style slide down similar to the Codelabs flutter example here. I'd like the foreground part to stop at the bottom of the backdrop content.
I know you can't get the height directly until after layout. Since the app starts with the foreground part at the top, I was thinking I could get the height after build and store it in the state, but I'm not exactly sure how to do this.
This is what I have now, just using a predefined 'overhang' that doesn't account for the backdrop content height:
Widget _buildForeground(BuildContext context, BoxConstraints constraints) {
Animation<RelativeRect> rectAnimation = new RelativeRectTween(
begin: new RelativeRect.fromLTRB(0.0, constraints.maxHeight - widget.overhang, 0.0, 0.0),
end: new RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller);
return new PositionedTransition(
rect: rectAnimation,
child: new Material(
shape: RoundedRectangleBorder(borderRadius: new BorderRadius.only(topLeft: new Radius.circular(15.0), topRight: new Radius.circular(15.0))),
elevation: 16.0,
child: widget.foreground,
)
);
}
#override
Widget build(BuildContext context) {
return new LayoutBuilder(
builder: (context, constraints) => new Stack(
children: <Widget>[
new Container(color: Theme.of(context).primaryColor,),
widget.background,
_buildForeground(context, constraints),
],
),
);
}

Thanks to Remi I came up with this solution using GlobalKey
void _toggleForeground() {
setState(() => top = backgroundKey?.currentContext?.size?.height);
_controller.fling(velocity: _isBackgroundVisible ? -widget.toggleVelocity : widget.toggleVelocity);
}
Widget _buildForeground() {
if (top == null) top = 500.0;
Animation<RelativeRect> rectAnimation = new RelativeRectTween(
begin: new RelativeRect.fromLTRB(0.0, top, 0.0, 0.0),
end: new RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
).animate(_controller);
return new PositionedTransition(
rect: rectAnimation,
child: new Material(
shape: RoundedRectangleBorder(borderRadius: new BorderRadius.only(topLeft: new Radius.circular(15.0), topRight: new Radius.circular(15.0))),
elevation: 16.0,
child: widget.foreground,
)
);
}
#override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
new Container(color: Theme.of(context).primaryColor),
new Column(
key: backgroundKey,
mainAxisSize: MainAxisSize.min,
children: <Widget>[widget.background],
),
_buildForeground(),
],
);
}

Related

How to manage form state with BLoC pattern?

I am currently working on a side project to learn about Rx and BLoC pattern.
I would like to manage the form state without using any setState().
I already have a BLoC that manage my 'events' which are stored in a SQLite db and added after validating this form.
Do I need to create a need BLoC specifically for this UI part, and how ? Is it OK to keep a code like that ? Should I change my actual BLoC ?
You can find my current code here :
class _EventsAddEditScreenState extends State<EventsAddEditScreen> {
bool hasDescription = false;
bool hasLocation = false;
bool hasChecklist = false;
DateTime eventDate;
TextEditingController eventNameController = new TextEditingController();
TextEditingController descriptionController = new TextEditingController();
#override
Widget build(BuildContext context) {
final eventBloc = BlocProvider.of<EventsBloc>(context);
return BlocBuilder(
bloc: eventBloc,
builder: (BuildContext context, EventsState state) {
return Scaffold(
body: Stack(
children: <Widget>[
Column(children: <Widget>[
Expanded(
child: ListView(
shrinkWrap: true,
children: <Widget>[
_buildEventImage(context),
hasDescription ? _buildDescriptionSection(context) : _buildAddSection('description'),
_buildAddSection('location'),
_buildAddSection('checklist'),
//_buildDescriptionSection(context),
],
))
]),
new Positioned(
//Place it at the top, and not use the entire screen
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.check), onPressed: () async{
if(this._checkAllField()){
String description = hasDescription ? this.descriptionController.text : null;
await eventBloc.dispatch(AddEvent(Event(this.eventNameController.text, this.eventDate,"balbla", description: description)));
print('Saving ${this.eventDate} ${eventNameController.text}');
}
},)
],
backgroundColor: Colors.transparent, //No more green
elevation: 0.0, //Shadow gone
),
),
],
),
);
},
);
}
Widget _buildAddSection(String sectionName) {
TextStyle textStyle = TextStyle(
color: Colors.black87, fontSize: 18.0, fontWeight: FontWeight.w700);
return Container(
alignment: Alignment.topLeft,
padding:
EdgeInsets.only(top: 20.0, left: 40.0, right: 40.0, bottom: 20.0),
child: FlatButton(
onPressed: () {
switch(sectionName){
case('description'):{
this.setState((){hasDescription = true;});
}
break;
case('checklist'):{
this.setState((){hasChecklist = true;});
}
break;
case('location'):{
this.setState((){hasLocation=true;});
}
break;
default:{
}
break;
}
},
padding: EdgeInsets.only(top: 0.0, left: 0.0),
child: Text(
'+ Add $sectionName',
style: textStyle,
),
),
);
}
Let's solve this step by step.
Your first question:
Do I need to create a need BLoC specifically for this UI part?
Well this relative of your needs and your app. You can have a BLoC for each screen if needed but you can have too a single BLoC for 2 or 3 widgets, there is no rule about it. If you think that in this case is a good approach implement another BLoC for your screen because the code will be more readable, organized and scaleable you can do this or if you think that is better make only one bloc with all inside you're free to this too.
Your second question: and how ?
Well in your code I only see setState calls in _buildAddSection so let's change this writing a new BLoc class and handle state changes with RxDart streams.
class LittleBloc {
// Note that all stream already start with an initial value. In this case, false.
final BehaviorSubject<bool> _descriptionSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasDescription => _descriptionSubject.stream;
final BehaviorSubject<bool> _checklistSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasChecklist => _checklistSubject.stream;
final BehaviorSubject<bool> _locationSubject = BehaviorSubject.seeded(false);
Observable<bool> get hasLocation => _locationSubject.stream;
void changeDescription(final bool status) => _descriptionSubject.sink.add(status);
void changeChecklist(final bool status) => _checklistSubject.sink.add(status);
void changeLocation(final bool status) => _locationSubject.sink.add(status);
dispose(){
_descriptionSubject?.close();
_locationSubject?.close();
_checklistSubject?.close();
}
}
Now I will use this BLoc in your widget. I will put the entire build method code below with the changes. Basically we'll use StreamBuilder to build widgets in widget tree.
final LittleBloc bloc = LittleBloc(); // Our instance of bloc
#override
Widget build(BuildContext context) {
final eventBloc = BlocProvider.of<EventsBloc>(context);
return BlocBuilder(
bloc: eventBloc,
builder: (BuildContext context, EventsState state) {
return Scaffold(
body: Stack(
children: <Widget>[
Column(children: <Widget>[
Expanded(
child: ListView(
shrinkWrap: true,
children: <Widget>[
_buildEventImage(context),
StreamBuilder<bool>(
stream: bloc.hasDescription,
builder: (context, snapshot){
hasDescription = snapshot.data; // if you want hold the value
if (snapshot.data)
return _buildDescriptionSection(context);//we got description true
return buildAddSection('description'); // we have description false
}
),
_buildAddSection('location'),
_buildAddSection('checklist'),
//_buildDescriptionSection(context),
],
),
),
]
),
new Positioned(
//Place it at the top, and not use the entire screen
top: 0.0,
left: 0.0,
right: 0.0,
child: AppBar(
actions: <Widget>[
IconButton(icon: Icon(Icons.check),
onPressed: () async{
if(this._checkAllField()){
String description = hasDescription ? this.descriptionController.text : null;
await eventBloc.dispatch(AddEvent(Event(this.eventNameController.text, this.eventDate,"balbla", description: description)));
print('Saving ${this.eventDate} ${eventNameController.text}');
}
},
),
],
backgroundColor: Colors.transparent, //No more green
elevation: 0.0, //Shadow gone
),
),
],
),
);
},
);
}
And no more setState calls in your _buildAddSection. Just need change a switch statement. The changes...calls will update the streams in BLoc class and this will make a rebuild of the widget that is listening the stream.
switch(sectionName){
case('description'):
bloc.changeDescription(true);
break;
case('checklist'):
bloc.changeChecklist(true);
break;
case('location'):
bloc.changeLocation(true);
break;
default:
// you better do something here!
break;
}
And don't forgot to call bloc.dispose() inside inside WidgetState dispose method.

Animated widget pushes other widgets

I have an animated widget (image) located top of the page and under the widget there should be a text.
I use size transition widget and it pushes all the widgets under itself. I don't want my animation to push any other widgets. Everything should stay where they are. I expect something like this:
layout
Widget _animation() {
Image img= Image.asset(
'assets/images/myImg.jpg',
);
_controller.forward(from: 0);
return SizeTransition(
child: img,
sizeFactor: CurvedAnimation(
curve: Curves.fastOutSlowIn,
parent: _controller,
),
);
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_animation(),
Text(
"My Text",
style: TextStyle(color: Colors.black87, fontSize: 30),
),
],
),
),
);
}
Actually the SizeTransition won't work if you have given it's parent a fixed size. I am showing you how to do it using Stack.
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Stack(
children: <Widget>[
Positioned(left: 0, child: _animation()),
Positioned(top: 100, child: Text("This is your text",)),
],
),
),
);
}
Try using a Stack and Positioned element, so that your element will clip above the elements that you're "moving". Think of it as a position: absolute in html.
Try using SlideTransition instead of SizeTransition because you want to move your image from top to real position.
Widget _animation() {
Image img= Image.asset(
'assets/images/myImg.jpg',
);
_controller.forward(from: 0);
return SlideTransition(
child: img,
position: Tween<Offset>(begin: Offset(0.0, -1.0), end: Offset.zero)
.animate(_controller),
);
}
More info here:
https://docs.flutter.io/flutter/widgets/SlideTransition-class.html

Fading Edge ListView - Flutter

has anyone come across something like fadingEdgeLength in Android for Flutter so that when you scroll up items start fading into the top of the screen?
Below is my interface built up of the Widgets.
If it helps these are the properties I'm referring to:
android:fadingEdgeLength="10dp"
android:requiresFadingEdge="horizontal">
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CMS Users'),
),
body: ListView.builder(
padding: EdgeInsets.only(top: 20.0, left: 4.0),
itemExtent: 70.0,
itemCount: data == null ? 0 : data.length,
itemBuilder: (BuildContext context, int index) {
return Card(
elevation: 10.0,
child: InkWell(
onTap: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new PeopleDetails("Profile Page", profiles[index]),
));
},
child: ListTile(
leading: CircleAvatar(
child: Text(profiles[index].getInitials()),
backgroundColor: Colors.deepPurple,
radius: 30.0,
),
title: Text(
data[index]["firstname"] + "." + data[index]["lastname"]),
subtitle: Text(
data[index]["email"] + "\n" + data[index]["phonenumber"]),
),
),
);
}),
);
}
}
As others have mentioned, you can put the ListView under a ShaderMask, but with minor extra parameterizations you can get much better results - at least if you want to achieve what I wanted.
Optionally you can set the [stops] list for the LinearGradient:
The [stops] list, if specified, must have the same length as [colors]. It specifies fractions of the vector from start to end, between 0.0 and 1.0, for each color.
Plus: There are blend modes, where the color channels of the source are ignored, only the opacity has an effect. BlendMode.dstOut is also such in the example below. As you can see in the screenshot, the purple color is not used concretely, only for the fractions of the vector.
You can play with the different [blendMode] settings, there are quite a few of them.
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: FadingListViewWidget(),
),
),
);
}
class FadingListViewWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Container(
height: 320,
child: ShaderMask(
shaderCallback: (Rect rect) {
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.purple, Colors.transparent, Colors.transparent, Colors.purple],
stops: [0.0, 0.1, 0.9, 1.0], // 10% purple, 80% transparent, 10% purple
).createShader(rect);
},
blendMode: BlendMode.dstOut,
child: ListView.builder(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Card(
color: Colors.orangeAccent,
child: ListTile(
title: Text('test test test test test test'),
),
);
},
),
),
),
);
}
}
You could apply a ShaderMask on top of ListView and use BlendMode to get what you want.
Widget animationsList() {
return Expanded(
child: ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Colors.transparent,Colors.red],
).createShader(bounds);
},
child: Container(height: 200.0, width: 200.0, color: Colors.blue,),
blendMode: BlendMode.dstATop,
),
);
I had similar request so I created a library for this task.
You can find it here: fading_edge_scrollview
To use it you need to add a ScrollController to your ListView and then pass this ListView as child to FadingEdgeScrollView.fromScrollView constructor
Wrap the Listview with Stack, add the Listview as the first child, the second is Positioned Container with LinearGradient.
Sample from my code:
Stack:
return Stack(
children: <Widget>[
ListView(
scrollDirection: Axis.horizontal,
children: _myListOrderByDate,
),
FadeEndListview(),
],
);
The overlay class:
class FadeEndListview extends StatelessWidget {
const FadeEndListview({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Positioned(
right: 0,
width: 8.0,
height: kYoutubeThumbnailsHeight,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerRight,
end: Alignment.centerLeft,
stops: [0.0, 1.0],
colors: [
Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.0),
],
),
),
),
);
}
}
And it will look something like this:
Try to use
Text(
'This is big text, I am using Flutter and trying to fade text',
overflow: TextOverflow.fade,
maxLines: 1,
),

How to center flutter text with other widgets in row

I created a row with a BackButton and a TextWidget.
I want to center the text to the middle of the screen. Actually flutter centeres the text to the containers width, but the containers width isnt the same as the screen width, because there is the backbutton. How do i fix that?
Expanded getTitle() {
return new Expanded(
child: new Text("Einloggen", style: new TextStyle(fontSize: 18.0), textAlign: TextAlign.center)
);
}
BackButton getBackButton() {
return new BackButton(
);
}
Row getHeader() {
return new Row(
children: <Widget>[
getBackButton(),
getTitle()
],
);
}
#override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return new Material(
child: new Container(
padding: new EdgeInsets.fromLTRB(0.0, statusBarHeight, 0.0, 0.0),
child: new Column(
children: <Widget>[
getHeader()
],
),
),
);
}
You can use the Row's mainAxisAlignment parameter to center align children of a row.
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
//children Widgets
]
);
Similarly, mainAxisAligment paramter can also be used to align Column's children. For more information check this out!
You can achieve the same UI using A Scaffold with AppBar
class mytab extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
centerTitle: true,
leading: new Icon(Icons.arrow_back),
title: new Text("Einloggen",
style: new TextStyle(fontSize: 18.0)),
),
);
}
}
To make the Title in the Center :
centerTitle: true
Based on your code
Widget getTitle() {
return const Text('Einloggen',
style: TextStyle(fontSize: 18.0), textAlign: TextAlign.center);
}
BackButton getBackButton() {
return const BackButton();
}
Row getHeader() {
return Row(
children: <Widget>[
Expanded(
child: getBackButton(),
),
const Spacer(),
getTitle(),
const Spacer(flex: 2)
],
);
}
#override
Widget build(BuildContext context) {
final double statusBarHeight = MediaQuery.of(context).padding.top;
return Material(
child: Container(
padding: EdgeInsets.fromLTRB(0.0, statusBarHeight, 0.0, 0.0),
child: Column(
children: <Widget>[getHeader()],
),
),
);
}
I don't know if it is still useful, but I found a solution thanks to widegtes: Container, Stack and Align.
Widget getTitle() {
return new Text("Einloggen", style: new TextStyle(fontSize: 18.0));
}
Widget getBackButton() {
return Container(
height: MediaQuery.of(context).size.height,
child: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: moveToLogin,
));
}
Widget getHeader() {
return Container(
height: 50.0,
// you must set a size to the Conteiener to make sure that the internal Align
// widens as much as possible.
child: new Stack(
// Stack places the objects in the upper left corner
children: <Widget>[
getBackButton(),
Align(alignment: Alignment.center, child: getTitle()),
],
),
);
}
final double statusBarHeight = MediaQuery.of(context).padding.top;
return new Container(
padding: new EdgeInsets.fromLTRB(0.0, statusBarHeight, 0.0, 0.0),
child: new Column(
children: <Widget>[getHeader()],
),
);
This is the result
Image

Can an AnimatedContainer animate its height?

I'd like to animate a gap between two items in a list. I thought of using an AminatedContainer with a height initially at zero but I'm not familiar with how to make this work. My code at the moment is:
new AnimatedContainer(
duration: const Duration(milliseconds: 200),
height: App.itemSelected==id ? 50.0 : 0.0,
curve: Curves.fastOutSlowIn,
),
That does change the height of the Container but not in an animated way as I had hoped. Any help would be gratefully received!
I am not sure if AnimatedSize is suitable for your use case, but I have added an example on how to make a simple animation with it:
The coloring is a bit off due to the recording but you should be able to test this yourself.
class MyAppState extends State<MyApp> with TickerProviderStateMixin {
double _height = 50.0;
double _width = 20.0;
var _color = Colors.blue;
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new AnimatedSize(
curve: Curves.fastOutSlowIn, child: new Container(
width: _width,
height: _height,
color: _color,
), vsync: this, duration: new Duration(seconds: 2),),
new Divider(height: 35.0,),
new Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new IconButton(
icon: new Icon(Icons.arrow_upward, color: Colors.green,),
onPressed: () =>
setState(() {
_color = Colors.green;
_height = 95.0;
})),
new IconButton(
icon: new Icon(Icons.arrow_forward, color: Colors.red,),
onPressed: () =>
setState(() {
_color = Colors.red;
_width = 45.0;
})),
],
)
],)
,)
);
}
}
You can use an AnimatedSize for that purpose.
https://api.flutter.dev/flutter/widgets/AnimatedSize-class.html

Resources