android: status bar and navigation bar colors from theme (#4568)

* android: status bar and navigation bar colors from theme

* padding

* background on desktop

* useless code

* colors

* removed unused param

* one more place
This commit is contained in:
Stanislav Dmitrenko
2024-08-03 07:48:41 +09:00
committed by GitHub
parent cb76c8079c
commit e38db7fb44
17 changed files with 127 additions and 32 deletions
@@ -13,6 +13,7 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent
import chat.simplex.common.*
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
@@ -24,11 +25,13 @@ import java.lang.ref.WeakReference
class MainActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
mainActivity = WeakReference(this)
platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
applyAppLocale(ChatModel.controller.appPrefs.appLanguage)
super.onCreate(savedInstanceState)
// testJson()
mainActivity = WeakReference(this)
// When call ended and orientation changes, it re-process old intent, it's unneeded.
// Only needed to be processed on first creation of activity
if (savedInstanceState == null) {
@@ -7,9 +7,13 @@ import chat.simplex.common.platform.Log
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.*
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.ViewCompat
import androidx.lifecycle.*
import androidx.work.*
import chat.simplex.app.model.NtfManager
@@ -20,8 +24,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.ui.theme.DefaultTheme
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage
@@ -268,6 +271,35 @@ class SimplexApp: Application(), LifecycleEventObserver {
uiModeManager.setApplicationNightMode(mode)
}
override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {
val window = mainActivity.get()?.window ?: return
@Suppress("DEPRECATION")
val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView)
val statusBar = (if (hasTop) {
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
backgroundColor
}).toArgb()
val navBar = (if (hasBottom) {
backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f)
} else {
backgroundColor
}).toArgb()
if (window.statusBarColor != statusBar) {
window.statusBarColor = statusBar
}
if (windowInsetController?.isAppearanceLightStatusBars != isLight) {
windowInsetController?.isAppearanceLightStatusBars = isLight
}
if (window.navigationBarColor != navBar) {
window.navigationBarColor = navBar
}
if (windowInsetController?.isAppearanceLightNavigationBars != isLight) {
windowInsetController?.isAppearanceLightNavigationBars = isLight
}
}
override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) {
val context = mainActivity.get() ?: return
val intent = Intent(context, CallActivity::class.java)
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.text.InputType
import android.util.Log
@@ -16,6 +17,7 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
@@ -56,7 +58,6 @@ actual fun PlatformTextField(
) {
val cs = composeState.value
val textColor = MaterialTheme.colors.onBackground
val tintColor = MaterialTheme.colors.secondaryVariant
val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp)
val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() }
val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() }
@@ -109,9 +110,7 @@ actual fun PlatformTextField(
editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType
editText.setTextColor(textColor.toArgb())
editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!!
DrawableCompat.setTint(drawable, tintColor.toArgb())
editText.background = drawable
editText.background = ColorDrawable(Color.Transparent.toArgb())
editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom)
editText.setText(cs.message)
if (Build.VERSION.SDK_INT >= 29) {
@@ -137,7 +136,6 @@ actual fun PlatformTextField(
}) {
it.setTextColor(textColor.toArgb())
it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get()
DrawableCompat.setTint(it.background, tintColor.toArgb())
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
it.isFocusableInTouchMode = it.isFocusable
if (cs.message != it.text.toString()) {
@@ -2,12 +2,15 @@ package chat.simplex.common.views.chat.item
import android.os.Build.VERSION.SDK_INT
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import chat.simplex.common.model.CIFile
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.helpers.ModalManager
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
@@ -21,6 +24,7 @@ actual fun SimpleAndAnimatedImageView(
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
smallView: Boolean,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
) {
val context = LocalContext.current
@@ -35,6 +39,14 @@ actual fun SimpleAndAnimatedImageView(
if (getLoadedFilePath(file) != null) {
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
ImageFullScreenView(imageProvider, close)
if (smallView) {
DisposableEffect(Unit) {
onDispose {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
}
}
}
@@ -30,6 +30,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
@@ -105,7 +106,10 @@ fun AppearanceScope.AppearanceLayout(
}
// }
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI)
SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
}
SectionDividerSpaced(maxTopPadding = true)
@@ -264,11 +264,25 @@ fun AndroidScreen(settingsState: SettingsViewState) {
snapshotFlow { chatModel.chatId.value }
.distinctUntilChanged()
.collect {
if (it == null) onComposed(null)
if (it == null) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
onComposed(null)
}
currentChatId.value = it
}
}
}
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.filter { chatModel.chatId.value == null }
.collect { modalBackground ->
if (modalBackground && !chatModel.newChatSheetVisible.value) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get())
}
}
}
Box(Modifier
.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }
.padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp)
@@ -83,6 +83,9 @@ object ChatModel {
// set when app is opened via contact or invitation URI (rhId, uri)
val appOpenUrl = mutableStateOf<Pair<Long?, URI>?>(null)
// Needed to check for bottom nav bar and to apply or not navigation bar color on Android
val newChatSheetVisible = mutableStateOf(false)
// preferences
val notificationPreviewMode by lazy {
mutableStateOf(
@@ -6,6 +6,7 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import chat.simplex.common.model.ChatId
import chat.simplex.common.model.NotificationsMode
import kotlinx.coroutines.Job
@@ -20,6 +21,7 @@ interface PlatformInterface {
fun androidChatInitializedAndStarted() {}
fun androidIsBackgroundCallAllowed(): Boolean = true
fun androidSetNightModeIfSupported() {}
fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {}
fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {}
fun androidPictureInPictureAllowed(): Boolean = true
fun androidCallEnded() {}
@@ -1,6 +1,7 @@
package chat.simplex.common.ui.theme
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.MutableState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@@ -103,6 +104,8 @@ object ThemeManager {
appPrefs.currentTheme.set(theme)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
platform.androidSetNightModeIfSupported()
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get())
}
fun changeDarkTheme(theme: String) {
@@ -120,6 +123,10 @@ object ThemeManager {
themeIds[nonSystemThemeName] = prevValue.themeId
appPrefs.currentThemeIds.set(themeIds)
CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get())
if (name == ThemeColor.BACKGROUND) {
val c = CurrentColors.value.colors
platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false)
}
}
fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState<ThemeModeOverride>) {
@@ -469,6 +469,20 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(),
showSearch = showSearch
)
if (appPlatform.isAndroid) {
val backgroundColor = MaterialTheme.colors.background
val backgroundColorState = rememberUpdatedState(backgroundColor)
LaunchedEffect(Unit) {
snapshotFlow { ModalManager.center.modalCount.value > 0 }
.collect { modalBackground ->
if (modalBackground) {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false)
} else {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, true)
}
}
}
}
}
}
is ChatInfo.ContactConnection -> {
@@ -872,6 +872,7 @@ fun ComposeView(
}
}
}
Divider()
Row(
modifier = Modifier.background(MaterialTheme.colors.background).padding(end = 8.dp),
verticalAlignment = Alignment.Bottom,
@@ -895,7 +896,7 @@ fun ComposeView(
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }),
Modifier.padding(bottom = if (appPlatform.isAndroid) 2.dp else with(LocalDensity.current) { 7.sp.toDp() }),
enabled = attachmentEnabled
) {
Icon(
@@ -209,7 +209,7 @@ fun CIImageView(
val loaded = res.value
if (loaded != null && file != null) {
val (imageBitmap, data, _) = loaded
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
} else {
imageView(base64ToBitmap(image), onClick = {
if (file != null) {
@@ -285,5 +285,6 @@ expect fun SimpleAndAnimatedImageView(
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
smallView: Boolean,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
)
@@ -12,8 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.pointer.*
import androidx.compose.ui.layout.onGloballyPositioned
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.chat.ProviderMedia
import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -55,6 +57,9 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
val scope = rememberCoroutineScope()
val playersToRelease = rememberSaveable { mutableSetOf<URI>() }
DisposableEffectOnGone(
always = {
platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false)
},
whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } }
)
@@ -46,12 +46,18 @@ import kotlin.time.Duration.Companion.seconds
private fun showNewChatSheet(oneHandUI: State<Boolean>, barTitle: String) {
ModalManager.start.closeModals()
ModalManager.end.closeModals()
chatModel.newChatSheetVisible.value = true
ModalManager.start.showModalCloseable(
closeOnTop = !oneHandUI.value,
closeBarTitle = if (oneHandUI.value) barTitle else null,
endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }
) { close ->
NewChatSheet(rh = chatModel.currentRemoteHost.value, close)
DisposableEffect(Unit) {
onDispose {
chatModel.newChatSheetVisible.value = false
}
}
}
}
@@ -64,7 +64,8 @@ class ModalData {
class ModalManager(private val placement: ModalPlacement? = null) {
private val modalViews = arrayListOf<Triple<Boolean, ModalData, (@Composable ModalData.(close: () -> Unit) -> Unit)>>()
private val modalCount = mutableStateOf(0)
private val _modalCount = mutableStateOf(0)
val modalCount: State<Int> = _modalCount
private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false)
// Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat():
@@ -97,7 +98,7 @@ class ModalManager(private val placement: ModalPlacement? = null) {
// to prevent unneeded animation on different situations
val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START)
modalViews.add(Triple(anim, data, modal))
modalCount.value = modalViews.size - toRemove.size
_modalCount.value = modalViews.size - toRemove.size
if (placement == ModalPlacement.CENTER) {
ChatModel.chatId.value = null
@@ -123,13 +124,13 @@ class ModalManager(private val placement: ModalPlacement? = null) {
if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex)
else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) }
}
modalCount.value = modalViews.size - toRemove.size
_modalCount.value = modalViews.size - toRemove.size
}
fun closeModals() {
modalViews.clear()
toRemove.clear()
modalCount.value = 0
_modalCount.value = 0
}
fun closeModalsExceptFirst() {
@@ -165,23 +165,14 @@ actual fun PlatformTextField(
},
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = { innerTextField ->
Surface(
shape = RoundedCornerShape(18.dp),
border = BorderStroke(1.dp, MaterialTheme.colors.secondary),
contentColor = LocalContentColor.current
) {
Row(
Modifier.background(MaterialTheme.colors.background),
verticalAlignment = Alignment.Bottom
Row(verticalAlignment = Alignment.Bottom) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
CompositionLocalProvider(
LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current
) {
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
Spacer(Modifier.height(8.dp))
innerTextField()
Spacer(Modifier.height(10.dp))
}
Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) {
Spacer(Modifier.height(8.dp))
innerTextField()
Spacer(Modifier.height(10.dp))
}
}
}
@@ -13,6 +13,7 @@ actual fun SimpleAndAnimatedImageView(
imageBitmap: ImageBitmap,
file: CIFile?,
imageProvider: () -> ImageGalleryProvider,
smallView: Boolean,
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
) {
// LALAL make it animated too