My progress bar is implemented to when the button is pressed down, the progress bar increments. When the button is released, the progress bar resets. The progress bar right now is very glitchy looking. I think it is because it gets called every second and so it jumps like that.
btn.setOnTouchListener{ view, motionEvent ->
Toast.makeText(context, "Long click detected", Toast.LENGTH_SHORT).show()
progressBar.visibility = View.VISIBLE
var i = 0
progressBar.progress = i
val countdownTimer = object: CountDownTimer(5000L, 500L){
override fun onTick(p0: Long) {
Log.d(TAG,"button up")
Log.d(TAG, "seconds: $p0")
if(motionEvent.action == MotionEvent.ACTION_UP){
i = 0
this.cancel()
}else{
i++
progressBar.progress = i*100/(5000/1000)
}
}
override fun onFinish() {
Log.d(TAG, "timer finished")
}
}.start()
true
}
Solution is to just decrease the time interval for the CountDownTimer so that the progress bar will update more frequently and progress smoother.
btn.setOnTouchListener{ view, motionEvent ->
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
if(!isTimerRunning) {
object : CountDownTimer(5000L, 50L) {
override fun onTick(p0: Long) {
if (motionEvent.action == MotionEvent.ACTION_UP) {
this.cancel()
} else {
val progress = 100 - ((p0.toFloat() / 5000f) * 100f).toInt()
progressBar.progress = progress
}
}
override fun onFinish() {
progressBar.progress = 100
Log.d(TAG, "timer finished")
}
}.start()
}
true
}
I recommend using the built-in animation utilities to do this. They will let you specify whatever total duration you want, and take care of computing the right "tick" times and rates. You'll get smooth updates without having to do much manual work.
val animator = ObjectAnimator.ofInt(progressBar, "progress", 0, 100)
animator.interpolator = LinearInterpolator()
animator.duration = 5_000 // milliseconds
btn.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
animator.start()
true
}
MotionEvent.ACTION_UP -> {
animator.cancel()
progressBar.progress = 0
true
}
else -> false
}
}
This framework will also let you execute code when the progress is finished (or other events, like on animation start):
animator.addListener(object: AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(context, "done", Toast.LENGTH_SHORT).show()
}
})
Related
I want the text view to be updated every second or less in a dynamic random way and at the end of the loop the text view show the last random number.
I tried to add a sleep method but it did not work every time I click the button the text view show the last random number directly.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rollButton: Button = findViewById(R.id.button1)
var result: TextView = findViewById(R.id.textView)
rollButton.setOnClickListener {
for (i in 1..10){
result.text = "${(1..6).random()}"
}
Toast.makeText(this, "Dice Rolled!", Toast.LENGTH_SHORT).show()
}
}
}
Coroutine delay is a way to do that, which is suggested in ltp's answer,
Another way is Runnable and Handler, an example function:
fun animateTextView(handler: Handler, textView: TextView, animate: Boolean = true) {
val runnable: Runnable = object : Runnable {
var randomNumber = 0
override fun run() {
randomNumber = (1..100).random()
textView.text = "$randomNumber"
handler.postDelayed(this, 500L)
}
}
// animate == false -> end text updates
if (animate) handler.postDelayed(runnable, 500L)
else handler.removeCallbacks(runnable)
}
Example use of the function:
private val textAnimaterHandler = Handler(Looper.getMainLooper())
animateTextView(textAnimaterHandler, binding.textView)
// Stop updates:
animateTextView(textAnimaterHandler, binding.textView, animate = false)
You can use Coroutine delay
rollButton.setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
//Disable button temporarily to prevent multiple clicks
it.isEnabled = false
for (i in 1..10) {
binding.textviewFirst.text = "${(1..6).random()}"
//One second delay before the next
delay(1000)
//If you want random delay - say 100ms to 1s
//delay((100L..1000L).random())
}
it.isEnabled = true
Toast.makeText(this#MainActivity, "Dice Rolled!", Toast.LENGTH_SHORT).show()
}
}
I have a plus and min button that work when pressed. Now I want to make them when you hold/press it down it goes up/down more then 1 at a time.
This is one of my regular buttons:
plusBtn.setOnClickListener {
if(isEmpty(PenaltyTimeInputTxt.text))
{
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
if(penaltyInput < 99){
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
}
else {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
}
}
is there a simple way of doing this? I saw something about onTouchListener.
EDIT ---
End result. Thanks to Tenfour04: include the whole fun view.doWhileHeld + this:
plusBtn.doWhileHeld(this.lifecycleScope) {
if(isEmpty(PenaltyTimeInputTxt.text)) {
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
while (isActive) {
if(penaltyInput < 99) {
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
}
else {
Toast.makeText(this#PenaltyConfigureActivity, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
break
}
delay(200)
}
}
Here is a helper class and function for this, which lets you do whatever you want while the button is held down:
fun View.doWhileHeld(
coroutineScope: CoroutineScope,
block: suspend CoroutineScope.() -> Unit
) = setOnTouchListener(object : View.OnTouchListener {
var job: Job? = null
var pointerInBounds = false
#SuppressLint("ClickableViewAccessibility")
override fun onTouch(view: View, event: MotionEvent): Boolean {
if (!isEnabled) {
job?.cancel()
return false
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
job = coroutineScope.launch(block = block)
pointerInBounds = true
}
MotionEvent.ACTION_MOVE -> {
val movedInBounds = event.x.roundToInt() in 0..view.width
&& event.y.roundToInt() in 0..view.height
if (pointerInBounds != movedInBounds) {
pointerInBounds = movedInBounds
if (pointerInBounds) {
job = coroutineScope.launch(block = block)
} else {
job?.cancel()
}
}
}
MotionEvent.ACTION_UP -> {
job?.cancel()
}
}
return false // allow click interactions
}
})
It runs a coroutine that restarts every time you click and hold. It also stops the coroutine and restarts it if you drag off the button and then back on, which is a conventional UI behavior.
To use it for your behavior, you can use a while loop:
plusBtn.doWhileHeld(viewLifecycleOwner.lifecycleScope) {
if(isEmpty(PenaltyTimeInputTxt.text)) {
PenaltyTimeInputTxt.setText("0")
}
penaltyInput = PenaltyTimeInputTxt.text.toString().toInt()
while (isActive) {
if(penaltyInput < 99) {
penaltyInput++
PenaltyTimeInputTxt.setText(penaltyInput.toString())
if (penaltyInput == 99) { // optional, might be nicer than showing toast
plusBtn.isEnabled = false
break
}
}
else {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
break
}
delay(500) // adjust for how fast to increment the value
}
}
try below code may help
plusBtn.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
// button pressed
object : CountDownTimer(99000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val temp = 99000 - (millisUntilFinished / 1000)
PenaltyTimeInputTxt.setText(""+temp)
}
override fun onFinish() {
Toast.makeText(this, "Penalty time cannot go over 99", Toast.LENGTH_SHORT).show()
}
}.start()
}
if (event.action == MotionEvent.ACTION_UP) {
// button released
}
true
}
I have an App with functionality such as Instagram Stories. I have 5 bars that are 1 by 1 being filled to indicate the length of the movie. This works fine, also when the video start playing again the bars get resetted.
But when I switch to the next video and I was in bar3 with the previous video, then bar0 starts (as expected) but also bar4 starts. Switching to a new video triggers onAnimationEnd and I want the animation to be cancelled so that I can reload a fresh progress indication. cancel(), end(), removeListener(this), they all don't work.
fun startAnimation(duration: Long, progressBars: ArrayList<ProgressBar>, currentBar: Int) {
Timber.d("test currentBar = $currentBar")
if (currentBar == 0) {
for (bar in progressBars) {
bar.setProgress(0)
}
}
animator?.end()
animator?.cancel()
animator?.removeAllListeners()
animator = null
animator = ValueAnimator.ofInt(0, progressBars[currentBar].getMax())
animator?.duration = duration
animator?.addUpdateListener { animation ->
progressBars[currentBar].setProgress(animation.animatedValue as Int)
}
animator?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
// animation.removeListener(this)
animation.cancel()
animation.end()
var nextBar = currentBar + 1
if (nextBar == progressBars.size) {
nextBar = 0
}
Timber.d("test nextBar = $nextBar")
startAnimation(duration, progressBars, nextBar)
}
})
animator?.start()
}
The first thing is; cancel() and end() methods are actually calling the onAnimationEnd() method. So, calling them inside of the onAnimationEnd() method is not helpful.
What you should have is a way to stop the already started animation series. Because it will keep starting the next animation because of the recursion.
Here is the code we want (or something prettier);
lateinit currentViewBarAnimation: CancelableAnimation
fun onCreate(){
currentViewBarAnimation = startAnimation(duration, progressBars, 0)
}
fun onNextVideoStarted(){
currentViewBarAnimation.stopOrCancelOrWhateverYouNameIt()
currentViewBarAnimation = startAnimation(duration, progressBars, 0)
}
CancellableAnimation can be a class that just has a boolean.
class CancelableAnimation(private var cancelled: Boolean = false){
fun cancel(){
cancelled = true
}
fun isCancelled() = cancelled
}
Now the final touch;
fun startAnimation(duration: Long, progressBars: ArrayList<ProgressBar>, currentBar: Int) : CancellableAnimation{
Timber.d("test currentBar = $currentBar")
if (currentBar == 0) {
for (bar in progressBars) {
bar.setProgress(0)
}
}
animator?.end()
animator?.cancel()
animator?.removeAllListeners()
animator = ValueAnimator.ofInt(0, progressBars[currentBar].getMax())
animator?.duration = duration
animator?.addUpdateListener { animation ->
progressBars[currentBar].setProgress(animation.animatedValue as Int)
}
val cancellableAnimation = CancellableAnimation()
animator?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
if(cancellableAnimation.isCancelled()) return
var nextBar = currentBar + 1
if (nextBar == progressBars.size) {
nextBar = 0
}
Timber.d("test nextBar = $nextBar")
startAnimation(duration, progressBars, nextBar)
}
})
animator?.start()
return cancellableAnimation
}
It looks a little confusing but it is the way to go since moving to a new video is an async action.
I have a BottomSheetDialogFragment. But even the slightest downward swipe dismisses the Dialog.
I do not want to make it static and remove the swipe down to dismiss behaviour. I want to be able to change the sensitivity, if the swipe is x pixels downwards, then dismiss
use BottomSheetBehavior
this will get the behavior for your BottomSheetDialogFragment view
var mBehavior: BottomSheetBehavior<*> = BottomSheetBehavior.from([your view reference])
then you can setup like this
val dismissOffset: Float = [-1..0] // 0 is the starting position. -1 is hidden. -0.5 is middle
var offset: Float? = null
mBehavior.setBottomSheetCallback(object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_SETTLING) {
if (offset!! > dismissOffset) {
mBehavior.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
mBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
}
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
offset = slideOffset
}
})
The answer of ChiChung Luk almost acceptable, but I have tried it with com.google.android.material:material:1.2.1 library and it didn't work as expected. First of all slideOffset changes from 1 to -1, and not from 0 to -1. The second issue was that even when we set mBehavior.setState(STATE_EXPANDED) in onStateChanged, the system any way sets the state to STATE_HIDDEN after the bottom sheet is expanded, from onStopNestedScroll > startSettlingAnimation. So there should be a flag that disallow hide before bottom sheet is not expanded.
Solution:
bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
val dismissOffset: Float = -0.2f // when less value then wider should be swipe gesture to dismiss
private var currOffset: Float = 1f // from 1 to -1
private var dismissAllowed: Boolean = true
override fun onStateChanged(
bottomSheet: View, #BottomSheetBehavior.State newState: Int
) {
if (newState == BottomSheetBehavior.STATE_SETTLING) {
if (currOffset > dismissOffset) {
dismissAllowed = false
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
} else {
bottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN
}
} else if (newState == BottomSheetBehavior.STATE_EXPANDED) {
dismissAllowed = true
} else if (newState == BottomSheetBehavior.STATE_HIDDEN) {
if (dismissAllowed) {
dialog.cancel()
}
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
currOffset = slideOffset
}
})
I'm going to assume that like me, you had a NestedScrollView in your bottom sheet (this is the only thing that caused the behavior you described to happen for me).
My solution was as follows:
/** Convenience function to fix https://github.com/material-components/material-components-android/issues/1055 */
private fun NestedScrollView.fixNestedScrolling(dialog: BottomSheetDialog) {
fun updateScrollView(scrollY: Int) {
val wasNestedScrollingEnabled = isNestedScrollingEnabled
isNestedScrollingEnabled = scrollY > 0
if (wasNestedScrollingEnabled != isNestedScrollingEnabled) {
// If property has changed, we need to requestLayout for it to apply to swipe gestures.
dialog.findViewById<View>(R.id.design_bottom_sheet)?.requestLayout()
}
}
setOnScrollChangeListener { _, _, scrollY, _, _ -> updateScrollView(scrollY) }
// Fire off initial update
updateScrollView(0)
}
The NestedScrollView still works correctly, and once scrollY == 0 (i.e. we're at the top), nested scrolling is disabled so the BottomSheetBehavior uses the (much more natural) calculations that it usually does before initiating a dismiss.
I want to increase the duration of the animation outside the original function, meaning that if a button is pressed the animation time should increase and when the button is released the animation time should jump back to normal.
This is the animation:
val animation = arrayOf(-820f).map { translation ->
ObjectAnimator.ofFloat(stripe, "translationX", translation).apply {
duration = 600
repeatCount = ObjectAnimator.INFINITE
}
}
val set = AnimatorSet()
set.playTogether(animation)
set.start()
And this is the function for the button when it is clicked:
button.setOnTouchListener(object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
//Increase animation duration to 1200
}// On click
}
when (event?.action) {
MotionEvent.ACTION_UP -> {
//Put animation duration back to 600
}// Released
}
return v?.onTouchEvent(event) ?: true
}
})
I need help finding a way to set a new duration time in ACTION_DOWN and set it then back to normal in ACTION_UP.
Thanks!
An Interpolator should be used to change animation speed.
For example:
val interpolationValue = floatArrayOf(1f)
...
val set = AnimatorSet()
set.playTogether(animation)
set.interpolator = object : LinearInterpolator() {
override fun getInterpolation(input: Float)
: Float {
return input * interpolationValue[0]
}
}
set.start()
...
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
interpolationValue[0] = 2f
//Increase animation duration to 1200
}// On click
MotionEvent.ACTION_UP -> {
interpolationValue[0] = 1f
//Put animation duration back to 600
}// Released
}