xmlkotlinlistenerandroid-buttonclicklistener

How to execute any code while the button is pressed?


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.


Solution

  • 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