ListView widget not entirely visible when scaled - flutter

When I scale a horizontal ListView widget, I observe that only a portion of the list items are visible when the widget is scrolled all the way to the right:
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final sideLength = 50.0;
final scale = 2.0;
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
home: Scaffold(
body: Transform.scale(
scale: scale,
alignment: Alignment(-1, -1),
child: Container(
height: sideLength * scale,
child: ListView.builder(
itemCount: 20,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
width: sideLength,
height: sideLength,
child: Text(index.toString()),
decoration: BoxDecoration(
border: Border.all(width: 3, color: Colors.red))),
)),
)));
}
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
#override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
On the Pixel 2 emulator, only the first 16 items are visible when I scroll to extreme right. For example:
When the scale is 1 or if the Transform.scale widget is not there, all 20 items are visible.
I observe the following behavior:
Total item count
Last item scrollable to
8
4
10
6
20
16
30
26
50
46
So it seems like the last 4 items are always left out.
Ultimately my goal is to create a responsive widget that scales according to the dimensions of screen, so I'm looking for a generic solution.
The custom scroll behavior is only there so that horizontal scrolling works on dartpad.dev, as per this answer.

Transform only affects how the child should be painted.
On the other hand, The scale argument multiplies the x and y axes.
For example: Imagine a photographer who is taking a landscape photo that contains a tree, now if the photographer gets closer to the tree, the upper part of the tree will slightly be out of the photo.
Try adding padding or margin to the Container and observe how the widget is affected by the scale.
Then you will know how to manipulate it.
body: Transform.scale(
scale: scale,
alignment: Alignment(0, -5),
child: Container(
height: sideLength * scale,
margin: EdgeInsets.only(left: 100, right: 100),
child: ListView.builder(

Give here width and height by MediaQuery
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
home: Scaffold(
body: Transform.scale(
scale: scale,
alignment: Alignment(-1, -1),
child: Container(
height: sideLength * scale,
child: ListView.builder(
itemCount: 20,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) => Container(
width: MediaQuery.of(context).size.width*0.90 ,
height: MediaQuery.of(context).size.height*0.80 ,
child: Text(index.toString()),
decoration: BoxDecoration(
border: Border.all(width: 3, color: Colors.red))),
)),
)));
}

I worked around this issue by:
setting itemCount to a higher value than the desired item count. This allows you to scroll to the last desired item in the ListView
having a scroll controller that checks whether you're past the last visible desired item within the viewport. If so, jump to the last possible scroll offset within the viewport
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final ScrollController _scrollController = ScrollController();
static const actualTotalItems = 20;
static const sideLength = 50.0;
static const scale = 2.0;
MyApp() {
_scrollController.addListener(() {
if (_scrollController.offset > sideLength * actualTotalItems - _scrollController.position.viewportDimension / scale) {
_scrollController.jumpTo(sideLength * actualTotalItems - _scrollController.position.viewportDimension / scale);
}
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
scrollBehavior: MyCustomScrollBehavior(),
home: Scaffold(
body: Container(
transform: Matrix4.diagonal3Values(scale, scale, 1),
height: sideLength * scale,
child: ListView.builder(
itemCount: actualTotalItems * 2,
scrollDirection: Axis.horizontal,
controller: _scrollController,
itemBuilder: (context, index) => Container(
width: sideLength,
height: sideLength,
child: Text(index.toString()),
decoration: BoxDecoration(border: Border.all(width: 3, color: Colors.red)))))));
}
}
class MyCustomScrollBehavior extends MaterialScrollBehavior {
// Override behavior methods and getters like dragDevices
#override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
};
}
I scaled the widget using the transform property in the Container to have a smaller widget tree, but that has no impact on the solution. The Transform widget could have been used as in the OP.

Related

How to animate multiple widgets while scrolling in Flutter

I need to implement custom animation while scrolling the list of users. See an example
My current view is composed of next elements:
SingleChildScrollView contains Column with:
Row of three top elements (each of is a custom widget with basically Stack of avatar, medal and details (Column))
Row as a table header
ListView of other users.
SingleChildScrollView is wrapped with NotificationListener for ScrollNotification which is populated to provider. The scroll value is then listened in every top element to perform animation of its own.
I would like to know some general path and algorithm here to take. I tried AnimatedPositioned but as soon as it is applied on multiple elements it causes performance issues. Should I use AnimationController or some more custom things so far? Any help would be appreciated.
As pskink mentioned, using SliverPersistentHeader can be archive, This is a demo widget to illustrate how it can be done. You need to play with value. My favorite part is using .lerp , doubleLerp... to position the items.
class Appx extends StatelessWidget {
const Appx({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: CustomSliverPersistentHeaderDelegate(),
),
const SliverToBoxAdapter(
child: SizedBox(
height: 3333,
width: 200,
),
),
],
),
);
}
}
class CustomSliverPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
#override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return LayoutBuilder(builder: (_, constraints) {
final t = shrinkOffset / maxExtent;
final width = constraints.maxWidth;
final itemMaxWidth = width / 4;
double xFactor = -.4;
return ColoredBox(
color: Colors.cyanAccent.withOpacity(.3),
child: Stack(
children: [
Align(
alignment:
Alignment.lerp(Alignment.center, Alignment(xFactor, -.2), t)!
..x,
child: buildRow(
color: Colors.deepPurple, itemMaxWidth: itemMaxWidth, t: t),
),
Align(
alignment: Alignment.lerp(
Alignment.centerRight, Alignment(xFactor, 0), t)!,
child:
buildRow(color: Colors.red, itemMaxWidth: itemMaxWidth, t: t),
),
Align(
alignment: Alignment.lerp(
Alignment.centerLeft, Alignment(xFactor, .2), t)!,
child: buildRow(
color: Colors.amber, itemMaxWidth: itemMaxWidth, t: t),
),
],
),
);
});
}
Container buildRow(
{required Color color, required double itemMaxWidth, required double t}) {
return Container(
width: lerpDouble(itemMaxWidth, itemMaxWidth * .3, t),
height: lerpDouble(itemMaxWidth, itemMaxWidth * .3, t),
color: color,
);
}
/// you need to increase when it it not pinned
#override
double get maxExtent => 400;
#override
double get minExtent => 300;
#override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
return false;
}
}

Flutter - Draggable AND Scaling Widgets

So for this application (Windows, Web) I have 2 requirements:
User can drag around widgets on the screen (drag and drop) to any location.
The app must scale to screen/window size
For (1) I used this answer.
For (2) I used this solution.
As mentioned in the code comment below I can't have both:
If I set logicWidth and logicHeight dynamically depending on the window size, the dragging works fine but the draggable widgets won't scale but instead stay the same size regardless of the window size.
If I set logicWidth and logicHeight to a constant value (the value of the current cleanHeight ) the dragging will be messed up for other screen sizes but then the draggable widgets will scale correctly with the window size.
In other words: for the dragging to work nicely these values need to be matching the window size at any time. But by changing these values I ruin the scaling I need.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:matrix_gesture_detector/matrix_gesture_detector.dart';
//containing widgets to drag around
const List<Widget> draggableWidgets = [
DraggableWidget(
draggableWidget: CircleAvatar(
backgroundColor: Colors.green,
radius: 32,
)),
DraggableWidget(
draggableWidget: CircleAvatar(
backgroundColor: Colors.red,
radius: 24,
)),
];
class FrontPageWidget extends ConsumerWidget {
const FrontPageWidget({Key? key}) : super(key: key);
static const routeName = '/frontPage';
#override
Widget build(BuildContext context, WidgetRef ref) {
//screen height and padding
final height = MediaQuery.of(context).size.height;
final padding = MediaQuery.of(context).viewPadding;
// Height (without status and toolbar)
final cleanHeight = height - padding.top - kToolbarHeight;
//either make those values dynamic (cleanHeight updates depending on screen size / window size) OR constant (961px is the cleanHeight on full screen)
//if values are dynamic => the draggable widgets not scaling to screen size BUT dragging works fine
//if values are constant => the draggable widgets do scale to screen size BUT dragging is messed
final logicWidth = cleanHeight; //961
final logicHeight = cleanHeight; //961
return Scaffold(
appBar: AppBar(
title: const Text('Main Page'),
),
body: SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: Container(
color: Colors.grey,
width: logicWidth,
height: logicHeight,
child: Stack(
children: draggableWidgets,
),
))),
);
}
}
class DraggableWidget extends StatelessWidget {
final Widget draggableWidget;
const DraggableWidget({Key? key, required this.draggableWidget})
: super(key: key);
#override
Widget build(BuildContext context) {
final ValueNotifier<Matrix4> notifier = ValueNotifier(Matrix4.identity());
return Center(
child: MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
},
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: [draggableWidget],
),
),
);
},
),
),
);
}
}
One way of doing it is wrapping the draggableWidget in a Transform widget and set the scale factor in relation to the dimensions:
child: AnimatedBuilder(
animation: notifier,
builder: (ctx, child) {
final height = MediaQuery.of(context).size.height;
return Transform(
transform: notifier.value,
child: Center(
child: Stack(
children: [
Transform.scale(
scale: height / 1000,
child: draggableWidget)
],
),
),
);
},
),
I had a similar issue, instead of getting the height from the MediaQuery get it from the LayoutBuilder, I noticed it is working much better when resizing the window.
body: LayoutBuilder(
builder: (context, constraints) {
return SizedBox.expand(
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: Container(
color: Colors.grey,
width: constraints.maxWidth,
height: constraints.maxHeight,
child: Stack(
children: draggableWidgets,
),
)
)
);
}
);
Another way of achieving this:
To drag around widgets on the screen (drag and drop) to any location.
Draggable Widget
Check Flutter Draggable class
And to scale screen/window size.
Relative Scale
FlutterScreenUtil

Flutter Scroll with fixed start end end point

I want to create a scroll that scrolls from one fixed point to another. With Positioned I can draw an Image to an absolute position and I want a scroll that works in a similar way. A bidirectional scroll that has coordinates where it should stop scrolling and isn't sized by the Widges inside it.
As an example it should be able to scroll to the top of the first image that has an absolute position.
It is supposed to be a bidirectional infinity scroll, without memory leaks. I want to remove and add images above without messing up the scrollprogress. That’s why I chose absolute positions for the images and want to know how to make it scrollable.
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Stack(children: <Widget>[
Container(
height: 1500,
width: double.maxFinite,
color: Colors.blue,
),
Positioned(
top: -100,
width: 500,
height: 600,
left: 0,
child: Image.network("https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg")),
Positioned(
top: 400,
width: 500,
height: 500,
left: 0,
child: Image.network("https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg")),
Positioned(
top: 900,
width: 500,
height: 500,
left: 0,
child: Image.network("https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg"))
]),
));
You can use LazyLoading package or infiniteScrollPagination. if you face some issue when parsing the list, check out this post tempListModel
Perhaps, you want to achieve a vertical (or maybe horizontal) carousel slider for images. If I'm not wrong, I recommend a package that can easily manage your code and save time.
Package link: https://pub.dev/packages/carousel_slider
Just import the carousel slider package from the below-mentioned site.
Link: https://pub.dev/packages/carousel_slider
Then, try this example code I wrote for you. Kindly explore the carousel options property. This package is popular as well as feature-rich.
My code:
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
class Demo extends StatefulWidget {
const Demo({Key? key}) : super(key: key);
#override
State<Demo> createState() => _DemoState();
}
class _DemoState extends State<Demo> {
final CarouselController _carouselController = CarouselController();
final List<String> _imageURLsList = [
"https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg",
"https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg",
"https://trade.recosurfaces.com/wp-content/uploads/2020/08/100X-PGB1-DBWT-800x800.jpg",
];
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
child: CarouselSlider.builder(
itemCount: _imageURLsList.length,
carouselController: _carouselController,
options: CarouselOptions(
scrollDirection: Axis.vertical,
enableInfiniteScroll: true,
),
itemBuilder: (context, index, realIndex) {
return Image.network(_imageURLsList[index]);
},
),
),
],
),
),
);
}
}

Flutter How to change container height based on the ListView's item height?

Hello I have a Scaffold wrapped with SingleChildScrollView and child is Column.
Inside Column; Container, TabBar and TabBarView.
First Container is just there for black space.
Container(
color: Colors.black,
height: 300,
),
The second widget of Column which mean TabBar:
(I know we can use it in AppBar but now it is what it is.)
const TabBar(
labelColor: Colors.red,
tabs: [
Tab(text: "Tab1"),
Tab(text: "Tab2"),
Tab(text: "Tab3"),
],
),
Last Column widget is TabBarView. It wrapped by Container that has 300 height.
Container(
height: 300, // here is problem
color: Colors.amber,
child: TabBarView(
children: [
buildContainer(200, Colors.red, 2),
buildContainer(100, Colors.red, 2),
buildContainer(150, Colors.red, 3),
],
),
),
and also this is buildContainer method;
buildContainer(double height, Color color, int count) => ListView.builder(
shrinkWrap: true,
itemCount: count,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
height: height,
color: color,
child: Center(
child: Text("my height ${height.toString()}"),
),
),
);
});
Here is my question. I have 3 tabs and I have three ListViewBuilder.Each one has own child count. But all of them height limited to 300 because of their parent that is Container. I want to set Tab's height dynamicly with each ListViewBuilder's item count.
How can I do that ? I accept dynamic height without child scrolling. I mean, I can scroll whole page for reach the last child. In example, Instagram profile tab. If I have 30 photo, height is phone height. But I have 300 photo, it scrolling all the way down. But also, I don't want to understand pagenation for this one. I am not going to do Instagram. I just want to that, If I create 5 container, okey show me your max height. If I create 1 container, show me just that without scrolling.
I added a dynamic height calculation depending on the number of objects in the ListView.builder. The formula is not perfect, you can add to it, but the point is to subtract the AppBar height and padding to get a clean screen area that the widget completely occupies.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: mainWidget(),
);
}
Widget mainWidget() {
AppBar appBar = AppBar(
toolbarHeight: 56, //You can manually set the AppBar height
title: const Text("App bar"),
);
print(appBar.preferredSize); // Or you can save this value and use it later, it will not be fixed, but will depend on the screen size
return Scaffold(
appBar: appBar,
body: HelpSO(color: Colors.red, count: 5),
);
}
}
class HelpSO extends StatelessWidget {
late double height;
Color color;
int count;
HelpSO({Key? key, required this.color, required this.count})
: super(key: key);
#override
Widget build(BuildContext context) {
// height = (deviceHeight / itemCount) - padding (top + bottom) - appbar.prefferedSize.height / 2;
height = (MediaQuery.of(context).size.height / count) - 16.0 - 56 / 2;
return ListView.builder(
shrinkWrap: true,
itemCount: count,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8.0), // Subtract this value
child: Container(
height: height,
color: color,
child: Center(
child: Text("My height ${height.toString()}"),
),
),
);
});
}
}
I'm new at flutter(2 m).I just come accros with this problem.
My solution was juste wrap first or second(base on your logic) container with a SingleChildScrollView
Hope it will be helpful

Resizing parent widget to fit child post 'Transform' in Flutter

I'm using Transforms in Flutter to create a scrolling carousel for selecting from various options.
This uses standard elements such as ListView.builder, which all works fine, aside from the fact that the parent widget of the Transform doesn't scale down to fit the content as seen here:
Here's the code used to generate the 'card' (there was actually a Card in there, but I've stripped it out in an attempt to get everything to scale correctly):
return Align(
child: Transform(
alignment: Alignment.center,
transform: mat,
child: Container(
height: 220,
color: color,
width: MediaQuery.of(context).size.width * 0.7,
child: Text(
offset.toString(),
style: TextStyle(color: Colors.white, fontSize: 12.0),
),
),
),
);
}
Even if I remove the 'height' parameter of the Container (so everything scales to fit the 'Text' widget), the boxes containing the Transform widgets still have the gaps around them.
Flutter doesn't seem to have any documentation to show how to re-scale the parent if the object within is transformed - anyone here knows or has any idea of a workaround?
EDIT: The widget returned from this is used within a build widget in a Stateful widget. The stack is Column > Container > ListView.builder.
If I remove the Transform, the Containers fit together as I'd like - it seems that performing a perspective transform on the Container 'shrinks' it's content (in this case, the color - check the linked screen grab), but doesn't re-scale the Container itself, which is what I'm trying to achieve.
I have a tricky solution for this: addPostFrameCallback + overlay.
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
// ignore: must_be_immutable
class ChildSizeWidget extends HookWidget {
final Widget Function(BuildContext context, Widget child, Size size) builder;
final Widget child;
final GlobalKey _key = GlobalKey();
OverlayEntry _overlay;
ChildSizeWidget({ this.child, this.builder });
#override
Widget build(BuildContext context) {
final size = useState<Size>(null);
useEffect(() {
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
_overlay = OverlayEntry(
builder: (context) => Opacity(
child: SingleChildScrollView(
child: Container(
child: child,
key: _key,
),
),
opacity: 0.0,
),
);
Overlay.of(context).insert(_overlay);
WidgetsBinding.instance.addPostFrameCallback((timestamp) {
size.value = _key.currentContext.size;
_overlay.remove();
});
});
return () => null;
}, [child]);
if (size == null || size.value == null) {
return child;
} else {
return builder(context, child, size.value);
}
}
}
Usage:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class HomeView extends HookWidget {
#override
Widget build(BuildContext context) {
final change = useState<bool>(false);
final normal = Container(
color: Colors.blueAccent,
height: 200.0,
width: 200.0,
);
final big = Container(
color: Colors.redAccent,
height: 300.0,
width: 200.0,
);
return Column(
children: [
Container(
alignment: Alignment.center,
child: ChildSizeWidget(
child: change.value ? big : normal,
builder: (context, child, size) => AnimatedContainer(
alignment: Alignment.center,
child: SingleChildScrollView(child: child),
duration: Duration(milliseconds: 250),
height: size.height,
),
),
color: Colors.grey,
),
FlatButton(
child: Text('Toggle child'),
onPressed: () => change.value = !change.value,
color: Colors.green,
),
],
);
}
}
I have a menu with several options, they have different height and with the help of the animations this is ok, it's working really nice for me.
Why are you using Align, as much as I can see in your code, there is no property set or used, to align anything. So try removing Align widget around Transform.
Because according to the documentation, Transform is such a widget that tries to be the same size as their children. So that would satisfy your requirement.
For more info check out this documentation: https://flutter.dev/docs/development/ui/layout/box-constraints
I hope it helps!