mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
feat(dual-screen): Dashboard mode for the bottom screen
A new bottom-screen mode that holds pages (downloads, screenshots, telemetry and more come in the next commits). It sizes itself from the bottom display it is shown on, is offered only while it has pages, and follows the existing Dual screen Auto/On/Off setting and display pick. The last page is remembered.
This commit is contained in:
1 parent
a81d1a4d4e
commit
31e6c08be9
9 files changed
+233
-14
No files matched your search
@@ -120,6 +120,8 @@ import com.droiddeck.launcher.ui.LoadingOverlay
|
||||
import com.droiddeck.launcher.ui.SessionDrawer
|
||||
import com.droiddeck.launcher.ui.SessionPausedOverlay
|
||||
import com.droiddeck.launcher.ui.SecondScreenPresentation
|
||||
import com.droiddeck.launcher.ui.BottomDashboard
|
||||
import com.droiddeck.launcher.ui.BottomPage
|
||||
import com.droiddeck.launcher.ui.DroidDeckTheme
|
||||
import com.droiddeck.launcher.wayland.CompositorHost
|
||||
import com.droiddeck.launcher.wayland.WaylandCompositor
|
||||
@@ -1177,6 +1179,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
touchAuto = getString(if (usingTouchpad()) R.string.session_touch_auto_touchpad else R.string.session_touch_auto_direct),
|
||||
fexPreset = fexPreset,
|
||||
secondScreenMode = secondScreenMode,
|
||||
dashboardAvailable = bottomPages().isNotEmpty(),
|
||||
secondScreenDisplays = secondScreenDisplays,
|
||||
selectedSecondScreenDisplay = selectedSecondScreenDisplay,
|
||||
onHud = { on -> SessionPrefs.setHudEnabled(this@SessionActivity, on); hudOn = on; hud.refresh() },
|
||||
@@ -3034,7 +3037,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (secondScreenDisplays.none { it.id == target.displayId }) return
|
||||
SessionState.secondScreenAutoOpened = true
|
||||
selectedSecondScreenDisplay = target.displayId
|
||||
selectSecondScreenMode(SecondScreenMode.opening(SessionPrefs.secondScreenPanel(this), SessionState.deckPad))
|
||||
selectSecondScreenMode(SecondScreenMode.opening(SessionPrefs.secondScreenPanel(this), SessionState.deckPad, bottomPages().isNotEmpty()))
|
||||
dualControlsProvisional = ::loading.isInitialized && loading.visible
|
||||
}
|
||||
|
||||
@@ -3042,7 +3045,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (!dualControlsProvisional || pipUi || !started || (::loading.isInitialized && loading.visible)) return
|
||||
dualControlsProvisional = false
|
||||
if (secondScreenMode == SecondScreenMode.NONE) return
|
||||
val wanted = SecondScreenMode.opening(SessionPrefs.secondScreenPanel(this), SessionState.deckPad)
|
||||
val wanted = SecondScreenMode.opening(SessionPrefs.secondScreenPanel(this), SessionState.deckPad, bottomPages().isNotEmpty())
|
||||
if (wanted != secondScreenMode) selectSecondScreenMode(wanted) else secondScreenPresentation?.retryTerminal()
|
||||
}
|
||||
|
||||
@@ -3112,8 +3115,21 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
menu = {
|
||||
DroidDeckTheme { SessionMenu(docked = true) }
|
||||
},
|
||||
dashboard = if (bottomPages().isEmpty()) null else ({
|
||||
DroidDeckTheme {
|
||||
BottomDashboard(
|
||||
bottomPages(), SessionPrefs.bottomPage(this),
|
||||
onPage = { SessionPrefs.setBottomPage(this, it) },
|
||||
onClose = { selectSecondScreenMode(SecondScreenMode.NONE) },
|
||||
onMenu = if (dockDrawer()) ({ drawerOpen = true }) else null,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
/** Pages for the bottom-screen dashboard; the dashboard is offered only while at least one exists. */
|
||||
private fun bottomPages(): List<BottomPage> = emptyList()
|
||||
|
||||
private fun tapSteamButton() {
|
||||
if (SessionState.mode == SessionService.MODE_STEAM) {
|
||||
padBridge?.applyTouch { st -> st.press(com.droiddeck.launcher.input.PadState.GUIDE, true) }
|
||||
|
||||
@@ -9,11 +9,14 @@ enum class SecondScreenMode(val id: String, @StringRes val label: Int) {
|
||||
NONE("none", R.string.second_mode_none),
|
||||
KEYBOARD_TRACKPAD("keyboard-trackpad", R.string.second_mode_keyboard_trackpad),
|
||||
TERMINAL("terminal", R.string.second_mode_terminal),
|
||||
DECK_CONTROLS("deck-controls", R.string.second_mode_deck_controls);
|
||||
DECK_CONTROLS("deck-controls", R.string.second_mode_deck_controls),
|
||||
DASHBOARD("dashboard", R.string.second_mode_dashboard);
|
||||
|
||||
companion object {
|
||||
fun opening(remembered: String?, deckPad: Boolean): SecondScreenMode =
|
||||
entries.firstOrNull { it != NONE && it.id == remembered && (it != DECK_CONTROLS || deckPad) } ?: if (deckPad) DECK_CONTROLS else KEYBOARD_TRACKPAD
|
||||
fun opening(remembered: String?, deckPad: Boolean, dashboard: Boolean = false): SecondScreenMode =
|
||||
entries.firstOrNull {
|
||||
it != NONE && it.id == remembered && (it != DECK_CONTROLS || deckPad) && (it != DASHBOARD || dashboard)
|
||||
} ?: if (deckPad) DECK_CONTROLS else KEYBOARD_TRACKPAD
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,12 @@ object SessionPrefs {
|
||||
prefs(context).edit().putString("secondScreenPanel", mode).apply()
|
||||
}
|
||||
|
||||
fun bottomPage(context: Context): String? = prefs(context).getString("bottomPage", null)
|
||||
|
||||
fun setBottomPage(context: Context, page: String) {
|
||||
prefs(context).edit().putString("bottomPage", page).apply()
|
||||
}
|
||||
|
||||
fun dualScreenLauncher(context: Context): Boolean = prefs(context).getBoolean("dualScreenLauncher", true)
|
||||
|
||||
fun setDualScreenLauncher(context: Context, on: Boolean) {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Menu
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.R
|
||||
|
||||
/** A page the bottom screen can show while the game runs on the main screen. */
|
||||
class BottomPage(
|
||||
val id: String,
|
||||
@StringRes val label: Int,
|
||||
val icon: ImageVector,
|
||||
val content: @Composable () -> Unit,
|
||||
)
|
||||
|
||||
internal fun pickBottomPage(remembered: String?, ids: List<String>): String? =
|
||||
ids.firstOrNull { it == remembered } ?: ids.firstOrNull()
|
||||
|
||||
/** Tabs show their names while every tab still fits at [TAB_WIDTH]; otherwise only the open tab keeps its name. */
|
||||
internal fun bottomTabsWithLabels(widthDp: Float, tabs: Int): Boolean = tabs * TAB_WIDTH + 2 * BAR_BUTTON <= widthDp
|
||||
|
||||
private const val TAB_WIDTH = 132f
|
||||
private const val BAR_BUTTON = 56f
|
||||
|
||||
@Composable
|
||||
fun BottomDashboard(
|
||||
pages: List<BottomPage>,
|
||||
remembered: String?,
|
||||
onPage: (String) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
onMenu: (() -> Unit)?,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
var current by remember { mutableStateOf(pickBottomPage(remembered, pages.map { it.id })) }
|
||||
val page = pages.firstOrNull { it.id == current } ?: pages.firstOrNull()
|
||||
BoxWithConstraints(Modifier.fillMaxSize().background(colors.background)) {
|
||||
val labels = bottomTabsWithLabels(maxWidth.value, pages.size)
|
||||
Column(Modifier.fillMaxSize().padding(horizontal = 12.dp, vertical = 10.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(Modifier.fillMaxWidth().height(48.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
BarButton(Icons.Outlined.Close, stringResource(R.string.deck_close), "bottom-close", onClose)
|
||||
Row(Modifier.weight(1f).horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
pages.forEach { p ->
|
||||
PageTab(p, p.id == page?.id, labels) {
|
||||
current = p.id
|
||||
onPage(p.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onMenu != null) BarButton(Icons.Outlined.Menu, stringResource(R.string.touch_droiddeck_menu), "bottom-menu", onMenu)
|
||||
}
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
if (page != null) page.content()
|
||||
else Text(stringResource(R.string.bottom_dashboard_empty), fontSize = 15.sp, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BarButton(icon: ImageVector, label: String, tag: String, onClick: () -> Unit) {
|
||||
val pal = LocalPalette.current
|
||||
Box(
|
||||
Modifier.size(44.dp).clip(RoundedCornerShape(22.dp)).background(pal.surface).border(1.dp, pal.line, RoundedCornerShape(22.dp))
|
||||
.semantics { contentDescription = label }.clickable(role = Role.Button, onClick = onClick).testTag(tag),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PageTab(page: BottomPage, selected: Boolean, label: Boolean, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val shape = RoundedCornerShape(20.dp)
|
||||
val name = stringResource(page.label)
|
||||
Row(
|
||||
Modifier.height(44.dp).clip(shape)
|
||||
.background(if (selected) pal.signal else Color.Transparent)
|
||||
.border(1.dp, if (selected) pal.signal else pal.line, shape)
|
||||
.semantics { contentDescription = name }
|
||||
.clickable(role = Role.Tab, onClick = onClick).testTag("bottom-page-${page.id}")
|
||||
.padding(horizontal = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
val tint = if (selected) colors.background else colors.onBackground
|
||||
Icon(page.icon, contentDescription = null, tint = tint, modifier = Modifier.size(20.dp))
|
||||
if (label || selected) Text(name, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = tint, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
@@ -56,9 +56,11 @@ class SecondScreenPresentation(
|
||||
private val onKeyboardClosed: () -> Unit = {},
|
||||
private val onMenu: (() -> Unit)? = null,
|
||||
private val menu: (@androidx.compose.runtime.Composable () -> Unit)? = null,
|
||||
private val dashboard: (@androidx.compose.runtime.Composable () -> Unit)? = null,
|
||||
) : Presentation(sessionActivity, display) {
|
||||
private val frame = FrameLayout(context)
|
||||
private var menuView: androidx.compose.ui.platform.ComposeView? = null
|
||||
private var dashboardView: androidx.compose.ui.platform.ComposeView? = null
|
||||
var menuShown = false
|
||||
private set
|
||||
private val root = LinearLayout(context).apply {
|
||||
@@ -155,12 +157,7 @@ class SecondScreenPresentation(
|
||||
menuShown = on
|
||||
if (on) {
|
||||
keyboardHost?.takeIf { it.shown }?.hide()
|
||||
window?.decorView?.let { decor ->
|
||||
decor.setViewTreeLifecycleOwner(sessionActivity)
|
||||
decor.setViewTreeViewModelStoreOwner(sessionActivity)
|
||||
decor.setViewTreeSavedStateRegistryOwner(sessionActivity)
|
||||
decor.setViewTreeOnBackPressedDispatcherOwner(sessionActivity)
|
||||
}
|
||||
ownComposeViews()
|
||||
val view = menuView ?: androidx.compose.ui.platform.ComposeView(context).apply {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
@@ -179,12 +176,22 @@ class SecondScreenPresentation(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ownComposeViews() {
|
||||
window?.decorView?.let { decor ->
|
||||
decor.setViewTreeLifecycleOwner(sessionActivity)
|
||||
decor.setViewTreeViewModelStoreOwner(sessionActivity)
|
||||
decor.setViewTreeSavedStateRegistryOwner(sessionActivity)
|
||||
decor.setViewTreeOnBackPressedDispatcherOwner(sessionActivity)
|
||||
}
|
||||
}
|
||||
|
||||
fun focusMenu(): Boolean = menuView?.requestFocus() == true
|
||||
|
||||
fun dispatchMenuKey(event: KeyEvent): Boolean = menuView?.dispatchKeyEvent(event) == true
|
||||
|
||||
fun closeControls() {
|
||||
showMenu(false)
|
||||
closeDashboard()
|
||||
closeTerminal()
|
||||
keyboardHost?.hide()
|
||||
keyboardHost = null
|
||||
@@ -218,19 +225,37 @@ class SecondScreenPresentation(
|
||||
keyboardButton = null
|
||||
keyboardPending = false
|
||||
closeTerminal()
|
||||
closeDashboard()
|
||||
root.removeAllViews()
|
||||
val mode = if (currentMode == SecondScreenMode.DECK_CONTROLS && !SessionState.deckPad) SecondScreenMode.NONE
|
||||
else currentMode
|
||||
val mode = when {
|
||||
currentMode == SecondScreenMode.DECK_CONTROLS && !SessionState.deckPad -> SecondScreenMode.NONE
|
||||
currentMode == SecondScreenMode.DASHBOARD && dashboard == null -> SecondScreenMode.NONE
|
||||
else -> currentMode
|
||||
}
|
||||
if (mode == SecondScreenMode.NONE) return
|
||||
|
||||
when (mode) {
|
||||
SecondScreenMode.KEYBOARD_TRACKPAD -> renderKeyboardAndTrackpad()
|
||||
SecondScreenMode.TERMINAL -> renderTerminal()
|
||||
SecondScreenMode.DECK_CONTROLS -> root.addView(DeckControlsPanel(context, onClose, onSteamMenu, onQam, pad, onMenu = onMenu), weightParams())
|
||||
SecondScreenMode.DASHBOARD -> renderDashboard()
|
||||
SecondScreenMode.NONE -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderDashboard() {
|
||||
val content = dashboard ?: return
|
||||
ownComposeViews()
|
||||
val view = androidx.compose.ui.platform.ComposeView(context).apply { setContent { content() } }
|
||||
dashboardView = view
|
||||
root.addView(view, weightParams())
|
||||
}
|
||||
|
||||
private fun closeDashboard() {
|
||||
dashboardView?.disposeComposition()
|
||||
dashboardView = null
|
||||
}
|
||||
|
||||
private fun renderKeyboardAndTrackpad() {
|
||||
val panel = FrameLayout(context)
|
||||
panel.addView(trackpadView(), FrameLayout.LayoutParams(-1, -1))
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Terminal
|
||||
import androidx.compose.material.icons.outlined.Dashboard
|
||||
import androidx.compose.material.icons.outlined.Keyboard
|
||||
import androidx.compose.material.icons.outlined.AutoFixHigh
|
||||
import androidx.compose.material.icons.outlined.ExpandMore
|
||||
@@ -240,6 +241,7 @@ class DrawerActions(
|
||||
val textureAnisotropy: Int? = 0,
|
||||
val textureLodBias: String? = TextureFiltering.LOD_BIAS_OFF,
|
||||
val secondScreenMode: SecondScreenMode,
|
||||
val dashboardAvailable: Boolean = false,
|
||||
val secondScreenDisplays: List<SecondScreenDisplay>,
|
||||
val selectedSecondScreenDisplay: Int,
|
||||
val onHud: (Boolean) -> Unit,
|
||||
@@ -893,6 +895,7 @@ private fun BottomScreenModes(a: DrawerActions, modifier: Modifier, onPick: () -
|
||||
if (com.droiddeck.launcher.session.SessionState.deckPad) add(SecondScreenMode.DECK_CONTROLS to Icons.Outlined.SportsEsports)
|
||||
add(SecondScreenMode.KEYBOARD_TRACKPAD to Icons.Outlined.Keyboard)
|
||||
add(SecondScreenMode.TERMINAL to Icons.Outlined.Terminal)
|
||||
if (a.dashboardAvailable) add(SecondScreenMode.DASHBOARD to Icons.Outlined.Dashboard)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -1190,7 +1193,8 @@ private fun SecondScreenGroup(host: MenuHost, a: DrawerActions, track: (String)
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val modes = listOf(SecondScreenMode.NONE, SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.TERMINAL) +
|
||||
if (com.droiddeck.launcher.session.SessionState.deckPad) listOf(SecondScreenMode.DECK_CONTROLS) else emptyList()
|
||||
(if (com.droiddeck.launcher.session.SessionState.deckPad) listOf(SecondScreenMode.DECK_CONTROLS) else emptyList()) +
|
||||
(if (a.dashboardAvailable) listOf(SecondScreenMode.DASHBOARD) else emptyList())
|
||||
SettingsGroup(stringResource(R.string.drawer_second_screen_mode)) {
|
||||
Column(Modifier.fillMaxWidth().selectableGroup()) {
|
||||
modes.forEach { mode ->
|
||||
|
||||
@@ -1876,6 +1876,8 @@
|
||||
<string name="second_mode_keyboard_trackpad">Keyboard + trackpad</string>
|
||||
<string name="second_mode_terminal">Terminal</string>
|
||||
<string name="second_mode_deck_controls">Deck grips + trackpads</string>
|
||||
<string name="second_mode_dashboard">Dashboard</string>
|
||||
<string name="bottom_dashboard_empty">Nothing to show here yet.</string>
|
||||
|
||||
<string name="rtinst_removing">Removing Linux runtime</string>
|
||||
<plurals name="rtinst_removing_entries">
|
||||
|
||||
@@ -23,4 +23,10 @@ class SecondScreenModeTest {
|
||||
assertEquals(SecondScreenMode.DECK_CONTROLS, SecondScreenMode.opening("none", deckPad = true))
|
||||
assertEquals(SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.opening("gone", deckPad = false))
|
||||
}
|
||||
|
||||
@Test fun theDashboardOpensAgainOnlyWhileItHasPages() {
|
||||
assertEquals(SecondScreenMode.DASHBOARD, SecondScreenMode.opening("dashboard", deckPad = true, dashboard = true))
|
||||
assertEquals(SecondScreenMode.DECK_CONTROLS, SecondScreenMode.opening("dashboard", deckPad = true, dashboard = false))
|
||||
assertEquals(SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.opening("dashboard", deckPad = false))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BottomDashboardTest {
|
||||
@Test fun theRememberedPageOpensAgain() {
|
||||
assertEquals("shots", pickBottomPage("shots", listOf("downloads", "shots")))
|
||||
}
|
||||
|
||||
@Test fun aGoneOrMissingPageFallsBackToTheFirst() {
|
||||
assertEquals("downloads", pickBottomPage("gone", listOf("downloads", "shots")))
|
||||
assertEquals("downloads", pickBottomPage(null, listOf("downloads", "shots")))
|
||||
assertNull(pickBottomPage("shots", emptyList()))
|
||||
}
|
||||
|
||||
@Test fun tabNamesShowOnlyWhileEveryTabFits() {
|
||||
// Retroid Pocket Duo bottom panel: 1280x960 at 320 dpi = 640 dp wide.
|
||||
assertTrue(bottomTabsWithLabels(640f, 4))
|
||||
assertFalse(bottomTabsWithLabels(640f, 5))
|
||||
assertFalse(bottomTabsWithLabels(360f, 3))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user