I'm trying to detect swipe direction in Compose. I'm using the draggable modifier for this. But draggable allows only one direction to detect (Vertical or Horizontal). I want to detect swipes for all directions (left, right, up, down). Can anyone help me how can I do this? Thanks!
You can use the pointerInput modifier controlling the dragging gesture with the detectDragGestures function.
Something like:
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.size(100.dp, 100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
val (x,y) = dragAmount
when {
x > 0 ->{ /* right */ }
x < 0 ->{ /* left */ }
}
when {
y > 0 -> { /* down */ }
y < 0 -> { /* up */ }
}
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
Modifier.dragGestureFilter detects dragging in any direction. Pass an instance of DragObserver and override onDrag. Here you can detect the swipe direction based on the Offset. This object has x and y values, which are positive or negative based on the direction.
Here's what your code could look like:
Box(
Modifier.dragGestureFilter(
dragObserver = object : DragObserver() {
override fun onDrag(dragDistance: Offset): Offset {
val (x, y) = dragDistance
when {
x > 0 -> { /* right */ }
x < 0 -> { /* left */ }
}
when {
y > 0 -> { /* down */ }
y < 0 -> { /* up */ }
}
}
}
)
)
To actually move the object, you would have to apply Modifier.offset with values that are updated in onDrag.
This is a more modified way to get the direction without conflict horizontal swipes with vertical swipes, and to make sure to return the direction after the user end swiping.
var direction by remember { mutableStateOf(-1)}
Box(
modifier = Modifier
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consumeAllChanges()
val (x, y) = dragAmount
if(abs(x) > abs(y)){
when {
x > 0 -> {
//right
direction = 0
}
x < 0 -> {
// left
direction = 1
}
}
}else{
when {
y > 0 -> {
// down
direction = 2
}
y < 0 -> {
// up
direction = 3
}
}
}
},
onDragEnd = {
when (direction){
0 -> {
//right swipe code here }
1 -> {
// left swipe code here
}
2 -> {
// down swipe code here
}
3 -> {
// up swipe code here
}
}
}
)
)
To complement the answers from #Gabriel and #J-Abdo I did something similar based on this answers, for me I need only left/right swipe and I added an "offset" with the min lenght that I want to be considered "swipe"
BoxWithConstraints {
var offsetX by remember { mutableStateOf(0f) }
val minSwipeOffset by remember {
mutableStateOf(
constraints.maxWidth / 4
)
}
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, dragAmount ->
change.consume()
val (x, y) = dragAmount
offsetX += dragAmount.x
},
onDragEnd = {
when {
(offsetX < 0F && abs(offsetX) > minSwipeOffset) -> {
SwipeDirection.Left
}
(offsetX > 0 && abs(offsetX) > minSwipeOffset) -> {
SwipeDirection.Right
}
else -> null
}?.let(onSwipe)
offsetX = 0F
}
)
}) {
}
}
and I'm using just a sealed class for the directions
sealed class SwipeDirection {
object Left : SwipeDirection()
object Right : SwipeDirection()
}
Related
I'm trying to show animation if the indexPath == 0 and scroll direction is downward, but I'm unable to get the indexPath of tableView in scrollViewdidScroll method. here's my code -->
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let indexPath = tableView.indexPath(for: ThirdTableViewCell() as UITableViewCell)
if (self.lastContentOffset > scrollView.contentOffset.y) && indexPath?.row == 0 {
print ("up")
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
// print ("down")
}
self.lastContentOffset = scrollView.contentOffset.y
}
I got the indexPath nil while debugging
You still haven't explained what you really want to do, but maybe this will help...
Start by setting up your scrollViewDidScroll like this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var partOfRowZeroIsVisible: Bool = false
var topOfRowZeroIsVisible: Bool = false
var userIsDraggingDown: Bool = false
// if user pulls table DOWN when Row Zero is at top,
// contentOffset.y will be negative and table will
// bounce back up...
// so, we make y AT LEAST 0.0
let y = max(0.0, tableView.contentOffset.y)
// point for the top visible content
let pt: CGPoint = CGPoint(x: 0, y: y)
if let idx = tableView.indexPathForRow(at: pt), idx.row == 0 {
// At least PART of Row Zero is visible...
partOfRowZeroIsVisible = true
} else {
// Row Zero is NOT visible...
partOfRowZeroIsVisible = false
}
if y == 0 {
// Table view is scrolled all the way to the top...
topOfRowZeroIsVisible = true
} else {
// Table view is NOT scrolled all the way to the top...
topOfRowZeroIsVisible = false
}
if (self.lastContentOffset >= y) {
// User is DRAGGING the table DOWN
userIsDraggingDown = true
} else {
// User is DRAGGING the table UP
userIsDraggingDown = false
}
// save current offset
self.lastContentOffset = y
Then, to show or hide your "arrow":
// if you want to show your "arrow"
// when ANY PART of Row Zero is visible
if partOfRowZeroIsVisible {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// ONLY when table view has been scrolled ALL THE WAY to the top
if topOfRowZeroIsVisible {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// when ANY PART of Row Zero is visible
// and
// ONLY if the user is dragging DOWN
if partOfRowZeroIsVisible && userIsDraggingDown {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
or:
// if you want to show your "arrow"
// ONLY when table view has been scrolled ALL THE WAY to the top
// and
// ONLY if the user is dragging DOWN
if topOfRowZeroIsVisible && userIsDraggingDown {
arrow.isHidden = false
} else {
arrow.isHidden = true
}
Creating a 2d game, and want to add moving platforms where I can control the pattern and rotation of the platform.
Example:
I want this platform to move in a clockwise motion, also when it reaches the top and bottom I want it to rotate accordingly as if it is facing the direction it is going (so the platform would essentially rotate 180 degrees as it arches at the top and bottom).
I can't use SKActions because I need the physics to work properly.
My idea is that I can use an Agent with behaviors and goals to do this. Not sure if I will need path finding as well.
I'm researching how to use these features, but the documentation and lack of tutorials is hard to decipher. I'm hoping someone could save my some time of trial and error by providing an example of how this would work.
Thanks in advance!
Using SKActions or adjusting the position manually will be sucky to get working the way you want, BUT it's always worth a shot to give it a go, since it would take 2 minutes to mock it up and see...
I'd suggest doing something like a Path class that sends velocity commands to the platform every frame ...
Actually, this could be a good exercise in learning the state machine..
MOVE RIGHT STATE: if X position > starting position X + 200, enter state "move down"
MOVE DOWN STATE: if Y position < starting position Y - 200, enter state "move left"
MOVE LEFT STATE: if X position < starting position X, enter state "move up"
MOVE UP STATE: if Y position > starting position Y, enter state "move right"
.. there are ways you could figure out to give it more curvature instead of just straight angle (when changing direction)
Otherwise you would have to translate that into a class / struct / component and give each platform it's own instance of it.
///
Another option is to take the physics out of the equation, and create a playerIsOnPlatform property... then you manually adjust the position of the player each frame... (or maybe a SKConstraint)
This would then require more code on jumping and such, and turns things to spaghetti pretty quickly (last time I tried it)
But, I was able to successfully clone this, using proper hit detection going that route:
https://www.youtube.com/watch?v=cnhlFZeIR7Q
UPDATE:
Here is a working project:
https://github.com/fluidityt/exo2/tree/master
Boilerplate swift stuff:
import SpriteKit
import GameplayKit
// Workaround to not having any references to the scene off top my head..
// Simply add `gScene = self` in your didMoveToViews... or add a base scene and `super.didMoveToView()`
var gScene = GameScene()
class GameScene: SKScene {
override func didMove(to view: SKView) {
gScene = self
}
var entities = [GKEntity]()
var graphs = [String : GKGraph]()
private var lastUpdateTime : TimeInterval = 0
func gkUpdate(_ currentTime: TimeInterval) -> TimeInterval {
if (self.lastUpdateTime == 0) {
self.lastUpdateTime = currentTime
}
let dt = currentTime - self.lastUpdateTime
for entity in self.entities {
entity.update(deltaTime: dt)
}
return currentTime
}
override func update(_ currentTime: TimeInterval) {
self.lastUpdateTime = gkUpdate(currentTime)
}
}
Here is the very basic component:
class Platforms_BoxPathComponent: GKComponent {
private enum MovingDirection: String { case up, down, left, right }
private var node: SKSpriteNode!
private var state: MovingDirection = .right
private lazy var startingPos: CGPoint = { self.node.position }()
#GKInspectable var startingDirection: String = "down"
#GKInspectable var uniqueName: String = "platform"
#GKInspectable var xPathSize: CGFloat = 400
#GKInspectable var yPathSize: CGFloat = 400
// Moves in clockwise:
private var isTooFarRight: Bool { return self.node.position.x > (self.startingPos.x + self.xPathSize) }
private var isTooFarDown: Bool { return self.node.position.y < (self.startingPos.y - self.yPathSize) }
private var isTooFarLeft: Bool { return self.node.position.x < self.startingPos.x }
private var isTooFarUp: Bool { return self.node.position.y > self.startingPos.y }
override func didAddToEntity() {
print("adding component")
// can't add node here because nodes aren't part of scene yet :(
// possibly do a thread?
}
override func update(deltaTime seconds: TimeInterval) {
if node == nil {
node = gScene.childNode(withName: uniqueName) as! SKSpriteNode
// for some reason this is glitching out and needed to be redeclared..
// showing 0 despite clearly not being 0, both here and in the SKS editor:
xPathSize = 300
}
let amount: CGFloat = 2 // Amount to move platform (could also be for for velocity)
// Moves in clockwise:
switch state {
case .up:
if isTooFarUp {
state = .right
fallthrough
} else { node.position.y += amount }
case .right:
if isTooFarRight {
state = .down
fallthrough
} else { node.position.x += amount }
case .down:
if isTooFarDown {
state = .left
fallthrough
} else { node.position.y -= amount }
case .left:
if isTooFarLeft {
state = .up
fallthrough
} else { node.position.x -= amount }
default:
print("this is not really a default, just a restarting of the loop :)")
node.position.y += amount
}
}
}
And here is what you do in the sks editor:
I have slide menu when I click on screen menu comes out from right to left. How can I write code if I click button to move back from left to right?
My slide menu:
My code:
private void UpdateTableViewPosition(CGPoint positon)
{
// set the position of the button based on the provided argument
_buttonToggleVisibility.Frame = new CoreGraphics.CGRect(positon.X , _buttonToggleVisibility.Frame.Y, _buttonToggleVisibility.Frame.Width, _buttonToggleVisibility.Frame.Height);
// TODO:
// if button is outside of the screen
// move it back into the screen
// then move tableview so it is right aligned of the button
_tableView.Frame = new CoreGraphics.CGRect(_buttonToggleVisibility.Frame.Right, _tableView.Frame.Y, _tableView.Frame.Width, _tableView.Frame.Height);
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
UITouch touch = (UITouch)touches.AnyObject;
_moving = false;
// TODO: fix so that only the button is clickable
// if the touch clicked the button
// open (or close) the view with the following animation code
UIView.Animate(0.9f, () =>
{
// TODO: this animation code is incorrect. Currently it moves the view 100px relatively to the
// current position, but it should rather either set the button to the left corner or the right
// corner at fixed positions
UpdateTableViewPosition(new CGPoint(_buttonToggleVisibility.Frame.X - _buttonToggleVisibility.Frame.Left, 0));
}, null);
}
public override void TouchesCancelled(NSSet touches, UIEvent evt)
{
base.TouchesCancelled(touches, evt);
_moving = false;
}
public override void TouchesBegan(Foundation.NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
UITouch touch = (UITouch)touches.AnyObject;
CoreGraphics.CGPoint pos = touch.LocationInView(this);
if (pos.X > _buttonToggleVisibility.Frame.X && pos.X < _buttonToggleVisibility.Frame.Right && pos.Y > _buttonToggleVisibility.Frame.Y && pos.Y < _buttonToggleVisibility.Frame.Bottom)
{
// did click on the view
_moving = true;
}
_buttonToggleVisibility.TouchUpInside += (sender, e) =>
{
};
}
}
I use AutoLayout and UIView animation to move a view of controls up and down. (Think a keyboard but with more flexibility.) The board has a "Done" button, which animatedly moves the board (and it's subviews) out of view.
Since I have an array of boards, there's extra code here addressing the tag property. That's where I find which board to work with.
The key things are to:
(1) Change the heightAnchor (or in your case the widthAnchor)
(2) Animate by call the superview's layoutIfNeeded
..
private var boardIn:[NSLayoutConstraint] = []
private var boardOut:[NSLayoutConstraint] = []
private var tabBoards:[TabBoard] = []
public func createControlBoard(
_ tag:Int
) -> ControlBoard {
let newBoard = ControlBoard(tag)
boardOut.append(newBoard.heightAnchor.constraint(equalToConstant: 100))
boardIn.append(newBoard.heightAnchor.constraint(equalToConstant: 0))
newBoard.doneButton.addTarget(self, action: #selector(hideTabBoard), for: .touchUpInside)
tabBoards.append(newBoard)
return newBoard
}
public func showTabBoard(_ tag:Int) {
for i in 0...tabBoards.count-1 {
NSLayoutConstraint.deactivate([boardOut[i]])
NSLayoutConstraint.activate([boardIn[i]])
tabBoards[i].makeControlsHidden(true)
}
NSLayoutConstraint.deactivate([boardIn[tag-1]])
NSLayoutConstraint.activate([boardOut[tag-1]])
tabBoards[tag-1].makeControlsHidden(false)
UIView.animate(withDuration: 0.3) { self.superview?.layoutIfNeeded() }
}
public func hideTabBoard(_ tag:Int) {
NSLayoutConstraint.deactivate([boardOut[tag-1]])
NSLayoutConstraint.activate([boardIn[tag-1]])
tabBoards[tag-1].makeControlsHidden(true)
UIView.animate(withDuration: 0.3) { self.superview?.layoutIfNeeded() }
}
i'm making an messenger app, like telegram, i would like to implement an infinite scroll, i use JSQMessageViewController for the chat.
this is the code for infinite scroll:
override func scrollViewDidScroll(scrollView: UIScrollView) {
if(scrollView.contentOffset.y >= 0.0 && scrollView.contentOffset.y <= 30.0 && loading == true){
self.loading = false
self.loadMore()
}
}
Then i call this function:
func loadMore() {
//Controllo se nell'array sono presenti altri messaggi
if ((chatList?.messages.count)!-1) != self.messages.count{
//
CATransaction.begin()
CATransaction.setDisableActions(true)
let oldBottomOffset = self.collectionView!.contentSize.height - self.collectionView!.contentOffset.y
UIView.performWithoutAnimation({ () -> Void in
self.collectionView!.performBatchUpdates({ () -> Void in
let idInit = (self.lastMessageId-self.messagePerPage<0) ? 0 : self.lastMessageId-1-self.messagePerPage
let idEnd = self.lastMessageId-1
var indexPaths: [NSIndexPath] = []
for(var i = 0; i<idEnd-idInit; i++){
print("Creo indexpath \(i)")
indexPaths.append(NSIndexPath(forItem: i, inSection: 0))
}
self.collectionView!.insertItemsAtIndexPaths(indexPaths)
let msg = self.chatList?.messages
var i = idEnd
repeat {
let mg = msg![i]
let messagechat:JSQMessage = JSQMessage(senderId: mg.sender, senderDisplayName: "", date:mg.time, text: mg.message)
messagechat.xmppMessageID = mg.messageID
messagechat.status = mg.messageStatus
self.messages.insert(messagechat, atIndex: 0)
self.lastMessageId = i
i--
} while i > idInit
// invalidate layout
self.collectionView!.collectionViewLayout.invalidateLayoutWithContext(JSQMessagesCollectionViewFlowLayoutInvalidationContext())
}, completion: {(finished) in
//scroll back to current position
self.finishReceivingMessageAnimated(false)
self.collectionView!.layoutIfNeeded()
self.collectionView!.contentOffset = CGPointMake(0, self.collectionView!.contentSize.height - oldBottomOffset)
CATransaction.commit()
})
})
}
else {
print("No more messages to load.")
}
}
All work fine but while scrolling the scroll is stopped for 1sec. i suppose is CATransaction that make this and sometimes i see a jump effect, if i add 10 new messages i see for a second the first message of the 10, and then offset is set correctly to the older position.
How can i fix this? i see telegram scrolling is perfect, and there is no jump effects while loading old messages.
I want to find out what the number of nodes is in a scene. For example, I want to create an if statement so that if the number of nodes in the scene is 0 or any other number, I would call a function.
This is what i have done but it only calls the function the first time children.count is 0 but ignores the other times.
I am not removing or adding any sprite nodes anywhere else in my code.
func dot(){
var dotTexture = SKTexture (imageNamed: "dot")
dotTexture.filteringMode = SKTextureFilteringMode.Nearest
var dot = SKSpriteNode(texture: dotTexture)
dot.setScale(0.5)
dot.position = CGPoint(x: self.frame.size.width * 0.5 , y: self.frame.size.height * 1.1)
dot.physicsBody = SKPhysicsBody (circleOfRadius: dot.size.height/2.0)
dot.physicsBody?.dynamic = true
dot.physicsBody?.allowsRotation = false
self.addChild(dot)
println("done")
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if children.count == 0 {
dot()
println("dot")
}
}
if you're doing this inside the scene itself you can do
if (self.children.count == x) {
yourFunction() //call your function
}
or
if (children.count == x) {
yourFunction() //call your function
}
in response to your comment:
there's an update method that runs every frame in sprite kit. Use it like this:
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if (children.count == x) {
yourFunction() //call your function
}
}
use this:
if (self.children.count == someNumber) {
<#enter code here#>
}
where some number is a trigger number or expression.
Objective-C, use:
if ([self children].count == someNumber) {
// enter code here
}
Also, where it says "enter code here," call your function and do what you need to do.
I want to find out what the number of nodes is in a scene
Use this method:
func nodeCount(scene:SKScene) -> Int {
return scene[".//*"].count
}
This will give you the count of all nodes in the entire scene heirarchy.