Last active
March 10, 2022 09:59
-
-
Save petedoyle/5e1a9791eab8a819c37f3ed849540e0d to your computer and use it in GitHub Desktop.
Kotlin extension functions to simplify edge-to-edge on Android Q
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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