feat: make boards drawer width adaptive by screen class
This commit is contained in:
@@ -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,13 +286,20 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun applyDrawerWidth() {
|
private fun applyDrawerWidth() {
|
||||||
|
drawerLayout.doOnLayout {
|
||||||
val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width)
|
val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width)
|
||||||
val displayWidthPx = resources.displayMetrics.widthPixels
|
val breakpointSwDp = resources.getInteger(R.integer.boards_drawer_tablet_breakpoint_sw_dp)
|
||||||
val computedWidth = minOf(displayWidthPx / 3, maxWidthPx)
|
val computedWidth = BoardsDrawerWidthCalculator.computeWidth(
|
||||||
|
smallestScreenWidthDp = resources.configuration.smallestScreenWidthDp,
|
||||||
|
tabletBreakpointSwDp = breakpointSwDp,
|
||||||
|
windowWidthPx = drawerLayout.width,
|
||||||
|
maxWidthPx = maxWidthPx,
|
||||||
|
)
|
||||||
drawerContent.layoutParams = drawerContent.layoutParams.apply {
|
drawerContent.layoutParams = drawerContent.layoutParams.apply {
|
||||||
width = computedWidth
|
width = computedWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun forceSignOutToLogin() {
|
private fun forceSignOutToLogin() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|||||||
+15
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<integer name="boards_drawer_tablet_breakpoint_sw_dp">600</integer>
|
||||||
|
</resources>
|
||||||
+78
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user