Return imageView rotation position and stop if at a particular position - kotlin

hoping someone can help. I am creating an app whereby the user will touch a series of images to rotate them. What I am trying to do. Is highlight the image once the user has rotated to a particular position.
Is this possible? If, so any tips greatly appreciated.

edit - ok here's an example instead!
First, the simplest way, based off the code example you just posted:
r1c1.setOnClickListener {
r1c1.animate().apply{ duration = 100 rotationBy(270f) }.start()
}
So the issue here is that you want to highlight the view when it's rotated to, say 90 degrees, right? But it has an animation to complete first. You have three options really
do something like if (r1c1.rotation + 270f == 90) and highlight now, as the animation starts, which might look weird
do that check now, but use withEndAction to run the highlighting code if necessary
use withEndAction to do the checking and highlighting, after the anim has finished
the latter probably makes the most sense - after the animation finishes, check if its display state needs to change. That would be something like this:
r1c1.animate().setDuration(100).rotationBy(270f).withEndAction {
// need to do modulo so 720 == 360 == 0 etc
if (r1c1.rotation % 360 == TARGET_ROTATION) highlight(r1c1)
}.start()
I'm assuming you have some way of highlighting the ImageViews and you weren't asking for ways to do that!
Unfortunately, the problem here is that if the user taps the view in the middle of animating, it will cancel that animation and start a new one, including the rotationBy(270) from whatever rotation the view currently happens to be at. Double tap and you'll end up with a view at an angle, and it will almost never match a 90-degree value now! That's why it's easier to just hold the state, change it by fixed, valid amounts, and just tell the view what it should look like.
So instead, you'd have a value for the current rotation, update that, and use that for your highlighting checks:
# var stored outside the click listener - this is your source of truth
viewRotation += 270f
# using rotation instead of rotationBy - we're setting a specific value, not an offset
r1c1.animate().setDuration(100).rotation(viewRotation).withEndAction {
// checking our internal rotation state, not the view!
if (viewRotation % 360 == TARGET_ROTATION) highlight(r1c1)
}.start()
I'm not saying have a single rotation var hanging around like that - you could, but see the next bit - it's gonna get messy real quick if you have a lot of ImageViews to wrangle. But this is just to demonstrate the basic idea - you hold your own state value, you're in control of what it can be set to, and the View just reflects that state, not the other way around.
Ok, so organisation - I'm guessing from r1c1 that you have a grid of cells, all with the same general behaviour. That means a lot of repeat code, unless you try and generalise it and stick it in one place - like one click listener, that does the same thing, just on whichever view it was clicked on
(I know you said youre a beginner, and I don't like loading too many concepts on someone at once, but from what it sounds like you're doing this could get incredibly bloated and hard to work with real fast, so this is important!)
Basically, View.onClickListener's onClick function passes in the view that was clicked, as a parameter - basically so you can do what I've been saying, reuse the same click listener and just do different things depending on what was passed in. Instead of a lambda (the code in { }, basically a quick and dirty function you're using in one place) you could make a general click listener function that you set on all your ImageViews
fun spin(view: View) {
// we need to store and look up a rotation for each view, like in a Map
rotations[view] = rotations[view] + 270f
// no explicit references like r1c1 now, it's "whatever view was passed in"
view.animate().setDuration(100).rotation(rotations[view]).withEndAction {
// Probably need a different target rotation for each view too?
if (rotations[view] % 360 == targetRotations[view]) highlight(view)
}.start()
}
then your click listener setup would be like
r1c1.setOnClickListener { spin(it) }
or you can pass it as a function reference (this is already too long to explain, but this works in this situation, so you can use it if you want)
r1c1.setOnClickListener(::spin)
I'd recommend generating a list of all your ImageView cells when you look them up (there are a few ways to handle this kind of thing) but having a collection lets you do things like
allCells.forEach { it.setOnClickListener(::spin) }
and now that's all your click listeners set to the same function, and that function will handle whichever view was clicked and the state associated with it. Get the idea?
So your basic structure is something like
// maybe not vals depending on how you initialise things!
val rotations: MutableMap<View, Float>
val targetRotations: Map<View, Float>
val allCells: List<ImageView>
// or onCreateView or whatever
fun onCreate() {
...
allCells.forEach { it.setOnClickListener(::spin) }
}
fun spin(view: View) {
rotations[view] = rotations[view] + 270f
view.animate().setDuration(100).rotation(rotations[view]).withEndAction {
val highlightActive = rotations[view] % 360 == targetRotations[view]
highlight(view, highlightActive)
}.start()
}
fun highlight(view: View, enable: Boolean) {
// do highlighting on view if enable is true, otherwise turn it off
}
I didn't get into the whole "wrapper class for an ImageView holding all its state" thing, which would probably be a better way to go, but I didn't want to go too far and complicate things. This is already a silly length. I might do a quick answer on it just as a demonstration or whatever

The other answer is long enough as it is, but here's what I meant about encapsulating things
class RotatableImageView(val view: ImageView, startRotation: Rotation, val targetRotation: Rotation) {
private var rotation = startRotation.degrees
init {
view.rotation = rotation
view.setOnClickListener { spin() }
updateHighlight()
}
private fun spin() {
rotation += ROTATION_AMOUNT
view.animate().setDuration(100).rotation(rotation)
.withEndAction(::updateHighlight).start()
}
private fun updateHighlight() {
val highlightEnabled = (rotation % 360f) == targetRotation.degrees
// TODO: highlighting!
}
companion object {
const val ROTATION_AMOUNT = 90f
}
}
enum class Rotation(var degrees: Float) {
ROT_0(0f), ROT_90(90f), ROT_180(180f), ROT_270(270f);
companion object {
// just avoids creating a new array each time we call random()
private val rotations = values()
fun random() = rotations.random()
}
}
Basically instead of having a map of Views to current rotation values, a map of Views to target values etc, all that state for each View is just bundled up into an object instead. Everything's handled internally, all you need to do from the outside is find your ImageViews in the layout, and pass them into the RotatableImageView constructor. That sets up a click listener and handles highlighting its ImageView if necessary, you don't need to do anything else!
The enum is just an example of creating a type to represent valid values - when you create a RotatableImageView, you have to pass one of these in, and the only possible values are valid rotation amounts. You could give them default values too (which could be Rotation.random() if you wanted) so the constructor call can just be RotatableImageView(imageView)
(you could make more use of this kind of thing, like using it for the internal rotation amounts too, but in this case it's awkward because 0 is not the same as 360 when animating the view, and it might spin the wrong way - so you pretty much have to keep track of the actual rotation value you're setting on the view)
Just as a quick FYI (and this is why I was saying what you're doing could get unwieldy enough that it's worth learning some tricks), instead of doing findViewById on a ton of IDs, it can be easier to just find all the ImageViews - wrapping them in a layout with an ID (like maybe a GridLayout?) can make it easier to find the things you want
val cells = findViewById<ViewGroup>(R.id.grid).children.filterIsInstance<ImageView>()
then you can do things like
rotatables = cells.map { RotatableImageView(it) }
depends what you need to do, but that's one possible way. Basically if you find yourself repeating the same thing with minor changes, like the infomercials say, There Has To Be A Better Way!

Related

Kotlin button listener

I want to increase the value of i every time the button is clicked
I've tried this code but it's not working.
val textview = findViewById<TextView>(R.id.texttest)
var i = 10
bookbutton.setOnClickListener {
i++
}
textview.text = "$i"
You have to set the text inside the listener:
bookbutton.setOnClickListener {
i++
textview.text = "$i"
}
Your listener is updating the value of i — but that's not having any visible effect because by then it's too late: the text shown in your textview has already been set.
Let's review the order of events:
Your code runs. That creates a text view and a variable, sets a listener on the button, and sets the text in the text view.
At some later point(s), the user might click on the button. That calls the listener, which updates the variable.
So while your code sets the listener, the listener does not run until later. It might not run at all, or it might run many times, depending on what the user does, but it won't run before you set the text in the text view.
So you need some way to update the text when you update the variable. The simplest way is to do explicitly it in the listener, e.g.:
bookbutton.setOnClickListener {
textview.text = "${++i}"
}
(There are other ways — for example, some UI frameworks provide ways to ‘bind’ variables to screen fields so that this sort of update happens automatically. But they tend to be a lot more complex; there's nothing wrong with the simple solution.)

Jetpack Compose: scrolling to the bottom of list on event

I have a composable representing list of results:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val coroutineScope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
coroutineScope.launch {
listState.animateScrollToItem(results.size)
}
}
}
}
Expected behaviour: The list smoothly scrolls to the last item whenever a new item is added
Actual behaviour: All is good, but whenever I manually scroll fast through the list, it is also automatically put on the bottom. Also, the scrolling is not smooth.
Your code gives the following error:
Calls to launch should happen inside a LaunchedEffect and not composition
You should not ignore it by calling the side effect directly from the Composable function. See side effects documentation for more details.
You can use LaunchedEffect instead (as this error suggests). By providing results.size as a key, you guarantee that this will be called only once when your list size changes:
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
val listState = rememberLazyListState()
LaunchedEffect(results.size) {
listState.animateScrollToItem(results.size)
}
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
Philip's solution will work for you. However, I'm posting this to ensure that you understand why
A.) The scroll was not smooth
B.) The list gets scrolled to the bottom when you scroll through it fast enough.
Explanation for A.)
It is because you are using animateScollTo. I've experienced issues with this method if called too often,
Explanation for this lies in how Lazy scrollers handle their children internally. You see, Lazy scrollers, as you might know, are meant to display only a small window of a large dataset to the user. To achieve this, it uses internal caching. So, the items on the screen, AND a couple of items to the top and bottom of the current window are cached.
Now, since in your code, you are making a call to animateScrollTo(size) inside the Composable's body (the items scope), the code will essentially be executed upon every composition.
Hence, on every recomposition, there is an active clash between the animateScrollTo method, and the users touch input. When the user scrolls past in a not-so-fast manner, this is what happens - user presses down, gently scrolls, then lifts up the finger. Now, remember this, for as long as the finger is actually pressed down, they animateScrollTo will seem to have no effect (because the user is actively holding a position on the scroller, so it won't be scrolled past it by the system). Hence, while the user is scrolling, some items ahead of the list are cached, but the animateScrollTo does not work. Then, because the motion is slow enough, the distance the scroller travels because of inertia is not a problem, since the list already has enough cached items to show for the distance. That also explains the second problem.
B.)
When you are scrolling through the list FAST enough, the exact same thing as the above case (the slow-scroll) happens. Only, this time the inertia carries the list too forward for the scroller to be handled based on the internal cache, and hence there is active recomposition. However, now since there is no active user input (they have lifted their finger off the screen), it actually does animate to the bottom, since their is no clash here for the animateScrollTo method.
For as long as your finger is pressed, no matter how fast you scroll, it won't scroll to the bottom (test that!)
Now to the solution of the actual problem. Philip your answer is brilliant. The only thing is that it might not work if the developer has an item remove implementation as well. Since only the size of the list is monitored, it will scroll to end when an item is added OR deleted. To counteract that, we would actually need some sort of reference value. So, either you can implement something of your own to provide you with a Boolean variable that actually confirms whether an item has been ADDED, or you could just use something like this.
#Composable
fun ResultsList(results: List<Pair<Square, Boolean>>) {
//Right here, create a variable to remember the current size
val currentSize by rememberSaveable { mutableStateOf (results.size) }
//Now, extract a Boolean to be used as a key for LaunchedEffect
var isItemAdded by mutableStateO(results.size > currentSize)
LaunchedEffect (isItemAdded){ //Won't be called upon item deletion
if(isItemAdded){
listState.animateScrollToItem(results.size)
currentSize = results.size
}
}
val listState = rememberLazyListState()
LazyRow(state = listState) {
items(results) { result ->
ResultsItem(result.first, result.second)
}
}
}
This should ensure the proper behaviour. Of course, let me know if there is anything else, happy to help.
Pretty obvious. Why are you calling:
listState.animateScrollToItem(results.size) inside your LazyList? Of course you're going to get extremely bad performance. You shouldn't be messing around with scrolling when items are being rendered. Get rid of this line of code.

How to switch to another XML layout after click on button?

I would like to have one kotlin file with the logic and I would like to allow users to switch between two different XLM layouts (logic of program is still the same, but layout of buttons shall be changed when clicking on button).
I simply add setContentView function to setOnClickListener for this button in order to load activity_main_second_layout.xml layout.
PS. activity_main_second_layout.xml is almost the same like activity_main.xml, I only changed the position of elements (not the names of elements)
button_switch_to_the_second_design.setOnClickListener {
setContentView(R.layout.activity_main_second_layout);
}
When clicking on the button, voala, the layout really changes to the second one.
BUT the functionality of the program is not working any more, the logic disappear. It seems that I need to resume running of the program somehow to make the code working again without interuption including loss of variables.
There is a lot of ways to do that.
In my opinion you should not try to change layout in runtime - it's possible, but you have to override setContentView and rebind all views and all listeners (or do it in other method, which will be called after changing the layout).
So... Sth like this:
fun sth() {
setContentView(R.layout.activity_main_second_layout)
rebindLayout(R.layout.activity_main_second_layout)
}
fun rebindLayout(#LayoutRes layoutId: Int) {
when (layoutId) {
R.layout.activity_main_first_layout -> { /* rebind views here */ }
R.layout.activity_main_second_layout -> { /* rebind views here */ }
}
}
The other's, better I think is to create independent fragments and change fragment via fragmentManager.
Others approches - ViewAnimator, ViewSwitcher.

Querying global mouse position in QML

I'm programming a small PoC in QML. In a couple of places in my code I need to bind to/query global mouse position (say, mouse position in a scene or game window). Even in cases where mouse is outside of MouseAreas that I've defined so far.
Looking around, the only way to do it seems to be having whole screen covered with another MouseArea, most likely with hovering enabled. Then I also need to deal with semi-manually propagating (hover) events to underlying mouseAreas..
Am I missing something here? This seems like a pretty common case - is there a simpler/more elegant way to achieve it?
EDIT:
The most problematic case seems to be while dragging outside a MouseArea. Below is a minimalistic example (it's using V-Play components and a mouse event spy from derM's answer). When I click the image and drag outside the MouseArea, mouse events are not coming anymore so the position cannot be updated unless there is a DropArea below.
The MouseEventSpy is taken from here in response to one of the answers. It is only modified to include the position as parameters to the signal.
import VPlay 2.0
import QtQuick 2.0
import MouseEventSpy 1.0
GameWindow {
id: gameWindow
activeScene: scene
screenWidth: 960
screenHeight: 640
Scene {
id: scene
anchors.fill: parent
Connections {
target: MouseEventSpy
onMouseEventDetected: {
console.log(x)
console.log(y)
}
}
Image {
id: tile
x: 118
y: 190
width: 200
height: 200
source: "../assets/vplay-logo.png"
anchors.centerIn: parent
Drag.active: mausA.drag.active
Drag.dragType: Drag.Automatic
MouseArea {
id: mausA
anchors.fill: parent
drag.target: parent
}
}
}
}
You can install a eventFilter on the QGuiApplication, where all mouse events will pass through.
How to do this is described here
In the linked solution, I drop the information about the mouse position when emitting the signal. You can however easily retrieve the information by casting the QEvent that is passed to the eventFilter(...)-method into a QMouseEvent and add it as parameters to the signal.
In the linked answer I register it as singleton available in QML and C++ so you can connect to the signal where ever needed.
As it is provided in the linked answer, the MouseEventSpy will only handle QMouseEvents of various types. Once you start dragging something, there won't be QMouseEvents but QDragMoveEvents e.t.c. Therefore you need to extend the filter method, to also handle those.
bool MouseEventSpy::eventFilter(QObject* watched, QEvent* event)
{
QEvent::Type t = event->type();
if (t == QEvent::MouseButtonDblClick
|| t == QEvent::MouseButtonPress
|| t == QEvent::MouseButtonRelease
|| t == QEvent::MouseMove) {
QMouseEvent* e = static_cast<QMouseEvent*>(event);
emit mouseEventDetected(e->x(), e->y());
}
if (t == QEvent::DragMove) {
QDragMoveEvent* e = static_cast<QDragMoveEvent*>(event);
emit mouseEventDetected(e->pos().x(), e->pos().y());
}
return QObject::eventFilter(watched, event);
}
You can then translate the coordinates to what ever you need to (Screen, Window, ...)
As you have only a couple of places where you need to query global mouse position, I would suggest you to use mapToGlobal or mapToItem methods.
I believe you can get cursor's coordinates from C++ side. Take a look on answer on this question. The question doesn't related to your problem but the solution works as well.
On my side I managed to get global coordinates by directly calling mousePosProvider.cursorPos() without any MouseArea.

Problems in my AS2 Game

Hey guys, I'm trying to make a 2D Platform style game similar to this game below:
http://www.gameshed.com/Puzzle-Games/Blockdude/play.html
I have finished making most of the graphic, and areas, and collision, but our character is still not able to carry things. I'm confused as to what code to use so that my character can carry the blocks. I need help as to how to make our character carry blocks that are in front of him, provided that the blocks that don't have anything on top of it. This has been confusing me for a week now, and any help would be highly appreciated. :D
I fondly remember my first AS2 game. The best approach is probably an object oriented approach, as I will explain.
In AS2, there is a hittest method automatically built into objects. There is a good tutorial on Kirupa here:
http://www.kirupa.com/developer/actionscript/hittest.htm
also
http://help.adobe.com/en_US/AS2LCR/Flash_10.0/help.html?content=00001314.html
First you'll want to generate your boxes using a Box class. Your class would need to look something like the following:
//Box.as pseudo-code
class Box {
var x_pos:Number;
var y_pos:Number;
var attachedToPlayer:Boolean;
function Box(_x:Number, _y:Number) {
this.x_pos = _x;
this.y_pos = _y;
}
//other code here
}
See this tutorial on how to attach a class to an object in the library:
http://www.articlesbase.com/videos/5min/86620312
To create a new Box, you'd then use something like
box1 = new Box(100,200);
// creates a box at position 100x,200y
However, you'll also want to store the blocks you want to pickup into some sort of array so you can loop through them. See http://www.tech-recipes.com/rx/1383/flash-actionscript-create-an-array-of-objects-from-a-unique-class/
Example:
//somewhere near the top of your main method, or whereever your main game loop is running from - note Box.as would need to be in the same folder
import Box;
//...then, somewhere before your game loop
//create an array to hold the objects
var boxArray:Array = new Array();
//create loop with i as the counter
for (var i=0; i<4; i++)
{
var _x:Number = 100 + i;
var _y:Number = 100 + i;
//create Box object
var box:Box = new Box();
//assign text to the first variable.
//push the object into the array
boxArray.push(box);
}
Similarly, you would need a class for your player, and to create a new Player object at the start of your game, e.g.
var player = new Player(0,0);
You could then run a hittest method for your player against the blocks in your array for the main game loop (i.e. the loop that updates your player's position and other game properties). There are probably more efficient ways of doing this, e.g. only looping for the blocks that are currently on the screen.
Once your array has been created, use a foreach loop to run a hittest against your player in your game's main loop, e.g.
//assuming you have an array called 'boxArray' and player object called 'player'
for(var box in boxArray){
if (player.hittest(box)) {
player.attachObjectMethod(box);
}
}
This is basically pseudo-code for "for every box that we have entered into the array, check if the player is touching the box. If the box is touching, use the box as the argument for a method in the player class (which I have arbitrarily called attachObjectMethod)".
In attachObjectMethod, you could then define some sort of behavior for attaching the box to the player. For example, you could create a get and set method(s) for the x and y position of your boxes inside the box class, along with a boolean called something useful like attachedToPlayer. When attachObjectMethod was called, it would set the box's boolean, e.g. in the Player class
//include Box.as at the top of the file
import Box;
//other methods, e.g. constructor
//somewhere is the Player.as class/file
public function attachObjectMethod (box:Box) {
box.setattachedToPlayer(true);
//you could also update fields on the player, but for now this is all we need
}
Now the attachedToPlayer boolean of the box the player has collided with would be true. Back in our game loop, we would then modify our loop to update the position of the boxes:
//assuming you have an array called 'boxArray' and player object called 'player'
for(var box in boxArray){
if (player.hittest(box)) {
player.attachObjectMethod(box);
}
box.updatePosition(player.get_Xpos, player.get_Ypos);
}
In our Box class, we now need to define 'updatePosition':
//Box.as pseudo-code
class Box {
var x_pos:Number;
var y_pos:Number;
var attachedToPlayer:Boolean;
function Box(box_x:Number, box_y:Number) {
this.x_pos = box_x;
this.y_pos = box_y;
}
public function updatePosition(_x:Number, _y:Number) {
if (this.attachedToPlayer) {
this.x_pos = _x;
this.y_pos = _y;
}
}
//other code here
}
As you can see we can pass the player's position, and update the box's position if the attachedToPlayer boolean has been set. Finally, we add a move method to the box:
public function move() {
if (this.attachedToPlayer) {
this._x = x_pos;
this._y = y_pos;
}
}
Examples of updating position:
http://www.swinburne.edu.au/design/tutorials/P-flash/T-How-to-smoothly-slide-objects-around-in-Flash/ID-17/
Finally, to make it all work we need to call the move method in the game loop:
//assuming you have an array called 'boxArray' and player object called 'player'
for(var box in boxArray){
if (player.hittest(box)) {
player.attachObjectMethod(box);
}
box.updatePosition(player.get_Xpos, player.get_Ypos);
box.move();
}
You have also specified that the blocks should only move with the player if they have nothing on top of them. When you call your attachedToPlayer method, you would also need to run a foreach loop inside the method between the box and the objects that might sit on top of the box. You should now have a fair idea from the above code how to do this.
I appreciate that this is quite a lengthy answer, and I haven't had an opportunity to test all the code (in fact I'm fairly positive I made a mistake somewhere) - don't hesitate to ask questions. My other advice is to understand the concepts thoroughly, and then write your own code one bit at a time.
Good luck!
The way I would do this is to design an individual hit test for each block he will be picking up, then code for the hit test to play a frame within the sprite's timeline of him carrying a block, and to play a frame within the block to be picked up's timeline of the block no longer at rest (disappeared?).
Good Luck if you're confused about what I've said just ask a little more about it and I'll try to help you if I can.