I need to create an overlapping pageview collection, but because of draw/layout order of items the second page always shows up in front of first page. There a way to create a collection list that the first items overlapping the others?
PAGE BUILDER ->
Widget buildList(PreloadPageController pageController, List data,
double currentPosition) {
return AspectRatio(
aspectRatio: 12.0 / 15.0,
child: PreloadPageView.builder(
itemCount: data.length,
controller: pageController,
preloadPagesCount: 2,
itemBuilder: (context, index) {
return CardWidget(
page: index,
currentPage: currentPosition,
);
},
),
);
}
CARD WIDGET ->
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, contraints) {
final double padding = 20.0;
var delta = currentPage - page;
var start = padding * delta.abs() * 10;
var top = padding + padding * max(-delta, 0.0);
var bottom = padding + padding * max(-delta, 0.0);
//print(start);
return Transform.translate(
offset: Offset(-start, 0),
child: Container(
padding: EdgeInsets.only(top: top, bottom: bottom),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Container(
color: _randomColor(page),
),
),
),
);
});
}
I was expecting create a collection effect so the second page would come from behind the first one, but actually second pages always appears overlapping the first.
I could use reverse in PageView.builder, but this collection needs to be a infinity list that loads more data when it reaches the end and with reverse the code will be alot trickier.
I'm achieving this:
But what I want is the blue card behind the red one.
So right now, to create your overlapping effect, you are offsetting the next-Page so that it overlaps the cur-Page, and as you mentioned, you discover that the next-Page visually over-laps instead of the desired under-lap.
Then, in additional to your offset, have you tried cropping off the overlapping portion of next-Page? This can simulate the effect of under-lapping.
Now, I tried replicating your sample, but I'm uncertain about your PreloadPageController (and maybe other details), so my sample might look glitchy. Additionally, I'm not wholly familiar with cropping widgets. But I bumped into this possible solution, all I did was wrap it with another ClipRect:
#override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final double padding = 20.0;
var delta = widget.currentPage - widget.myPage;
var start = padding * delta.abs() * 10;
var top = padding + padding * max(-delta, 0.0);
var bottom = padding + padding * max(-delta, 0.0);
return ClipRect(
child: Transform.translate(
offset: Offset(-start, 0),
child: Container(
padding: EdgeInsets.only(top: top, bottom: bottom),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.0),
child: Container(
color: redOrBlue(widget.myPage),
),
),
),
),
);
});
}
This additional ClipRect basically clips off your offset portion. Feel free to explore and modify as needed!
There's an Overlay widget but it may cause more headaches, especially at scale. You can wrap your pages in a ClipRect, but only the widgets at the end of the list will need it. So in the builder function, set up a bool:
clipNeeded = (controller.page + 0.5) <= index;
Then in the new ClipRect widget:
clipper: clipNeeded ? CustomRect() : null,
Then you'll need to create that CustomRect class, extending CustomClipper<Rect>
The exact math required depends on the implementation, but in this left to right scroll example, it'll be something like
class CustomRect extends CustomClipper<Rect>{
#override
Rect getClip(Size size) {
double leftLine = /* some calculation */;
return Rect.fromLTRB(leftLine, 0.0, size.width, size.height);
}
#override
bool shouldReclip(CustomRect oldClipper) {
return true;
}
}
Related
I am building a dynamic scroll view. The current result is this one.
The problem is that the items are not positioned on top at position 0, as I intended.
I tried already placing the searchbar over the top extra space of the scroll view, but when you start scrolling, the cards are overlapping the searchbar widget.
So my question is: what is causing this extra space, and how can I remove it?
ScrollList class:
class ScrollListWidget extends DersObject {
late ListHandler listHandler;
int cardsPerScreen;
double margin;
double getCardHeight(){
return this.height / this.cardsPerScreen - heightPercentageAsDouble(this.margin) * 2;
}
ScrollListWidget(width, height, listType, {this.cardsPerScreen = 3, this.margin = 0.01}): super(width: width, height: height){
this.listHandler = ListHandler(width, getCardHeight(), listType);
}
#override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
child: new Scaffold(
body: new ListView.separated
(
separatorBuilder: (BuildContext context, int index) {
return Line(height: heightPercentageAsDouble(this.margin), width: width,
color: Color.fromARGB(255, 255, 255, 255));
},
itemCount: listHandler.getList().length,
itemBuilder: (BuildContext ctxt, int index) {
return listHandler.getList()[index];
}
)
)
);
}
}
From documentation of ListView:
By default, ListView will automatically pad the list's scrollable extremities to avoid partial obstructions indicated by MediaQuery's padding. To avoid this behavior, override with a zero padding property.
So try to add this to the ListView constructor:
padding: EdgeInsets.zero,
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.
I'm trying to create a parallax background in a Flutter app, and the most efficient way to build it is to use a Stack with the image filling the screen as a background and then my list on top. The image is tiled with an ImageRepeat set on the Y axis. The plan is to then offset the origin of the tile in sync with the ScrollController I'm using for my list. I can then adjust the origin of the tiled image to create the parallax effect. It should be really simple. Here's some code for context:
Stack(
children: [
SizedBox.expand(
child: Image(
image: AssetImage('assets/images/tiled_background_leaf.jpg'),
repeat: ImageRepeat.repeatY,
),
),
CustomScrollView(
controller: _controller,
slivers: [ ...
My problem is that Image does not have an offset property, or an origin position. I need some advice on the easiest way to do this. I've seen that there are custom painters, canvas methods etc, but they all seem massively over-complicated when there should be a more elegant solution within the Image widget, or possibly within another widget that would give me the same parallax effect.
Thanks to #pskink for the answer to this (see comments above).
Here's some code for a dashboard that has a scrolling list of articles and the parallax scrolling tiled image as a background ...
class DashboardRoot extends StatefulWidget {
DashboardRoot({Key key}) : super(key: key);
#override
_DashboardRootState createState() => _DashboardRootState();
}
class _DashboardRootState extends State<DashboardRoot> {
int _currentIndex = 0;
ScrollController _controller;
double _offsetY = 0.0;
_scrollListener() {
setState(() {
_offsetY = _controller.offset;
});
}
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
var state = Provider.of<ArticlesState>(context, listen: false);
state.initArticleStream();
});
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: AppBottomNavigationBar(),
body: Stack(
children: [
SizedBox.expand(
child: Image(
image: AssetImage('assets/images/tiled_background_leaf.jpg'),
repeat: ImageRepeat.repeatY,
alignment: FractionalOffset(0, (_offsetY / 1000) * -1),
),
),
CustomScrollView(
controller: _controller,
slivers: [
SliverAppBar(
elevation: 0.0,
floating: true,
expandedHeight: 120,
flexibleSpace: FlexibleSpaceBar(
title: Text(NavigationManager
.instance.menuItems[_currentIndex].title),
),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
onPressed: () => {
locator<NavigationService>()
.navigateTo(SettingsNavigator.routeName)
},
),
IconButton(
icon: Icon(Icons.menu),
onPressed: () => {RootScaffold.openDrawer(context)},
),
],
),
Consumer<ArticlesState>(
builder: (context, state, child) {
final List<Article> list = state.articles;
if (list == null) {
return SliverToBoxAdapter(
child: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.amber, strokeWidth: 1),
),
);
} else if (list.length > 0) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 1.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
Article article = list[index];
return ArticleCell(
article: article,
cellTapHandler: () {
Navigator.pushNamed(
context, ArticleDetail.routeName,
arguments: new ArticleDetailArguments(
article.docId, article.heading));
});
},
childCount: list.length,
),
);
} else {
return Center(
child: Text("No Articles"),
);
}
},
),
],
),
],
));
}
}
Notice the Stack has the background image inside an expanded SizedBox so it fills the screen space. The layer above is the CustomScrollView which has the SliverGrid and other stuff.
The important bit is the Image:
child: Image(
image: AssetImage('assets/images/tiled_background_leaf.jpg'),
repeat: ImageRepeat.repeatY,
alignment: FractionalOffset(0, (_offsetY / 1000) * -1),
),
and also the property _offsetY which is set by the ScrollController listener as the users scroll:
double _offsetY = 0.0;
_scrollListener() {
setState(() {
_offsetY = _controller.offset;
});
}
The Image alignment property is used to set the alignment to top, centre, left etc. but it can also be an arbitrary offset. The FractionalOffset value is a range 0..1 but setting it as a larger number above or below zero is also absolutely fine. Because the image is also tiled using ImageRepeat.repeatY the origin of the tiled image is redrawn using alignment, and by messing around with the number, you can create a nice parallax scrolling effect.
Notice that FractionalOffset(0, (_offsetY / 1000) * -1) has the offset value divided by 1000 (this is your speed, and the higher the value the slower the parallax of the background (think of it as the distance between the two layers). Multiplying a number by -1 switches between a positive and negative number, and changes the direction of the parallax.
I want to implement this horizontal ListView effect.
Planning to create a horizontal ListView like this. Example the horizontal listView will show 2 items and 1 item only show up 20%.
When scrolling it will become like this. Example front and end show up 20% and center show 2 items.
Edited : Code I'm using right now :
viewportFraction = 1 / 2.3;
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double itemWidth =
(constraints.maxWidth - padding.horizontal) * this.viewportFraction;
final double itemHeight = (itemWidth * this.aspectRatio);
return new Container(
height: itemHeight,
child: new ListView.custom(
scrollDirection: Axis.horizontal,
controller: new PageController(
initialPage: this.initialPage,
viewportFraction: this.viewportFraction,
),
physics: const PageScrollPhysics(),
padding: this.padding,
itemExtent: itemWidth,
childrenDelegate: this.childrenDelegate,
),
);
});
The easiest way of doing this is probably with an horizontal ListView builder
#override
Widget build(BuildContext context) {
return ListView.builder(
scrollDirection:Axis.horizontal,
itemCount:10,
itemBuilder:(context,index){
return Padding(
padding:EdgeInsets.symmetric(horizontal:MediaQuery.of(context).size.width*0.05),
child:Container(
color: Colors.blue,
child:Text(index.toString()),
height:20,
width:MediaQuery.of(context).size.width*0.25,
)
);
}
);
}
The MediaQuery.of(context).size allows you to get information about your sreen geometry
I'll let you do some math to find the right fraction of the screen to use to get the final result that you want
I have a widget wrapped in a Consumer, that i want to smoothly grow and then return to its original size on notification from a ChangeNotifier.
I've managed to get the animation to grow, but not shrink again, so it just keeps getting bigger and bigger with each notifyListeners() call. I did that with an AnimatedContainer widget. I got the animation i wanted when i defined it in the initState method and manually calling animationController.forward().whenComplete(() => animationController.reverse()).
Any help to do that using Consumer as my trigger.
(edit)
This is the AnimatedContainer. The height and width aren't related to the value coming back from the state, but i'm using state.value ^ 0 just to trigger the resize. I know this will only grow in size, but thats why i'm asking how to shrink the sucker.
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
height: _height * 1.1 * (state.value ^ 0),
width: _width * 1.1 * (state.value ^ 0),
child: SomeWidgetUsingTheState()...
),```
UPDATE
Since you want to have some kind of "StaggeredAnimaiton", meaning animate between multiple values (here back and forth) and you are using the implicit approach by using AnimatedContainer, the following code could be used (not recommended though):
My state / store
class ContainerState extends ChangeNotifier {
double height = 100.0;
double width = 150.0;
/// Needs the duration of the AnimatedContainer so we know
/// when we can start the reverse animation (by setting the values
/// back to normal)
startWiggle(Duration duration) {
this.height = 2 * height;
this.width = 2 * width;
notifyListeners();
Future.delayed(duration, () {
this.height = height / 2;
this.width = width / 2;
notifyListeners();
});
}
}
Widget tree for reference
class BaseView extends StatelessWidget {
#override
Widget build(BuildContext context) {
/// Since we are using ChangeNotifier as our base type for
/// indicating State objects, Provider has a designated Widget for that
return ChangeNotifierProvider(
create: (context) => ContainerState(),
builder: (context, _) => Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text('Title'),
pinned: true,
),
SliverList(
delegate: SliverChildListDelegate(
[
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Align(
child: GestureDetector(
onTap: () => context
.read<ContainerState>()
.startWiggle(Duration(milliseconds: 500)),
/// Gets rebuilded every time notifyListeners is called
/// inside ContainerState, therefore when we change the size
child: Consumer<ContainerState>(
builder: (context, containerState, child) =>
AnimatedContainer(
duration: const Duration(milliseconds: 500),
curve: Curves.easeIn,
height: containerState.height,
width: containerState.width,
color: Colors.red,
),
),
),
),
),
],
),
),
],
),
),
);
}
}
What you want to achieve here is not really suitable for this kind of animation (implicit animation). I would recommend to switch to direct animations using your own AnimationController instead so you have better control of how it should behave. My slides may help you here to get started: https://assets.kounex.com/flutter/uni_project_2020/en/04_flutter_animations.pdf
I don't understand what the caret sign is for. But I would do something like this :
animateContainer() async {
value = containerSize;
await Future.delayed(Duration(milliseconds: 500));
value = 0;
}