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
Related
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.
I have created a custom paint drawer, which works well, but I liked to create custom brush types like a calligraphy brush, crayon brush, etc...
class DrawingPainter extends CustomPainter {
DrawingPainter({this.pointsList}) : super();
List<DrawingPoints> pointsList;
#override
void paint(Canvas canvas, Size size) {
for (int i = 0; i < pointsList.length - 1; i++) {
if (pointsList[i] != null) {
canvas.saveLayer(Offset.zero & size, Paint());
if (shouldDrawLine(i)) {
canvas.drawLine(pointsList[i].points, pointsList[i + 1].points, pointsList[i].paint);
} else if (shouldDrawPoint(i)) {
canvas.drawPoints(dartUI.PointMode.polygon, [pointsList[i].points,pointsList[i].points], pointsList[i].paint);
}
canvas.restore();
}
}
}
bool shouldDrawPoint(int i) =>
pointsList[i] != null && pointsList.length > i + 1 && pointsList[i + 1] == null;
bool shouldDrawLine(int i) =>
pointsList[i] != null && pointsList.length > i + 1 && pointsList[i + 1] != null;
#override
bool shouldRepaint(DrawingPainter oldDelegate) => true;
}
this is my code for the drawing. How can I modify the brush drawing pattern like those types of brushes?
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;
}
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);