Handle window insets with a new way

For example, switching pages in home should only have scale and alpha animations, but a "translate y" animation shows. This is because Data Binding is triggered later (like "in the next frame"), causing the animation runs before view attribute changes.

This commit introduces WindowInsetsHelper class and use it to handle all window insets. With the help of LayoutInflaterFactory from the previous commit, we can control insets behavior by adding our attributes to the XML and anything is done by WindowInsetsHelper class.

As changes are highly coupling, this commit also contains new ItemDecoration for lists, replacing the random combination of padding and empty drawable. And "fixEdgeEffect" extension for RecyclerView, making edge effects respect padding.
This commit is contained in:
RikkaW 2020-10-23 12:58:39 +08:00 committed by John Wu
parent 385853a290
commit 0df891b336
29 changed files with 667 additions and 121 deletions

View File

@ -97,8 +97,6 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
ensureInsets()
}
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {

View File

@ -1,12 +1,9 @@
package com.topjohnwu.magisk.arch
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
val viewRoot: View
val viewModel: VM
@ -17,47 +14,8 @@ interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
}
}
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
/**
* Called for all [ViewEvent]s published by associated viewModel.
*/
fun onEventDispatched(event: ViewEvent) {}
fun ensureInsets() {
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
insets.asInsets()
.also { viewModel.insets = it }
.let { consumeSystemWindowInsets(it) }
?.subtractBy(insets) ?: insets
}
if (ViewCompat.isAttachedToWindow(viewRoot)) {
ViewCompat.requestApplyInsets(viewRoot)
} else {
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) = Unit
override fun onViewAttachedToWindow(v: View) {
ViewCompat.requestApplyInsets(v)
}
})
}
}
private fun WindowInsetsCompat.asInsets() = Insets.of(
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom
)
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
Insets.of(
insets.systemWindowInsetLeft - left,
insets.systemWindowInsetTop - top,
insets.systemWindowInsetRight - right,
insets.systemWindowInsetBottom - bottom
)
).build()
}

View File

@ -24,8 +24,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
override val viewRoot: View get() = binding.root
private val navigation get() = activity.navigation
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveEvents()
@ -65,7 +63,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
return true
}
})
ensureInsets()
}
override fun onResume() {

View File

@ -0,0 +1,178 @@
@file:Suppress("unused")
package com.topjohnwu.magisk.ktx
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import android.widget.EdgeEffect
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
fun RecyclerView.addVerticalPadding(paddingTop: Int = 0, paddingBottom: Int = 0) {
addItemDecoration(VerticalPaddingDecoration(paddingTop, paddingBottom))
}
private class VerticalPaddingDecoration(private val paddingTop: Int = 0, private val paddingBottom: Int = 0) : RecyclerView.ItemDecoration() {
private var allowTop: Boolean = true
private var allowBottom: Boolean = true
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.adapter == null) {
return
}
val position = parent.getChildAdapterPosition(view)
val count = parent.adapter!!.itemCount
if (position == 0 && allowTop) {
outRect.top = paddingTop
} else if (position == count - 1 && allowBottom) {
outRect.bottom = paddingBottom
}
}
}
fun RecyclerView.addSimpleItemDecoration(
left: Int = 0,
top: Int = 0,
right: Int = 0,
bottom: Int = 0,
) {
addItemDecoration(SimpleItemDecoration(left, top, right, bottom))
}
private class SimpleItemDecoration(
private val left: Int = 0,
private val top: Int = 0,
private val right: Int = 0,
private val bottom: Int = 0
) : RecyclerView.ItemDecoration() {
private var allowLeft: Boolean = true
private var allowTop: Boolean = true
private var allowRight: Boolean = true
private var allowBottom: Boolean = true
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.adapter == null) {
return
}
if (allowLeft) {
outRect.left = left
}
if (allowTop) {
outRect.top = top
}
if (allowRight) {
outRect.right = right
}
if (allowBottom) {
outRect.top = bottom
}
}
}
fun RecyclerView.fixEdgeEffect(overScrollIfContentScrolls: Boolean = true, alwaysClipToPadding: Boolean = true) {
if (overScrollIfContentScrolls) {
val listener = OverScrollIfContentScrollsListener()
addOnLayoutChangeListener(listener)
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, listener)
} else {
val listener = getTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener) as? OverScrollIfContentScrollsListener
if (listener != null) {
removeOnLayoutChangeListener(listener)
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, null)
}
}
edgeEffectFactory = if (alwaysClipToPadding && !clipToPadding) {
AlwaysClipToPaddingEdgeEffectFactory()
} else {
RecyclerView.EdgeEffectFactory()
}
}
private class OverScrollIfContentScrollsListener : View.OnLayoutChangeListener {
private var show = true
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
if (shouldDrawOverScroll(v as RecyclerView) != show) {
show = !show
if (show) {
v.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS)
} else {
v.setOverScrollMode(View.OVER_SCROLL_NEVER)
}
}
}
fun shouldDrawOverScroll(recyclerView: RecyclerView): Boolean {
if (recyclerView.layoutManager == null || recyclerView.adapter == null || recyclerView.adapter!!.itemCount == 0) {
return false
}
if (recyclerView.layoutManager is LinearLayoutManager) {
val itemCount = recyclerView.layoutManager!!.itemCount
val firstPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findFirstCompletelyVisibleItemPosition()
val lastPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition()
return firstPosition != 0 || lastPosition != itemCount - 1
}
return true
}
}
private class AlwaysClipToPaddingEdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
return object : EdgeEffect(view.context) {
private var ensureSize = false
private fun ensureSize() {
if (ensureSize) return
ensureSize = true
when (direction) {
DIRECTION_LEFT -> {
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
view.measuredWidth - view.paddingLeft - view.paddingRight)
}
DIRECTION_TOP -> {
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
view.measuredHeight - view.paddingTop - view.paddingBottom)
}
DIRECTION_RIGHT -> {
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
view.measuredWidth - view.paddingLeft - view.paddingRight)
}
DIRECTION_BOTTOM -> {
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
view.measuredHeight - view.paddingTop - view.paddingBottom)
}
}
}
override fun draw(c: Canvas): Boolean {
ensureSize()
val restore = c.save()
when (direction) {
DIRECTION_LEFT -> {
c.translate(view.paddingBottom.toFloat(), 0f)
}
DIRECTION_TOP -> {
c.translate(view.paddingLeft.toFloat(), view.paddingTop.toFloat())
}
DIRECTION_RIGHT -> {
c.translate(-view.paddingTop.toFloat(), 0f)
}
DIRECTION_BOTTOM -> {
c.translate(view.paddingRight.toFloat(), view.paddingBottom.toFloat())
}
}
val res = super.draw(c)
c.restoreToCount(restore)
return res
}
}
}
}

View File

@ -39,14 +39,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
override val viewModel by viewModel<MainViewModel>()
override val navHost: Int = R.id.main_nav_host
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
//implementation doesn't favor us against the design library, so on re-create it's often given
//upper hand.
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
binding.mainNavigation.setPadding(0)
}
private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) {
@ -100,8 +92,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
(currentFragment as? ReselectionTarget)?.onReselected()
}
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
else intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate()
@ -121,11 +111,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
}
}
override fun onDestroy() {
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()

View File

@ -12,6 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ktx.hideKeyboard
import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -49,6 +52,21 @@ class HideFragment : BaseUIFragment<HideViewModel, FragmentHideMd2Binding>() {
}
})
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.hideContent.addVerticalPadding(
l_50,
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
)
binding.hideContent.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.hideContent.fixEdgeEffect()
val lama = binding.hideContent.layoutManager ?: return
lama.isAutoMeasureEnabled = false
}

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui.inflater
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
@ -24,6 +25,10 @@ open class LayoutInflaterFactory(private val delegate: AppCompatDelegate) : Layo
open fun onViewCreated(view: View?, parent: View?, name: String, context: Context, attrs: AttributeSet) {
if (view == null) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
WindowInsetsHelper.attach(view, attrs)
}
}
}

View File

@ -0,0 +1,284 @@
@file:Suppress("unused")
package com.topjohnwu.magisk.ui.inflater
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.Gravity.*
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.topjohnwu.magisk.R
private typealias ApplyInsetsCallback<T> = (insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) -> T
private class ApplyInsets(private val out: Rect) : ApplyInsetsCallback<Unit> {
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) {
out.left += if (left) insets.left else 0
out.top += if (top) insets.top else 0
out.right += if (right) insets.right else 0
out.bottom += if (bottom) insets.bottom else 0
}
}
private class ConsumeInsets : ApplyInsetsCallback<Insets> {
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean): Insets {
val insetsLeft = if (left) 0 else insets.left
val insetsTop = if (top) 0 else insets.top
val insetsRight = if (right) 0 else insets.right
val insetsBottom = if (bottom) 0 else insets.bottom
return Insets.of(insetsLeft, insetsTop, insetsRight, insetsBottom)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
open class WindowInsetsHelper private constructor(
private val view: View,
private val fitSystemWindows: Int,
private val layout_fitsSystemWindowsInsets: Int,
private val consumeSystemWindows: Int) : OnApplyWindowInsetsListener {
internal var initialPaddingLeft: Int = view.paddingLeft
internal var initialPaddingTop: Int = view.paddingTop
internal var initialPaddingRight: Int = view.paddingRight
internal var initialPaddingBottom: Int = view.paddingBottom
private var initialMargin = false
internal var initialMarginLeft: Int = 0
internal var initialMarginTop: Int = 0
internal var initialMarginRight: Int = 0
internal var initialMarginBottom: Int = 0
internal var initialMarginStart: Int = 0
internal var initialMarginEnd: Int = 0
private var lastInsets: WindowInsetsCompat? = null
open fun setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
initialPaddingLeft = left
initialPaddingTop = top
initialPaddingRight = right
initialPaddingBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
open fun setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRTL) {
setInitialPadding(start, top, end, bottom)
} else {
setInitialPadding(start, top, end, bottom)
}
}
open fun setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
initialPaddingLeft = left
initialPaddingTop = top
initialPaddingRight = right
initialPaddingBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
open fun setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
initialMarginStart = start
initialMarginTop = top
initialMarginEnd = end
initialMarginBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
@SuppressLint("RtlHardcoded")
private fun <T> applyInsets(insets: Insets, fit: Int, callback: ApplyInsetsCallback<T>): T {
val relativeMode = (fit and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
val left: Boolean
val top = fit and TOP == TOP
val right: Boolean
val bottom = fit and BOTTOM == BOTTOM
if (relativeMode) {
val start = fit and START == START
val end = fit and END == END
left = (!isRTL && start) || (isRTL && end)
right = (!isRTL && end) || (isRTL && start)
} else {
left = fit and LEFT == LEFT
right = fit and RIGHT == RIGHT
}
return callback.invoke(insets, left, top, right, bottom)
}
private fun applyWindowInsets(windowInsets: WindowInsetsCompat): WindowInsetsCompat {
if (fitSystemWindows != 0) {
val padding = Rect(initialPaddingLeft, initialPaddingTop, initialPaddingRight, initialPaddingBottom)
applyInsets(windowInsets.systemWindowInsets, fitSystemWindows, ApplyInsets(padding))
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
}
if (layout_fitsSystemWindowsInsets != 0) {
if (!initialMargin) {
initialMarginLeft = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0
initialMarginTop = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0
initialMarginRight = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0
initialMarginBottom = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0
initialMarginStart = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart ?: 0
initialMarginEnd = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginEnd ?: 0
initialMargin = true
}
val margin = if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION)
Rect(initialMarginLeft, initialMarginTop, initialMarginRight, initialMarginBottom)
else
Rect(initialMarginStart, initialMarginTop, initialMarginEnd, initialMarginBottom)
applyInsets(windowInsets.systemWindowInsets, layout_fitsSystemWindowsInsets, ApplyInsets(margin))
val lp = view.layoutParams
if (lp is ViewGroup.MarginLayoutParams) {
lp.topMargin = margin.top
lp.bottomMargin = margin.bottom
if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION) {
lp.marginStart = margin.left
lp.marginEnd = margin.right
} else {
lp.leftMargin = margin.left
lp.rightMargin = margin.right
}
view.layoutParams = lp
}
}
val systemWindowInsets = if (consumeSystemWindows != 0) applyInsets(windowInsets.systemWindowInsets, consumeSystemWindows, ConsumeInsets()) else windowInsets.systemWindowInsets
return WindowInsetsCompat.Builder(windowInsets)
.setSystemWindowInsets(systemWindowInsets)
.build()
}
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
if (lastInsets == insets) {
return insets
}
lastInsets = insets
return applyWindowInsets(insets)
}
companion object {
@JvmStatic
fun attach(view: View, attrs: AttributeSet) {
val a = view.context.obtainStyledAttributes(attrs, R.styleable.WindowInsetsHelper, 0, 0)
val edgeToEdge = a.getBoolean(R.styleable.WindowInsetsHelper_edgeToEdge, false)
val fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_fitsSystemWindowsInsets, 0)
val layout_fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_layout_fitsSystemWindowsInsets, 0)
val consumeSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_consumeSystemWindowsInsets, 0)
a.recycle()
attach(view, edgeToEdge, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
}
@JvmStatic
fun attach(view: View, edgeToEdge: Boolean, fitsSystemWindowsInsets: Int, layout_fitsSystemWindowsInsets: Int, consumeSystemWindowsInsets: Int) {
if (edgeToEdge) {
view.systemUiVisibility = (view.systemUiVisibility
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
if (fitsSystemWindowsInsets == 0 && layout_fitsSystemWindowsInsets == 0 && consumeSystemWindowsInsets == 0) {
return
}
val listener = WindowInsetsHelper(view, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
ViewCompat.setOnApplyWindowInsetsListener(view, listener)
view.setTag(R.id.tag_rikka_material_WindowInsetsHelper, listener)
if (!view.isAttachedToWindow) {
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
}
}
val View.windowInsetsHelper: WindowInsetsHelper?
get() {
val value = getTag(R.id.tag_rikka_material_WindowInsetsHelper)
return if (value is WindowInsetsHelper) value else null
}
val View.initialPaddingLeft: Int
get() = windowInsetsHelper?.initialPaddingLeft ?: 0
val View.initialPaddingTop: Int
get() = windowInsetsHelper?.initialPaddingTop ?: 0
val View.initialPaddingRight: Int
get() = windowInsetsHelper?.initialPaddingRight ?: 0
val View.initialPaddingBottom: Int
get() = windowInsetsHelper?.initialPaddingBottom ?: 0
val View.initialPaddingStart: Int
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingRight else initialPaddingLeft
val View.initialPaddingEnd: Int
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingLeft else initialPaddingRight
fun View.setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
windowInsetsHelper?.setInitialPadding(left, top, right, bottom)
}
fun View.setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
windowInsetsHelper?.setInitialPaddingRelative(start, top, end, bottom)
}
val View.initialMarginLeft: Int
get() = windowInsetsHelper?.initialMarginLeft ?: 0
val View.initialMarginTop: Int
get() = windowInsetsHelper?.initialMarginTop ?: 0
val View.initialMarginRight: Int
get() = windowInsetsHelper?.initialMarginRight ?: 0
val View.initialMarginBottom: Int
get() = windowInsetsHelper?.initialMarginBottom ?: 0
val View.initialMarginStart: Int
get() = windowInsetsHelper?.initialMarginStart ?: 0
val View.initialMarginEnd: Int
get() = windowInsetsHelper?.initialMarginEnd ?: 0
fun View.setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
windowInsetsHelper?.setInitialMargin(left, top, right, bottom)
}
fun View.setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
windowInsetsHelper?.setInitialMarginRelative(start, top, end, bottom)
}

View File

@ -9,6 +9,9 @@ import androidx.core.view.isVisible
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -42,6 +45,21 @@ class LogFragment : BaseUIFragment<LogViewModel, FragmentLogMd2Binding>() {
binding.logFilterToggle.setOnClickListener {
isMagiskLogVisible = true
}
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.logFilterSuperuser.logSuperuser.addVerticalPadding(
0,
l1
)
binding.logFilterSuperuser.logSuperuser.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.logFilterSuperuser.logSuperuser.fixEdgeEffect()
}

View File

@ -13,6 +13,9 @@ import com.topjohnwu.magisk.arch.ReselectionTarget
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ktx.hideKeyboard
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
@ -62,6 +65,37 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
}
})
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.moduleList.apply {
addVerticalPadding(
l_50,
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
)
addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
fixEdgeEffect()
}
binding.moduleFilterInclude.moduleFilterList.apply {
addVerticalPadding(
l_50,
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
)
addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
fixEdgeEffect()
}
}
override fun onDestroyView() {

View File

@ -5,6 +5,9 @@ import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ktx.setOnViewReadyListener
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -24,6 +27,21 @@ class SettingsFragment : BaseUIFragment<SettingsViewModel, FragmentSettingsMd2Bi
binding.settingsList.setOnViewReadyListener {
binding.settingsList.scrollToPosition(0)
}
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.settingsList.addVerticalPadding(
0,
l1
)
binding.settingsList.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.settingsList.fixEdgeEffect()
}
override fun onResume() {

View File

@ -1,8 +1,13 @@
package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
@ -15,6 +20,25 @@ class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd
activity.title = resources.getString(R.string.superuser)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.superuserList.addVerticalPadding(
l_50,
l1
)
binding.superuserList.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.superuserList.fixEdgeEffect()
}
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
}

View File

@ -17,8 +17,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@{viewModel.insets.left}"
android:paddingRight="@{viewModel.insets.right}"
app:consumeSystemWindowsInsets="start|end"
app:edgeToEdge="true"
app:fitsSystemWindowsInsets="start|end"
tools:ignore="RtlHardcoded">
<androidx.fragment.app.FragmentContainerView
@ -34,7 +35,7 @@
style="@style/WidgetFoundation.Appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@{viewModel.insets.top}">
app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar"
@ -72,7 +73,9 @@
android:layout_gravity="bottom|center_horizontal"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}"
android:layout_marginBottom="@dimen/l1"
android:fitsSystemWindows="false"
app:layout_fitsSystemWindowsInsets="bottom"
tools:layout_marginBottom="64dp">
<com.google.android.material.bottomnavigation.BottomNavigationView
@ -81,6 +84,9 @@
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:textStyle="bold"
android:fitsSystemWindows="false"
android:paddingBottom="0dp"
app:fitsSystemWindowsInsets="start|end"
app:elevation="0dp"
app:itemHorizontalTranslationEnabled="false"
app:itemIconTint="@color/color_menu_tint"

View File

@ -18,7 +18,8 @@
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:layout_marginTop="@dimen/internal_action_bar_size"
app:layout_fitsSystemWindowsInsets="top"
tools:layout_marginTop="@dimen/internal_action_bar_size">
<androidx.recyclerview.widget.RecyclerView
@ -31,9 +32,7 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingLeft="@{viewModel.insets.left}"
android:paddingRight="@{viewModel.insets.right}"
android:paddingBottom="@{viewModel.insets.bottom}"
app:fitsSystemWindowsInsets="start|end|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_console_md2" />
@ -46,12 +45,13 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/l1"
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}"
android:layout_marginBottom="@dimen/l1"
android:onClick="@{() -> viewModel.restartPressed()}"
android:text="@string/reboot"
android:textAllCaps="false"
android:textColor="?colorOnPrimary"
android:textStyle="bold"
app:layout_fitsSystemWindowsInsets="bottom"
app:backgroundTint="?colorPrimary"
app:icon="@drawable/ic_restart"
app:iconTint="?colorOnPrimary" />

View File

@ -17,7 +17,6 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/hide_content"
dividerVertical="@{@drawable/divider_l_50}"
invisibleUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
@ -25,10 +24,8 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_md2"
tools:paddingTop="40dp" />
@ -41,8 +38,9 @@
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:layout_marginBottom="@dimen/l1"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:layout_fitsSystemWindowsInsets="bottom"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" />

View File

@ -22,8 +22,9 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l3}"
android:paddingTop="@dimen/internal_action_bar_size"
android:paddingBottom="@dimen/l3"
app:fitsSystemWindowsInsets="top|bottom"
tools:layout_marginTop="24dp">
<LinearLayout

View File

@ -20,8 +20,9 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
android:paddingTop="@dimen/internal_action_bar_size"
android:paddingBottom="@dimen/l2"
app:fitsSystemWindowsInsets="top|bottom"
tools:paddingTop="24dp">
<FrameLayout
@ -34,7 +35,7 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="@dimen/l1">
android:paddingTop="@dimen/l_50">
<com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card"

View File

@ -29,7 +29,8 @@
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:layout_marginBottom="@dimen/l1"
app:layout_fitsSystemWindowsInsets="bottom"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:srcCompat="@drawable/ic_folder_list"
app:tint="?colorPrimary"

View File

@ -31,8 +31,6 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l_50}"
gone="@{viewModel.loading &amp;&amp; viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
@ -40,10 +38,8 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingEnd="0dp"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_module_md2" />
@ -54,7 +50,8 @@
android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:layout_marginBottom="@dimen/l1"
app:layout_fitsSystemWindowsInsets="bottom"
app:backgroundTint="?colorSurfaceSurfaceVariant"
app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorPrimary"

View File

@ -22,8 +22,8 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
tools:paddingBottom="48dp"
tools:paddingTop="24dp">

View File

@ -15,8 +15,6 @@
<androidx.recyclerview.widget.RecyclerView
adapter="@{viewModel.adapter}"
dividerHorizontal="@{@drawable/divider_l_50}"
dividerVertical="@{@drawable/divider_l_50}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:id="@+id/settings_list"
@ -25,10 +23,8 @@
android:focusableInTouchMode="false"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingEnd="@dimen/l_50"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:layout_marginTop="24dp"
tools:listitem="@layout/item_settings"

View File

@ -20,8 +20,6 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/superuser_list"
adapter="@{viewModel.adapter}"
dividerHorizontal="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l_50}"
goneUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
@ -29,9 +27,8 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_policy_md2" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
@ -17,15 +18,17 @@
android:clipToPadding="false"
android:fillViewport="true"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingTop="@dimen/internal_action_bar_size"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}">
android:paddingBottom="@dimen/l1"
app:fitsSystemWindowsInsets="top|bottom">
<LinearLayout
android:id="@+id/theme_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/l1"
android:useDefaultMargins="true">
<include

View File

@ -21,7 +21,8 @@
android:paddingStart="@dimen/l1"
android:paddingTop="@dimen/l1"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:paddingBottom="@dimen/l1"
app:fitsSystemWindowsInsets="bottom"
tools:layout_gravity="bottom"
tools:paddingBottom="64dp">

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
@ -32,8 +33,8 @@
precomputedText="@{viewModel.consoleText}"
android:textAppearance="@style/AppearanceFoundation.Caption"
android:textSize="10sp"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingBottom="@{viewModel.insets.bottom}"
android:paddingTop="@dimen/internal_action_bar_size"
app:layout_fitsSystemWindowsInsets="top|bottom"
tools:text="@tools:sample/lorem/random" />
</HorizontalScrollView>

View File

@ -16,16 +16,15 @@
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_superuser"
itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_log_access_md2"
tools:paddingTop="24dp" />

View File

@ -18,14 +18,13 @@
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l1}"
android:paddingBottom="@dimen/l1"
app:fitsSystemWindowsInsets="bottom"
tools:layout_gravity="bottom"
tools:paddingBottom="64dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/module_filter_list"
dividerHorizontal="@{@drawable/divider_l1}"
dividerVertical="@{@drawable/divider_l_50}"
itemBinding="@{viewModel.itemSearchBinding}"
items="@{viewModel.itemsSearch}"
android:layout_width="match_parent"
@ -33,8 +32,8 @@
android:layout_marginBottom="@dimen/l1"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingStart="@dimen/l1"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
android:paddingTop="@dimen/internal_action_bar_size"
app:fitsSystemWindowsInsets="top|bottom"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/module_filter_title_search"

View File

@ -19,4 +19,32 @@
<!--endregion-->
<declare-styleable name="WindowInsetsHelper">
<attr name="edgeToEdge" format="boolean"/>
<attr name="fitsSystemWindowsInsets" format="flags">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="start" value="0x00800003" />
<flag name="end" value="0x00800005" />
</attr>
<attr name="consumeSystemWindowsInsets" format="flags">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="start" value="0x00800003" />
<flag name="end" value="0x00800005" />
</attr>
<attr name="layout_fitsSystemWindowsInsets" format="flags">
<flag name="top" value="0x30" />
<flag name="bottom" value="0x50" />
<flag name="left" value="0x03" />
<flag name="right" value="0x05" />
<flag name="start" value="0x00800003" />
<flag name="end" value="0x00800005" />
</attr>
</declare-styleable>
</resources>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="recyclerScrollListener" type="id" />
<item name="coroutineScope" type="id"/>
<item name="coroutineScope" type="id" />
<item name="tag_rikka_material_WindowInsetsHelper" type="id"/>
<item name="tag_rikka_recyclerView_OverScrollIfContentScrollsListener" type="id"/>
</resources>