Related
I have a long list of items that are being updated frequently. The list is rendered using ListView.builder wrapped in a container to apply borders etc.
My problems is, that when the ListView is located in a container, all items are rendered including elements outside the screen. As the list is redrawn very often, it affects performance severely.
I've created two examples to illustrate the problem. To keep it simple, the examples are stateless, in real life the list is stateful.
Example 1 - ListView is root widget. Notice in console that only visible cards are build.
https://dartpad.dev/ac436b786ae48383c107a64bb3db8070
Example 2 - ListView is wrapped in container. Notice all cards are build.
https://dartpad.dev/109f1f97085c1726c0826f6ea8b731ec
How do I make a scrollable container with a list of items, that only renders visible items?
The source of Example 2 here:
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SingleChildScrollView(
child: Container(
color: Colors.green,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 700,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.44),
spreadRadius: 0,
blurRadius: 15,
offset:
Offset(0, 1), // changes position of shadow
),
]),
margin: EdgeInsets.all(24.0),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: 20,
itemBuilder: (context, index) {
var text = 'Card $index';
print('Building card $text');
return Card(
child: Container(
height: 130,
child: Center(
child: Text(text),
)));
})
]))),
],
))));
}
}
You should give a height limit to the listView wrapped in container.
Just like this:
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: SingleChildScrollView(
child: Container(
color: Colors.green,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 700,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.44),
spreadRadius: 0,
blurRadius: 15,
offset:
Offset(0, 1),
),
]),
margin: EdgeInsets.all(24.0),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox( // add a SizedBox and set a specific height
height: 1000,
child: ListView.builder(
scrollDirection: Axis.vertical,
shrinkWrap: true,
itemCount: 20,
itemBuilder: (context, index) {
var text = 'Card $index';
print('testtest Building card $text');
return Card(
child: Container(
height: 130,
child: Center(
child: Text(text),
)));
}),
)
]))),
],
))));
}
}
I am just trying to display the camera's preview and align the user to take a selfie.
I have the following snippet of code and I would like to make the person's image transparent (not white) and fill the surrounding area with color.
but the problem that I am facing, if I set the SVG with transparent color, it will display the color of the parent (container),
I need to make the person's image totally transparent to see the camera's preview with filling the surrounding area with color.
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: [
Container(
height: MediaQuery.of(context).size.height,
child: Center(
child: _cameraPreviewWidget(),
),
),
Container(
height: MediaQuery.of(context).size.height,
color: Colors.grey[700].withOpacity(0.8),
child: Align(
alignment: Alignment.bottomCenter,
child: SvgPicture.asset(
'assets/svg/selfie_person.svg',
alignment: Alignment.bottomCenter,
fit: BoxFit.cover,
color: Colors.white,
),
),
),
Container(
height: MediaQuery.of(context).size.height,
child: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height * 0.05,
bottom: MediaQuery.of(context).size.height * 0.15,
),
child: Column(
children: [
ListTile(
leading: InkWell(
onTap: () {
Navigator.pop(context, null);
},
child: FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.white,
),
),
title: Center(
child: Text(
"Take a selfie",
style: Theme.of(context).textTheme.subtitle2,
),
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.05,
),
child: Text(
"Take a quick selfie so we know it's you, this is never public",
style: Theme.of(context).textTheme.subtitle2,
overflow: TextOverflow.ellipsis,
maxLines: 3,
textAlign: TextAlign.center,
),
),
Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: _captureButton(),
),
),
],
),
),
),
],
),
),
);
I don't understand your question well, but i think you should try Opacity class .. wrap your SVG with Opacity
You can check it :
https://api.flutter.dev/flutter/widgets/Opacity-class.html
I am using the https://pub.dev/packages/flutter_sliding_up_panel dependency and I want to round the edges of the panel below.
This is the code I am using
SlidingUpPanelWidget(
child: Column(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(35.0),
topRight: Radius.circular(35.0),
),
child: Container(
color: Colors.red,
alignment: Alignment.center,
child: Row(
children: <Widget>[
Icon(
Icons.menu,
size: 30,
),
Padding(
padding: EdgeInsets.only(
left: 8.0,
),
),
Text(
'click or drag',
)
],
mainAxisAlignment: MainAxisAlignment.center,
),
height: 50.0,
),
),
Divider(
height: 0.5,
color: Colors.grey,
),
Flexible(
child: Container(
child: ListView.separated(
controller: scrollController,
physics: ClampingScrollPhysics(),
itemBuilder: (context, index) {
return ListTile(
title: Text('list item $index'),
);
},
separatorBuilder: (context, index) {
return Divider(
height: 0.5,
);
},
shrinkWrap: true,
itemCount: 20,
),
color: Colors.white,
),
),
],
mainAxisSize: MainAxisSize.min,
),
controlHeight: 50.0,
anchor: 0.4,
panelController: panelController,
),
and this is what the collapsed panel currently looks like:
I want the entire panel to be as rounded as the red container.
You can't do it. The library has a hardcoded Material widget around the widget you give it.
The only thing you can do is to copy the whole library and modify it. It is a small library only has one file you need to copy.
At line 192 in the library you have this code:
child: Material(
key: _childKey,
color: Theme.of(context).backgroundColor,
elevation: widget.elevation,
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: widget.child,
),
),
Modify it like this:
child: Material(
key: _childKey,
color: Colors.transparent,
shadowColor: Colors.transparent,
elevation: widget.elevation,
child: SizedBox(
height: MediaQuery.of(context).size.height,
child: widget.child,
),
),
and thank you in advance for any assistance.
So, I have images load properly when I use them within a container in other areas of the code, but I also want some of those images to display randomly within the AppBar.
Given that they do load in other locations within the code, I don't suspect a Pubyaml error.
I'm still fairly new to flutter, and lists in general. So I wouldn't be surprised if I messed up something with the list itself, or the way it's called.
It gives me this error:
Syncing files to device iPhone...
Reloaded 8 of 502 libraries in 411ms.
════════ Exception caught by image resource service ════════════════════════════════════════════════
The following assertion was thrown resolving an image codec:
Unable to load asset: Image(image: AssetImage(bundle: null, name: "images/Mem2.JPG"), frameBuilder: null, loadingBuilder: null, alignment: center, this.excludeFromSemantics: false, filterQuality: low)
When the exception was thrown, this was the stack:
#0 PlatformAssetBundle.load (package:flutter/src/services/asset_bundle.dart:221:7)
<asynchronous suspension>
#1 AssetBundleImageProvider._loadAsync (package:flutter/src/painting/image_provider.dart:484:44)
#2 AssetBundleImageProvider.load (package:flutter/src/painting/image_provider.dart:469:14)
#3 ImageProvider.resolve.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:flutter/src/painting/image_provider.dart:327:17)
...
Image provider: AssetImage(bundle: null, name: "Image(image: AssetImage(bundle: null, name: "images/Mem2.JPG"), frameBuilder: null, loadingBuilder: null, alignment: center, this.excludeFromSemantics: false, filterQuality: low)")
Image key: AssetBundleImageKey(bundle: PlatformAssetBundle#c99db(), name: "Image(image: AssetImage(bundle: null, name: "images/Mem2.JPG"), frameBuilder: null, loadingBuilder: null, alignment: center, this.excludeFromSemantics: false, filterQuality: low)", scale: 1.0)
════════════════════════════════════════════════════════════════════════════════════════════════════
Here's the code:
class ContactProfilePage extends StatefulWidget {
#override
_ContactProfilePageState createState() => _ContactProfilePageState();
}
class _ContactProfilePageState extends State<ContactProfilePage> {
List<Attr> attr = [
Attr(name: ''),
];
dynamic randAppBarImg = [
"images/Mem2.JPG",
"images/Mem3.JPG",
"images/Mem4.JPG",
"images/Mem6.jpg",
"images/memory - 1.jpeg",
"images/memory - 2.jpeg",
"images/memory - 3.jpeg",
"images/memory - 4.jpeg",
"images/memory - 5.jpeg",
"images/memory - 6.jpeg",
];
Random rnd;
Image img() {
int min = 0;
int max = randAppBarImg.length - 1;
rnd = Random();
int r = min + rnd.nextInt(max - min);
String image_name = randAppBarImg[r].toString();
return Image.asset(image_name);
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.lightBlueAccent,
child: Icon(Icons.add),
onPressed: () {
//Modal sheet brings up the whole bottom pane when the plus button is pressed.
showModalBottomSheet(
context: context,
builder: (context) => AddAttrScreen(
(newAttrTitle) {
setState(() {
//adds the Attr to the list anytime the user presses "Add"
attr.add(Attr(name: newAttrTitle));
});
Navigator.pop(context);
//Hides the floating add Attr screen after pressing "Add"
},
),
);
//TODO Change it so the categories don't have to be selected every time. Instead, have the add button WITHIN each category.
// This floating button should instead create a new category.
},
),
drawer: DrawerMain(),
body: NestedScrollView(
//This allows for the top bar to collapse, while retaining the image.
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
floating: true,
snap: true,
pinned: true,
flexibleSpace: Stack(
children: <Widget>[
Positioned.fill(
child: Image.asset(
img().toString(),
fit: BoxFit.cover,
))
],
),
),
];
},
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: CircleAvatar(
maxRadius: 50.0,
backgroundImage: AssetImage("images/fox.jpeg")),
),
Container(
//TODO ADD ON PRESSED FUNCTION TO SOCIAL ICONS
margin: EdgeInsets.all(15.0),
child: Icon(
FontAwesomeIcons.facebook,
size: 40.0,
color: Color(0xFF306060),
),
),
Container(
margin: EdgeInsets.all(15.0),
child: Icon(
FontAwesomeIcons.instagram,
size: 40.0,
color: Color(0xFF306060),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Armando Pacheco Ortiz',
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: Color(0xFF306060)),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Freelance App Developer from Puerto Rico',
style: TextStyle(
fontSize: 20.0,
fontStyle: FontStyle.italic,
color: Color(0xFF306060)),
),
SizedBox(
child: Divider(),
),
],
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
children: <Widget>[
Text('Memories',
style: TextStyle(
fontSize: 20.0,
fontStyle: FontStyle.italic,
color: Color(0xFF306060),
fontWeight: FontWeight.bold))
],
),
),
//Horizontal scrolling images.
//TODO These need to update based on uploads, perhaps use something like google's face detection to auto add?
Padding(
padding: const EdgeInsets.all(15.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
height: 310,
width: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: (Image.asset(
'images/Mem6.jpg',
fit: BoxFit.cover,
)),
),
),
SizedBox(
width: 20,
),
Container(
height: 310,
width: 200,
//ClipRRect allows for it to have the border radius. Container is painted behind the image.
//For that reason, adding a border radius to the container doesn't work.
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: (Image.asset(
'images/Mem2.JPG',
fit: BoxFit.cover,
)),
),
),
SizedBox(
width: 20,
),
Container(
height: 310,
width: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: (Image.asset(
'images/memory - 4.jpeg',
fit: BoxFit.cover,
)),
),
),
I pasted the code as far down as the first image, to show how I have it set up in the other locations.
I'm not sure if the issue is with random images within the appBar, or maybe some code I'm missing.
Hopefully, someone can shed some light on this!
Many thanks.
UPDATE:
So that helped a ton and definitely got things running! But I did notice two new problems cropping up.
I had to rename all the images from: "memory - 6" to "Mem6", and that finally allowed them to show up.
Otherwise, sometimes it would revert back to the default teal color, and not display anything, besides an error about not being able to load the asset again. I imagine it's just a naming error I was unknowingly causing?
Scrolling down will cause the Appbar to "refresh" again, and randomize the pictures with the slightest scrolling until you stop. If I scroll back up, it'll do it again. It doesn't create an error, but it does crash the app eventually from so much refreshing I guess.
How would I get around this? Is there an override, or should I simply create the widget with some Finals, or stateless/stateful widgets? I'm still learning the lingo and all that, so my apologies for the ignorance.
You can copy paste run full code below
Your img() has bug, you return an Image.asset and wrap in Image.asset again cause duplicate
Image.asset(
img().toString(),
fit: BoxFit.cover,
))
code snippet just return String of image_name will work
and do not include space in image name
String img() {
int min = 0;
int max = randAppBarImg.length - 1;
rnd = Random();
int r = min + rnd.nextInt(max - min);
String image_name = randAppBarImg[r].toString();
//return Image.asset(image_name);
return image_name;
}
working demo
full code
import 'package:flutter/material.dart';
import 'dart:math';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ContactProfilePage(),
);
}
}
class Attr {
String name;
Attr({this.name});
}
class ContactProfilePage extends StatefulWidget {
#override
_ContactProfilePageState createState() => _ContactProfilePageState();
}
class _ContactProfilePageState extends State<ContactProfilePage> {
List<Attr> attr = [
Attr(name: ''),
];
dynamic randAppBarImg = [
"images/Mem2.JPG",
"images/Mem3.JPG",
/*"images/Mem4.JPG",
"images/Mem6.jpg",
"images/memory-1.jpeg",
"images/memory-2.jpeg",
"images/memory-3.jpeg",
"images/memory-4.jpeg",
"images/memory-5.jpeg",
"images/memory-6.jpeg",*/
];
Random rnd;
String img() {
int min = 0;
int max = randAppBarImg.length - 1;
rnd = Random();
int r = min + rnd.nextInt(max - min);
String image_name = randAppBarImg[r].toString();
//return Image.asset(image_name);
return image_name;
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.lightBlueAccent,
child: Icon(Icons.add),
onPressed: () {
//Modal sheet brings up the whole bottom pane when the plus button is pressed.
/* showModalBottomSheet(
context: context,
builder: (context) => AddAttrScreen(
(newAttrTitle) {
setState(() {
//adds the Attr to the list anytime the user presses "Add"
attr.add(Attr(name: newAttrTitle));
});
Navigator.pop(context);
//Hides the floating add Attr screen after pressing "Add"
},
),
);*/
//TODO Change it so the categories don't have to be selected every time. Instead, have the add button WITHIN each category.
// This floating button should instead create a new category.
},
),
//drawer: DrawerMain(),
body: NestedScrollView(
//This allows for the top bar to collapse, while retaining the image.
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
expandedHeight: 200.0,
floating: true,
snap: true,
pinned: true,
/*flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
title: Text("",
style: TextStyle(
color: Colors.white,
fontSize: 16.0,
)),
background: Image.asset(
"images/Mem2.JPG",
fit: BoxFit.cover,
)),*/
flexibleSpace: Stack(
children: <Widget>[
Positioned.fill(
child: Image.asset(
img().toString(),
fit: BoxFit.cover,
))
],
),
),
];
},
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(15.0),
child: CircleAvatar(
maxRadius: 50.0,
backgroundImage: AssetImage("images/fox.jpeg")),
),
Container(
//TODO ADD ON PRESSED FUNCTION TO SOCIAL ICONS
margin: EdgeInsets.all(15.0),
child: Icon(
FontAwesomeIcons.facebook,
size: 40.0,
color: Color(0xFF306060),
),
),
Container(
margin: EdgeInsets.all(15.0),
child: Icon(
FontAwesomeIcons.instagram,
size: 40.0,
color: Color(0xFF306060),
),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Armando Pacheco Ortiz',
style: TextStyle(
fontSize: 25.0,
fontWeight: FontWeight.bold,
color: Color(0xFF306060)),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 15.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Freelance App Developer from Puerto Rico',
style: TextStyle(
fontSize: 20.0,
fontStyle: FontStyle.italic,
color: Color(0xFF306060)),
),
SizedBox(
child: Divider(),
),
],
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: Row(
children: <Widget>[
Text('Memories',
style: TextStyle(
fontSize: 20.0,
fontStyle: FontStyle.italic,
color: Color(0xFF306060),
fontWeight: FontWeight.bold))
],
),
),
//Horizontal scrolling images.
//TODO These need to update based on uploads, perhaps use something like google's face detection to auto add?
Padding(
padding: const EdgeInsets.all(15.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Container(
height: 310,
width: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: (Image.asset(
'images/Mem6.jpg',
fit: BoxFit.cover,
)),
),
),
SizedBox(
width: 20,
),
Container(
height: 310,
width: 200,
//ClipRRect allows for it to have the border radius. Container is painted behind the image.
//For that reason, adding a border radius to the container doesn't work.
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: Image.asset(
'images/Mem2.JPG',
fit: BoxFit.cover,
),
),
),
SizedBox(
width: 20,
),
Container(
height: 310,
width: 200,
child: ClipRRect(
borderRadius: BorderRadius.circular(20.0),
child: (Image.asset(
'images/memory-4.jpeg',
fit: BoxFit.cover,
)),
),
),
])))
]))));
}
}
I'm attempting to create my own Hero style transition using a SlideTransition with the position Offset starting at where the user taps the item on the previous screen.
This is what I'm currently using for receiving the coordinate value of where the user is tapping the screen (I only need the dy value):
GestureDetector(
child: //stuff
onTapDown: (TapDownDetails details) async {
RenderBox box = context.findRenderObject();
double position = box.localToGlobal(details.globalPosition).dy;
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondPage(startPosition: position);
}));
},
)
I then pass this onto the SecondPage and use it as the starting position of the Animation<Offset> in my initState:
#override
void initState() {
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeInOut);
offset = new Tween<Offset>(begin: Offset(0.0, widget.startPosition), end: Offset.zero).animate(curve);
controller.forward();
super.initState();
}
The issue I'm having is finding a way to properly convert the dy value to one which matches what the Tween<Offset> uses as the dy value comes in with a value of say 250-300 for half way down the screen but the same for the Offset(0.0, widget.startPosition) would be around 2.0 for it to match the same position. I've tried various maths to match these (such as dividing the dy by the screens height) but I haven't found anything which matches it exactly.
If anyone knows the correct method/exact maths I have to perform on matching these values I'll love you forever.
Edit: Self contained example of what I'm trying to achieve which you can play around with. This is currently with me using double position = (box.globalToLocal(details.globalPosition).dy) / box.size.height * 3; as this is around the closest match I've found.
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
#override
State createState() => HomePageState();
}
class HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.indigo.shade200,
floatingActionButton: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height / 35),
child: FloatingActionButton(
child: Icon(Icons.add),
backgroundColor: Colors.grey.shade200,
foregroundColor: Colors.black,
onPressed: () {},
)),
body: Stack(children: <Widget>[
SingleChildScrollView(
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height / 11),
child:
Column(children: <Widget>[Stack(children: getCards())]))),
Container(
height: MediaQuery.of(context).size.height / 5,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(100.0))),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.sentiment_satisfied),
),
)),
Text('Tab1',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.trending_up),
),
)),
Text('Tab2',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.favorite_border),
),
)),
Text('Tab3',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
)
])))
]));
}
List<Widget> getCards() {
List<Widget> widgets = new List<Widget>();
for (int i = 0; i < 5; i++) {
widgets.add(GestureDetector(
child: Container(
margin: EdgeInsets.only(top: (i * 175).toDouble()),
padding: EdgeInsets.all(60.0),
height: 300.0,
decoration: BoxDecoration(
color: getColor(i),
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(100.0)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Center(
child: Text('Text ' + (i + 1).toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 26.0,
fontWeight: FontWeight.bold)))
],
),
),
onTapDown: (TapDownDetails details) async {
RenderBox box = context.findRenderObject();
double position = (box.globalToLocal(details.globalPosition).dy) / box.size.height * 3;
await Navigator.push(context, CustomPageRoute(builder: (context) {
return SecondPage(index: i, startPosition: position);
}));
},
));
}
return widgets.reversed.toList();
}
}
class SecondPage extends StatefulWidget {
final int index;
final double startPosition;
SecondPage({this.index, this.startPosition});
#override
State createState() => SecondPageState();
}
class SecondPageState extends State<SecondPage> with TickerProviderStateMixin {
AnimationController controller;
Animation curve;
Animation<Offset> offset;
#override
void initState() {
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.easeInOut);
offset = new Tween<Offset>(
begin: Offset(0.0, widget.startPosition), end: Offset.zero)
.animate(curve);
controller.forward();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.indigo,
body: Material(
color: Colors.transparent,
child: Stack(children: <Widget>[
SlideTransition(
position: offset,
child: Container(
height: 200.0,
decoration: BoxDecoration(
color: getColor(widget.index),
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(100.0))),
child: Padding(
padding: EdgeInsets.only(top: 28.0),
child: Stack(
children: <Widget>[
Align(
alignment: Alignment.topLeft,
child: IconButton(
icon: Icon(Icons.arrow_back,
color: Colors.white),
onPressed: () {
Navigator.of(context).pop(true);
},
)),
Align(
alignment: Alignment.topRight,
child: IconButton(
icon:
Icon(Icons.launch, color: Colors.white),
onPressed: () {
print("launch website");
},
)),
Align(
alignment: Alignment.center,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context)
.size
.width /
5),
child: Material(
color: Colors.transparent,
child: Text('Text ' + (widget.index + 1).toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 26.0,
fontWeight:
FontWeight.bold)))))
],
))))
])));
}
}
class CustomPageRoute<T> extends MaterialPageRoute<T> {
CustomPageRoute({WidgetBuilder builder, RouteSettings settings})
: super(builder: builder, settings: settings);
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return child;
}
}
Color getColor(int index) {
switch (index) {
case 0:
return Colors.pink.shade300;
case 1:
return Colors.purple.shade300;
case 2:
return Colors.deepPurple.shade400;
case 3:
return Colors.deepPurple.shade900;
case 4:
return Colors.indigo.shade900;
default:
return Colors.red;
}
}
Finding a renderBox is not useful in this case, because all of the cards are places in a stack the context.findRenderObject finds the top most stack. That stack covers the entire screen so when you transform from global to local position you get the same position.
You can calculate the correct offset for you second screen like this:
var scrollOffset = controller.position.pixels;
double position = ((i * 175).toDouble() + 100 - scrollOffset) / 200;
(i * 175) : Offset of every card from the top.
100 : Height difference between height of cards in the first screen
and second screen (300 in first and 200 in second). We add this to
compansate for the difference so the cards position will be the
same.
200 : Height of the card in the second screen. We divide by this
because
The translation is expressed as an Offset scaled to the child's size
Finally you will need to give a scrollController to your SingleChildScrollView to get the scroll offset. Without the scroll offset you can't calculate the correct position if the cards are scrolled (i.e Card 4 or Card 5)
Here is how your HomePageState should look like.
class HomePageState extends State<HomePage> {
ScrollController controller = new ScrollController();
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.indigo.shade200,
floatingActionButton: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height / 35),
child: FloatingActionButton(
child: Icon(Icons.add),
backgroundColor: Colors.grey.shade200,
foregroundColor: Colors.black,
onPressed: () {},
)),
body: Stack(children: <Widget>[
SingleChildScrollView(
controller: controller,
child: Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).size.height / 11),
child:
Column(children: <Widget>[Stack(children: getCards())]))),
Container(
height: MediaQuery.of(context).size.height / 5,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(100.0))),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.sentiment_satisfied),
),
)),
Text('Tab1',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.trending_up),
),
)),
Text('Tab2',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
child: Padding(
padding: EdgeInsets.all(6.0),
child: Container(
height: 40.0,
width: 40.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
width: 2.0, color: Colors.pink.shade300)),
child: Icon(Icons.favorite_border),
),
)),
Text('Tab3',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.bold,
color: Colors.black38))
],
)
])))
]));
}
List<Widget> getCards() {
List<Widget> widgets = new List<Widget>();
for (int i = 0; i < 5; i++) {
widgets.add(GestureDetector(
child: Container(
margin: EdgeInsets.only(top: (i * 175).toDouble()),
padding: EdgeInsets.all(60.0),
height: 300.0,
decoration: BoxDecoration(
color: getColor(i),
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(100.0)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Center(
child: Text('Text ' + (i + 1).toString(),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 26.0,
fontWeight: FontWeight.bold)))
],
),
),
onTapDown: (TapDownDetails details) async {
var scrollOffset = controller.position.pixels;
double position = ((i * 175).toDouble() + 100 - scrollOffset) / 200;
await Navigator.push(context, CustomPageRoute(builder: (context) {
return SecondPage(index: i, startPosition: position);
}));
},
));
}
return widgets.reversed.toList();
}
}