Scaffold responsive Android notification log shell
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
package se.ajpanton.notificationlog
|
||||
|
||||
import android.os.Bundle
|
||||
import android.content.res.Configuration
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.GravityCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import se.ajpanton.notificationlog.databinding.ActivityMainBinding
|
||||
|
||||
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var drawerToggle: ActionBarDrawerToggle
|
||||
private var permanentSidebar = false
|
||||
private var currentItemId = R.id.nav_view_logs
|
||||
private var drawerNavigationBasePaddingLeft: Int? = null
|
||||
private var permanentNavigationBasePaddingLeft: Int? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Keep every app surface below the system bars rather than relying on edge-to-edge drawing.
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val isNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
WindowInsetsControllerCompat(window, binding.root).isAppearanceLightNavigationBars = !isNightMode
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
setupNavigationView(binding.navViewDrawer)
|
||||
setupNavigationView(binding.navViewPermanent)
|
||||
drawerToggle = ActionBarDrawerToggle(
|
||||
this,
|
||||
binding.drawerLayout,
|
||||
binding.toolbar,
|
||||
R.string.navigation_open,
|
||||
R.string.navigation_close,
|
||||
)
|
||||
binding.drawerLayout.addDrawerListener(drawerToggle)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
if (!permanentSidebar) {
|
||||
binding.drawerLayout.openDrawer(GravityCompat.START)
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (!permanentSidebar && binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
return
|
||||
}
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
})
|
||||
|
||||
currentItemId = savedInstanceState?.getInt(STATE_CURRENT_ITEM_ID) ?: R.id.nav_view_logs
|
||||
navigateTo(currentItemId)
|
||||
applyNavigationDrawerInsets()
|
||||
syncDrawerToggle()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putInt(STATE_CURRENT_ITEM_ID, currentItemId)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.nav_settings_header) {
|
||||
return false
|
||||
}
|
||||
navigateTo(item.itemId)
|
||||
if (!permanentSidebar) {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun navigateTo(itemId: Int) {
|
||||
currentItemId = itemId
|
||||
syncDrawerSelection(itemId)
|
||||
val page = Page.fromMenuItem(itemId)
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.content_frame, PageFragment.newInstance(page.titleRes))
|
||||
runOnCommit(::applyPageTitleVisibility)
|
||||
}
|
||||
title = getString(page.titleRes)
|
||||
}
|
||||
|
||||
private fun setupNavigationView(navigationView: NavigationView) {
|
||||
navigationView.setNavigationItemSelectedListener(this)
|
||||
navigationView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
updateNavigationHeaderHeight(navigationView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncDrawerSelection(itemId: Int) {
|
||||
syncNavigationSelection(binding.navViewDrawer, itemId)
|
||||
syncNavigationSelection(binding.navViewPermanent, itemId)
|
||||
}
|
||||
|
||||
private fun syncNavigationSelection(navigationView: NavigationView, itemId: Int) {
|
||||
val menu = navigationView.menu ?: return
|
||||
for (index in 0 until menu.size()) {
|
||||
val item = menu.getItem(index)
|
||||
if (item.itemId == R.id.nav_settings_header) {
|
||||
item.isCheckable = false
|
||||
} else {
|
||||
item.isCheckable = true
|
||||
item.isChecked = item.itemId == itemId
|
||||
}
|
||||
}
|
||||
navigationView.invalidate()
|
||||
}
|
||||
|
||||
private fun applyNavigationDrawerInsets() {
|
||||
binding.drawerLayout.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
applyNavigationDrawerMarginsFromRootInsets()
|
||||
}
|
||||
binding.drawerLayout.post(::applyNavigationDrawerMarginsFromRootInsets)
|
||||
}
|
||||
|
||||
private fun applyNavigationDrawerMarginsFromRootInsets() {
|
||||
val rootInsets = ViewCompat.getRootWindowInsets(binding.drawerLayout)
|
||||
val safeInsets = rootInsets?.getInsetsIgnoringVisibility(
|
||||
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
|
||||
) ?: Insets.NONE
|
||||
applyNavigationDrawerSafeArea(safeInsets.left, safeInsets.right)
|
||||
}
|
||||
|
||||
private fun applyNavigationDrawerSafeArea(leftInset: Int, rightInset: Int) {
|
||||
val safeLeft = maxOf(0, leftInset, binding.drawerLayout.paddingLeft)
|
||||
val safeRight = maxOf(0, rightInset, binding.drawerLayout.paddingRight)
|
||||
binding.drawerLayout.setDrawerSafeInsets(safeLeft, safeRight)
|
||||
drawerNavigationBasePaddingLeft = applyNavigationLeftPadding(
|
||||
binding.navViewDrawer,
|
||||
drawerNavigationBasePaddingLeft,
|
||||
safeLeft,
|
||||
)
|
||||
permanentNavigationBasePaddingLeft = applyNavigationLeftPadding(
|
||||
binding.navViewPermanent,
|
||||
permanentNavigationBasePaddingLeft,
|
||||
0,
|
||||
)
|
||||
updatePermanentSidebarMode()
|
||||
}
|
||||
|
||||
private fun applyNavigationLeftPadding(
|
||||
navigationView: NavigationView,
|
||||
basePaddingLeft: Int?,
|
||||
safeLeft: Int,
|
||||
): Int {
|
||||
val base = basePaddingLeft ?: navigationView.paddingLeft
|
||||
val leftPadding = base + safeLeft
|
||||
if (navigationView.paddingLeft != leftPadding || navigationView.translationX != 0f) {
|
||||
navigationView.translationX = 0f
|
||||
navigationView.setPadding(
|
||||
leftPadding,
|
||||
navigationView.paddingTop,
|
||||
navigationView.paddingRight,
|
||||
navigationView.paddingBottom,
|
||||
)
|
||||
navigationView.requestLayout()
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
private fun updatePermanentSidebarMode() {
|
||||
val rootWidth = binding.drawerLayout.width
|
||||
if (rootWidth <= 0) {
|
||||
return
|
||||
}
|
||||
val drawerWidth = resources.getDimensionPixelSize(R.dimen.navigation_drawer_width)
|
||||
val foldedPortraitWidth = resources.getDimensionPixelSize(R.dimen.folded_portrait_width)
|
||||
val shouldUsePermanentSidebar = rootWidth >= drawerWidth +
|
||||
(foldedPortraitWidth * PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER).toInt()
|
||||
|
||||
if (permanentSidebar != shouldUsePermanentSidebar) {
|
||||
permanentSidebar = shouldUsePermanentSidebar
|
||||
drawerToggle.isDrawerIndicatorEnabled = !permanentSidebar
|
||||
binding.toolbar.visibility = if (permanentSidebar) View.GONE else View.VISIBLE
|
||||
if (permanentSidebar) {
|
||||
binding.navViewPermanent.visibility = View.VISIBLE
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START, false)
|
||||
} else {
|
||||
binding.drawerLayout.closeDrawer(GravityCompat.START, false)
|
||||
binding.navViewPermanent.visibility = View.GONE
|
||||
}
|
||||
syncDrawerToggle()
|
||||
}
|
||||
if (permanentSidebar) {
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START)
|
||||
binding.navViewDrawer.visibility = View.GONE
|
||||
} else {
|
||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START)
|
||||
binding.navViewDrawer.visibility = View.VISIBLE
|
||||
}
|
||||
updateNavigationHeaderHeight(binding.navViewDrawer)
|
||||
updateNavigationHeaderHeight(binding.navViewPermanent)
|
||||
applyPageTitleVisibility()
|
||||
}
|
||||
|
||||
private fun updateNavigationHeaderHeight(navigationView: NavigationView) {
|
||||
if (navigationView.headerCount == 0 || navigationView.height <= 0) {
|
||||
return
|
||||
}
|
||||
val menuRows = countVisibleMenuRows(navigationView.menu)
|
||||
val menuHeight = menuRows * resources.getDimensionPixelSize(R.dimen.navigation_menu_item_height)
|
||||
val desiredHeaderHeight = (navigationView.height - menuHeight).coerceIn(
|
||||
resources.getDimensionPixelSize(R.dimen.drawer_header_min_height),
|
||||
resources.getDimensionPixelSize(R.dimen.drawer_header_max_height),
|
||||
)
|
||||
val header = navigationView.getHeaderView(0) ?: return
|
||||
if (header.layoutParams.height != desiredHeaderHeight) {
|
||||
header.layoutParams.height = desiredHeaderHeight
|
||||
header.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private fun countVisibleMenuRows(menu: Menu): Int = (0 until menu.size()).count { menu.getItem(it).isVisible }
|
||||
|
||||
private fun applyPageTitleVisibility() {
|
||||
(supportFragmentManager.findFragmentById(R.id.content_frame) as? PageFragment)
|
||||
?.setPageTitleVisible(permanentSidebar)
|
||||
}
|
||||
|
||||
private fun syncDrawerToggle() {
|
||||
drawerToggle.syncState()
|
||||
drawerToggle.drawerArrowDrawable.color = getColor(R.color.on_primary)
|
||||
}
|
||||
|
||||
private enum class Page(val menuId: Int, val titleRes: Int) {
|
||||
VIEW_LOGS(R.id.nav_view_logs, R.string.page_view_logs),
|
||||
SETTINGS(R.id.nav_settings, R.string.page_settings),
|
||||
APPEARING(R.id.nav_appearing, R.string.page_appearing),
|
||||
DISAPPEARING(R.id.nav_disappearing, R.string.page_disappearing),
|
||||
TEXT_CONTENT(R.id.nav_text_content, R.string.page_text_content),
|
||||
IMAGE_CONTENT(R.id.nav_image_content, R.string.page_image_content),
|
||||
EDITS(R.id.nav_edits, R.string.page_edits),
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromMenuItem(menuId: Int): Page = entries.firstOrNull { it.menuId == menuId } ?: VIEW_LOGS
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val STATE_CURRENT_ITEM_ID = "current_item_id"
|
||||
const val PERMANENT_SIDEBAR_FOLDED_WIDTH_MULTIPLIER = 1.1f
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user