Flutter - Fast swipe detection - flutter

I am building an application with swipeable cards in a deck.
Swiping works perfectly fine at any speed so long as I am swiping sort of at least 50% of the distance of the screen.
People don't tend to do this however, it is a better user experience to just swipe a very short distance quickly left or right and have the application detect the intent and complete the scroll action.
My code is as follows:
void _onHorizontalDragStart(DragStartDetails details) {
startDrag = details.globalPosition;
startDragPercentScroll = scrollPercent;
}
void _onHorizontalDragUpdate(DragUpdateDetails details) {
final currentDrag = details.globalPosition;
final dragDistance = currentDrag.dx - startDrag.dx;
final singleCardDragPercent = dragDistance / context.size.width;
setState(() {
scrollPercent = (startDragPercentScroll + (-singleCardDragPercent / widget.cards.length)).clamp(0.0, 1.0 - (1 / widget.cards.length));
});
}
void _onHorizontalDragEnd(DragEndDetails details) {
finishScrollStart = scrollPercent;
finishScrollEnd = (scrollPercent * widget.cards.length).round() / widget.cards.length;
finishScrollController.forward(from: 0.0);
setState(() {
startDrag = null;
startDragPercentScroll = null;
});
}
I am certain there is just something wrong with my math there somewhere that is preventing short, quick swipes from working.
What happens instead is the card moves a short distance and then snaps back to the same card.

Related

GPU Jank when generate endless 2D terrain

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,
);

Flutter/Flame Why my code is not working normally and getting buggy

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));
}

Flutter desktop custom game loop appears to drop frames

I'm testing a basic game loop for Flutter Desktop using the code below but it is not entirely smooth. It sort of seems to very briefly pause every half second or so and jump forward. I suspect that some frames are being dropped but there is nothing intensive going on so I can't see why.
The first thing I though of was that perhaps I need to adjust my update() function to allow for the exact microsecond delta since the last frame - but that didn't fix it regardless of whether I used the timestamp provided by the gameLoop() callback or measured it directly from DateTime.now().
I'm running on a fast gaming laptop so hardware isn't the problem. I am starting to think it could be something in the Flutter framework itself. Perhaps it drops a frame because it is garbage collecting? Or perhaps it skips a frame if it doesn't detect it has changed?
By comparison I created a similar loop in javascript which ran perfectly smoothly. The image used in both the Flutter and Javascript tests was a test 60wx72h png in the local directory.
I'd appreciate any pointers as to whether this is something in my code or something in the Flutter framework.
By the way, there is a discussion of this issue on the Flutter group which explores some other approaches to this problem. See flutter-dev.
Flutter/Dart:
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart' hide Image;
import 'package:flutter/services.dart';
Canvas canvas;
Sprite sprite;
PictureRecorder pictureRecorder;
SceneBuilder builder;
main() async {
WidgetsFlutterBinding.ensureInitialized();
WidgetsBinding.instance.window.onBeginFrame = gameLoop;
sprite = Sprite();
WidgetsBinding.instance.window.scheduleFrame();
}
void gameLoop(Duration now) {
pictureRecorder = PictureRecorder();
canvas = Canvas(pictureRecorder, Rect.fromLTWH(
0.0, 0.0, WidgetsBinding.instance.window.physicalSize.width, WidgetsBinding.instance.window.physicalSize.height));
sprite.update();
sprite.render();
builder = SceneBuilder();
builder.addPicture(Offset.zero, pictureRecorder.endRecording());
WidgetsBinding.instance.window.render(builder.build());
WidgetsBinding.instance.window.scheduleFrame();
}
class Sprite {
double x=0, y=400, dx=8, dy=0;
String imageName = 'assets/bobR.png';
Image image;
void update(){
x+= dx;
y+= dy;
if((x<0 && dx<0)||(x>1000 && dx>0)) dx=-dx;
if((y<0 && dy<0)||(y>1000 && dy>0)) dy=-dy;
}
void render(){
if (image!=null) canvas.drawImage(image, Offset(x,y), Paint());
}
Sprite(){
loadAssets();
}
void loadAssets(){
rootBundle.load(imageName).then((byteData){
Uint8List lst = Uint8List.view(byteData.buffer);
instantiateImageCodec(lst).then((codec){
codec.getNextFrame().then((frameInfo){
image = frameInfo.image;
});
});
});
}
}
Javascript equivalent:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
"use strict";
let canvas;
let context;
let image = new Image();
let x=0,y=0;
let dx=4,dy=0;
let turnX, turnY;
image.src = "bobR.png";
window.onload = init;
function resize(){
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
turnX = canvas.width - image.width;
turnY = canvas.height - image.height;
}
function init(){
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
window.onresize = resize;
resize();
window.requestAnimationFrame(gameLoop);
}
function gameLoop(timeStamp){
context.clearRect(0, 0, canvas.width, canvas.height);
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update(){
x += dx;
y += dy;
if ((x<0 && dx<0) || (x>turnX && dx>0)) dx = -dx;
if ((y<0 && dy<0) || (y>turnY && dy>0)) dy = -dy;
}
function render(){
context.drawImage(image, x, y);
}
</script>
<body>
<canvas id="canvas">To play this game, use a more up to date browser</canvas>
</body>
</html>
EDIT
I tried using the Flame game engine but still get the same slightly unsmooth motion compared to Javascript. Code is below:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flame/game.dart';
import 'package:flame/flame.dart';
import 'package:flame/position.dart';
import 'package:flame/sprite.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
TestGame game = TestGame();
runApp(game.widget);
}
class TestGame extends Game {
Size screenSize;
Player player;
TestGame() {
initialize();
}
void initialize() async {
Flame.images.load('bobR.png');
player = Player();
resize(await Flame.util.initialDimensions());
}
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Colors.white;
canvas.drawRect(bgRect, bgPaint);
player.render(canvas);
}
void update(double t) {
player.update(t);
}
void resize(Size size) {
screenSize = size;
}
}
class Player {
Position p = Position(0,400); double dx = 8, dy = 0;
Sprite sprite;
Player() {
sprite = Sprite('bobR.png');
}
void render(Canvas c) {
sprite.renderPosition(c, p);
}
void update(double t) {
p.x += dx;
p.y += dy;
if ((p.x < 0 && dx < 0) || (p.x > 1000 && dx > 0))
dx = -dx;
if ((p.y < 0 && dy < 0) || (p.y > 1000 && dy > 0))
dy = -dy;
}
}
Allocations in tight loops (such as gameLoop) can create pressure on the garbage collector.
Try moving your allocations out of your game loop by setting up an initialization routine that only runs once and reuse the objects created.
The JavaScript implementation does just that, which is why it runs smoothly.
I recommend you check out Flame https://flame-engine.org/docs/#/ which is a minimal game engine on top of Flutter.
Try drawing using the Canvas.drawRawAtlas method
An extended example can be found here
I also recorded a video, as I am interested in checking what Flutter is capable of and for which games it is suitable.
At 500 sprites, some GPU processes after Flutter Engine start to go beyond 16 ms in one frame.

Snapping Behavior Custom ScrollPhysics

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;
}

Flutter - How to sync two or more PageControllers

In Flutter I can´t assing the same PageController to many PageView. So in need to use two or more PageControllers.
I need to synchronize my ViewPages so that when I slide one another it also slides
How can I sync two or more PageController or PageView?
What I want is every PageView can control others PageView make them all sync no matter which slide.
So if a have A, B and C PageView and I slide A then B and C slides as well... if I slide B then A and C slides... and so on.
You can reference https://github.com/braulio94/menu_flutter
The trick is use NotificationListener to listen ScrollUpdateNotification
and compare two page
_backgroundPageController.page != _pageController.page
code snippet at line 183
new NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.depth == 0 &&
notification is ScrollUpdateNotification) {
selectedIndex.value = _pageController.page;
if (_backgroundPageController.page != _pageController.page) {
_backgroundPageController.position
// ignore: deprecated_member_use
.jumpToWithoutSettling(_pageController.position.pixels /
_kViewportFraction);
}
setState(() {});
}
return false;
}
full code
https://github.com/braulio94/menu_flutter/blob/master/lib/screens/pager.dart
To sync two pageviews (in case you want to use different viewfractions for example)
create two controllers, upper with view fraction 0,7 and bottom with 1.0
Now to sync add listener to upper and get current scroll offset value.
#override
void initState() {
super.initState();
_controller_upper = new PageController(viewportFraction: 0.7);
_controller_bottom = new PageController();
_controller.addListener(_onScroll);
}
void _onScroll() {
scrollValue = _controller.page;
//page returns value between 0 to 1 for 1st page, 1 to 2 for second and s on, need to multiply it by screen width
_controller_bottom.jumpTo(scrollValue*MediaQuery.of(context).size.width);
}
There is another solution. When you create controllers you can add listeners for each of. In every listener you should describe logic for another controller.
To be more clear I will describe in more detail. If necessary, you can combine the logic into one method, but it will be less clear.
int _previousPage = 0;
bool _isController1 = false;
bool _isController2 = false;
void resetMoveInfo(){
_isController1 = false;
_isController2 = false;
}
void _onController1Scroll() {
if (_isController2)
return;
_isController1 = true;
if (_controller1.page.toInt() == _controller1.page) {
_previousPage = _controller1.page.toInt();
resetMoveInfo();
}
_controller2.position
// ignore: deprecated_member_use
.jumpToWithoutSettling(_controller1.position.pixels * _viewPortFraction);
}
void _onController2Scroll() {
if (_isController1)
return;
_isController2 = true;
if (_controller2.page.toInt() == _controller2.page) {
_previousPage = _controller2.page.toInt();
resetMoveInfo();
}
_controller1.position
// ignore: deprecated_member_use
.jumpToWithoutSettling(_controller2.position.pixels / _viewPortFraction);
}
When you initialize your controller you should add listener for each of them
_controller1 = PageController(
initialPage: _previousPage,
keepPage: false,
viewportFraction: 1)
..addListener(_onController1Scroll);