I have a button
and imageView
and I need to execute some code that changes image properties while button
is pressed. But I do not know how to realize it. I tried to use onTouchListener
by executing code:
while(event?.action != MotionEvent.ACTION_UP)
But it causes the app to hang.
CodePudding user response:
You want to start your task (whatever it is) when you get an ACTION_DOWN
event (i.e. the user has pressed your View
) and stop it when you get an ACTION_UP
event (the user has lifted their finger or whatever) or an ACTION_CANCEL
(e.g. the user's dragged their finger outside of the View
).
That'll give you the while the button is held behaviour. But that task needs to run asynchronously - coroutines, a thread, a delayed Runnable
posted to the main looper (you can do this through a View
by calling one of the post
methods).
You can't just spin in a loop, the system can't do anything else (including displaying UI changes and responding to touches) until your code has finished running. And if you're waiting for an ACTION_UP
while blocking the thread, you're not going to get one. (A new MotionEvent
would come through a later function call anyway.)
Here's a simple example using the looper:
class MainFragment : Fragment(R.layout.fragment_main) {
lateinit var binding: FragmentMainBinding
// This is a reusable Runnable that changes a background, then reposts itself
// to the task queue to run again in the future.
private val colourCycleTask = object : Runnable {
private fun rnd() = (0..255).random()
override fun run() {
binding.someView.setBackgroundColor(Color.rgb(rnd(), rnd(), rnd()))
binding.someView.postDelayed(this, 250L)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMainBinding.bind(view)
binding.button.addHoldListener()
}
private fun View.addHoldListener() {
setOnTouchListener { view, event ->
var handled = true
when(event.action) {
MotionEvent.ACTION_DOWN -> view.post(colourCycleTask) // run the task
MotionEvent.ACTION_UP -> {
view.removeCallbacks(colourCycleTask) // remove the task from the queue
view.performClick()
}
MotionEvent.ACTION_CANCEL -> view.removeCallbacks(colourCycleTask)
else -> handled = false
}
handled
}
}
}
Posting a Runnable
to the main Looper
is basically adding a bit of your code to a task queue - so you're not blocking the thread and preventing anything else from happening, you're saying to the system "hey, do this at this time please" and it'll try its best to hit that time. And because the Runnable
re-posts itself at the end, you get that looping behaviour while allowing other code to run, because you're not seizing control of execution. You're just deferring a bit of code to run later, then allowing execution to continue.
Coroutines are a neater way to do this I think, but I like to use the Looper
as an example because it's been a part of Android since the old times, and it can be a simple way to get this kind of behaviour when you have main-thread work that needs a delay or to run for a significant amount of time
CodePudding user response:
You can't just do infinite loops in the main thread, it will hang your app. But it's possible to do asynchronous code that won't block the main thread. Kotlin coroutines can help to do this.
If you're using kotlin coroutines in your project, you can launch a new coroutine with an infinite loop on the ACTION_DOWN
event. And cancel the corresponding job on the ACTION_UP
event:
var job: Job? = null
button.setOnTouchListener { v, event ->
when (event.action) {
ACTION_DOWN -> {
job = launch {
while (true) {
// increasing counter as an example:
textView.text = "${counter }"
delay(100)
}
}
}
ACTION_UP -> {
job?.cancel()
}
}
false
}