Improve widescreen layout for large devices

This commit is contained in:
William Brawner 2024-02-11 15:44:38 -07:00
parent 6acacf5edb
commit a48f243e48
3 changed files with 139 additions and 81 deletions

View file

@ -110,6 +110,7 @@ play {
} }
dependencies { dependencies {
implementation("androidx.compose.material3:material3-window-size-class-android:1.2.0")
val navigationVersion = "2.7.2" val navigationVersion = "2.7.2"
implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion") implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion") implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion")

View file

@ -20,6 +20,9 @@ import androidx.compose.material.icons.filled.Help
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.PrivacyTip import androidx.compose.material.icons.filled.PrivacyTip
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -48,6 +51,7 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
) )
} }
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -90,6 +94,7 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
} }
AppCompatDelegate.setDefaultNightMode(darkMode) AppCompatDelegate.setDefaultNightMode(darkMode)
} }
val windowSizeClass = calculateWindowSizeClass(this)
SimpleMarkdownTheme { SimpleMarkdownTheme {
val navController = rememberNavController() val navController = rememberNavController()
NavHost( NavHost(
@ -121,6 +126,7 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
MainScreen( MainScreen(
navController = navController, navController = navController,
viewModel = viewModel, viewModel = viewModel,
enableWideLayout = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded,
enableAutosave = autosaveEnabled, enableAutosave = autosaveEnabled,
enableReadability = readabilityEnabled, enableReadability = readabilityEnabled,
darkMode = darkModePreference darkMode = darkModePreference

View file

@ -4,15 +4,19 @@ import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -43,6 +47,9 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -72,16 +79,17 @@ import com.wbrawner.simplemarkdown.MarkdownViewModel
import com.wbrawner.simplemarkdown.R import com.wbrawner.simplemarkdown.R
import com.wbrawner.simplemarkdown.Route import com.wbrawner.simplemarkdown.Route
import com.wbrawner.simplemarkdown.model.Readability import com.wbrawner.simplemarkdown.model.Readability
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URI import java.net.URI
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
fun MainScreen( fun MainScreen(
navController: NavController, navController: NavController,
viewModel: MarkdownViewModel, viewModel: MarkdownViewModel,
enableWideLayout: Boolean,
enableAutosave: Boolean, enableAutosave: Boolean,
enableReadability: Boolean, enableReadability: Boolean,
darkMode: String, darkMode: String,
@ -204,6 +212,7 @@ fun MainScreen(
menuExpanded = false menuExpanded = false
saveFileLauncher.launch(fileName) saveFileLauncher.launch(fileName)
}) })
if (!enableWideLayout) {
DropdownMenuItem(text = { DropdownMenuItem(text = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text("Lock Swiping") Text("Lock Swiping")
@ -217,14 +226,61 @@ fun MainScreen(
}) })
} }
} }
}
}) })
}) { paddingValues -> }) { paddingValues ->
val pagerState = rememberPagerState { 2 } val markdown by viewModel.markdown.collectAsState()
val (textFieldValue, setTextFieldValue) = remember(clearText) {
val annotatedMarkdown = if (enableReadability) {
markdown.annotateReadability()
} else {
AnnotatedString(markdown)
}
mutableStateOf(TextFieldValue(annotatedMarkdown))
}
val setTextFieldAndViewModelValues: (TextFieldValue) -> Unit = {
setTextFieldValue(it)
coroutineScope.launch {
viewModel.updateMarkdown(it.text)
}
}
if (enableWideLayout) {
Row(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
MarkdownTextField(modifier = Modifier.fillMaxHeight().weight(1f), textFieldValue, setTextFieldAndViewModelValues)
Spacer(modifier = Modifier.fillMaxHeight().width(1.dp).background(color = MaterialTheme.colorScheme.primary))
MarkdownPreview(modifier = Modifier.fillMaxHeight().weight(1f), markdown, darkMode)
}
} else {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(paddingValues)
) { ) {
TabbedMarkdownEditor(
coroutineScope,
lockSwiping,
textFieldValue,
setTextFieldAndViewModelValues,
markdown,
darkMode
)
}
}
}
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun TabbedMarkdownEditor(
coroutineScope: CoroutineScope,
lockSwiping: Boolean,
textFieldValue: TextFieldValue,
setTextFieldAndViewModelValues: (TextFieldValue) -> Unit,
markdown: String,
darkMode: String
) {
val pagerState = rememberPagerState { 2 }
TabRow(selectedTabIndex = pagerState.currentPage) { TabRow(selectedTabIndex = pagerState.currentPage) {
Tab(text = { Text("Edit") }, Tab(text = { Text("Edit") },
selected = pagerState.currentPage == 0, selected = pagerState.currentPage == 0,
@ -234,10 +290,9 @@ fun MainScreen(
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }) onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } })
} }
HorizontalPager( HorizontalPager(
modifier = Modifier.weight(1f), state = pagerState, modifier = Modifier.fillMaxSize(1f), state = pagerState,
userScrollEnabled = !lockSwiping userScrollEnabled = !lockSwiping
) { page -> ) { page ->
val markdown by viewModel.markdown.collectAsState()
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(page) { LaunchedEffect(page) {
when (page) { when (page) {
@ -245,54 +300,12 @@ fun MainScreen(
else -> keyboardController?.hide() else -> keyboardController?.hide()
} }
} }
var textFieldValue by remember(clearText) {
val annotatedMarkdown = if (enableReadability) {
markdown.annotateReadability()
} else {
AnnotatedString(markdown)
}
mutableStateOf(TextFieldValue(annotatedMarkdown))
}
if (page == 0) { if (page == 0) {
TextField( MarkdownTextField(modifier = Modifier.fillMaxSize(), textFieldValue, setTextFieldAndViewModelValues)
modifier = Modifier
.fillMaxSize()
.imePadding(),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
value = textFieldValue,
onValueChange = {
textFieldValue = if (enableReadability) {
it.copy(annotatedString = it.text.annotateReadability())
} else {
it
}
coroutineScope.launch {
viewModel.updateMarkdown(it.text)
}
},
placeholder = {
Text("Markdown here…")
},
textStyle = TextStyle.Default.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
)
} else { } else {
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown, darkMode) MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown, darkMode)
} }
} }
}
}
}
} }
private fun String.annotateReadability(): AnnotatedString { private fun String.annotateReadability(): AnnotatedString {
@ -389,3 +402,41 @@ fun MarkdownTopAppBar(
} }
}, actions = actions ?: {}) }, actions = actions ?: {})
} }
@Composable
fun MarkdownTextField(
modifier: Modifier,
textFieldValue: TextFieldValue,
setTextFieldValue: (TextFieldValue) -> Unit,
enableReadability: Boolean = false
) {
TextField(
modifier = modifier.imePadding(),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent
),
value = textFieldValue,
onValueChange = {
setTextFieldValue(
if (enableReadability) {
it.copy(annotatedString = it.text.annotateReadability())
} else {
it
}
)
},
placeholder = {
Text("Markdown here…")
},
textStyle = TextStyle.Default.copy(
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
),
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
)
}