From b6da868103d1edc95cf4fdb8e67ba8f3944e1a4f Mon Sep 17 00:00:00 2001 From: Wally Hackenslacker Date: Thu, 30 Apr 2026 12:36:35 -0400 Subject: [PATCH] feat: make boards drawer width adaptive by screen class --- DESIGN.md | 4 +- .../kanbn4droid/app/BoardsFlowTest.kt | 16 +++- .../kanbn4droid/app/BoardsActivity.kt | 19 +++-- .../app/boards/BoardsDrawerWidthCalculator.kt | 15 ++++ app/src/main/res/values/integers.xml | 4 + .../boards/BoardsDrawerWidthCalculatorTest.kt | 78 +++++++++++++++++++ 6 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculator.kt create mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculatorTest.kt diff --git a/DESIGN.md b/DESIGN.md index 7f3e8a2..b5ca394 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -149,7 +149,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel **Settings view** - The view is available from a side panel that can be shown in the Boards list view by pulling in from the left side of the screen. - - The side panel only occupies up to a third of the screen. + - On smaller screens the side panel occupies full width; on larger screens it occupies up to two-thirds of the screen, capped by a max width. - The view behind the side panel dims when the side panel is open. - The side panel shows the following content from top to bottom: - Username as a title in bold text of the currently active user obtained with the "users/me" endpoint of the Kan.bn API. @@ -169,7 +169,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel - All settings are managed using the AndroidX Preferences library. - Changing any settings makes it apply instantly when leaving the settings view without logging out. - Current status: implemented through a left-side drawer in `BoardsActivity` plus an in-place settings dialog (`SettingsDialogFragment` + `SettingsPreferencesFragment`) using AndroidX Preferences. - - Drawer behavior is implemented with `DrawerLayout`: left-edge gesture + toolbar open action, dimmed background, and runtime width capped to one-third of screen width. + - Drawer behavior is implemented with `DrawerLayout`: left-edge gesture + toolbar open action, dimmed background, and adaptive runtime width (small screens: full width; large screens: min(two-thirds of window, max width cap)). - Drawer content is implemented with profile header (`users/me`), workspaces list with active highlight, settings entry, retry/error states, and logout action with confirmation. - Workspace switching is implemented with active-workspace persistence and boards refresh; switch failures restore previous selection and unauthorized responses force sign-out. - Settings dialog implements Theme/Base URL/API key drafts, save-and-close apply flow, immediate theme application, credential re-auth on URL/key changes, and safe rollback on apply failure. diff --git a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt index 570837d..a6ed9e2 100644 --- a/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt +++ b/app/src/androidTest/java/space/hackenslacker/kanbn4droid/app/BoardsFlowTest.kt @@ -281,7 +281,7 @@ class BoardsFlowTest { } @Test - fun drawerWidthNeverExceedsOneThirdOfScreen() { + fun drawerWidthIsAdaptiveForSmallAndLargeScreens() { MainActivity.dependencies.apiClientFactory = { FakeBoardsApiClient( boards = mutableListOf(BoardSummary("1", "Alpha")), @@ -292,9 +292,19 @@ class BoardsFlowTest { val scenario = ActivityScenario.launch(BoardsActivity::class.java) scenario.onActivity { activity -> + val drawerLayout = activity.findViewById(R.id.boardsDrawerLayout) val drawerContent = activity.findViewById(R.id.boardsDrawerContent) - val displayWidthPx = activity.resources.displayMetrics.widthPixels - assertTrue(drawerContent.layoutParams.width <= displayWidthPx / 3) + val resources = activity.resources + val breakpointSwDp = resources.getInteger(R.integer.boards_drawer_tablet_breakpoint_sw_dp) + val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width) + val isSmallScreen = resources.configuration.smallestScreenWidthDp < breakpointSwDp + val expectedWidth = if (isSmallScreen) { + drawerLayout.width + } else { + minOf((drawerLayout.width * 2) / 3, maxWidthPx) + } + + assertEquals(expectedWidth, drawerContent.layoutParams.width) } } diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/BoardsActivity.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/BoardsActivity.kt index fa094b0..1ac9a36 100644 --- a/app/src/main/java/space/hackenslacker/kanbn4droid/app/BoardsActivity.kt +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/BoardsActivity.kt @@ -13,6 +13,7 @@ import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GravityCompat +import androidx.core.view.doOnLayout import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager @@ -40,6 +41,7 @@ import space.hackenslacker.kanbn4droid.app.boards.BoardsRepository import space.hackenslacker.kanbn4droid.app.boards.BoardsUiEvent import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState import space.hackenslacker.kanbn4droid.app.boards.BoardsViewModel +import space.hackenslacker.kanbn4droid.app.boards.BoardsDrawerWidthCalculator import space.hackenslacker.kanbn4droid.app.boards.DrawerDataErrorCode import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity import space.hackenslacker.kanbn4droid.app.settings.SettingsDialogFragment @@ -284,11 +286,18 @@ class BoardsActivity : AppCompatActivity() { } private fun applyDrawerWidth() { - val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width) - val displayWidthPx = resources.displayMetrics.widthPixels - val computedWidth = minOf(displayWidthPx / 3, maxWidthPx) - drawerContent.layoutParams = drawerContent.layoutParams.apply { - width = computedWidth + drawerLayout.doOnLayout { + val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width) + val breakpointSwDp = resources.getInteger(R.integer.boards_drawer_tablet_breakpoint_sw_dp) + val computedWidth = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = resources.configuration.smallestScreenWidthDp, + tabletBreakpointSwDp = breakpointSwDp, + windowWidthPx = drawerLayout.width, + maxWidthPx = maxWidthPx, + ) + drawerContent.layoutParams = drawerContent.layoutParams.apply { + width = computedWidth + } } } diff --git a/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculator.kt b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculator.kt new file mode 100644 index 0000000..cc3631d --- /dev/null +++ b/app/src/main/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculator.kt @@ -0,0 +1,15 @@ +package space.hackenslacker.kanbn4droid.app.boards + +object BoardsDrawerWidthCalculator { + fun computeWidth( + smallestScreenWidthDp: Int, + tabletBreakpointSwDp: Int, + windowWidthPx: Int, + maxWidthPx: Int, + ): Int { + if (smallestScreenWidthDp < tabletBreakpointSwDp) { + return windowWidthPx + } + return minOf((windowWidthPx * 2) / 3, maxWidthPx) + } +} diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 0000000..85a1593 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ + + + 600 + diff --git a/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculatorTest.kt b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculatorTest.kt new file mode 100644 index 0000000..10a3d4a --- /dev/null +++ b/app/src/test/java/space/hackenslacker/kanbn4droid/app/boards/BoardsDrawerWidthCalculatorTest.kt @@ -0,0 +1,78 @@ +package space.hackenslacker.kanbn4droid.app.boards + +import org.junit.Assert.assertEquals +import org.junit.Test + +class BoardsDrawerWidthCalculatorTest { + @Test + fun smallScreenUsesFullWidth() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 411, + tabletBreakpointSwDp = 600, + windowWidthPx = 1080, + maxWidthPx = 900, + ) + + assertEquals(1080, result) + } + + @Test + fun smallScreenUsesFullWidthIgnoringCap() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 411, + tabletBreakpointSwDp = 600, + windowWidthPx = 1080, + maxWidthPx = 320, + ) + + assertEquals(1080, result) + } + + @Test + fun boundaryAtBreakpointUsesLargeRule() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 600, + tabletBreakpointSwDp = 600, + windowWidthPx = 1000, + maxWidthPx = 700, + ) + + assertEquals(666, result) + } + + @Test + fun largeScreenAppliesTwoThirdsWithCap() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 1024, + tabletBreakpointSwDp = 600, + windowWidthPx = 1024, + maxWidthPx = 700, + ) + + assertEquals(682, result) + } + + @Test + fun boundaryBelowBreakpointUsesSmallRule() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 599, + tabletBreakpointSwDp = 600, + windowWidthPx = 1000, + maxWidthPx = 700, + ) + + assertEquals(1000, result) + } + + @Test + fun largeScreenUsesCapWhenTwoThirdsExceedsCap() { + val result = BoardsDrawerWidthCalculator.computeWidth( + smallestScreenWidthDp = 840, + tabletBreakpointSwDp = 600, + windowWidthPx = 840, + maxWidthPx = 560, + ) + + assertEquals(560, result) + } +}