After some tinkering and Googling all over the place I became super frustrated with the API / lack of documentation for ScrollPhysics.
On Android you can use what's called a SnapHelper inside your RecyclerView (analogous to a ListView in Flutter) that will automatically snap to a certain position.
The SnapHelper does this on a position based API.
You can ask which View is currently in your chosen ViewPort and get its position and ask the RecyclerView to animate to that position.
Flutter on the other hand wants us to work with logical pixels, which makes this super trivial, common pattern difficult to implement.
All the solutions I found was to use items inside the list that have a fixed width/height and don't account for flinging gestures.
tl;dr How to implement this ๐in Flutter so it works for any item in a ListView?
I'm including the poor mans version we are currently using.
Which works, but not like we are used to on Android.
Especially passing the itemWidth is an eyesore
class SnappingListScrollPhysics extends ScrollPhysics {
final double itemWidth;
const SnappingListScrollPhysics({
#required this.itemWidth,
ScrollPhysics parent,
}) : super(parent: parent);
#override
SnappingListScrollPhysics applyTo(ScrollPhysics ancestor) => SnappingListScrollPhysics(
parent: buildParent(ancestor),
itemWidth: itemWidth,
);
double _getItem(ScrollPosition position) => (position.pixels) / itemWidth;
double _getPixels(ScrollPosition position, double item) => min(item * itemWidth, position.maxScrollExtent);
double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
double item = _getItem(position);
if (velocity < -tolerance.velocity) {
item -= 0.5;
} else if (velocity > tolerance.velocity) {
item += 0.5;
}
return _getPixels(position, item.roundToDouble());
}
#override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
return super.createBallisticSimulation(position, velocity);
}
final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity);
if (target != position.pixels) {
return ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
}
return null;
}
#override
bool get allowImplicitScrolling => false;
}
Related
I'm making a runner game which has the infinity mode.
So, I got to generate a terrain every 1s approximately.
It has some lag with my LG Q9 One, I checked the performance and it appeared that there's some problem with Raster thread.
I tried the game on Samsung Galaxy Note 10 which has better gpu and it shows less janks(but it has janks anyway).
I adds my code here. Any better way to enhance the performance?
class MyGame extends FlameGame {
final Grid grid = Grid();
final EndlessMap endlessMap = EndlessMap();
final Runner runner = Runner();
double speed = 100.0;
#override
Future<void>? onLoad() {
add(grid);
add(endlessMap);
add(runner);
return super.onLoad();
}
}
/// Camera follows the runner.
class Runner extends PositionComponent with HasGameRef<MyGame> {
#override
Future<void>? onLoad() {
gameRef.camera.followComponent(this);
return super.onLoad();
}
#override
void update(double dt) {
x += gameRef.speed * dt;
super.update(dt);
}
}
/// Set the size of tile and grid
class Grid extends Component with HasGameRef<MyGame> {
late int rows = 10;
late int columns;
late int mapColumns;
late double tileSize;
#override
void onGameResize(Vector2 size) {
tileSize = (size.y / rows).floorToDouble();
columns = (size.x / tileSize).ceil();
mapColumns = columns + 2;
super.onGameResize(size);
}
}
/// Generate terrains endlessly
class EndlessMap extends PositionComponent with HasGameRef<MyGame> {
late final Sprite terrainSprite;
late final List<SpriteComponent> terrainSpritePool;
int firstTerrainIndex = 0;
// Init terrain sprite
#override
Future<void> onLoad() async {
terrainSprite = Sprite(
await Flame.images.load('terrains.png'),
srcPosition: Vector2(4.0, 0.0),
srcSize: Vector2(32.0, 32.0 * 10),
);
terrainSpritePool = List.generate(
gameRef.grid.mapColumns,
(index) => SpriteComponent(sprite: terrainSprite),
);
for (var i = 0; i < terrainSpritePool.length; i++) {
terrainSpritePool[i].size.x = gameRef.grid.tileSize;
terrainSpritePool[i].position = Vector2(
i * gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
add(terrainSpritePool[i]);
}
return super.onLoad();
}
#override
void update(double dt) {
final dx = gameRef.speed * dt;
final lastTerrainIndex = firstTerrainIndex == 0
? terrainSpritePool.length - 1
: firstTerrainIndex - 1;
// When the first terrain is behind the camera,
if (terrainSpritePool[firstTerrainIndex].position.x +
gameRef.grid.tileSize <=
gameRef.camera.position.x + dx) {
// Move the first terrain to the end
terrainSpritePool[firstTerrainIndex].size.x = gameRef.grid.tileSize;
terrainSpritePool[firstTerrainIndex].position = Vector2(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
firstTerrainIndex = (firstTerrainIndex + 1) % terrainSpritePool.length;
}
super.update(dx);
}
}
You can find a whole code in github.
2022.09.12. ----------------
It seems like the issue with that my phone(LG Q9 One) has a bad gpu to run it normally. I removed all the codes except moving the camera, it still shows janks.
In order to reduce the shader compilation and run your animations smoothly, there is an article in the official flutter website explaining about a solution
Hope this helps
Try to not create new objects in the update method.
So when you are doing:
terrainSpritePool[firstTerrainIndex].position = Vector2(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
Since the SpriteComponent already has a Vector2 you can re-use it like this:
terrainSpritePool[firstTerrainIndex].position.setValues(
terrainSpritePool[lastTerrainIndex].position.x + gameRef.grid.tileSize,
gameRef.grid.tileSize,
);
I followed a youtube tutorial about flutter flame game design which was published recently (Flame v1.2.0) so all the versions should be up to date. But when i write the code its not working like it should. Here is a youtube link of video so you can see how it should work: https://www.youtube.com/watch?v=kknJMhnKYNc
Can someone please explain to me why my code is bugging my character (it spins so fast) while moving and it can go outside of map at bottom and right side of my screen. I cant go further because of this and you are my only hope. Here is my code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
Flame.device.fullScreen();
Flame.device.setLandscape();
runApp(GameWidget(game: MyGame()));
}
class MyGame extends FlameGame with HasDraggables {
SpriteComponent background = SpriteComponent();
late SpriteAnimationComponent ghost;
late final JoystickComponent joystick;
bool ghostFlipped = false;
#override
Future<void> onLoad() async {
await super.onLoad();
var ghostImage = await images.load('ghost.png');
//Loading background
add(background
..sprite = await loadSprite('newyork.jpg')
..size = size);
//adding joystick for controlling to Ghost
final buttonPaint = BasicPalette.red.withAlpha(150).paint();
final backgroundPaint = BasicPalette.black.withAlpha(100).paint();
joystick = JoystickComponent(
knob: CircleComponent(radius: 30, paint: buttonPaint),
background: CircleComponent(radius: 100, paint: backgroundPaint),
margin: const EdgeInsets.only(left: 40, bottom: 40));
add(joystick);
//Loading ghost character
var ghostAnimation = SpriteAnimation.fromFrameData(
ghostImage,
SpriteAnimationData.sequenced(
amount: 4, stepTime: 0.17, textureSize: Vector2(32, 32)));
ghost = SpriteAnimationComponent()
..animation = ghostAnimation
..size = Vector2(120,120)
..position = Vector2(500, 250);
//ghost.flipHorizontallyAroundCenter();
add(ghost);
}
#override
void update(double dt) {
super.update(dt);
bool moveUp = joystick.relativeDelta[1] < 0;
bool moveDown = joystick.relativeDelta[1] > 0;
bool moveLeft = joystick.relativeDelta[0] < 0;
bool moveRight = joystick.relativeDelta[0] > 0;
double ghostVectorX = (joystick.relativeDelta * 300 * dt) [0];
double ghostVectorY = (joystick.relativeDelta * 300 * dt) [1];
//When ghost is moving on X direction
if((moveLeft && ghost.x > 0) || (moveRight && ghost.x < size[0])) {
ghost.position.add(Vector2(ghostVectorX,0));
}
//when ghost is moving on Y direction
if((moveUp && ghost.y > 0) || (moveDown && ghost.y < size[1])){
ghost.position.add(Vector2(0, ghostVectorY));
}
if(joystick.relativeDelta[0] < 0 && ghostFlipped) {
ghostFlipped = true;
ghost.flipHorizontallyAroundCenter();
}
if(joystick.relativeDelta[0] > 0 && !ghostFlipped) {
ghostFlipped = false;
ghost.flipHorizontallyAroundCenter();
}
}
}
I'm not sure what you mean with "spins so fast", but I'm guessing that the animation is playing too fast?
To resolve that, set the stepTime to something higher (where you have 0.17 here):
SpriteAnimationData.sequenced(
amount: 4, stepTime: 0.17, textureSize: Vector2(32, 32)));
To solve the problem you're having with the ghost being able to exit to the right and in the bottom you have to add a check with the ghosts width and height taken into account when you check against the screen size. The default anchor is top left, so that is where the position will be checked from.
In the following code snippet you can see how I've added ghost.width and ghost.height to the check against the game size:
if((moveLeft && ghost.x > 0) || (moveRight && ghost.x + ghost.width < size.x)) {
ghost.position.add(Vector2(ghostVectorX,0));
}
//when ghost is moving on Y direction
if((moveUp && ghost.y > 0) || (moveDown && ghost.y + ghost.height < size.y)){
ghost.position.add(Vector2(0, ghostVectorY));
}
I am using this indexed_list_view extension to jump to a particular index in my finite ListView. This extension has limitations because of which if I scroll in the reverse direction, empty space is visible.
Limitation(as written on package readme): The list is always infinite both to positive and negative indexes. In other words, it can be scrolled indefinitely both to the top and to the bottom.
I want to disable reverse swipe when at first index in my ListView. I am using custom scroll physics to disable the left swipe, but I want to modify it so that the reverse scroll is disabled at top of the ListView, not always. Any help would be highly appreciable. Thanks.
import 'package:flutter/material.dart';
class CustomScrollPhysics extends ScrollPhysics {
CustomScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);
bool isGoingLeft = false;
#override
ScrollPhysics applyTo(ScrollPhysics? ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor)!);
}
#override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
isGoingLeft = offset < 0;
return offset;
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
//print("applyBoundaryConditions");
assert(() {
if (value == position.pixels) {
throw FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
if (value < position.pixels &&
position.pixels <= position.minScrollExtent) {
return value - position.pixels;
}
if (position.maxScrollExtent <= position.pixels &&
position.pixels < value) {
return value - position.pixels;
}
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels) {
return value - position.minScrollExtent;
}
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value) {
return value - position.maxScrollExtent;
}
if (!isGoingLeft) {
return value - position.pixels;
}
return 0.0;
}
}
I think if you just apply a controller to the ListView it should stop scrolling negatively.
From Here: https://api.flutter.dev/flutter/widgets/GestureDetector/onPanEnd.html
there is velocity concept.
I want to remove after drag by some velocity.
How to remove following moving or make velocity to 0?
I want to very not flexible interactive viewer.
EDIT:
STICKY TRANSFORMCONTROLLER CODE:
class CustomSecondTransformationController extends TransformationController {
CustomSecondTransformationController([Matrix4? value])
: super(value ?? Matrix4.identity());
set before(Offset value) {
_before = value;
}
Offset toScene(Offset viewportPoint) {
Offset vp = viewportPoint;
final Matrix4 inverseMatrix = Matrix4.inverted(value);
final Vector3 untransformed = inverseMatrix.transform3(Vector3(
vp.dx,
vp.dy,
0,
));
return Offset(untransformed.x / 3, untransformed.y / 3);
}
and today when I am developing my app, I just find out that I need a feature that can only allow user to scroll to the right page, but not the left page, and the PageController does not provide any functionality that can allow me to implement that, so that's the reason why I am here!
I also visited this link:
StackOverflow: Custom ScrollPhysics, but it does not contain any explanation, you know it's painful to use other people's code without knowing what it is doing, right? So please help me!!! ^_^
You can create your own ScrollPhysics to lock the scroll to a direction, or you can use the horizontal_blocked_scroll_physics lib to help you.
Here is an example of a CustomScrollPhysic that enable only right scrolling
class CustomScrollPhysics extends ScrollPhysics {
CustomScrollPhysics({ScrollPhysics parent}) : super(parent: parent);
bool isGoingLeft = false;
#override
CustomScrollPhysics applyTo(ScrollPhysics ancestor) {
return CustomScrollPhysics(parent: buildParent(ancestor));
}
#override
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
isGoingLeft = offset.sign < 0;
return offset;
}
#override
double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(() {
if (value == position.pixels) {
throw FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n');
}
return true;
}());
if (value < position.pixels && position.pixels <= position.minScrollExtent)
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value)
return value - position.pixels;
if (value < position.minScrollExtent &&
position.minScrollExtent < position.pixels)
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent &&
position.maxScrollExtent < value)
return value - position.maxScrollExtent;
if (!isGoingLeft) {
return value - position.pixels;
}
return 0.0;
}
}
You can use as the example below, CustomScrollPhysics as value to physics parameter.
PageView.builder(
physics: CustomScrollPhysics(),
);
Some references that could explain better how ScrollPhysics works
Flutter Doc
David Anaya's Medium article