Skip to content

Instantly share code, notes, and snippets.

@GrJanKandrac
Created November 10, 2020 08:24
Show Gist options
  • Save GrJanKandrac/1bd49da9727c6326e21cab4e16d99160 to your computer and use it in GitHub Desktop.
Save GrJanKandrac/1bd49da9727c6326e21cab4e16d99160 to your computer and use it in GitHub Desktop.
Convenient functions to handle Window Insets in Android Applications
package app.common.extensions
import android.content.Context
import android.graphics.Rect
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.annotation.Px
import androidx.core.view.*
import gr.extensions.preferences
private const val PREFERENCE_LAST_KNOWN_PADDING_LEFT = "insets_PL"
private const val PREFERENCE_LAST_KNOWN_PADDING_TOP = "insets_PT"
private const val PREFERENCE_LAST_KNOWN_PADDING_RIGHT = "insets_PR"
private const val PREFERENCE_LAST_KNOWN_PADDING_BOTTOM = "insets_PB"
const val INSET_DIRECTION_LEFT = 0b0001
const val INSET_DIRECTION_TOP = 0b0010
const val INSET_DIRECTION_RIGHT = 0b0100
const val INSET_DIRECTION_BOTTOM = 0b1000
@Suppress("DEPRECATION")
val WindowInsets.leftInset: Int get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.getInsets(WindowInsets.Type.systemBars()).left } else { this.systemWindowInsetLeft }
@Suppress("DEPRECATION")
val WindowInsets.topInset: Int get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.getInsets(WindowInsets.Type.systemBars()).top } else { this.systemWindowInsetTop }
@Suppress("DEPRECATION")
val WindowInsets.rightInset: Int get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.getInsets(WindowInsets.Type.systemBars()).right } else { this.systemWindowInsetRight }
@Suppress("DEPRECATION")
val WindowInsets.bottomInset: Int get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.getInsets(WindowInsets.Type.systemBars()).bottom } else { this.systemWindowInsetBottom }
private val WindowInsets.rect: Rect get() = Rect(leftInset, topInset, rightInset, bottomInset)
/**
* Add padding to view and resize it so that padding won't affect content of the view
*/
fun View.addPaddingAndResize(
@Px left: Int = 0,
@Px top: Int = 0,
@Px right: Int = 0,
@Px bottom: Int = 0
) {
val oldWidth = layoutParams.width
val oldHeight = layoutParams.height
if (oldWidth >= 0 || oldHeight >= 0) {
layoutParams = layoutParams.apply {
width = if (oldWidth != ViewGroup.LayoutParams.WRAP_CONTENT) oldWidth + left + right else oldWidth
height = if (oldHeight != ViewGroup.LayoutParams.WRAP_CONTENT) oldHeight + bottom + top else oldHeight
}
}
updatePadding(
paddingLeft + left,
paddingTop + top,
paddingRight + right,
paddingBottom + bottom
)
}
/**
* Adds margin to view if it belongs to [ViewGroup]
*/
fun View.addMargin(
@Px left: Int = 0,
@Px top: Int = 0,
@Px right: Int = 0,
@Px bottom: Int = 0
) {
layoutParams.let { params ->
if (params is ViewGroup.MarginLayoutParams) {
params.setMargins(
marginLeft + left,
marginTop + top,
marginRight + right,
marginBottom + bottom
)
requestLayout()
}
}
}
/**
* Kotlin-ized [setPadding] function, convenient to use with default values
*/
fun View.updatePadding(
@Px left: Int = 0,
@Px top: Int = 0,
@Px right: Int = 0,
@Px bottom: Int = 0
) {
setPadding(left, top, right, bottom)
}
/**
* Resize view by given [byWidth] [byHeight] dimensions
*
* @param byWidth by how much width should be changed (can be negative)
* @param byHeight by how much height should be changed (can be negative)
*
* @see resizeTo
*/
fun View.resizeBy(
@Px byWidth: Int = 0,
@Px byHeight: Int = 0
) {
val oldWidth = layoutParams.width
val oldHeight = layoutParams.height
resizeTo(oldWidth + byWidth, oldHeight + byHeight)
}
/**
* Resize view to given [newWidth] [newHeight] dimensions
*
* @param newWidth to how much width should be changed
* @param newHeight to how much height should be changed
*
* @see resizeBy
*/
fun View.resizeTo(
@Px newWidth: Int,
@Px newHeight: Int
) {
val oldWidth = layoutParams.width
val oldHeight = layoutParams.height
if (oldWidth >= 0 || oldHeight >= 0) {
layoutParams = layoutParams.apply {
width = if (oldWidth != ViewGroup.LayoutParams.WRAP_CONTENT) newWidth else oldWidth
height = if (oldHeight != ViewGroup.LayoutParams.WRAP_CONTENT) newHeight else oldHeight
}
}
}
/**
* If this View supports margins, set them to given [left], [top], [right], [bottom] values.
*
* @param left left margin to be set
* @param top top margin to be set
* @param right right margin to be set
* @param bottom bottom margin to be set
*
* @see updatePadding
*/
fun View.updateMargin(
@Px left: Int = 0,
@Px top: Int = 0,
@Px right: Int = 0,
@Px bottom: Int = 0
) {
layoutParams.let { params ->
if (params is ViewGroup.MarginLayoutParams) {
params.setMargins(left, top, right, bottom)
parent?.requestLayout() ?: requestLayout()
}
}
}
// Last known insets have to be stored in shared preferences so that if some activity crashes
// other activity can reconstruct the insets, since addInsetPadding/Margin is called, but
// setOnApplyWindowInsetsListener's listener is not being called
// If application is not crashing, storing insets is not needed
private fun getLastKnownInsets(context: Context) = Rect(
context.preferences.getInt(PREFERENCE_LAST_KNOWN_PADDING_LEFT, 0),
context.preferences.getInt(PREFERENCE_LAST_KNOWN_PADDING_TOP, 0),
context.preferences.getInt(PREFERENCE_LAST_KNOWN_PADDING_RIGHT, 0),
context.preferences.getInt(PREFERENCE_LAST_KNOWN_PADDING_BOTTOM, 0)
)
private fun setLastKnownInsets(context: Context, rect: Rect) {
context.preferences.edit().putInt(PREFERENCE_LAST_KNOWN_PADDING_LEFT, rect.left)
.putInt(PREFERENCE_LAST_KNOWN_PADDING_TOP, rect.top)
.putInt(PREFERENCE_LAST_KNOWN_PADDING_RIGHT, rect.right)
.putInt(PREFERENCE_LAST_KNOWN_PADDING_BOTTOM, rect.bottom)
.apply()
}
/**
* Adds padding to view based on window insets. Allowed flags are
*
* [INSET_DIRECTION_LEFT], [INSET_DIRECTION_BOTTOM],
* [INSET_DIRECTION_RIGHT], [INSET_DIRECTION_BOTTOM]
*
* @param direction flags
* @param once whether insets should be applied just once (ignoring further updates)
* @param resize whether view should be resized as well
*
* @see WindowInsets
* @see onInsetsChanged
*/
fun View.addInsetPadding(direction: Int, once: Boolean = false, resize: Boolean = true) {
onInsetsChanged(
once = once,
originalValue = Rect(paddingLeft, paddingTop, paddingRight, paddingBottom),
onChange = { originalPadding, insets ->
if (resize) {
resizeTo(
width - paddingLeft - paddingRight + insets.left + insets.right,
height - paddingTop - paddingBottom + insets.top + insets.bottom
)
}
updatePadding(
originalPadding.left + if (direction and INSET_DIRECTION_LEFT > 0)insets.left else 0,
originalPadding.top + if (direction and INSET_DIRECTION_TOP > 0) insets.top else 0,
originalPadding.right + if (direction and INSET_DIRECTION_RIGHT > 0) insets.right else 0,
originalPadding.bottom + if (direction and INSET_DIRECTION_BOTTOM > 0) insets.bottom else 0
)
})
}
/**
* Adds margin to view based on window insets. Allowed flags are
*
* [INSET_DIRECTION_LEFT], [INSET_DIRECTION_BOTTOM],
* [INSET_DIRECTION_RIGHT], [INSET_DIRECTION_BOTTOM]
*
* @param direction flags
* @param once whether insets should be applied just once (ignoring further updates)
*
* @see WindowInsets
* @see onInsetsChanged
*/
fun View.addInsetMargin(direction: Int, once: Boolean = false) {
onInsetsChanged(
once = once,
originalValue = Rect(marginLeft, marginTop, marginRight, marginBottom),
onChange = { originalMargin, insets ->
updateMargin(
left = originalMargin.left + if (direction and INSET_DIRECTION_LEFT > 0) insets.left else 0,
top = originalMargin.top + if (direction and INSET_DIRECTION_TOP > 0) insets.top else 0,
right = originalMargin.right + if (direction and INSET_DIRECTION_RIGHT > 0) insets.right else 0,
bottom = originalMargin.bottom + if (direction and INSET_DIRECTION_BOTTOM > 0) insets.bottom else 0
)
})
}
/**
* Sets listener for this view in order to listen for window inset changes.
*
* It is highly recommended to use [addInsetPadding] or [addInsetMargin] if you need just simple
* margin/padding update for example for your toolbar.
*
* @param once whether insets should be handled just once (ignoring further updates)
* @param originalValue value before any inset is applied
* @param onChange listener implementation with original value and insets as parameters
*
* @see OnApplyWindowInsetsListener
* @see WindowInsets
* @see addInsetMargin
* @see addInsetMargin
*/
fun <T> View.onInsetsChanged(
once: Boolean = false,
originalValue: T,
onChange: View.(originalValue: T, newInsets: Rect) -> Unit
) {
onChange(originalValue, getLastKnownInsets(context))
setOnApplyWindowInsetsListener { v, newInsets ->
onChange(originalValue, newInsets.rect)
if (once) setOnApplyWindowInsetsListener { _, insets -> insets }
setLastKnownInsets(v.context, newInsets.rect)
newInsets
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment