How do you position a PageView inside a Stack? - flutter

I have a PageView and a button inside a Stack (the PageView is at the bottom of the Stack). I have the button in the bottomCenter using Stack's alignment property.
I want to position the PageView inside the Stack so that it appears above the button (as in vertically above, not above in the Stack). However, it seems that you can't use the Positioned widget with a PageView to accomplish this.
I am using a Stack because the PageView contains a zoomable widget using InteractiveViewer, and I want the button to stay on top when this widget is zoomed in.
Wrapping the PageView with a Positioned widget gives different errors based on if you run the app from scratch, if you do a Flutter Hot Restart, or if you do a Flutter Hot Reload. (However, there are no errors if you do not specify any of the other Positioned parameters, i.e. bottom, left, etc.)
Another problem that occurs when you hot reload is that it does do something to the PageView. It shifts it up, but by more than the specified amount in the bottom parameter. It also cuts off a lot of the width (but doesn't give any overflow errors). You can see this in the second picture that is included.
If you specify the bottom parameter and then do a hot reload it gives this error:
The following assertion was thrown during performResize():
Horizontal viewport was given unbounded width.
Viewports expand in the scrolling direction to fill their container. In this case, a horizontal viewport was given an unlimited amount of horizontal space in which to expand. This situation typically happens when a scrollable widget is nested inside another scrollable widget.
If this widget is always nested in a scrollable widget there is no need to use a viewport because there will always be enough horizontal space for the children. In this case, consider using a Row instead. Otherwise, consider using the "shrinkWrap" property (or a ShrinkWrappingViewport) to size the width of the viewport to the sum of the widths of its children.
If you specify the bottom parameter, then either stop and run the app from scratch or do a hot restart, it gives these errors:
Error 1:
The following NoSuchMethodError was thrown during performLayout():
The method 'toStringAsFixed' was called on null.
Receiver: null
Tried calling: toStringAsFixed(1)
Error 2 (several variations of this error):
RenderBox was not laid out: RenderPointerListener#99b5a relayoutBoundary=up8 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1785 pos 12: 'hasSize'
Here is a simple app that shows my problem:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) => MaterialApp(home: MyHomePage());
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
PageController _pageController = PageController(initialPage: 0);
int currentIndex = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
TopToolbar(currentIndex == 0
? 'Car'
: currentIndex == 1
? 'Bike'
: 'Walk'),
Expanded(
child: Stack(
alignment: AlignmentDirectional.bottomCenter,
children: [
Positioned(
// vvvvvvvvvvvvvvvv Specifying this causes the errors: vvvvvvvvvvvvvvvvvvvvvvvv
bottom: 50, // 50 because that is the height of the button
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
child: PageView(
controller: _pageController,
onPageChanged: (page) {
setState(() {
currentIndex = page;
});
},
children: [
CarScreen(),
BikeScreen(),
WalkScreen(),
],
),
),
Container(
height: 50, // <-------------------- Height of the button
color: Colors.lightBlueAccent,
child: Text('A button', style: TextStyle(fontSize: 20)))
],
),
),
],
),
),
// ==== I don't think the code below is relevant to the question, but who knows:
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (value) {
setState(() {
currentIndex = value;
_pageController.jumpToPage(value);
});
},
items: [
BottomNavigationBarItem(
icon: Icon(Icons.directions_car), label: 'Car'),
BottomNavigationBarItem(
icon: Icon(Icons.directions_bike), label: 'Bike'),
BottomNavigationBarItem(
icon: Icon(Icons.directions_walk), label: 'Walk'),
],
),
);
}
}
class TopToolbar extends StatelessWidget {
final String label;
TopToolbar(this.label);
#override
Widget build(BuildContext context) => Container(
color: Colors.lightBlueAccent,
child: Row(
children: [
Text('Custom toolbar for $label', style: TextStyle(fontSize: 20)),
],
),
);
}
class CarScreen extends StatelessWidget {
#override
Widget build(BuildContext context) => ScreenWidget('Car');
}
class BikeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) => ScreenWidget('Bike');
}
class WalkScreen extends StatelessWidget {
#override
Widget build(BuildContext context) => ScreenWidget('Walk');
}
class ScreenWidget extends StatelessWidget {
final String title;
ScreenWidget(this.title);
#override
Widget build(BuildContext context) => Center(
child: Container(
width: 200,
height: 550,
alignment: Alignment.center,
color: Colors.grey,
child: Text('$title Screen', style: TextStyle(fontSize: 20))));
}
What it looks like without the bottom parameter specified. See that the button covers up part of the PageView, I don't want this to happen, this is what I'm trying to fix:
What it looks like when you specify the bottom parameter and then do a hot reload. You can see it shifts it up by more than what's specified by bottom, and it cuts off a lot of the width without giving overflow errors:
What it looks like when you specify the bottom parameter and then either hot restart, or stop and run the app from scratch. The PageView and the button are gone:

Related

how to disable scrollview when pinch zoom in image

I am using the lib pinch_zoom_release_unzoom to pinch zoom image. I create it inside SingleChildScrollView but when user use 2 finger to pinch zoom image. it very hard to zoom because sometime page is Scrollable. so I want to solve this problem
this is my example code
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:pinch_zoom_release_unzoom/pinch_zoom_release_unzoom.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Tutorial',
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
String imageUrl = 'https://cdn.pixabay.com/photo/2015/04/23/22/00/tree-736885__480.jpg';
TransformationController controller = TransformationController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Tutorial'),
),
body: Column(
children: [
Center(
child: ElevatedButton(
onPressed: () {
showMaterialModalBottomSheet(
expand: false,
context: context,
builder: (context) => PinchZoomReleaseUnzoomWidget(
child: SingleChildScrollView(
controller: ModalScrollController.of(context),
physics: const ClampingScrollPhysics(),
child: Column(
children: [
const SizedBox(
height: 100,
),
Image.network(imageUrl),
const SizedBox(
height: 1000,
),
],
),
),
),
);
},
child: const Text(
'showModalBottomSheet',
),
),
),
],
),
);
}
}
You can interact with physics of scrollable widget to make scrolling different. For that purpose, you should change your physic inside SingleChildScrollView , whenever your zooming state changes. For example:
lass ParentWidget extends StatefulWidget {
const ParentWidget ({Key? key,}) : super(key: key);
#override
State<ParentWidget > createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget > {
late bool isScrolling;
#override
void initState() {
isScrolling = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
physics: isScrolling ? NeverScrollableScrollPhysics() : null,
child: YourWidget(function: (currentState) => setState(() {
isScrolling = currentState;
}));
class YourWidget extends StatelessWidget {
const SceneManaging({Key? key, this.callback}) : super(key: key);
final Function(bool isScrolling)? function;
}
In this case, you should call function inside your child widget whenever you use zoom. When it zooms - pass to it a true, to disable your parent widget scroll, whenever zooms actions stops - pass false.
physics: NeverScrollableScrollPhysics()
How the scroll view should respond to user input.
For example, determines how the scroll view continues to animate after the user stops dragging the scroll view.
Defaults to matching platform conventions. Furthermore, if primary is false, then the user cannot scroll if there is insufficient content to scroll, while if primary is true, they can always attempt to scroll.
To force the scroll view to always be scrollable even if there is insufficient content, as if primary was true but without necessarily setting it to true, provide an AlwaysScrollableScrollPhysics physics object, as in:
physics: const AlwaysScrollableScrollPhysics(),
To force the scroll view to use the default platform conventions and not be scrollable if there is insufficient content, regardless of the value of primary, provide an explicit ScrollPhysics object, as in:
physics: const ScrollPhysics(),
The physics can be changed dynamically (by providing a new object in a subsequent build), but new physics will only take effect if the class of the provided object changes. Merely constructing a new instance with a different configuration is insufficient to cause the physics to be reapplied. (This is because the final object used is generated dynamically, which can be relatively expensive, and it would be inefficient to speculatively create this object each frame to see if the physics should be updated.)

How to put two floating-like Slivers inside a CustomScrollView with SliverAppBar-like behavior?

Say we have the following, simple CustomScrollView:
A SliverAppBar
Widget 1
Widget 2
Widget 3
The previous example must be able to meet the following requirements:
The SliverAppBar must have floating: true, so it appears when we scroll up again. Easy enough;
Widget 1 and Widget 2 should behave something like the SliverAppBar. Let me elaborate this:
These two widgets aren't an AppBar, which means that no drawer should be painted on them, no insets or anything else should be reserved. They're plain widgets, I want to use my own implementations there;
When I scroll down, I expect those two widget to just scroll and disappear... Again, easy enough;
When I scroll up again, though, these two widgets should appear one after the other on the screen like so: SliverAppBar -> Widget 1 -> Widget 2, without needing to scroll to the top, i.e. behave just like the SliverAppBar with the floating:true option, but they must respect the aforementioned scroll order.
Widget 1 and Widget 2 are hideable: see example below;
Widget 3 is just the actual content, and behaves like a normal scrollable widget.
Here's the code of what I have right now. I tried to implement Widget 1 and Widget 2 with different approaches:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var showGreen = false;
var showRed = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
drawer: const Drawer(),
body: Center(
child: CustomScrollView(
slivers: [
SliverAppBar(
title: const Text('My app bar'),
floating: true,
actions: [
IconButton(
icon: const Icon(Icons.grade_outlined),
tooltip: 'Add',
onPressed: () {
setState(() {
showGreen = !showGreen;
});
},
),
IconButton(
icon: const Icon(Icons.grade),
tooltip: 'Remove',
onPressed: () {
setState(() {
showRed = !showRed;
});
},
),
],
),
if (showGreen)
SliverAppBar(
toolbarHeight:
200, // I don't want hard-coded values in here!!
leadingWidth: 0,
titleSpacing: 0,
floating: true,
title: Container(
height: 200, // I don't want hard-coded values in here!!
color: Colors.green,
child: const Placeholder(),
),
),
SliverPersistentHeader(
floating: true,
delegate: MyDelegate(showRed),
),
SliverToBoxAdapter(
child: Container(
color: Colors.blue,
height: 2500,
child: const Placeholder(),
),
),
],
),
),
),
);
}
}
class MyDelegate extends SliverPersistentHeaderDelegate {
final bool showContents;
const MyDelegate(this.showContents) : super();
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return showContents
? Container(
color: Colors.red,
child: const Text(
"my contents",
style: TextStyle(fontSize: 36),
),
)
: const SizedBox.shrink();
}
#override
// But this is not what I want! I want the maxExtent to be as much as the build() method needs!
// I DO NOT want to hard code heights here!
double get maxExtent => 200;
#override
// For some reason... the content isn't disappearing right away and instead I get an overflow error?
double get minExtent => 0;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}
Here's what I tried so far, with no luck:
I tried to implement Widget 1 and Widget 2 as two SliverAppBars, but:
Removing the Drawer is hacky (see example: you have to force the title to expand): is it actually there and just not shown, or did the framework remove that button entirely?
I want to leave these Widgets' height unbounded (no, I don't want anything fixed, I need their height to fit the content in a Flexible way); inserting anything besides a Placeholder breaks the AppBar as I'm not able to tell how much is needed in my contents beforehand!
This still feels like a hacky solution. I don't want to have three SliverAppBars semantically speaking (!)
This seems to be the "as-close-as-I-can-get" behavior, tho
I tried SliverLists, SliverGrids... the behavior is impossible to reproduce with the "out-of-the-box" Widgets;
I tried using a SliverPersistentHeader and its SliverPersistentHeaderDelegate, but I had no luck. I can't seem to understand how to consistently reproduce what I want with the maxExtent and minExtent parameters: I couldn't care less for fixed values (and as you see in the example, it just won't work well). Also, the floating behavior is just not there.
I am so lost with this one. Is anyone able to answer this?

How to add a container to a column on tap in Flutter

I have a situation where I have a listview of containers, and I would like when I double tap each container another container pops up below with information. Currently what I am trying to do is wrap each container within a column and do something like:
onDoubleTap() {showBox = true}, and in the column have code:
children: [post(), showbox == true? infobox() : container()] but I am not sure of the correct implementation. Any help would be great!
you should maintain a list of containers:
class ContainerAdder extends StatefulWidget {
const ContainerAdder({Key? key}) : super(key: key);
#override
_ContainerAdderState createState() => _ContainerAdderState();
}
class _ContainerAdderState extends State<ContainerAdder> {
List<Widget> containers = <Widget>[];
Random random = Random();
List<Color> colors = [
Colors.blue,
Colors.green,
Colors.red,
Colors.orange,
Colors.purple,
Colors.pink,
Colors.teal,
Colors.yellow,
];
addContainer() {
setState(() {
int r = random.nextInt(colors.length);
containers.add(
InkWell(
onDoubleTap: () => addContainer(),
child: Container(
margin: const EdgeInsets.only(bottom: 1.0),
height: 50.0,
color: colors[r],
),
),
);
});
}
#override
void initState() {
super.initState();
addContainer();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [...containers],
),
);
}
}
As you can notice, the method addContainer() adds a container which is wrapped in an InkWell to have the tap listener. The doubleTap calls the method addContainer().
I simply spread the containers list inside ListView widget.
In the addContainer() method, I wrap the codes inside setState() so as to refresh the tree. You can use any other state management architecture if you so wish.
For the first time, I call addContainer() inside initState(), in order to populate the list with the first element.

How to display an image partially in flutter to get a curtain-raising effect?

I want to display an image in flutter such that it gives an effect of curtain-raising animation based on the slider values.
For example, I am displaying the IMAGE.jpg which has a fixed height and width on the flutter app.
The slider has 0 to 10 range.
When 0: the slider value is 0 then the image layout area is displayed 100% black.
When 1: Bottom 10% is displayed and the top 90% is black
When 2: Bottom 20% is displayed and the top 80% is black
and similarly
When 10: 100% is displayed
How can this effect be created?
To create this curtain effect you can use the Slider and Align widgets. You can set heightFactor inside the Align class to create a percentage curtain effect. Inside the Slider widget you can now set the value of the heightFactor to create the curtain effect.
Here a minimal working example:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(home: CurtainScaffold());
}
}
class CurtainScaffold extends StatefulWidget {
#override
_CurtainScaffoldState createState() => _CurtainScaffoldState();
}
class _CurtainScaffoldState extends State<CurtainScaffold> {
double curtain = 0.0;
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Slider(
onChanged: (double val) {
setState(() {
this.curtain = val;
});
},
value: curtain,
min: 0.0,
max: 1.0,
),
ClipRect(
child: Align(
alignment: Alignment.bottomCenter,
heightFactor: curtain,
child: Image.network(
'https://upload.wikimedia.org/wikipedia/commons/b/bd/Dts_news_bill_gates_wikipedia.JPG'),
),
),
],
),
),
);
}
}

Fix last element of ListView to the bottom of screen

I am trying to implement a custom navigation drawer using Flutter. I would like to attach log out option to the bottom of the drawer. The problem is that number of elements above log out option is unknow (from 3 to 17).
So if these widgets take half of the space of a drawer, then log out option will be on the bottom, and if there is too much of them and you have to scroll to see them all, then the log out option will be simply the last.
I am also trying to give the first two options a green background color. Which widget tree would you recommend me? I had a thought about the ListView widget, it takes List of widgets as an argument in constructor.
Therefore I can solve the different background color for the first two items. But I still can't figure out how to attach the log out option to the bottom. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than screen size and in that case, it should be placed at the bottom of whole list.
EDIT: I've add a design to the question. The logout option is the one called Odhlášení. In this case it's at the bottom of drawer, but it can happen, that other options will be bigger than the screen size and in that case, it should be placed at the bottom of whole list.
Design:
You can simply use ListView to manage the "17" navigation options. Wrap this ListView inside an Column. The ListView will be the first child of the Column the second child, therefore placing at the bottom, will be your logout action.
If you are using transparent widgets (like ListTile) inside your ListView to display the navigation options, you can simply wrap it inside a Container. The Container, besides many other widgets, allows you to set a new background color with its color attribute.
Using this approach the widget tree would look like the following:
- Column // Column to place your LogutButton always below the ListView
- ListView // ListView to wrap all your navigation scrollable
- Container // Container for setting the color to green
- GreenNavigation
- Container
- GreenNavigation
- Navigation
- Navigation
- ...
- LogOutButton
Update 1 - Sticky LogOutButton :
To achieve the LogOutButton sticking to the end of the ListView you'll neeed to do two things:
Replace the Expanded with an Flexible
Set shrinkWrap: true inside the ListView
Update 2 - Spaced LogOutButton until large List:
Achieving the described behavior is a more difficult step. You'll have to check if the ListView exceeds the screen and is scrollable.
To do this I wrote this short snippet:
bool isListLarge() {
return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
}
It will return true if the ListView exceeds its limitations. Now we can refresh the state of the view, depending on the result of isListViewLarge. Below again a full code example.
Standalone code example (Update 2: Spaced LogOutButton until large List):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatefulWidget {
#override
_MyDrawerState createState() => _MyDrawerState();
}
class _MyDrawerState extends State<MyDrawer> {
ScrollController controller = ScrollController();
ScrollPhysics physics = ScrollPhysics();
int entries = 4;
#override
Widget build(BuildContext context) {
Widget logout = IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () => {setState(() => entries += 4)});
List<Widget> navigationEntries = List<int>.generate(entries, (i) => i)
.map<Widget>((i) => ListTile(
title: Text(i.toString()),
))
.toList();
if (this.isListLarge()) { // if the List is large, add the logout to the scrollable list
navigationEntries.add(logout);
}
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // place the logout at the end of the drawer
children: <Widget>[
Flexible(
child: ListView(
controller: controller,
physics: physics,
shrinkWrap: true,
children: navigationEntries,
),
),
this.isListLarge() ? Container() : logout // if the List is small, add the logout at the end of the drawer
],
),
);
}
bool isListLarge() {
return controller.positions.isNotEmpty && physics.shouldAcceptUserOffset(controller.position);
}
}
Standalone code example (Update 1: Sticky LogOutButton):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatefulWidget {
#override
_MyDrawerState createState() => _MyDrawerState();
}
class _MyDrawerState extends State<MyDrawer> {
int entries = 4;
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
Flexible(
child: ListView(
shrinkWrap: true,
children: List<int>.generate(entries, (i) => i)
.map((i) => ListTile(
title: Text(i.toString()),
))
.toList(),
),
),
IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () => {setState(() => entries += 4)})
],
),
);
}
}
Standalone code example (Old: Sticking to bottom):
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(),
drawer: MyDrawer(),
),
);
}
}
class MyDrawer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Drawer(
child: Column(
children: <Widget>[
Expanded(
child: ListView(
children: List<int>.generate(40, (i) => i + 1)
.map((i) => ListTile(
title: Text(i.toString()),
))
.toList(),
),
),
IconButton(icon: Icon(Icons.exit_to_app), onPressed: () => {})
],
),
);
}
}