improve visuals for Logo & Login screens for compact devices

This commit is contained in:
2024-07-14 00:57:44 +03:00
parent 5f57d35ce5
commit c573803921
8 changed files with 254 additions and 182 deletions
+2
View File
@@ -181,4 +181,6 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.kotlin.serialization)
implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04")
}
@@ -15,6 +15,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -28,6 +29,7 @@ import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.core.layout.WindowWidthSizeClass
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState
import com.meloda.app.fast.MainViewModel
@@ -57,7 +59,11 @@ class MainActivity : AppCompatActivity() {
val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
val systemBarStyle = when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb())
Configuration.UI_MODE_NIGHT_NO -> SystemBarStyle.light(
Color.Transparent.toArgb(),
Color.Transparent.toArgb()
)
Configuration.UI_MODE_NIGHT_YES -> SystemBarStyle.dark(Color.Transparent.toArgb())
else -> error("Illegal State, current mode is $currentNightMode")
}
@@ -104,6 +110,14 @@ class MainActivity : AppCompatActivity() {
}
}
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
val isDeviceCompact by remember(windowAdaptiveInfo) {
derivedStateOf {
windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
}
}
val theme by userSettings.theme.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalTheme provides ThemeConfig(
@@ -112,7 +126,8 @@ class MainActivity : AppCompatActivity() {
selectedColorScheme = theme.selectedColorScheme,
usingAmoledBackground = theme.usingAmoledBackground,
usingBlur = theme.usingBlur,
multiline = theme.multiline
multiline = theme.multiline,
isDeviceCompact = isDeviceCompact
)
) {
val currentTheme = LocalTheme.current
@@ -40,7 +40,8 @@ class UserSettingsImpl(
selectedColorScheme = selectedColorScheme(),
usingAmoledBackground = isUsingAmoledBackground(),
usingBlur = isUsingBlur(),
multiline = isMultiline()
multiline = isMultiline(),
isDeviceCompact = false
)
)
@@ -7,5 +7,5 @@ data class ThemeConfig(
val usingAmoledBackground: Boolean,
val usingBlur: Boolean,
val multiline: Boolean,
val bubblesWithPinch: Boolean = true
val isDeviceCompact: Boolean
)
@@ -110,7 +110,8 @@ val LocalTheme = compositionLocalOf {
selectedColorScheme = 0,
usingAmoledBackground = false,
usingBlur = false,
multiline = false
multiline = false,
isDeviceCompact = false
)
}
@@ -6,11 +6,13 @@ import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.union
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@@ -20,6 +22,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -48,6 +51,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.common.UiText
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.TextFieldErrorText
import com.meloda.app.fast.designsystem.autoFillRequestHandler
@@ -60,8 +64,8 @@ import com.meloda.fast.auth.login.LoginViewModelImpl
import com.meloda.fast.auth.login.model.CaptchaArguments
import com.meloda.fast.auth.login.model.LoginError
import com.meloda.fast.auth.login.model.LoginScreenState
import com.meloda.fast.auth.login.model.LoginValidationArguments
import com.meloda.fast.auth.login.model.LoginUserBannedArguments
import com.meloda.fast.auth.login.model.LoginValidationArguments
import org.koin.androidx.compose.koinViewModel
import com.meloda.app.fast.designsystem.R as UiR
@@ -153,6 +157,7 @@ fun LoginScreen(
onPasswordFieldGoAction: () -> Unit = {},
onSignInButtonClicked: () -> Unit = {}
) {
val currentTheme = LocalTheme.current
val focusManager = LocalFocusManager.current
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
@@ -181,183 +186,198 @@ fun LoginScreen(
}
)
Scaffold { padding ->
val titleStyle = if (currentTheme.isDeviceCompact) {
MaterialTheme.typography.displaySmall
} else {
MaterialTheme.typography.displayMedium
}
val titleSpacerSize = if (currentTheme.isDeviceCompact) {
24.dp
} else {
58.dp
}
val bottomPadding = if (currentTheme.isDeviceCompact) {
10.dp
} else {
30.dp
}
Scaffold(
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.padding(top = 30.dp)
.padding(horizontal = 30.dp)
.padding(bottom = bottomPadding)
) {
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(30.dp)
.imePadding()
.fillMaxWidth()
.align(Alignment.Center)
) {
Column(
Text(
text = stringResource(id = UiR.string.sign_in_to_vk),
color = MaterialTheme.colorScheme.onBackground,
style = titleStyle
)
Spacer(modifier = Modifier.height(titleSpacerSize))
TextField(
modifier = Modifier
.height(58.dp)
.fillMaxWidth()
.align(Alignment.Center)
) {
Text(
text = stringResource(id = UiR.string.sign_in_to_vk),
color = MaterialTheme.colorScheme.onBackground,
style = MaterialTheme.typography.displayMedium
)
.clip(RoundedCornerShape(10.dp))
.handleEnterKey {
passwordFocusable.requestFocus()
true
}
.handleTabKey {
passwordFocusable.requestFocus()
true
}
.focusRequester(loginFocusable)
.connectNode(handler = autoFillEmailHandler)
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
value = loginText,
onValueChange = { newText ->
val text = newText.text
if (text.isEmpty()) {
autoFillEmailHandler.requestVerifyManual()
}
Spacer(modifier = Modifier.height(58.dp))
TextField(
modifier = Modifier
.height(58.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.handleEnterKey {
passwordFocusable.requestFocus()
true
loginText = newText
onLoginInputChanged(text)
},
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
leadingIcon = {
Icon(
painter = painterResource(id = UiR.drawable.ic_round_person_24),
contentDescription = "Login icon",
tint = if (showLoginError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
.handleTabKey {
passwordFocusable.requestFocus()
true
}
.focusRequester(loginFocusable)
.connectNode(handler = autoFillEmailHandler)
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
value = loginText,
onValueChange = { newText ->
val text = newText.text
if (text.isEmpty()) {
autoFillEmailHandler.requestVerifyManual()
}
loginText = newText
onLoginInputChanged(text)
},
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
leadingIcon = {
Icon(
painter = painterResource(id = UiR.drawable.ic_round_person_24),
contentDescription = "Login icon",
tint = if (showLoginError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Email
),
keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }),
isError = showLoginError,
singleLine = true
)
AnimatedVisibility(visible = showLoginError) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
}
Spacer(modifier = Modifier.height(16.dp))
TextField(
modifier = Modifier
.height(58.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.handleEnterKey {
focusManager.clearFocus()
onPasswordFieldEnterKeyClicked()
true
}
.focusRequester(passwordFocusable)
.connectNode(handler = autoFillPasswordHandler)
.defaultFocusChangeAutoFill(handler = autoFillPasswordHandler),
value = passwordText,
onValueChange = { newText ->
val text = newText.text
if (text.isEmpty()) {
autoFillPasswordHandler.requestVerifyManual()
}
passwordText = newText
onPasswordInputChanged(text)
},
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
leadingIcon = {
Icon(
painter = painterResource(id = UiR.drawable.round_vpn_key_24),
contentDescription = "Password icon",
tint = if (showPasswordError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
)
},
trailingIcon = {
val imagePainter = painterResource(
id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24
else UiR.drawable.round_visibility_24
)
IconButton(onClick = onPasswordVisibilityButtonClicked) {
Icon(
painter = imagePainter,
contentDescription = if (screenState.passwordVisible) "Password visible icon"
else "Password invisible icon"
)
}
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Password
),
keyboardActions = KeyboardActions(
onGo = {
focusManager.clearFocus()
onPasswordFieldGoAction()
}
),
isError = showPasswordError,
visualTransformation = if (screenState.passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
singleLine = true
)
AnimatedVisibility(visible = showPasswordError) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
}
)
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Email
),
keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }),
isError = showLoginError,
singleLine = true
)
AnimatedVisibility(visible = showLoginError) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
}
Box(
modifier = Modifier.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
Spacer(modifier = Modifier.height(16.dp))
FloatingActionButton(
onClick = {
TextField(
modifier = Modifier
.height(58.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.handleEnterKey {
focusManager.clearFocus()
onSignInButtonClicked()
},
containerColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.testTag("sing_in_fab")
) {
onPasswordFieldEnterKeyClicked()
true
}
.focusRequester(passwordFocusable)
.connectNode(handler = autoFillPasswordHandler)
.defaultFocusChangeAutoFill(handler = autoFillPasswordHandler),
value = passwordText,
onValueChange = { newText ->
val text = newText.text
if (text.isEmpty()) {
autoFillPasswordHandler.requestVerifyManual()
}
passwordText = newText
onPasswordInputChanged(text)
},
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
leadingIcon = {
Icon(
painter = painterResource(id = UiR.drawable.ic_arrow_end),
contentDescription = "Sign in icon",
tint = MaterialTheme.colorScheme.onSecondaryContainer
painter = painterResource(id = UiR.drawable.round_vpn_key_24),
contentDescription = "Password icon",
tint = if (showPasswordError) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
)
}
AnimatedVisibility(
visible = screenState.isLoading,
enter = fadeIn(),
exit = fadeOut()
) {
CircularProgressIndicator()
}
},
trailingIcon = {
val imagePainter = painterResource(
id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24
else UiR.drawable.round_visibility_24
)
IconButton(onClick = onPasswordVisibilityButtonClicked) {
Icon(
painter = imagePainter,
contentDescription = if (screenState.passwordVisible) "Password visible icon"
else "Password invisible icon"
)
}
},
keyboardOptions = KeyboardOptions.Default.copy(
imeAction = ImeAction.Go,
keyboardType = KeyboardType.Password
),
keyboardActions = KeyboardActions(
onGo = {
focusManager.clearFocus()
onPasswordFieldGoAction()
}
),
isError = showPasswordError,
visualTransformation = if (screenState.passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
singleLine = true
)
AnimatedVisibility(visible = showPasswordError) {
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
}
}
Box(
modifier = Modifier.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
FloatingActionButton(
onClick = {
focusManager.clearFocus()
onSignInButtonClicked()
},
containerColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.testTag("sing_in_fab")
) {
Icon(
painter = painterResource(id = UiR.drawable.ic_arrow_end),
contentDescription = "Sign in icon",
tint = MaterialTheme.colorScheme.onSecondaryContainer
)
}
AnimatedVisibility(
visible = screenState.isLoading,
enter = fadeIn(),
exit = fadeOut()
) {
CircularProgressIndicator()
}
}
}
@@ -13,6 +13,7 @@ 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.width
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -30,6 +31,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.fast.auth.login.LoginViewModel
import com.meloda.fast.auth.login.LoginViewModelImpl
import org.koin.androidx.compose.koinViewModel
@@ -64,6 +66,8 @@ fun LogoScreen(
onLogoLongClicked: () -> Unit = {},
onGoNextButtonClicked: () -> Unit = {}
) {
val currentTheme = LocalTheme.current
Scaffold { padding ->
val topPadding by animateDpAsState(
targetValue = padding.calculateTopPadding(),
@@ -83,6 +87,24 @@ fun LogoScreen(
label = "startPaddingAnimation"
)
val iconWidth = if (currentTheme.isDeviceCompact) {
100.dp
} else {
134.dp
}
val appNameTextStyle = if (currentTheme.isDeviceCompact) {
MaterialTheme.typography.displaySmall
} else {
MaterialTheme.typography.displayMedium
}
val bottomAdditionalPadding = if (currentTheme.isDeviceCompact) {
10.dp
} else {
30.dp
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -92,7 +114,9 @@ fun LogoScreen(
end = endPadding,
bottom = bottomPadding
)
.padding(30.dp)
.padding(top = 30.dp)
.padding(horizontal = 30.dp)
.padding(bottom = bottomAdditionalPadding)
) {
Column(
modifier = Modifier
@@ -104,17 +128,19 @@ fun LogoScreen(
painter = painterResource(id = UiR.drawable.ic_logo_big),
contentDescription = "Application Logo",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onLongClick = onLogoLongClicked,
onClick = {}
)
modifier = Modifier
.width(iconWidth)
.combinedClickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onLongClick = onLogoLongClicked,
onClick = {}
)
)
Spacer(modifier = Modifier.height(46.dp))
Text(
text = stringResource(id = UiR.string.fast_messenger),
style = MaterialTheme.typography.displayMedium,
style = appNameTextStyle,
color = MaterialTheme.colorScheme.onBackground
)
}
@@ -60,6 +60,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -163,16 +164,16 @@ fun MessagesHistoryScreen(
)
}
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollForward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!listState.canScrollForward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
Column(modifier = Modifier.fillMaxWidth()) {
TopAppBar(
modifier = Modifier
@@ -370,7 +371,13 @@ fun MessagesHistoryScreen(
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
),
placeholder = { Text(text = stringResource(id = UiR.string.message_input_hint)) }
placeholder = {
Text(
text = stringResource(id = UiR.string.message_input_hint),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
IconButton(onClick = onAttachmentButtonClicked) {