Animating widget positions as the screen scrolls in flutter (GIF included) - user-interface

I am trying to animate two Rows of widgets to collapse into 1 Row of these widgets as one scroll. I am trying to achieve this behavior inside a SliverAppBar.
For clarification, I have included a GIF here for reference. I would like the behavior you see in the app bar, but instead of 1 row to 2, I would like 2 rows becoming 1.
Here is a quick snippet of what I have so far. I wrapped 2 Row widgets that contain 3 shrinkableBox widgets each into a Wrap widget. I dynamically adjust the size of these boxes by hooking into _scrollController.offset and doing some calculations. The rows do move around dynamically but they don't animate and move abruptly instead.
double kExpandedHeight = 300.0;
Widget build(BuildContext context) {
double size = !_scrollController.hasClients || _scrollController.offset == 0 ? 75.0 : 75 - math.min(45.0, (45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
title: new Text(
"Title!",
),
bottom: PreferredSize(child: Wrap(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
ShrinkableBox(
onClick: () {
print("tapped");
},
size: size,
),
],
),
],
), preferredSize: new Size.fromHeight(55),),
)
// ...
// ...Other sliver list content here...
// ...

You could use a Stack together with Positioned widgets to position the ShrinkableBoxes as you need. Since what controls the animation is the scroll offset, you don't need to use animated widgets or an animation controller or something like it. Here's a working example which calculates the positions by linearly interpolating the initial and final position of the boxes (you can get different animation paths by changing the Curves.linear to other curves):
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: Home()));
}
class Home extends StatefulWidget {
#override
State createState() => HomeState();
}
class HomeState extends State<Home> {
static const double kExpandedHeight = 300.0;
static const double kInitialSize = 75.0;
static const double kFinalSize = 30.0;
static const List<Color> kBoxColors = [
Colors.red,
Colors.green,
Colors.yellow,
Colors.purple,
Colors.orange,
Colors.grey,
];
ScrollController _scrollController = new ScrollController();
#override
void initState() {
_scrollController.addListener(() {
setState(() { /* State being set is the Scroll Controller's offset */ });
});
}
#override
void dispose() {
_scrollController.dispose();
}
Widget build(BuildContext context) {
double size = !_scrollController.hasClients || _scrollController.offset == 0
? 75.0
: 75 -
math.min(45.0,
(45 / kExpandedHeight * math.min(_scrollController.offset, kExpandedHeight) * 1.5));
return Scaffold(
body: CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: kExpandedHeight,
title: Text("Title!"),
bottom: PreferredSize(
preferredSize: Size.fromHeight(55),
child: buildAppBarBottom(size),
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
),
);
}
Widget buildAppBarBottom(double size) {
double t = (size - kInitialSize) / (kFinalSize - kInitialSize);
const double initialContainerHeight = 2 * kInitialSize;
const double finalContainerHeight = kFinalSize;
return Container(
height: lerpDouble(initialContainerHeight, finalContainerHeight, t),
child: LayoutBuilder(
builder: (context, constraints) {
List<Widget> stackChildren = [];
for (int i = 0; i < 6; i++) {
Offset offset = getInterpolatedOffset(i, constraints, t);
stackChildren.add(Positioned(
left: offset.dx,
top: offset.dy,
child: buildSizedBox(size, kBoxColors[i]),
));
}
return Stack(children: stackChildren);
},
),
);
}
Offset getInterpolatedOffset(int index, BoxConstraints constraints, double t) {
Curve curve = Curves.linear;
double curveT = curve.transform(t);
Offset a = getOffset(index, constraints, kInitialSize, 3);
Offset b = getOffset(index, constraints, kFinalSize, 6);
return Offset(
lerpDouble(a.dx, b.dx, curveT),
lerpDouble(a.dy, b.dy, curveT),
);
}
Offset getOffset(int index, BoxConstraints constraints, double size, int columns) {
int x = index % columns;
int y = index ~/ columns;
double horizontalMargin = (constraints.maxWidth - size * columns) / 2;
return Offset(horizontalMargin + x * size, y * size);
}
Widget buildSizedBox(double size, Color color) {
return Container(
height: size,
width: size,
color: color,
);
}
}

Related

Flutter random Image.asset changes on hovering Button?

I want to show a random picture everytime the user enters the page. I also have a Button (the red container with hovering) on this page, and when the user is hovering it, a new random picture shows, but it shouldn't change since the page was loaded. I think it has something to do with the setState(), but I don't know what to do. Code:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:core';
import 'dart:math';
class Home2 extends StatefulWidget {
const Home2({Key? key}) : super(key: key);
#override
_Home2State createState() => _Home2State();
}
class _Home2State extends State<Home2> {
dynamic listCinematicImages = [
"assets/cinematic/1.jpg",
"assets/cinematic/2.jpg",
"assets/cinematic/3.jpg",
"assets/cinematic/4.jpg",
"assets/cinematic/5.jpg",
"assets/cinematic/6.jpg",
"assets/cinematic/7.jpg",
];
late Random rnd;
#override
bool isHoveringButton = false;
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.black,
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: double.infinity,
width: size.width * 0.5,
alignment: Alignment.center,
child: InkWell(
onTap: () {
},
onHover: (hovering) {
setState(() => isHoveringButton = hovering);
},
child: Container(
height: 50,
width: 50,
color: Colors.red,
),
),
),
Container(
height: double.infinity,
width: size.width * 0.5,
child: img(),
),
],
),
);
}
Image img() {
int min = 0;
int max = listCinematicImages.length-1;
rnd = new Random();
int r = min + rnd.nextInt(max - min);
String image_name = listCinematicImages[r].toString();
return Image.asset(image_name, fit: BoxFit.cover,);
}
}
I dont know if this helps but this is some error given out:
Error: Expected a value of type 'Map<String, dynamic>', but got one of type 'Null'
at Object.throw_ [as throw] (http://localhost:60569/dart_sdk.js:5054:11)
at Object.castError (http://localhost:60569/dart_sdk.js:5013:15)
at Object.cast [as as] (http://localhost:60569/dart_sdk.js:5336:17)
at Function.as_C [as as] (http://localhost:60569/dart_sdk.js:4959:19)
alright there's a lot a things going on here. Let's think about this step by step. You're trying to show a new image on certain events. So we need to create a variable to keep track of the current image:
class Home2 extends StatefulWidget {
const Home2({Key? key}) : super(key: key);
#override
_Home2State createState() => _Home2State();
}
class _Home2State extends State<Home2> {
static const listCinematicImages = [
"assets/cinematic/1.jpg",
"assets/cinematic/2.jpg",
"assets/cinematic/3.jpg",
"assets/cinematic/4.jpg",
"assets/cinematic/5.jpg",
"assets/cinematic/6.jpg",
"assets/cinematic/7.jpg",
];
late String currentImage;
}
before we start worrying about the events, let's figure out how to select a random image from your list. the function you provided has the right elements but there's some funky stuff going on. a simple example I'd give is:
String _getRandomImage() {
final randomIndex = Random().nextInt(listCinematicImages.length-1);
return listCinematicImages[randomIndex];
}
now we have all the elements, we just have to update the widget at the correct time. firstly you want to set a new image every time this widget is loaded. we can do this using the initState method:
#override
void initState() {
super.initState();
final newImage = _getRandomImage();
setState(() => currentImage = newImage);
}
and you want to change the image again when a user hovers over the image. You can indeed do this with an InkWell but keep in mind according to the documentation the onHover will provide a callback every time a mouse enters AND leaves the region, so we have to make sure to only update the image when we enter the region:
InkWell(
onHover: (bool hasEntered) {
if(!hasEntered) return;
final newImage = _getRandomImage();
setState(() => currentImage = newImage);
}
);
And that's it! to put it all together with your example:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:core';
import 'dart:math';
class Home2 extends StatefulWidget {
const Home2({Key? key}) : super(key: key);
#override
_Home2State createState() => _Home2State();
}
class _Home2State extends State<Home2> {
static const listCinematicImages = [
"assets/cinematic/1.jpg",
"assets/cinematic/2.jpg",
"assets/cinematic/3.jpg",
"assets/cinematic/4.jpg",
"assets/cinematic/5.jpg",
"assets/cinematic/6.jpg",
"assets/cinematic/7.jpg",
];
late String currentImage;
#override
void initState() {
super.initState();
final newImage = _getRandomImage();
setState(() => currentImage = newImage);
}
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.black,
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: double.infinity,
width: size.width * 0.5,
alignment: Alignment.center,
child: InkWell(
onHover: (bool hasEntered) {
if(!hasEntered) return;
final newImage = _getRandomImage();
setState(() => currentImage = newImage);
},
child: Container(
height: 50,
width: 50,
color: Colors.red,
),
),
),
Container(
height: double.infinity,
width: size.width * 0.5,
child: Image.asset(currentImage, fit: BoxFit.cover,);,
),
],
),
);
}
String _getRandomImage() {
final randomIndex = Random().nextInt(listCinematicImages.length-1);
return listCinematicImages[randomIndex];
}
}

Flutter ListView.separated frame drop when loading huge list

I'm trying to display a lazy list in flutter for windows. The list has approximately 2300 elements in it. The list is within a FutureBuilder, whose future is to fetch the 2300 elements from Hive database. Each element in the list is a MaterialButton with some properties. Im not getting smooth scrolling when scrolled fast. Some frames are being dropped. I have tried cacheextend and setting automatickeepalives to true. Still having the same problem. When ItemExtend is set to a large number(say 40), the scrollView works fine without frame drop. In release mode, it has better performance, but still some frames are being dropped. What is the best solution available to this problem?
//this Rawscrollbar is returned if the future is have some data
RawScrollbar(
isAlwaysShown: true,
controller: scrollControllerMLC,
thickness: context.percentWidth * .8,
radius: Radius.zero,
thumbColor:
SearchLeftContainerColors.headPoolListThumbColor,
child: ListView.separated(
padding: EdgeInsets.fromLTRB(
context.percentWidth * .5,
context.percentHeight * 0,
context.percentWidth * 1,
context.percentHeight * 1),
itemCount: lengthOfBoxes,
controller: scrollControllerMLC,
// addAutomaticKeepAlives: true,
// physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, int index) {
return ListButtonMEDLC(data[index], index);
},
separatorBuilder: (BuildContext context, int index) {
return Divider(
color: SearchLeftContainerColors
.headsPoolSeparatorColour,
height: 1,
);
},
));
class ListButtonMEDLC extends StatelessWidget {
final String text;
final int index;
ListButtonMEDLC(this.text, this.index);
#override
Widget build(BuildContext context) {
return MaterialButton(
color:
(context.watch<ListButtonColorChangerMEDLC>().isSelectedList[index])
? SearchLeftContainerColors.headPoolListSelectedColor
: SearchLeftContainerColors.headPoolListColor,
hoverColor:
(context.watch<ListButtonColorChangerMEDLC>().isSelectedList[index])
? SearchLeftContainerColors.headPoolListSelectedColor
: SearchLeftContainerColors.headPoolListHoverColor,
highlightColor: SearchLeftContainerColors.headPoolListHighLightedColor,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
text,
style: TextStyle(
fontSize: context.percentWidth * 1.1,
color: (context
.watch<ListButtonColorChangerMEDLC>()
.isSelectedList[index])
? Colors.white
: Colors.black),
),
),
onPressed: () {
context.read<ListButtonColorChangerMEDLC>().changeIsSelectedList(index);
},
);
}
}
//this it the future of the future builder;
loadDrugBox() async {
Map boxes = await DB.boxes.getBoxAsMap("drugs");
lengthOfBoxes = boxes.length;
return boxes;
}
//Provider
class ListButtonColorChangerMEDLC extends ChangeNotifier {
List<bool> isSelectedList = List.generate(lengthOfBoxes, (index) => false);
changeIsSelectedList(int indexOfSelected) {
for (int i = 0; i < lengthOfBoxes; i++) {
if (i == indexOfSelected) {
isSelectedList[i] = true;
} else
isSelectedList[i] = false;
}
notifyListeners();
}
}
Yes. I solved this issue by replacing material button which was an expensive widget with gestureDetector.

Flutter make image with GestureDetectors also Draggable

My goal is to have an image that I can zoom & move around inside a CustomClipperImage and it should also be Draggable!
Right now I can scale the image in its Clip and this looks like this:
Screenvideo
This is the code for it:
child: Container(
height: _containetWidth,
width: _containetWidth,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.white, width: 5),
),
child: GestureDetector(
onTap: () => print("tapped"),
onScaleStart: (details) {
_startingFocalPoint.value = details.focalPoint;
_previousOffset.value = _offset.value;
_previousZoom.value = _zoom.value;
},
onScaleUpdate: (details) {
_zoom.value = _previousZoom.value * details.scale;
final Offset normalizedOffset =
(_startingFocalPoint.value - _previousOffset.value) /
_previousZoom.value;
_offset.value =
details.focalPoint - normalizedOffset * _zoom.value;
},
child: Stack(
children: [
ClipPath(
clipper: CustomClipperImage(),
child: Transform(
transform: Matrix4.identity()
..translate(_offset.value.dx, _offset.value.dy)
..scale(_zoom.value),
child: Image.asset('assets/images/example.jpg',
width: _containetWidth,
height: _containetWidth,
fit: BoxFit.fill),
),
),
CustomPaint(
painter: MyPainter(),
child: Container(
width: _containetWidth, height: _containetWidth),
),
],
),
),
),
But I can not make it Draggable... I tried wrapping the whole Container or also just the Image.asset inside Draggable but when doing this, scaling stops working and Draggable is not working either.
What is the best way to achieve this? I couldn't find anything on this... Let me know if you need more details!
The problem you have is a conflict between:
zooming and dragging the image inside the custom ClipPath
dragging the images between two custom ClipPath
The solution I propose is to use drag handles to swap the images
!!! SPOILER : It does not work (yet) !!!
To implement this drag-n-drop with custom ClipPath, we need the support of HitTestBehavior.deferToChild on DragTarget.
The good news is... It's already available in Flutter master channel! [ref]
So, if you can wait a bit for it to be released in stable, here is my solution:
The main idea is to have the zoomable images as DragTargets and for each image a drag handle as Draggable.
I added a layer of State Management to keep the zoom level and offset when swapping the images.
I also improved the zoomable feature to ensure that the image always covers the full ClipPath.
Full source code (250 lines)
import 'dart:math' show min, max;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
part '66474773.drag.freezed.dart';
void main() {
runApp(
ProviderScope(
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
home: HomePage(),
),
),
);
}
class HomePage extends HookWidget {
#override
Widget build(BuildContext context) {
final images = useProvider(imagesProvider.state);
final _width = MediaQuery.of(context).size.shortestSide * .8;
void swapImages() => context.read(imagesProvider).swap();
return Scaffold(
backgroundColor: Colors.black87,
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Container(
height: _width,
width: _width,
child: Stack(
children: [
DragTarget<VerticalDirection>(
hitTestBehavior: HitTestBehavior.deferToChild,
onWillAccept: (direction) =>
direction == VerticalDirection.up,
onAccept: (_) => swapImages(),
builder: (_, __, ___) => _Zoomable(
key: GlobalKey(),
width: _width,
pathFn: topPathFn,
imageId: 0,
),
),
DragTarget<VerticalDirection>(
hitTestBehavior: HitTestBehavior.deferToChild,
onWillAccept: (direction) =>
direction == VerticalDirection.down,
onAccept: (_) => swapImages(),
builder: (_, __, ___) => _Zoomable(
key: GlobalKey(),
width: _width,
pathFn: bottomPathFn,
imageId: 1,
),
),
Positioned.fill(
child: Align(
alignment: Alignment.topLeft,
child: _DragHandle(
direction: VerticalDirection.down,
imgAssetPath: images[0].assetPath,
),
),
),
Positioned.fill(
child: Align(
alignment: Alignment.bottomRight,
child: _DragHandle(
direction: VerticalDirection.up,
imgAssetPath: images[1].assetPath,
),
),
),
],
)),
),
);
}
}
class _DragHandle extends StatelessWidget {
final VerticalDirection direction;
final String imgAssetPath;
const _DragHandle({Key key, this.direction, this.imgAssetPath})
: super(key: key);
#override
Widget build(BuildContext context) {
return Draggable<VerticalDirection>(
data: direction,
child: Container(
decoration: BoxDecoration(
color: Colors.grey.shade200,
border: Border.all(color: Colors.grey.shade700),
),
child: Icon(Icons.open_with),
),
childWhenDragging: Container(),
feedback: Image.asset(imgAssetPath, width: 80),
);
}
}
class _Zoomable extends HookWidget {
final double width;
final Path Function(Size) pathFn;
final int imageId;
const _Zoomable({
Key key,
this.width,
this.pathFn,
this.imageId,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final image =
useProvider(imagesProvider.state.select((state) => state[imageId]));
final _startingFocalPoint = useState(Offset.zero);
final _previousOffset = useState<Offset>(null);
final _offset = useState(image.offset);
final _previousZoom = useState<double>(null);
final _zoom = useState(image.zoom);
return CustomPaint(
painter: MyPainter(pathFn: pathFn),
child: GestureDetector(
onTap: () {}, // onScaleUpdate not triggered if onTap is not defined
onScaleStart: (details) {
_startingFocalPoint.value = details.focalPoint;
_previousOffset.value = _offset.value;
_previousZoom.value = _zoom.value;
},
onScaleUpdate: (details) {
_zoom.value = max(1, _previousZoom.value * details.scale);
final newOffset = details.focalPoint -
(_startingFocalPoint.value - _previousOffset.value) *
details.scale;
_offset.value = Offset(
min(0, max(-width * (_zoom.value - 1), newOffset.dx)),
min(0, max(-width * (_zoom.value - 1), newOffset.dy)),
);
},
onScaleEnd: (_) => context.read(imagesProvider).update(
imageId, image.copyWith(zoom: _zoom.value, offset: _offset.value)),
child: ClipPath(
clipper: MyClipper(pathFn: pathFn),
child: Transform(
transform: Matrix4.identity()
..translate(_offset.value.dx, _offset.value.dy)
..scale(_zoom.value),
child: Image.asset(
image.assetPath,
width: width,
height: width,
fit: BoxFit.fill,
),
),
),
),
);
}
}
Path bottomPathFn(Size size) => Path()
..moveTo(size.width, 0)
..lineTo(0, size.height)
..lineTo(size.height, size.height)
..close();
Path topPathFn(Size size) => Path()
..moveTo(size.width, 0)
..lineTo(0, size.height)
..lineTo(0, 0)
..close();
class MyClipper extends CustomClipper<Path> {
final Path Function(Size) pathFn;
MyClipper({this.pathFn});
#override
getClip(Size size) => pathFn(size);
#override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
class MyPainter extends CustomPainter {
final Path Function(Size) pathFn;
Path _path;
MyPainter({this.pathFn});
#override
void paint(Canvas canvas, Size size) {
_path = pathFn(size);
final paint = Paint()
..color = Colors.white
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
canvas.drawPath(_path, paint);
}
#override
bool hitTest(Offset position) {
return _path?.contains(position);
}
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
final imagesProvider =
StateNotifierProvider<ImagesNotifier>((ref) => ImagesNotifier([
ZoomedImage(assetPath: 'images/abstract.jpg'),
ZoomedImage(assetPath: 'images/abstract2.jpg'),
]));
class ImagesNotifier extends StateNotifier<List<ZoomedImage>> {
ImagesNotifier(List<ZoomedImage> state) : super(state);
void swap() {
state = state.reversed.toList();
}
void update(int id, ZoomedImage updatedImage) {
state = [...state]..[id] = updatedImage;
}
}
#freezed
abstract class ZoomedImage with _$ZoomedImage {
const factory ZoomedImage({
String assetPath,
#Default(1.0) double zoom,
#Default(Offset.zero) Offset offset,
}) = _ZoomedImage;
}

Flutter AnimatedList still janky?

I'm currently using an AnimatedList in my Flutter app and having problems with the way removed list items are animated out. The animation itself works as expected but once the removed item finishes animating, it just disappears causing the other widgets to jump into its place. I had expected the other items to transition into the place of the removed item ...
I tried wrapping my list items with a ScaleTransition but that didn't help - the other list items still do not react to the removed item until it has finished the animation.
This kind of defies the purpose of AnimatedList, right? Or did I do something wrong? The "Widget of the week" video about AnimatedList clearly shows that list items react to newly inserted items by changing their position ...
Here is my code:
#override
Widget build(BuildContext context) {
return AnimatedList(
padding: EdgeInsets.only(top: REGULAR_DIM,
bottom: REGULAR_DIM + kBottomNavigationBarHeight),
initialItemCount: data.length,
itemBuilder: (context, index, animation) {
return MyCustomWidget(
data: data[index],
animation: animation,
disabled: false
);
},
);
}
class MyCustomWidget extends AnimatedWidget {
final MyModel data;
final bool disabled;
MyCustomWidget({
#required this.data,
#required Animation<double> animation,
this.disabled = false
}) : super(listenable: animation);
Animation<double> get animation => listenable;
#override
Widget build(BuildContext context) {
final content = ... ;
return ScaleTransition(
scale: CurvedAnimation(
parent: animation,
curve: Interval(0, 0.25)
).drive(Tween(begin: 0, end: 1)),
child: FadeTransition(
opacity: animation,
child: SlideTransition(
position: animation.drive(
Tween(begin: Offset(-1, 0), end: Offset(0, 0))
.chain(CurveTween(curve: Curves.easeOutCubic))),
child: content,
),
),
);
}
}
And then somewhere in the MyCustomWidget I invoke this function:
void _remove(BuildContext context) async {
final animatedList = AnimatedList.of(context);
// obtain myModel asynchronously
myModel.removeData(data);
animatedList.removeItem(index, (context, animation) => MyCustomWidget(
data: data,
animation: animation,
disabled: true,
), duration: Duration(milliseconds: 350));
}
The key is to trigger two Transitions one SlideTranstion() and another SizeTransition to eliminate to jump when the item is removed
here is some sample code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: Text('Update AnimatedList data')),
body: BodyWidget(),
),
);
}
}
class BodyWidget extends StatefulWidget {
#override
BodyWidgetState createState() {
return new BodyWidgetState();
}
}
class BodyWidgetState extends State<BodyWidget>
with SingleTickerProviderStateMixin {
// the GlobalKey is needed to animate the list
final GlobalKey<AnimatedListState> _listKey = GlobalKey(); // backing data
List<String> _data = ['Horse', 'Cow', 'Camel', 'Sheep', 'Goat'];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(
height: 400,
child: AnimatedList(
key: _listKey,
initialItemCount: _data.length,
itemBuilder: (context, index, animation) {
return _buildItem(
_data[index],
animation,
);
},
),
),
RaisedButton(
child: Text(
'Insert single item',
style: TextStyle(fontSize: 20),
),
onPressed: () {
_onButtonPress();
},
),
RaisedButton(
child: Text(
'Remove single item',
style: TextStyle(fontSize: 20),
),
onPressed: () {
_removeSingleItems();
},
),
],
);
}
Widget _buildItem(String item, Animation<double> animation, {direction: 0}) {
return (direction == 0)
? SizeTransition(
sizeFactor: animation,
child: Card(
color: Colors.amber,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
)
: Stack(
children: [
SizeTransition(
sizeFactor: animation,
child: Card(
color: Colors.transparent,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
),
Align(
alignment: Alignment.topCenter,
heightFactor: 0,
child: SlideTransition(
position: animation
.drive(Tween(begin: Offset(-1, 0), end: Offset(0, 0))),
child: Card(
color: Colors.red,
child: ListTile(
title: Text(
item,
style: TextStyle(fontSize: 20),
),
),
),
),
),
],
);
}
void _onButtonPress() {
_insertSingleItem();
}
void _insertSingleItem() {
String item = "Pig";
int insertIndex = 2;
_data.insert(insertIndex, item);
_listKey.currentState.insertItem(insertIndex);
}
void _removeSingleItems() {
int removeIndex = 2;
String removedItem = _data.removeAt(removeIndex);
// This builder is just so that the animation has something
// to work with before it disappears from view since the
// original has already been deleted.
AnimatedListRemovedItemBuilder builder = (context, animation) {
// A method to build the Card widget.
return _buildItem(removedItem, animation, direction: 1);
};
_listKey.currentState.removeItem(removeIndex, builder);
}
void _updateSingleItem() {
final newValue = 'I like sheep';
final index = 3;
setState(() {
_data[index] = newValue;
});
}
}
enter code here
You need to test the performance with the release version of your app.

How to scroll or jump to position of PageView.builder or PageController in Flutter?

Issue: Unable to scroll to POSITION after loading the Pageviews using PageController *
like ViewPager scroll to specific page in Android
Widget _buildCarousel(BuildContext context, int selectedIndex) {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SizedBox(
// you may want to use an aspect ratio here for tablet support
height: 400.0,
width: 240,
child: PageView.builder(
itemCount: assetImageList.length,
controller: controller,
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
)
],
);
}
Finally found the answer. Just set the initialPage: mSelectedPosition attribute like:
child: PageView.builder(
itemCount: mTemplateModelList.length,
controller: PageController(initialPage: mSelectedPosition, keepPage: true, viewportFraction: 1),
itemBuilder: (BuildContext context, int itemIndex) {
return _buildCarouselItem(context, selectedIndex, itemIndex);
},
),
OR if you want to scroll the page after the button is clicked then, you can use jumpTo() method using PageController which is clearly mentioned below by another user: #android.
Currently there's 2 options to handle your request:
PageView.builder(
controller: _pageController,
itemCount: _list.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_pageController.jumpToPage(index); // for regular jump
_pageController.animateToPage(_position, curve: Curves.decelerate, duration: Duration(milliseconds: 300)); // for animated jump. Requires a curve and a duration
},
child: Container();
);
}
),
You can use jumpTo() method to scroll position for PageView.
I have create one changePageViewPostion() method in below example:
import 'package:flutter/material.dart';
class MyPageView extends StatefulWidget {
createState() {
return StateKeeper();
}
}
class StateKeeper extends State<MyPageView> {
PageController controller = PageController(viewportFraction: 1, keepPage: true);
var currentPageValue = 0.0;
var mItemCount = 10;
#override
void initState() {
// TODO: implement initState
super.initState();
controller.addListener(() {
setState(() {
currentPageValue = controller.page;
});
});
}
void changePageViewPostion(int whichPage) {
if(controller != null){
whichPage = whichPage + 1; // because position will start from 0
double jumpPosition = MediaQuery.of(context).size.width / 2;
double orgPosition = MediaQuery.of(context).size.width / 2;
for(int i=0; i<mItemCount; i++){
controller.jumpTo(jumpPosition);
if(i==whichPage){
break;
}
jumpPosition = jumpPosition + orgPosition;
}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PageView position change'),
),
body: PageView.builder(
controller: controller,
itemBuilder: (context, position) {
return Container(
color: position % 2 == 0 ? Colors.blue : Colors.pink,
child: Column(
children: <Widget>[
Center(
child: Text(
"Page " + (position + 1).toString(),
style: TextStyle(color: Colors.white, fontSize: 22.0),
),
),
Align(
alignment: FractionalOffset.bottomCenter,
child: Padding(padding: EdgeInsets.only(bottom: 20),
child: FloatingActionButton(
elevation: 0.0,
child: new Icon(Icons.check),
backgroundColor: new Color(0xFFE57373),
onPressed: (){
changePageViewPostion(5);
}
),),
),
],
),
);
},
itemCount: mItemCount,
)
);
}
}
We can get current position with controller as below:
controller.addListener(() {
setState(() {
currentPageValue = controller.page.toInt();
print((currentPageValue + 1).toString());
});
});
Hope it helps :)
If you just want to scroll to the next page using a button, you can simply use the following method.
//Create a PageController variable
late PageController _pageController;
//Initialize the variable in the init method.
#override
void initState() {
_pageController = PageController(
initialPage: _activePage, keepPage: true, viewportFraction: 1);
super.initState();
}
//Use this nextPage() method in the onPressed() method.
onPressed: () {
setState(() {
_activePage < 2
? _activePage++
: Navigator.pushReplacementNamed(
context, LoginScreen.id);
});
_pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.decelerate,
);
}

Resources