Revision control
Copy as Markdown
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
package org.mozilla.focus.widget
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.ViewTreeObserver
import org.mozilla.focus.R
private const val ANIMATION_LENGTH_MS = 200L
/**
* A helper class to implement a ViewGroup that resizes dynamically (by adding padding to the bottom)
* based on whether a keyboard is visible or not.
*
* Implementation based on:
*
* A View using this delegate needs to forward the calls to onAttachedToWindow() and onDetachedFromWindow()
* to this class.
*/
internal class ResizableKeyboardViewDelegate(
private val delegateView: View,
attrs: AttributeSet?,
) {
private val rect = Rect()
private var decorView: View? = null
private var shouldAnimate = false
private var isAnimating = false
private val layoutListener = ViewTreeObserver.OnGlobalLayoutListener {
if (isAnimating) {
return@OnGlobalLayoutListener
}
val difference = calculateDifferenceBetweenHeightAndUsableArea()
// If difference > 0, keyboard is showing.
// If difference =< 0, keyboard is not showing or is in multiview mode.
if (difference > 0) {
// Keyboard showing -> Set difference has bottom padding.
if (delegateView.paddingBottom != difference) {
updateBottomPadding(difference)
}
} else {
// Keyboard not showing -> Reset bottom padding.
if (delegateView.paddingBottom != 0) {
updateBottomPadding(0)
}
}
}
init {
val styleAttributeArray = delegateView.context.theme.obtainStyledAttributes(
attrs,
R.styleable.ResizableKeyboardViewDelegate,
0,
0,
)
shouldAnimate = try {
styleAttributeArray.getBoolean(R.styleable.ResizableKeyboardViewDelegate_animate, false)
} finally {
styleAttributeArray.recycle()
}
}
fun onAttachedToWindow() {
delegateView.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
}
fun onDetachedFromWindow() {
delegateView.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
}
private fun updateBottomPadding(value: Int) {
if (shouldAnimate) {
animateBottomPaddingTo(value)
} else {
delegateView.setPadding(0, 0, 0, value)
}
}
private fun animateBottomPaddingTo(value: Int) {
isAnimating = true
val animator = ValueAnimator.ofInt(delegateView.paddingBottom, value)
animator.addUpdateListener { animation: ValueAnimator ->
delegateView.setPadding(
0,
0,
0,
animation.animatedValue as Int,
)
}
animator.duration = ANIMATION_LENGTH_MS
animator.addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
isAnimating = false
}
},
)
animator.start()
}
private fun calculateDifferenceBetweenHeightAndUsableArea(): Int {
if (decorView == null) {
decorView = delegateView.rootView
}
decorView!!.getWindowVisibleDisplayFrame(rect)
return if (rect.height() >= rect.width()) {
delegateView.resources.displayMetrics.heightPixels - rect.bottom
} else {
delegateView.resources.displayMetrics.widthPixels - rect.right
}
}
}