feat: make boards drawer width adaptive by screen class

This commit is contained in:
2026-04-30 12:36:35 -04:00
parent 028c05c0c8
commit b6da868103
6 changed files with 126 additions and 10 deletions
+2 -2
View File
@@ -149,7 +149,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
**Settings view** **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 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 view behind the side panel dims when the side panel is open.
- The side panel shows the following content from top to bottom: - 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. - 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. - All settings are managed using the AndroidX Preferences library.
- Changing any settings makes it apply instantly when leaving the settings view without logging out. - 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. - 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. - 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. - 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. - 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.
@@ -281,7 +281,7 @@ class BoardsFlowTest {
} }
@Test @Test
fun drawerWidthNeverExceedsOneThirdOfScreen() { fun drawerWidthIsAdaptiveForSmallAndLargeScreens() {
MainActivity.dependencies.apiClientFactory = { MainActivity.dependencies.apiClientFactory = {
FakeBoardsApiClient( FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha")), boards = mutableListOf(BoardSummary("1", "Alpha")),
@@ -292,9 +292,19 @@ class BoardsFlowTest {
val scenario = ActivityScenario.launch(BoardsActivity::class.java) val scenario = ActivityScenario.launch(BoardsActivity::class.java)
scenario.onActivity { activity -> scenario.onActivity { activity ->
val drawerLayout = activity.findViewById<DrawerLayout>(R.id.boardsDrawerLayout)
val drawerContent = activity.findViewById<View>(R.id.boardsDrawerContent) val drawerContent = activity.findViewById<View>(R.id.boardsDrawerContent)
val displayWidthPx = activity.resources.displayMetrics.widthPixels val resources = activity.resources
assertTrue(drawerContent.layoutParams.width <= displayWidthPx / 3) 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)
} }
} }
@@ -13,6 +13,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.core.view.doOnLayout
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager 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.BoardsUiEvent
import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState
import space.hackenslacker.kanbn4droid.app.boards.BoardsViewModel 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.boards.DrawerDataErrorCode
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity
import space.hackenslacker.kanbn4droid.app.settings.SettingsDialogFragment import space.hackenslacker.kanbn4droid.app.settings.SettingsDialogFragment
@@ -284,11 +286,18 @@ class BoardsActivity : AppCompatActivity() {
} }
private fun applyDrawerWidth() { private fun applyDrawerWidth() {
val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width) drawerLayout.doOnLayout {
val displayWidthPx = resources.displayMetrics.widthPixels val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width)
val computedWidth = minOf(displayWidthPx / 3, maxWidthPx) val breakpointSwDp = resources.getInteger(R.integer.boards_drawer_tablet_breakpoint_sw_dp)
drawerContent.layoutParams = drawerContent.layoutParams.apply { val computedWidth = BoardsDrawerWidthCalculator.computeWidth(
width = computedWidth smallestScreenWidthDp = resources.configuration.smallestScreenWidthDp,
tabletBreakpointSwDp = breakpointSwDp,
windowWidthPx = drawerLayout.width,
maxWidthPx = maxWidthPx,
)
drawerContent.layoutParams = drawerContent.layoutParams.apply {
width = computedWidth
}
} }
} }
@@ -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)
}
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="boards_drawer_tablet_breakpoint_sw_dp">600</integer>
</resources>
@@ -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)
}
}