Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Reply] - UI Update when the Foldable Display type is FLAT #1343

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Reply/app/src/main/AndroidManifest.xml
Expand Up @@ -25,6 +25,7 @@
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Reply"
android:configChanges="orientation|screenLayout|screenSize|smallestScreenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
78 changes: 77 additions & 1 deletion Reply/app/src/main/java/com/example/reply/ui/MainActivity.kt
Expand Up @@ -16,23 +16,36 @@

package com.example.reply.ui

import android.app.UiModeManager
import android.content.Context
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.annotation.RequiresApi
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.window.layout.FoldingFeature
import com.example.reply.data.local.LocalEmailsDataProvider
import com.example.reply.ui.theme.ContrastAwareReplyTheme
import com.google.accompanist.adaptive.calculateDisplayFeatures
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged

class MainActivity : ComponentActivity() {

Expand All @@ -44,7 +57,16 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)

setContent {
ContrastAwareReplyTheme {
val contrastLevel =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
uiModeContrastLevel().collectAsStateWithLifecycle(initialValue = 0f).value
} else {
0f
}

ContrastAwareReplyTheme(
uiModeContrastLevel = contrastLevel
) {
val windowSize = calculateWindowSizeClass(this)
val displayFeatures = calculateDisplayFeatures(this)
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Expand All @@ -66,6 +88,22 @@ class MainActivity : ComponentActivity() {
}
}
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun Context.uiModeContrastLevel(): Flow<Float> = callbackFlow {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager

val listener = UiModeManager.ContrastChangeListener { contrast ->
trySend(contrast)
}

uiModeManager.addContrastChangeListener(Dispatchers.Default.asExecutor(), listener)

awaitClose {
uiModeManager.removeContrastChangeListener(listener)
}
}
.distinctUntilChanged()
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
Expand Down Expand Up @@ -132,3 +170,41 @@ fun ReplyAppPreviewDesktopPortrait() {
)
}
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Preview(showBackground = true, device = Devices.FOLDABLE)
@Composable
fun ReplayAppPreviewFoldablePortrait() {
ContrastAwareReplyTheme {
val fakeFoldingFeature = FakePreviewFoldingFeature(
orientation = FoldingFeature.Orientation.VERTICAL
)

ReplyApp(
replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
windowSize = WindowSizeClass.calculateFromSize(DpSize(673.dp, 841.dp)),
displayFeatures = listOf(fakeFoldingFeature),
)
}
}

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Preview(showBackground = true, widthDp = 841, heightDp = 673)
@Composable
fun ReplayAppPreviewFoldableLandscape() {
ContrastAwareReplyTheme {
ReplyApp(
replyHomeUIState = ReplyHomeUIState(emails = LocalEmailsDataProvider.allEmails),
windowSize = WindowSizeClass.calculateFromSize(DpSize(841.dp, 673.dp)),
displayFeatures = listOf(FakePreviewFoldingFeature()),
)
}
}

class FakePreviewFoldingFeature(
override val state: FoldingFeature.State = FoldingFeature.State.FLAT,
override val bounds: Rect = Rect(),
override val isSeparating: Boolean = true,
override val occlusionType: FoldingFeature.OcclusionType = FoldingFeature.OcclusionType.NONE,
override val orientation: FoldingFeature.Orientation = FoldingFeature.Orientation.HORIZONTAL
) : FoldingFeature
27 changes: 12 additions & 15 deletions Reply/app/src/main/java/com/example/reply/ui/ReplyApp.kt
Expand Up @@ -80,13 +80,9 @@ fun ReplyApp(
val foldingFeature = displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()

val foldingDevicePosture = when {
isBookPosture(foldingFeature) ->
DevicePosture.BookPosture(foldingFeature.bounds)

isSeparating(foldingFeature) ->
DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)

else -> DevicePosture.NormalPosture
isBookPosture(foldingFeature) -> DevicePosture.BOOK_POSTURE
isSeparating(foldingFeature) -> DevicePosture.SEPARATING
else -> DevicePosture.NORMAL_POSTURE
}

when (windowSize.widthSizeClass) {
Expand All @@ -96,17 +92,18 @@ fun ReplyApp(
}
WindowWidthSizeClass.Medium -> {
navigationType = ReplyNavigationType.NAVIGATION_RAIL
contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) {
ReplyContentType.DUAL_PANE
} else {
ReplyContentType.SINGLE_PANE
contentType = when (foldingDevicePosture) {
DevicePosture.BOOK_POSTURE -> ReplyContentType.DUAL_PANE
else -> ReplyContentType.SINGLE_PANE
}
}
WindowWidthSizeClass.Expanded -> {
navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
ReplyNavigationType.NAVIGATION_RAIL
} else {
ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
navigationType = when (foldingDevicePosture) {
DevicePosture.BOOK_POSTURE,
DevicePosture.SEPARATING
-> ReplyNavigationType.NAVIGATION_RAIL

else -> ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
}
contentType = ReplyContentType.DUAL_PANE
}
Expand Down
44 changes: 23 additions & 21 deletions Reply/app/src/main/java/com/example/reply/ui/theme/Theme.kt
Expand Up @@ -15,9 +15,8 @@
*/

package com.example.reply.ui.theme

import android.app.Activity
import android.app.UiModeManager
import android.content.Context
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
Expand Down Expand Up @@ -266,42 +265,45 @@ fun isContrastAvailable(): Boolean {
}

@Composable
fun selectSchemeForContrast(isDark: Boolean,): ColorScheme {
val context = LocalContext.current
var colorScheme = if (isDark) darkScheme else lightScheme
if (isContrastAvailable()) {
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
val contrastLevel = uiModeManager.contrast
fun selectSchemeForContrast(
isDark: Boolean,
contrastLevel: Float,
): ColorScheme = when {
!isContrastAvailable() -> {
if (isDark) darkScheme else lightScheme
}

colorScheme = when (contrastLevel) {
in 0.0f..0.33f -> if (isDark)
darkScheme else lightScheme
contrastLevel <= 0.33f -> {
if (isDark) darkScheme else lightScheme
}

in 0.34f..0.66f -> if (isDark)
mediumContrastDarkColorScheme else mediumContrastLightColorScheme
contrastLevel <= 0.66f -> {
if (isDark) mediumContrastDarkColorScheme else mediumContrastLightColorScheme
}

in 0.67f..1.0f -> if (isDark)
highContrastDarkColorScheme else highContrastLightColorScheme
contrastLevel <= 1.0f -> {
if (isDark) highContrastDarkColorScheme else highContrastLightColorScheme
}

else -> if (isDark) darkScheme else lightScheme
}
return colorScheme
} else return colorScheme
else -> if (isDark) darkScheme else lightScheme
}

@Composable
fun ContrastAwareReplyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable() () -> Unit
// Contrast color is available on Android 14+
uiModeContrastLevel: Float = 0f,
content: @Composable () -> Unit
) {
val replyColorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

else -> selectSchemeForContrast(darkTheme)
else -> selectSchemeForContrast(darkTheme, uiModeContrastLevel)
}
val view = LocalView.current
if (!view.isInEditMode) {
Expand Down
Expand Up @@ -16,27 +16,10 @@

package com.example.reply.ui.utils

import android.graphics.Rect
import androidx.window.layout.FoldingFeature
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
object NormalPosture : DevicePosture

data class BookPosture(
val hingePosition: Rect
) : DevicePosture

data class Separating(
val hingePosition: Rect,
var orientation: FoldingFeature.Orientation
) : DevicePosture
}

@OptIn(ExperimentalContracts::class)
fun isBookPosture(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
Expand Down Expand Up @@ -70,3 +53,10 @@ enum class ReplyNavigationContentPosition {
enum class ReplyContentType {
SINGLE_PANE, DUAL_PANE
}

/**
* Information about the posture of the device
*/
enum class DevicePosture {
NORMAL_POSTURE, BOOK_POSTURE, SEPARATING
}