I have a button in my activity. When the user clicks on the button, progress is shown and a request is sent to the server, after receiving a response to the request, the progress is hided and the next activity is opened.
However, if the user has time to press the button several times, two or more activities will open.
For preventing double click I use DebouncedOnClickListener. Here is my code:
abstract class DebouncedOnClickListener protected constructor() : View.OnClickListener {
private val minimumIntervalMillis: Long = 1000
private val lastClickMap: MutableMap<View, Long>
abstract fun onDebouncedClick(v: View?)
override fun onClick(clickedView: View) {
val previousClickTimestamp = lastClickMap[clickedView]
val currentTimestamp = SystemClock.uptimeMillis()
lastClickMap[clickedView] = currentTimestamp
if (previousClickTimestamp == null || abs(currentTimestamp - previousClickTimestamp) > minimumIntervalMillis) {
onDebouncedClick(clickedView)
}
}
init {
lastClickMap = WeakHashMap()
}
}
This method works in many cases. However, the request can be processed for an unknown amount of time. And the user can click on the button while the request is completed, the progress will be closed and the process of opening the next activity will start. I do not know how long a new activity can be launched, and at this moment another click on the button can occur, which will subsequently lead to the opening of two activities.
How can you avoid double clicking until the activity opens, please help me.
P.S. Even when I try to disable the button, double-clicking can still happen during the launch of the activity
Just paste this on button click
button.setEnabled(false);
You can use a boolean flag like following.
private val onClicked: Boolean = false
override fun onClick(clickedView: View) {
if(!onClicked) {
onClicked = true
// Do something
onClicked = false
}
}
Add required functions in Utils and wherever it's required call it by passing view as parameter.
const val DIGIT_THOUSAND = 1000
fun preventMultipleTap(view: View) {
preventMultipleTap(view, DIGIT_THOUSAND.toLong())
}
fun preventMultipleTap(view: View, delayInMillis: Long) {
view.isEnabled = false
view.postDelayed({ view.isEnabled = true }, delayInMillis)
}
Related
I want to start a timer (i think in this case CountDownTimer) as soon as I get a specific wakelock. Once the countdown timer finishes, i want to display an alert dialog. When the wakelock is released, the timer should be killed. When the timer is running and I get a user activity, I want to kill the previous timer and start a new one (or maybe reset this one)
What would be the best way for me to implement this?
Yes you can do it, the only thing you need to do is like this -->
`val dialogListener = DialogInterface.OnClickListener { dialog, which ->
when (which) {
BUTTON_POSITIVE -> {
}
DialogInterface.BUTTON_NEGATIVE -> {
}
}
}
val dialog = AlertDialog.Builder(this)
.setTitle(“YOUR TITLE HERE”)
.setMessage(“YOUR MESSAGE HERE)
.setPositiveButton(“TEXT OF BUTTON”)
.setNegativeButton(“TEXT OF BUTTON”)
.create()
dialog.setOnShowListener(object : OnShowListener {
private val AUTO_DISMISS_MILLIS = 5000 //Timer in this case 5 Seconds
override fun onShow(dialog: DialogInterface) {
//if you want to have stuff done by your buttons it's going go be here with a respective call like (dialog as AlertDialog).getButton(The button you want positive or negative)
then everything you want to happen on it
*************************************************
//here is the timer associated to the button
val defaultButton: Button =
dialog.getButton(AlertDialog.BUTTON_POSITIVE)
val positiveButtonText: CharSequence = defaultButton.text
object : CountDownTimer(AUTO_DISMISS_MILLIS.toLong(), 100) {
override fun onTick(millisUntilFinished: Long) {
defaultButton.text = java.lang.String.format(
Locale.getDefault(), "%s (%d)",
positiveButtonText,
TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished) + 1
)
}
override fun onFinish() {
//everything you wanna do on the moment the timer is off is going to happen here if you wanna open another dialog you need to call it here
}
}.start()
}
})
dialog.show()
}
`
I have a function SendInstruction which send instructions to the user to press a button with label x for 3 seconds. I have four buttons with labels y,z and w.
fun SendInstruction(buttonLabel:String):String{
return buttonLabel
}
I have a counter that either increments by 20 if correct sent instruction matches user clicked button and decrements if not correct.
But when correct for 3 seconds then another instruction can be sent for button y otherwise user must pass button x to go to y and also must pass y to go to z and must pass z to go to w
Here is what i tried so far
var counter:Int = 0
GlobalScope.launch{
SendInstruction("click button x")
if(userclicked)//always true for 3 seconds
delay(3000)//check in 3s if instructions matches user clicked
counter +=20
SendInstruction("click button y")
if(userclicked)//always true for 3 s
delay(3000)//check in 3s if instruction matches user clicked
else
counter = 0
//restart
}
But it just doesn't work, can anyone help me with this please.
You could use a timeout pattern, like a reverse debounce catch, to help here. Here is an example, where you set a click target and a coroutine that will reset the target in 3 seconds. If you click the target in that time the coroutine is cancelled and you move to the next target.
private var job: Job? = null
private var target = ""
private var score = 0
fun onClick(btn: String, next: String) {
if(btn == target) {
advance(next)
}
else {
// Handle wrong click by starting over
reset()
}
}
fun advance(next: String) {
// Handle a correct click by cancelling
// the coroutine, updating the score,
// and setting the next target
// Cancel the timeout
job?.cancel()
// Add to the score
score += 20
// Set the next/end case
if(next.isEmpty()) {
SendInstruction("you won")
}
else {
updateTarget(next)
}
}
fun updateTarget(next: String) {
// Update the target, notify the user,
// and start a coroutine to reset
// the target in 3 seconds
target = next
SendInstruction("Click on $next")
// Reset the target after 3 seconds
// unless the coroutine is cancelled
// by a correct button click first
job = lifecycleScope.launch {
delay(3000)
reset()
}
}
fun reset() {
// Start over on a wrong click
// or a timeout. Alternately could
// just show "game over" and make
// them click "Go" or something to start over
job?.cancel()
target = ""
score = 0
updateTarget("x")
}
fun start() {
// Call to start the game, probably from onResume or when the user clicks "Go" or something
updateTarget("x")
}
// Call these when the specific buttons are clicked
fun onClickX() {
onClick("x", "y")
}
fun onClickY() {
onClick("y", "z")
}
fun onClickZ() {
onClick("z", "w")
}
fun onClickW() {
onClick("w", "")
}
I have an app with a splashscreen, which stays for about 2 seconds.
After that, it switches to another activity A.
In A, I set a value in a SeekBar and after that, click a Button to confirm.
When I simply start a recorded Espresso test doing this, it tries to play while on the splashscreen. So when it tried to set the SeekBar value or click the Button, I get a NoMatchingViewException. So my first attempt at fixing this was to simply add a sleep(5000). This worked.
However, I don't want to put a manual sleep in after every Activity switch.
Because it seems like unnecessary code
Because it would mean unnecessary waiting time for running the test
The timing might be arbitrary and could be different for different devices
So I tried to check whether or not I'm in the right Activity/can see the right views. I did this using some SO links: Wait for Activity and Wait for View.
However, even that does not work 100%.
I have these two functions:
fun <T: AppCompatActivity> waitForActivity(activity: Class<T>, timeout: Int = 5000, waitTime: Int = 100) {
val maxTries = timeout / waitTime
var tries = 0
for(i in 0..maxTries) {
var currentActivity: Activity? = null
getInstrumentation().runOnMainSync { run { currentActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(
Stage.RESUMED).elementAtOrNull(0) } }
if(activity.isInstance(currentActivity)) {
break
} else {
tries++
sleep(waitTime.toLong())
}
}
}
fun waitForView(
#IntegerRes id: Int,
waitMillis: Int = 5000,
waitMillisPerTry: Long = 100
): ViewInteraction {
// Derive the max tries
val viewMatcher = allOf(
withId(id),
isDisplayed()
)
val maxTries = waitMillis / waitMillisPerTry.toInt()
var tries = 0
for (i in 0..maxTries)
try {
tries++
val element = onView(viewMatcher)
element.check { view, noViewFoundException ->
if(view == null) {
throw noViewFoundException ?: Exception("TEST")
}
if(view.hasWindowFocus()) {
throw noViewFoundException ?: Exception("TEST2")
}
}
return element
} catch (e: Exception) {
if (tries == maxTries) {
throw e
}
sleep(waitMillisPerTry)
}
throw Exception("Error finding a view matching $viewMatcher")
}
Neither of those work 100%. Both of them seem to return within the timeout restrictions, and have "found" the activity/view. However, the expected view, e.g. a Button is not yet ready to perform, for example, element.perform(click()). It does not lead to a NoMatchingViewException, but it does not perform the click I did either. For the SeekBar, I use this:
private fun setSeekValue(seekBar: ViewInteraction, age: Int) {
val fullPercentage = .9f
val step = 1/99f
seekBar.perform(
GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val pos = IntArray(2)
view?.getLocationOnScreen(pos)
FloatArray(2).apply {
this[0] = pos[0] + view!!.width * (.05f + fullPercentage*step*age)
this[1] = pos[1] + view.height * .5f
}
},
PrecisionDescriber {
FloatArray(2).apply {
this[0] = .1f
this[1] = 1f
}
},
InputDevice.SOURCE_MOUSE,
MotionEvent.ACTION_BUTTON_PRESS
)
)
}
However, when I use these functions and just put a very short sleep, e.g. sleep(100) after it, it works. This again however, would go against the three reasons listed above, which I'm trying to avoid here.
As you can see in the function waitForView, I tried to check if the View is "usable", using hasWindowFocus(). But this still does not perform the click, except for when I again put a sleep(80) or something after it. So it waits for the splashscreen to switch to A, finds the view it's looking for and then can't perform the click, except for when I wait a little bit.
I have also tried these functions of View:
isEnabled
isShown
visibility
getDrawingRect
isFocusable
isFocused
isLayoutDirectionResolved
Neither of them worked as I expected. With all of them, after the needed value was returned on the element.check part of waitForView, they would still not be accessible without putting a short sleep after.
Is there a way to reliably check if I can perform a click on a view/safely can perform ViewInteraction.perform()
Either by checking, if an activity is fully loaded to a point where its views are usable. Or by directly checking if a view is usable.
I need some help to achieve something that is maybe simple. My code is like this :
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
repeatAction()
val stoprepeat = findViewById(R.id.btn) as Button
stoprepeat.setOnClickListener{
// I need to completely stop the actions that are repeating from here
}
}
private fun repeatAction(){
var repeataction = FixedRateTimer("mytimer",false,2400,20000){
this#HomeActivity.runOnUiThread {
// Stuff to repeat (3 or 4 actions)
}
}
}
}
What I'm trying to do is to stop the FixedTimeRate tasks when I Click on the button.
Also, is there a way to prevent fixedRateTimer from crashing the app after maybe 10 minutes of running?
how about intruducing a var keepRunning: Boolean = true in HomeActivity that is checked every cycle by repeatAction() ie. while(keepRunning){ ... } and is set to false by stoprepeat.setOnClickListener?
I want to have a custom keyboard class that has different buttons. This class will implement the onClick listener when each button is called but I want another class to handle this event. For example:
class Keyboard {
Button A
Button B
...
fun onClick() {
// forward this action to the class that contains me
}
}
class MainActivity {
val keyboard = Keyboard()
keyboard.onClickListener {
// handle the click events
}
}
How to approach this?
If your objective is to communicate between the MainActivity and the Keyboard then a callback will do just fine. You could implement it like this:
typealias KeyboardCallback = (Button) -> Unit
// Do not recommend doing this, it's for the example only
// It's probably better parsing the keyboard input as Char or Int
enum class Button {
A, B, C, D
}
class Keyboard {
private var callback : KeyboardCallback? = null
fun onKeyPressedListener(listener: KeyboardCallback) {
callback = listener
}
fun onClick(button: Button) {
// forward this action to the class that contains me
callback?.invoke(button)
}
}
class MainActivity {
val keyboard = Keyboard()
keyboard.onKeyPressedListener { key: Button ->
// parse buttons here
}
// Somewhere else (Will call the callback)
keyboard.onClick(Button.A)
}
But if you need to listen to the Keyboard from multiples places, then this implementation will not work because as soon as you register a second callback, the first one gets stale (you lose the reference to it, since the callback variable can only hold one listener), you can see this problem here.
If this is ok for your implementation, then do it (it's known as the Command Pattern**). If it's not, then you need to implement the Observable/Observer Pattern , which would be more like this:
typealias KeyboardCallback = (Button) -> Unit
// Do not recommend doing this, it's for the example only
// It's probably better parsing the keyboard input as Char
enum class Button {
A, B, C, D
}
class Keyboard {
private val listeners = ArrayList<KeyboardCallback>()
fun onKeyPressedListener(listener: KeyboardCallback) {
callback.add(listener)
}
fun onClick(button: Button) {
// forward this action to the class that contains me
for (callback in listeners) {
callback(button)
}
}
}
class MainActivity {
val keyboard = Keyboard()
keyboard.onKeyPressedListener { key: Button ->
// parse buttons here
}
keyboard.onKeyPressedListener { another: Button ->
// added another listener
}
// Somewhere else (Will call the callback)
keyboard.onClick(Button.A)
}
I've made a simple example for the observable in this kotlin playground.
** Well, not exactly, it's a simplified version of it, since the command pattern is implemented using a interface and a class to represent the "command/callback", and that class can store arbitrary state, which the function pointer cannot.
Create attribute clickListener in Keyboard class
Create setter for this attribut
In your onCLick method in Keyboard just call clickListener?.onClick(buttonClicked) where buttonClicked is button in keyboard which was clicked
In your MainActivity set listener and handle clickEvent
Example:
class Keyboard {
private var clickListener: View.OnClickListener? = null
fun setListener(listener: View.OnClickListener) {
clickListener = listener
}
fun onClick() {
clickListener?.onClick(buttonClicked) // pass button which was clicked
}
}
class MainActivity {
private val keyboard = Keyboard()
init {
keyboard.setListener(View.OnClickListener {
//do something
//it -> button which was clicked
})
}
}