Skip to content

Instantly share code, notes, and snippets.

@petedoyle
Last active March 10, 2022 09:59
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save petedoyle/5e1a9791eab8a819c37f3ed849540e0d to your computer and use it in GitHub Desktop.
Save petedoyle/5e1a9791eab8a819c37f3ed849540e0d to your computer and use it in GitHub Desktop.
Kotlin extension functions to simplify edge-to-edge on Android Q
/**
* Applies window transformation flags (WTFs) to aid in supporting
* edge-to-edge layouts on Android Q and higher.
*/
fun Activity?.applyEdgeToEdge(
lightStatusBar: Boolean = false,
lightNavigationBar: Boolean = false,
transparentStatusBar: Boolean = true,
immersive: Boolean = false
) {
if (this == null) {
return
}
var windowTransformFlags = View.SYSTEM_UI_FLAG_VISIBLE
// Set edge-to-edge flags
windowTransformFlags = windowTransformFlags
.or(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) // Render behind the status bar
.or(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) // Render behind the navigation bar
.or(View.SYSTEM_UI_FLAG_LAYOUT_STABLE) // Ensure the window insets are their most extreme and never change
// Set light status bar (dark icons on light background), if enabled
if (lightStatusBar) {
windowTransformFlags = windowTransformFlags.or(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
}
// Set light navigation bar (dark icons on light background), if enabled
if (lightNavigationBar && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
windowTransformFlags = windowTransformFlags.or(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
}
// Set immersive mode, if enabled
if (immersive) {
windowTransformFlags = windowTransformFlags
.or(View.SYSTEM_UI_FLAG_FULLSCREEN)
.or(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
.or(View.SYSTEM_UI_FLAG_IMMERSIVE)
.or(View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
// Apply the flags
window.decorView.systemUiVisibility = windowTransformFlags
// Apply transparent status bar, if enabled
if (transparentStatusBar) {
window.statusBarColor = resources.getColor(android.R.color.transparent, theme)
}
}
class SampleFragment : Fragment() {
private lateinit var binding: FragmentSampleBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_sample, container, false)
return binding.root
}
override fun onStart() {
super.onStart()
activity.applyEdgeToEdge(lightNavigationBar = true)
}
override fun onStop() {
super.onStop()
activity.applyEdgeToEdge(lightNavigationBar = true, lightStatusBar = true)
}
}
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import androidx.core.view.updateLayoutParams
import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment
/**
* Ask the system to call [View.onApplyWindowInsets] with insets. Unlike
* [View.requestApplyInsets], this is safe to call before a view is
* attached to a window, for example: from [Fragment.onCreateView].
*
* Sourced from Chris Banes' post here:
* https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
*/
fun View.requestApplyInsetsWhenAttached() {
when (isAttachedToWindow) {
true -> requestApplyInsets() // View is attached to window, just request as normal
else -> { // Wait until view is attached to window, then request
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
}
fun View.doOnApplyWindowInsets(function: (View, WindowInsets, InitialPadding, InitialMargin) -> Unit) {
val initialPadding = recordInitialPaddingForView(this) // Remember the intrinsic padding from XML
val initialMargin = recordInitialMarginForView(this) // Remember the intrinsic margin from XML
// Set an actual OnApplyWindowInsetsListener which proxies to the given
// lambda, also passing in the original padding state
setOnApplyWindowInsetsListener { view, insets ->
function(view, insets, initialPadding, initialMargin)
insets // Always return the insets, so that children can also use them
}
requestApplyInsetsWhenAttached() // request some insets
}
data class InitialPadding(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
)
data class InitialMargin(
val left: Int,
val top: Int,
val right: Int,
val bottom: Int
)
private fun recordInitialPaddingForView(view: View) = InitialPadding(
view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom
)
private fun recordInitialMarginForView(view: View): InitialMargin {
val lp = view.layoutParams
return when (lp is ViewGroup.MarginLayoutParams) {
true -> InitialMargin(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin)
else -> InitialMargin(0, 0, 0, 0)
}
}
@BindingAdapter(
"paddingLeftSystemWindowInsets",
"paddingTopSystemWindowInsets",
"paddingRightSystemWindowInsets",
"paddingBottomSystemWindowInsets",
"marginLeftSystemWindowInsets",
"marginTopSystemWindowInsets",
"marginRightSystemWindowInsets",
"marginBottomSystemWindowInsets",
requireAll = false
)
fun View.applySystemWindows(
applyPaddingLeft: Boolean = false,
applyPaddingTop: Boolean = false,
applyPaddingRight: Boolean = false,
applyPaddingBottom: Boolean = false,
applyMarginLeft: Boolean = false,
applyMarginTop: Boolean = false,
applyMarginRight: Boolean = false,
applyMarginBottom: Boolean = false
) {
doOnApplyWindowInsets { view, insets, initialPadding, initialMargin ->
val paddingLeft = if (applyPaddingLeft) insets.systemWindowInsetLeft else 0
val paddingTop = if (applyPaddingTop) insets.systemWindowInsetTop else 0
val paddingRight = if (applyPaddingRight) insets.systemWindowInsetRight else 0
val paddingBottom = if (applyPaddingBottom) insets.systemWindowInsetBottom else 0
val marginLeft = if (applyMarginLeft) insets.systemWindowInsetLeft else 0
val marginTop = if (applyMarginTop) insets.systemWindowInsetTop else 0
val marginRight = if (applyMarginRight) insets.systemWindowInsetRight else 0
val marginBottom = if (applyMarginBottom) insets.systemWindowInsetBottom else 0
view.setPadding(
initialPadding.left + paddingLeft,
initialPadding.top + paddingTop,
initialPadding.right + paddingRight,
initialPadding.bottom + paddingBottom
)
if (layoutParams is ViewGroup.MarginLayoutParams) {
updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = initialMargin.left + marginLeft
topMargin = initialMargin.top + marginTop
rightMargin = initialMargin.right + marginRight
bottomMargin = initialMargin.bottom + marginBottom
}
// Before Oreo/8.x, updating a child's layout params did
// not clear the parent ViewGroup's measure cache. Thus,
// we do that here for versions < Oreo.
//
// Note that this clears the measure cache, to be
// re-computed on the next frame. Thus, multiple calls
// to requestLayout() will not result in one layout
// pass per call.
//
// Fixed in Oreo and later via this commit:
// https://android.googlesource.com/platform/frameworks/base/+/5429daaa510ae144ca9a9a7052980faf8d9b2087
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
view.parent.requestLayout()
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment