Simplify inset logic, fix landscape mode, fix cutout overlapping

The commit primarily fixes a few overlapping issues caused by the window
inset handling. Previously, there were two main issues:

* Because setTransparentNavigationBar() checked for portrait mode, the
  inset logic never executed in landscape mode. This caused the app to
  overlap the status bar and navigation bar.

* The inset logic did not have handling for displayCutout insets. In
  landscape mode, this would cause the app to overlap the notch or
  camera hole punch area on phones.

In addition to fixing those issues, this commit simplifies the inset
logic a bit:

* applyWindowInsets() now accepts an EnumSet of WindowInsetPosition to
  avoid needing to duplicate logic for the various position
  combinations.

* Insets are now applied to the main container in the layout instead of
  individual elements where possible. This eliminates the need for the
  previous manual IME height handling logic in BOTTOM_IME vs
  TOP_BOTTOM_IME (for avoiding the insets being applied twice).

* Since insets are now applied to the main layout container,
  applyWindowInsets() now takes systemBars, displayCutout, and ime all
  into consideration. This should avoid all possible overlapping.

* Add support for using padding instead of margins for insets. This is
  used for GroupActivity's navigation drawer, where Material design
  intends for the drawer background to be drawn behind system bars.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2025-10-04 15:37:45 -04:00
parent 84a62a32ff
commit 852378e484
6 changed files with 105 additions and 54 deletions

View File

@@ -23,7 +23,6 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -38,13 +37,10 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.viewModels
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.BlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
@@ -83,11 +79,13 @@ import com.kunzisoft.keepass.view.hideByFading
import com.kunzisoft.keepass.view.setTransparentNavigationBar
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.EntryViewModel
import java.util.EnumSet
import java.util.UUID
class EntryActivity : DatabaseLockActivity() {
private var footer: ViewGroup? = null
private var container: View? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var collapsingToolbarLayout: CollapsingToolbarLayout? = null
private var appBarLayout: AppBarLayout? = null
@@ -139,6 +137,7 @@ class EntryActivity : DatabaseLockActivity() {
// Get views
footer = findViewById(R.id.activity_entry_footer)
container = findViewById(R.id.activity_entry_container)
coordinatorLayout = findViewById(R.id.toolbar_coordinator)
collapsingToolbarLayout = findViewById(R.id.toolbar_layout)
appBarLayout = findViewById(R.id.app_bar)
@@ -154,8 +153,12 @@ class EntryActivity : DatabaseLockActivity() {
setTransparentNavigationBar {
// To fix margin with API 27
ViewCompat.setOnApplyWindowInsetsListener(collapsingToolbarLayout!!, null)
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
// Empty title

View File

@@ -100,6 +100,7 @@ import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.ColorPickerViewModel
import com.kunzisoft.keepass.viewmodels.EntryEditViewModel
import java.util.EnumSet
import java.util.UUID
class EntryEditActivity : DatabaseLockActivity(),
@@ -180,8 +181,12 @@ class EntryEditActivity : DatabaseLockActivity(),
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
container?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
container?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}
stopService(Intent(this, ClipboardEntryNotificationService::class.java))

View File

@@ -48,6 +48,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.GravityCompat
import androidx.core.view.WindowInsetsCompat
@@ -117,6 +118,7 @@ import com.kunzisoft.keepass.view.updateLockPaddingStart
import com.kunzisoft.keepass.viewmodels.GroupEditViewModel
import com.kunzisoft.keepass.viewmodels.GroupViewModel
import org.joda.time.LocalDateTime
import java.util.EnumSet
class GroupActivity : DatabaseLockActivity(),
@@ -131,6 +133,7 @@ class GroupActivity : DatabaseLockActivity(),
private var header: ViewGroup? = null
private var footer: ViewGroup? = null
private var drawerLayout: DrawerLayout? = null
private var constraintLayout: ConstraintLayout? = null
private var databaseNavView: NavigationDatabaseView? = null
private var coordinatorLayout: CoordinatorLayout? = null
private var coordinatorError: CoordinatorLayout? = null
@@ -279,6 +282,7 @@ class GroupActivity : DatabaseLockActivity(),
header = findViewById(R.id.activity_group_header)
footer = findViewById(R.id.activity_group_footer)
drawerLayout = findViewById(R.id.drawer_layout)
constraintLayout = findViewById(R.id.activity_group_container_view)
databaseNavView = findViewById(R.id.database_nav_view)
coordinatorLayout = findViewById(R.id.group_coordinator)
coordinatorError = findViewById(R.id.error_coordinator)
@@ -296,8 +300,19 @@ class GroupActivity : DatabaseLockActivity(),
// To apply fit window with transparency
setTransparentNavigationBar(applyToStatusBar = true) {
drawerLayout?.applyWindowInsets(WindowInsetPosition.TOP_BOTTOM_IME)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM_IME)
constraintLayout?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
// The background of the drawer is meant to overlap system bars, so use padding
databaseNavView?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_PADDING,
WindowInsetPosition.BOTTOM_PADDING,
// Only on the start side, since the drawer is anchored to one side of the screen
WindowInsetPosition.START_PADDING,
))
}
lockView?.setOnClickListener {

View File

@@ -70,8 +70,12 @@ open class SettingsActivity
// To apply navigation bar with background color
/* TODO Settings nav bar
setTransparentNavigationBar {
coordinatorLayout?.applyWindowInsets(WindowInsetPosition.TOP)
footer?.applyWindowInsets(WindowInsetPosition.BOTTOM)
coordinatorLayout?.applyWindowInsets(EnumSet.of(
WindowInsetPosition.TOP_MARGINS,
WindowInsetPosition.BOTTOM_MARGINS,
WindowInsetPosition.START_MARGINS,
WindowInsetPosition.END_MARGINS,
))
}*/
mExternalFileHelper = ExternalFileHelper(this)

View File

@@ -24,7 +24,6 @@ import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
@@ -58,7 +57,6 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.snackbar.Snackbar
@@ -66,6 +64,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.helper.getLocalizedMessage
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import java.util.EnumSet
/**
@@ -306,9 +305,7 @@ fun CollapsingToolbarLayout.changeTitleColor(color: Int) {
@Suppress("DEPRECATION")
fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, applyWindowInsets: () -> Unit) {
// Only in portrait
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1
&& resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.navigationBarColor = ContextCompat.getColor(this, R.color.surface_selector)
if (applyToStatusBar) {
@@ -324,7 +321,7 @@ fun Activity.setTransparentNavigationBar(applyToStatusBar: Boolean = false, appl
/**
* Apply a margin to a view to fix the window inset
*/
fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.BOTTOM) {
fun View.applyWindowInsets(positions: EnumSet<WindowInsetPosition>) {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
var consumed = false
@@ -340,52 +337,78 @@ fun View.applyWindowInsets(position: WindowInsetPosition = WindowInsetPosition.B
}
}
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
when (position) {
WindowInsetPosition.TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()
or WindowInsetsCompat.Type.displayCutout()
or WindowInsetsCompat.Type.ime())
val isRtl = layoutDirection == View.LAYOUT_DIRECTION_RTL
val wantTopMargins = positions.contains(WindowInsetPosition.TOP_MARGINS)
val wantBottomMargins = positions.contains(WindowInsetPosition.BOTTOM_MARGINS)
val wantStartMargins = positions.contains(WindowInsetPosition.START_MARGINS)
val wantEndMargins = positions.contains(WindowInsetPosition.END_MARGINS)
if (view.layoutParams is ViewGroup.MarginLayoutParams
&& (wantTopMargins || wantBottomMargins || wantStartMargins || wantEndMargins)) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (wantTopMargins) {
topMargin = insets.top
}
if (wantBottomMargins) {
bottomMargin = insets.bottom
}
if (wantStartMargins) {
if (isRtl) {
rightMargin = insets.right
} else {
leftMargin = insets.left
}
}
}
WindowInsetPosition.LEGIT_TOP -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = 0
}
}
}
WindowInsetPosition.BOTTOM -> {
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
}
WindowInsetPosition.BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = if (imeHeight > 1) 0 else insets.bottom
}
}
}
WindowInsetPosition.TOP_BOTTOM_IME -> {
val imeHeight = windowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom
if (view.layoutParams is ViewGroup.MarginLayoutParams) {
view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
bottomMargin = if (imeHeight > 1) imeHeight else 0
if (wantEndMargins) {
if (isRtl) {
leftMargin = insets.left
} else {
rightMargin = insets.right
}
}
}
}
val wantTopPadding = positions.contains(WindowInsetPosition.TOP_PADDING)
val wantBottomPadding = positions.contains(WindowInsetPosition.BOTTOM_PADDING)
val wantStartPadding = positions.contains(WindowInsetPosition.START_PADDING)
val wantEndPadding = positions.contains(WindowInsetPosition.END_PADDING)
if (wantTopPadding || wantBottomPadding || wantStartPadding || wantEndPadding) {
val topPadding = if (wantTopPadding) insets.top else 0
val bottomPadding = if (wantBottomPadding) insets.bottom else 0
var leftPadding = 0
var rightPadding = 0
if (wantStartPadding) {
if (isRtl) {
rightPadding = insets.right
} else {
leftPadding = insets.left
}
}
if (wantEndPadding) {
if (isRtl) {
leftPadding = insets.left
} else {
rightPadding = insets.right
}
}
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
}
// If any of the children consumed the insets, return an appropriate value
if (consumed) WindowInsetsCompat.CONSUMED else windowInsets
}
}
enum class WindowInsetPosition {
TOP, BOTTOM, LEGIT_TOP, BOTTOM_IME, TOP_BOTTOM_IME
TOP_MARGINS, BOTTOM_MARGINS, START_MARGINS, END_MARGINS,
TOP_PADDING, BOTTOM_PADDING, START_PADDING, END_PADDING,
}

View File

@@ -20,6 +20,7 @@
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:id="@+id/activity_entry_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:filterTouchesWhenObscured="true">