I imagine a new kind of screen, and I would like to do it with Flutter since it's very powerful for rendering fast and smoothly.
I want to achieve a kind of infinite screen with kind of square or zone where you can move into. Actually exactly like a map (in fact not infinite but very large) but where I can:
Drag and translate
Zoom in and out
Click and press on the different component of the screen (square or whatever)
I imagine use GestureDetector on my widget "map" combine with transform on each component insde and refresh the screen after each move, or redrawing each component with draw but I'm not sure it's the best way to follow with this.
Thanks for helping if you have any idea !!
I've implemented part of things you asked for except for the "infinite map".
The code is quite beefy so I've described this stuff in an article on Medium.
It allows:
move map by dragging
place new objects on the map
zoom in/out
click objects
GitHub repo.
Interesting proposal. I don't have the implementation, after all, it's up to you but I have some pointers.
Translation, I imagine, can easily be handled by 2 nested ListViews. One, that scrolls X and one that scrolls in Y direction. ScrollController can be queries for all kinds of info.
Zoom is also fairly easy at first blick: you can wrap the entire screen in a Transform.scale() widget.
You could wrap each tappable widget in a GuestureDetector, query for their RenderBox to get their position on screen in local or global coordinates, get their size.
Note: in games, there is a concept called clipping distance. Figuring out how to implement that in Flutter is going to be a fun challenge. It allows you not to render those Widgets that are too small, you zoomed out a lot eg. Let me know how it goes! Curious.
The InteractiveViewer widget
One solution could be using the InteractiveViewer widget with its constrained property set to false as it will out of the box support:
Drag and translate
Zooming in and out - Simply set minScale and maxScale
Clicking and pressing widgets like normal
InteractiveViewer as featured on Flutter Widget of the Week: https://www.youtube.com/watch?v=zrn7V3bMJvg
Infinite size
Regarding the question's infinite size part, a maximum size for the child widget must be specified, however this can be very large, so large that it is actually hard to re-find widgets in the center of the screen.
Alignment of the child content
By default the child content will start from the top left, and panning will show content outside the screen. However, by providing a TransformationController the default position can be changed by providing a Matrix4 object in the constructor, f.ex. the content can be center aligned if desired this way.
Example code
The code contains an example widget that uses the InteractiveViewer to show an extremely large widget, the example centers the content.
class InteractiveViewerExample extends StatefulWidget {
const InteractiveViewerExample({
Key? key,
required this.viewerSize,
required this.screenHeight,
required this.screenWidth,
}) : super(key: key);
final double viewerSize;
final double screenHeight;
final double screenWidth;
#override
State<InteractiveViewerExample> createState() =>
_InteractiveViewerExampleState();
}
class _InteractiveViewerExampleState extends State<InteractiveViewerExample> {
late TransformationController controller;
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InteractiveViewer.builder(
boundaryMargin: const EdgeInsets.all(40.0),
minScale: 0.001,
maxScale: 50,
transformationController: controller,
builder: (BuildContext context, vector.Quad quad) {
return Center(
child: SizedBox(
width: widget.viewerSize,
height: widget.viewerSize,
child: const InteractiveViewerContent(),
),
);
},
),
),
);
}
#override
void initState() {
super.initState();
// Initiate the transformation controller with a centered position.
// If you want the InteractiveViewer TopLeft aligned remove the
// TransformationController code, as the default controller in
// InteractiveViewer does that.
controller = TransformationController(
Matrix4.translation(
vector.Vector3(
(-widget.viewerSize + widget.screenWidth) / 2,
(-widget.viewerSize + widget.screenHeight) / 2,
0,
),
),
);
}
}
// Example content; some centered and top left aligned widgets,
// and a gradient background.
class InteractiveViewerContent extends StatelessWidget {
const InteractiveViewerContent({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
TextStyle? style = Theme.of(context).textTheme.headline6;
return Container(
padding: const EdgeInsets.all(32.0),
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[Colors.orange, Colors.red, Colors.yellowAccent],
),
),
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.topLeft,
child: SelectableText("Top Left", style: style),
),
SelectableText("Center", style: style),
],
),
);
}
}
Usage
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' as vector;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: InteractiveViewerScreen(),
);
}
}
class InteractiveViewerScreen extends StatelessWidget {
const InteractiveViewerScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: InteractiveViewerExample(
viewerSize: 50000,
screenHeight: MediaQuery.of(context).size.height,
screenWidth: MediaQuery.of(context).size.width,
),
),
);
}
}
What does the code do
Related
i have the following full simple code
import 'dart:developer';
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
var globalKey = GlobalKey();
getCurrentOffest(){
RenderBox renderBoxRed = globalKey.currentContext!.findRenderObject() as RenderBox;
log(renderBoxRed.localToGlobal(Offset.zero).toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: RotatedBox(
quarterTurns: 0,
child: GestureDetector(
onTap: (){
getCurrentOffest();
},
child: Container(
key: globalKey,
color: Colors.red,
width: 250,
height: 250,
),
),
),
);
}
}
Now as shown , i provided global key to my red Container in order to know the current offset of where my red Container is located , as long as my container is not rotated yet (quarterTurns: 0,) it prints correct result . when i tap on it.. it show the following
Offset(0.0, 0.0)
and that's seems correct .
But when i rotate my Container using quarterTurns from zero to 2 and get to know the current offset it prints the following
Offset(250.0, 250.0)
How ? Why? Where did these values come from?
my container is still at his offset also i gave the width and height same value in order to make sure there is no any effect of rotation !!!
why this happening ? How Could i prevent these weird values ?
EDIT
that's happening because offset collocate the values according to the firt point in widget (dx ,dy)and when widget get rotated so the first point is also rotated .. i think the best way is to know the center point offset in widget to avoid this behavior but how to know the center offset ?
I am creating a flutter Web application, but have issues when resizing the window with a SingleChildScrollView + ScrollController.
When I resize the browser window, the page "snaps" back up to the very top. Being a web app, most of the page "sections" are made from Columns with responsively coded widgets as children, with such widgets as Flexible or Expanded. From what I have read, the SingleChildScrollView widget doesn't work well with Flexible or Expanded widgets, so I thought that may be my issue.
For testing purposes, I created a new page with a single SizedBox that had a height of 3000, which would allow me to scroll. After scrolling to the bottom and resizing the window, I was still snapped up to the top of the page. Thus, with or without using Expanded or Flexible widgets, I have the same result.
Test with a SizedBox only
#override
Widget build(BuildContext context) {
return Scaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
primary: false,
child: Column(
children: [
SizedBox(
width: 150,
height: 3000,
),
],
),
),
);
}
I am using Getx with this project to try getting a demo app up and running a bit quicker while I am still learning the core concepts. Below is my controller.
Controller
class HomePageScrollControllerX extends GetxController {
late ScrollController scrollController;
#override
void onInit() {
super.onInit();
scrollController = ScrollController(
initialScrollOffset: 0.0,
keepScrollOffset: true,
);
}
}
Thank you in advance for any insight on this subject!
EDIT
I have added a listener on my ScrollController, which is able to print to the console that I am scrolling. However, the listener does not get called when I resize the window (tested in both Chrome and Edge).
Currently, I believe my only option is to use the listener to update an "offset" variable in the controller, and pass the window's width over to the controller when the widget rebuilds. If done properly, I should be able to use the controller to scroll to the saved offset. Something like this:
if (scrollController.hasClients) {
if (offset > scrollController.position.maxScrollExtent) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
} else if (offset < scrollController.position.minScrollExtent) {
scrollController.jumpTo(scrollController.position.minScrollExtent);
} else {
scrollController.jumpTo(offset);
}
}
However, I feel like this shouldn't be necessary - and I bet this solution would be visually evident to the user.
Edit 2
While I did get this to work with adding the below code just before the return statement, it appears that my initial thoughts were correct. When I grab the edge of the window and move it, it pops up to the top of the window, then will jump to the correct scroll position. It looks absolutely terrible!
#override
Widget build(BuildContext context) {
Future.delayed(Duration.zero, () {
controller.setWindowWithAndScroll(MediaQuery.of(context).size.width);
});
return PreferredScaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
......
I implemented your code without getX by initializing the scrollController as a final variable outside your controller class. I ran it on microsoft edge and did not face the issue you are describing. What's causing your problem is most probably the way you are handling your state management with getX. I'm guessing your onInit function is run multiple times when you are resizing your web page and that's why the page snaps back up. I would recommend logging how many times the onInit function is called.
I found the answer, and it was my scaffold causing the issue - specifically, the scaffold key. But, before that, the Getx usage to get the answer is very easy, so for those of you looking for that particular answer, it is shown below.
Getx Controller
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
class HomePageScrollControllerX extends GetxController {
late ScrollController scrollController;
#override
void onInit() {
super.onInit();
scrollController =
ScrollController(keepScrollOffset: true, initialScrollOffset: 0.0);
}
#override
void onClose() {
super.onClose();
scrollController.dispose();
}
}
Stateless Widget Build Function
class HomePage extends StatelessWidget {
HomePage({
Key? key,
}) : super(key: key);
// All child widgets can use Get.find(); to get instance
final HomePageScrollControllerX controller =
Get.put(HomePageScrollControllerX());
#override
Widget build(BuildContext context) {
return PreferredScaffold(
color: Colors.white,
body: SingleChildScrollView(
controller: controller.scrollController,
primary: false,
... Etc
So, why didn't this work for me? I created a class called "PreferredScaffold" to save myself a few lines of repetitive code.
PreferredScaffold
class PreferredScaffold extends StatelessWidget {
final Widget? body;
final Color? color;
const PreferredScaffold({Key? key, this.body, this.color = Colors.white})
: super(key: key);
#override
Widget build(BuildContext context) {
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
return Scaffold(
key: scaffoldState,
backgroundColor: color,
appBar: myNavBar(context, scaffoldState),
drawer: const Drawer(
child: DrawerWidget(),
),
body: body,
);
}
}
The problem with the above is, when the window is adjusted, the build function is called. When the build function is called for the scaffold, the scaffoldKey is being set. When set, it returns the scroll position back to 0, or the top of the screen.
In the end, I had to make another Controller that would basically hand over the same instance of a key to the scaffold, so it wouldn't be reset when the build function was called.
ScaffoldController
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class ScaffoldControllerX extends GetxController {
static ScaffoldControllerX instance = Get.find();
final GlobalKey<ScaffoldState> scaffoldState = GlobalKey();
}
This changed my PreferredScaffold to the following
PreferredScaffold (version 2)
import 'package:flutter/material.dart';
import 'drawer/drawer_widget.dart';
import 'nav/my_navigation_bar.dart';
class PreferredScaffold extends StatelessWidget {
final Widget? body;
final Color? color;
PreferredScaffold({Key? key, this.body, this.color = Colors.white})
: super(key: key);
final ScaffoldControllerX scaffoldControllerX = ScaffoldControllerX();
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldControllerX.scaffoldState,
backgroundColor: color,
appBar: NavBar(context, scaffoldControllerX.scaffoldState),
drawer: const Drawer(
child: DrawerWidget(),
),
body: body,
);
}
}
I hope this helps if someone has a similar situation.
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.)
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 can i have floating action button pinned on every screen? When i change the screen float action button on home screen gets dissapear. Is there any way for it?
Thanks
honestly i am not sure this is best way or not but ...
you can Define a class like below and use it as parent of your widgets in every screen you want FAB,
class MyParent extends StatelessWidget {
final Widget child;
const MyParent({Key key, this.child}) : super(key: key);
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
child,
Positioned(
bottom: 0,
left: 0,
child: FloatingActionButton(
onPressed: (){},
),
)
],
);
}
}
Here you can define all the floastingAction buttons like this
floatingActionButton:myFloatingActionButton()
And then in a new page create a widget like this
FloatingActionButton myFloatingActionButton({here you can add variables if you want a bit different things on every page}){
return FloatingActionButton(
child:Icon(Icons.add),//You can make any child,
//Any customization like colors and all
onPressed:(){}
);
}