diff --git a/FluentUI.Demo/build.gradle b/FluentUI.Demo/build.gradle
index 694d47eb7..83b2efb15 100644
--- a/FluentUI.Demo/build.gradle
+++ b/FluentUI.Demo/build.gradle
@@ -103,6 +103,8 @@ dependencies {
debugImplementation("androidx.compose.ui:ui-test-manifest")
debugImplementation "androidx.test:monitor:$androidTestMonitor"
implementation "androidx.compose.runtime:runtime-livedata"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleComposeVersion"
+ implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleComposeVersion"
// App Center
dogfoodImplementation "com.microsoft.appcenter:appcenter-analytics:$appCenterSdkVersion"
diff --git a/FluentUI.Demo/src/main/AndroidManifest.xml b/FluentUI.Demo/src/main/AndroidManifest.xml
index fe4af3bed..d937e187a 100644
--- a/FluentUI.Demo/src/main/AndroidManifest.xml
+++ b/FluentUI.Demo/src/main/AndroidManifest.xml
@@ -34,6 +34,8 @@
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AcrylicActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AcrylicActivity.kt
index eb134fbcc..a3a23b5c0 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AcrylicActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AcrylicActivity.kt
@@ -42,6 +42,9 @@ import com.microsoft.fluentui.theme.token.FluentAliasTokens
import com.microsoft.fluentui.theme.token.FluentAliasTokens.NeutralForegroundColorTokens.Foreground2
import com.microsoft.fluentui.theme.token.FluentIcon
import com.microsoft.fluentui.theme.token.FluentStyle
+import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneInfo
+import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneOrientation
+import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneTokens
import com.microsoft.fluentui.tokenized.SearchBar
import com.microsoft.fluentui.tokenized.controls.RadioButton
import com.microsoft.fluentui.tokenized.drawer.DrawerValue
@@ -60,8 +63,10 @@ class V2AcrylicPaneActivity : V2DemoActivity() {
setupActivity(this)
}
- override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-18" //TODO: Update this URL
- override val controlTokensUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-18"
+ override val paramsUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#params-18" //TODO: Update this URL
+ override val controlTokensUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-18"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -74,32 +79,81 @@ class V2AcrylicPaneActivity : V2DemoActivity() {
@Composable
fun CreateAcrylicPaneActivityUI(
context: Context
-){
- var acrylicPaneSizeFraction by rememberSaveable { mutableFloatStateOf(0.5F) }
- var acrylicPaneStyle by rememberSaveable { mutableStateOf(FluentStyle.Neutral) }
+) {
+ var acrylicPaneSize by rememberSaveable { mutableFloatStateOf(250.0f) }
+ var acrylicPaneOrientation by rememberSaveable { mutableStateOf(AcrylicPaneOrientation.BOTTOM) }
+ var acrylicPaneBlurRadius by rememberSaveable { mutableStateOf(0.0f) }
+ val acrylicPaneTokens: AcrylicPaneTokens = object : AcrylicPaneTokens() {
+ @Composable
+ override fun acrylicPaneBlurRadius(acrylicPaneInfo: AcrylicPaneInfo): Int {
+ return acrylicPaneBlurRadius.toInt()
+ }
+ }
AcrylicPane(
- paneHeight = (acrylicPaneSizeFraction * 500).toInt().dp,
- acrylicPaneStyle = acrylicPaneStyle,
- component = { acrylicPaneContent(context = context) },
+ paneHeight = acrylicPaneSize.toInt().dp,
+ orientation = acrylicPaneOrientation,
+ component = { AcrylicPaneContent(context = context) },
backgroundContent = {
Column(
- modifier = Modifier.verticalScroll(rememberScrollState()).fillMaxWidth().padding(10.dp),
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .fillMaxWidth()
+ .padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(Modifier.height(300.dp))
ListItem.Header(
- title = "Acrylic Pane Size",
+ title = "Acrylic Pane Orientation",
titleMaxLines = 2,
modifier = Modifier
.clearAndSetSemantics {
- this.contentDescription = "Acrylic Pane Size"
+ this.contentDescription = "Acrylic Pane Orientation"
+ },
+ )
+ val checkBoxSelectedValues = List(3) { rememberSaveable { mutableStateOf(false) } }
+ when (acrylicPaneOrientation) {
+ AcrylicPaneOrientation.TOP -> checkBoxSelectedValues[0].value = true
+ AcrylicPaneOrientation.CENTER -> checkBoxSelectedValues[1].value = true
+ AcrylicPaneOrientation.BOTTOM -> checkBoxSelectedValues[2].value = true
+ }
+ val acrylicPaneOrientations = listOf(
+ AcrylicPaneOrientation.TOP,
+ AcrylicPaneOrientation.CENTER,
+ AcrylicPaneOrientation.BOTTOM,
+ )
+ val orientations = listOf("Top", "Center", "Bottom")
+ for (i in 0..2) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 10.dp, vertical = 3.dp)
+ ) {
+ Text(text = "Orientation ${orientations[i]}")
+ Spacer(modifier = Modifier.width(320.dp))
+ RadioButton(
+ onClick = {
+ selectRadioGroupButton(i, checkBoxSelectedValues)
+ acrylicPaneOrientation = acrylicPaneOrientations[i]
+ },
+ selected = checkBoxSelectedValues[i].value
+ )
+ }
+ }
+ ListItem.Header(
+ title = "Blur Radius",
+ titleMaxLines = 2,
+ modifier = Modifier
+ .clearAndSetSemantics {
+ this.contentDescription = "Acrylic Pane Blur Radius"
},
)
Slider(
- value = acrylicPaneSizeFraction,
- onValueChange = { acrylicPaneSizeFraction = it },
- valueRange = 0F..1F,
+ value = acrylicPaneBlurRadius,
+ onValueChange = { acrylicPaneBlurRadius = it },
+ valueRange = 0F..200F,
colors = SliderDefaults.colors(
thumbColor = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
FluentTheme.themeMode
@@ -121,36 +175,37 @@ fun CreateAcrylicPaneActivityUI(
steps = 9
)
ListItem.Header(
- title = "Acrylic Pane Theme",
+ title = "Acrylic Pane Size",
titleMaxLines = 2,
modifier = Modifier
.clearAndSetSemantics {
- this.contentDescription = "Acrylic Pane Theme"
+ this.contentDescription = "Acrylic Pane Size"
},
)
- var checkBoxSelectedValues = List(2) { rememberSaveable { mutableStateOf(false) } }
- var acrylicPaneStyles = listOf(
- FluentStyle.Neutral,
- FluentStyle.Brand
- )
- for (i in 0..1) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start,
- modifier = Modifier.fillMaxWidth()
- .padding(horizontal = 10.dp, vertical = 3.dp)
- ) {
- Text(text = "Theme $i")
- Spacer(modifier = Modifier.width(320.dp))
- RadioButton(
- onClick = {
- selectRadioGroupButton(i, checkBoxSelectedValues)
- acrylicPaneStyle = acrylicPaneStyles[i]
- },
- selected = checkBoxSelectedValues[i].value
+ Slider(
+ value = acrylicPaneSize,
+ onValueChange = { acrylicPaneSize = it },
+ valueRange = 0F..500F,
+ colors = SliderDefaults.colors(
+ thumbColor = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ FluentTheme.themeMode
+ ),
+ activeTrackColor = FluentTheme.aliasTokens.brandColor[FluentAliasTokens.BrandColorTokens.Color80],
+ inactiveTrackColor = FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background3].value(
+ FluentTheme.themeMode
+ ),
+ disabledThumbColor = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ FluentTheme.themeMode
+ ),
+ disabledActiveTrackColor = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ FluentTheme.themeMode
+ ),
+ disabledInactiveTrackColor = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ FluentTheme.themeMode
)
- }
- }
+ ),
+ steps = 9
+ )
ListItem.Header(
title = "Test Bottom Drawer",
titleMaxLines = 2,
@@ -159,7 +214,7 @@ fun CreateAcrylicPaneActivityUI(
this.contentDescription = "Test Bottom Drawer"
},
)
- showBottomDrawer()
+ ShowBottomDrawer()
ListItem.Header(
title = "Scroll Test",
titleMaxLines = 2,
@@ -172,24 +227,38 @@ fun CreateAcrylicPaneActivityUI(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
- modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 5.dp)
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 12.dp, vertical = 5.dp)
) {
- Text(text = "Text $it", fontSize = 14.sp,
+ Text(
+ text = "Text $it", fontSize = 14.sp,
style = FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1]
- .merge(TextStyle(color = FluentTheme.aliasTokens.neutralForegroundColor[Foreground2].value(themeMode = FluentTheme.themeMode)))
+ .merge(
+ TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[Foreground2].value(
+ themeMode = FluentTheme.themeMode
+ )
+ )
+ )
)
}
}
}
- }
+ },
+ acrylicPaneTokens = acrylicPaneTokens
)
}
@Composable
-fun showBottomDrawer(){
+fun ShowBottomDrawer() {
val scope = rememberCoroutineScope()
- val drawerState = rememberBottomDrawerState(initialValue = DrawerValue.Closed, expandable = true, skipOpenState = false)
+ val drawerState = rememberBottomDrawerState(
+ initialValue = DrawerValue.Closed,
+ expandable = true,
+ skipOpenState = false
+ )
val open: () -> Unit = {
scope.launch { drawerState.open() }
@@ -228,7 +297,7 @@ fun showBottomDrawer(){
}
@Composable
-fun acrylicPaneContent(context: Context){
+fun AcrylicPaneContent(context: Context) {
val scope = rememberCoroutineScope()
val microphonePressedString = getDemoAppString(DemoAppStrings.MicrophonePressed)
@@ -244,7 +313,11 @@ fun acrylicPaneContent(context: Context){
val showCustomizedAppBar = false
Column {
Spacer(modifier = Modifier.height(80.dp))
- Row(Modifier.height(5.dp).padding(20.dp)) {
+ Row(
+ Modifier
+ .height(5.dp)
+ .padding(20.dp)
+ ) {
SearchBar(
onValueChange = { query, selectedPerson ->
scope.launch {
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomDrawerActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomDrawerActivity.kt
index b69817619..c4b45f361 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomDrawerActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomDrawerActivity.kt
@@ -1,46 +1,87 @@
package com.microsoft.fluentuidemo.demos
+import SearchViewModel
+import SearchViewModelFactory
+import Searchable
import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
+import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Slider
import androidx.compose.material.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.theme.token.FluentAliasTokens.NeutralBackgroundColorTokens.Background1
+import com.microsoft.fluentui.theme.token.FluentAliasTokens.NeutralBackgroundColorTokens.Background1Pressed
+import com.microsoft.fluentui.theme.token.FluentAliasTokens.NeutralBackgroundColorTokens.Background1Selected
+import com.microsoft.fluentui.theme.token.StateBrush
+import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
+import com.microsoft.fluentui.theme.token.controlTokens.BorderType
+import com.microsoft.fluentui.theme.token.controlTokens.ListItemInfo
+import com.microsoft.fluentui.theme.token.controlTokens.ListItemTokens
+import com.microsoft.fluentui.tokenized.SearchBar
import com.microsoft.fluentui.tokenized.controls.RadioButton
import com.microsoft.fluentui.tokenized.controls.ToggleSwitch
import com.microsoft.fluentui.tokenized.drawer.BottomDrawer
import com.microsoft.fluentui.tokenized.drawer.DrawerValue
import com.microsoft.fluentui.tokenized.drawer.rememberBottomDrawerState
import com.microsoft.fluentui.tokenized.listitem.ListItem
+import com.microsoft.fluentui.util.KeyboardVisibilityObserver
+import com.microsoft.fluentui.util.getStringResource
import com.microsoft.fluentuidemo.R
import com.microsoft.fluentuidemo.V2DemoActivity
import com.microsoft.fluentuidemo.util.PrimarySurfaceContent
import com.microsoft.fluentuidemo.util.getAndroidViewAsContent
import com.microsoft.fluentuidemo.util.getDrawerAsContent
import com.microsoft.fluentuidemo.util.getDynamicListGeneratorAsContent
+import generateUniqueId
import kotlinx.coroutines.launch
class V2BottomDrawerActivity : V2DemoActivity() {
@@ -49,18 +90,24 @@ class V2BottomDrawerActivity : V2DemoActivity() {
}
override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-9"
- override val controlTokensUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-9"
- private val onBackCallback = object: OnBackPressedCallback(true) { //callback to end the activity
- override fun handleOnBackPressed() {
- finish()
+ override val controlTokensUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-9"
+ private val onBackCallback =
+ object : OnBackPressedCallback(true) { //callback to end the activity
+ override fun handleOnBackPressed() {
+ finish()
+ }
+
}
- }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setActivityContent {
CreateActivityUI()
- LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher?.addCallback(this, onBackCallback) //registering the callback to end the activity when back button is pressed
+ LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher?.addCallback(
+ this,
+ onBackCallback
+ ) //registering the callback to end the activity when back button is pressed
}
}
}
@@ -71,6 +118,7 @@ private fun CreateActivityUI() {
var dynamicSizeContent by rememberSaveable { mutableStateOf(false) }
var nestedDrawerContent by rememberSaveable { mutableStateOf(false) }
var listContent by rememberSaveable { mutableStateOf(true) }
+ var searchableDrawerContent by rememberSaveable { mutableStateOf(false) }
var expandable by rememberSaveable { mutableStateOf(true) }
var skipOpenState by rememberSaveable { mutableStateOf(false) }
var selectedContent by rememberSaveable { mutableStateOf(ContentType.FULL_SCREEN_SCROLLABLE_CONTENT) }
@@ -79,31 +127,46 @@ private fun CreateActivityUI() {
var enableSwipeDismiss by rememberSaveable { mutableStateOf(true) }
var maxLandscapeWidthFraction by rememberSaveable { mutableFloatStateOf(1F) }
var preventDismissalOnScrimClick by rememberSaveable { mutableStateOf(false) }
- var isLandscapeOrientation: Boolean = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
+ var isLandscapeOrientation: Boolean =
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
- slideOver = slideOver,
- scrimVisible = scrimVisible,
- skipOpenState = skipOpenState,
- expandable = expandable,
- showHandle = showHandle,
- preventDismissalOnScrimClick = preventDismissalOnScrimClick,
- enableSwipeDismiss = enableSwipeDismiss,
- maxLandscapeWidthFraction = maxLandscapeWidthFraction,
- drawerContent =
- if (listContent)
- getAndroidViewAsContent(selectedContent)
- else if (nestedDrawerContent) {
- getDrawerAsContent()
- } else {
- getDynamicListGeneratorAsContent()
- }
- )
+ if (searchableDrawerContent) {
+ CreateSearchableDrawerWithButtonOnPrimarySurfaceToInvokeIt(
+ slideOver = slideOver,
+ expandable = expandable,
+ skipOpenState = skipOpenState,
+ scrimVisible = scrimVisible,
+ showHandle = showHandle,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ enableSwipeDismiss = enableSwipeDismiss,
+ maxLandscapeWidthFraction = maxLandscapeWidthFraction
+ )
+ } else {
+ CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
+ slideOver = slideOver,
+ scrimVisible = scrimVisible,
+ skipOpenState = skipOpenState,
+ expandable = expandable,
+ showHandle = showHandle,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ enableSwipeDismiss = enableSwipeDismiss,
+ maxLandscapeWidthFraction = maxLandscapeWidthFraction,
+ drawerContent =
+ if (listContent)
+ getAndroidViewAsContent(selectedContent)
+ else if (nestedDrawerContent) {
+ getDrawerAsContent()
+ } else {
+ getDynamicListGeneratorAsContent()
+ }
+ )
+ }
//Other content on Primary surface
LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) {
item {
ListItem.Header(title = stringResource(id = R.string.drawer_select_drawer_type))
- ListItem.Item(text = stringResource(id = R.string.drawer_bottom),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_bottom),
subText = stringResource(id = R.string.drawer_bottom_description),
subTextMaxLines = Int.MAX_VALUE,
onClick = { slideOver = false },
@@ -116,7 +179,8 @@ private fun CreateActivityUI() {
)
}
)
- ListItem.Item(text = stringResource(id = R.string.drawer_bottom_slide_over),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_bottom_slide_over),
subText = stringResource(id = R.string.drawer_bottom_slide_over_description),
subTextMaxLines = Int.MAX_VALUE,
onClick = { slideOver = true },
@@ -132,25 +196,27 @@ private fun CreateActivityUI() {
}
item {
val scrimVisibleText = stringResource(id = R.string.drawer_scrim_visible)
- ListItem.Header(title = scrimVisibleText, modifier = Modifier
- .toggleable(
- value = scrimVisible,
- role = Role.Switch,
- onValueChange = { scrimVisible = !scrimVisible }
- )
- .clearAndSetSemantics {
- this.contentDescription = scrimVisibleText
- }, trailingAccessoryContent = {
- ToggleSwitch(
- onValueChange = { scrimVisible = !scrimVisible },
- checkedState = scrimVisible
- )
- }
+ ListItem.Header(
+ title = scrimVisibleText, modifier = Modifier
+ .toggleable(
+ value = scrimVisible,
+ role = Role.Switch,
+ onValueChange = { scrimVisible = !scrimVisible }
+ )
+ .clearAndSetSemantics {
+ this.contentDescription = scrimVisibleText
+ }, trailingAccessoryContent = {
+ ToggleSwitch(
+ onValueChange = { scrimVisible = !scrimVisible },
+ checkedState = scrimVisible
+ )
+ }
)
}
item {
val expandableText = stringResource(id = R.string.drawer_expandable)
- ListItem.Header(title = expandableText,
+ ListItem.Header(
+ title = expandableText,
modifier = Modifier
.toggleable(
value = expandable,
@@ -170,10 +236,10 @@ private fun CreateActivityUI() {
ToggleSwitch(
onValueChange = {
expandable = it
- if(!it) {
+ if (!it) {
skipOpenState = false
}
- },
+ },
checkedState = expandable,
enabledSwitch = !skipOpenState
)
@@ -182,7 +248,8 @@ private fun CreateActivityUI() {
}
item {
val skipOpenStateText = stringResource(id = R.string.skip_open_state)
- ListItem.Header(title = skipOpenStateText,
+ ListItem.Header(
+ title = skipOpenStateText,
modifier = Modifier
.toggleable(
value = skipOpenState,
@@ -213,20 +280,26 @@ private fun CreateActivityUI() {
)
}
item {
- val preventDismissalOnScrimClickText = stringResource(id = R.string.prevent_scrim_click_dismissal)
- ListItem.Header(title = preventDismissalOnScrimClickText,
+ val preventDismissalOnScrimClickText =
+ stringResource(id = R.string.prevent_scrim_click_dismissal)
+ ListItem.Header(
+ title = preventDismissalOnScrimClickText,
modifier = Modifier
.toggleable(
value = preventDismissalOnScrimClick,
role = Role.Switch,
- onValueChange = { preventDismissalOnScrimClick = !preventDismissalOnScrimClick }
+ onValueChange = {
+ preventDismissalOnScrimClick = !preventDismissalOnScrimClick
+ }
)
.clearAndSetSemantics {
this.contentDescription = preventDismissalOnScrimClickText
},
trailingAccessoryContent = {
ToggleSwitch(
- onValueChange = { preventDismissalOnScrimClick = !preventDismissalOnScrimClick },
+ onValueChange = {
+ preventDismissalOnScrimClick = !preventDismissalOnScrimClick
+ },
checkedState = preventDismissalOnScrimClick
)
}
@@ -234,7 +307,8 @@ private fun CreateActivityUI() {
}
item {
val showHandleText = stringResource(id = R.string.drawer_show_handle)
- ListItem.Header(title = showHandleText,
+ ListItem.Header(
+ title = showHandleText,
modifier = Modifier
.toggleable(
value = showHandle,
@@ -254,7 +328,8 @@ private fun CreateActivityUI() {
}
item {
val showDismissText = stringResource(id = R.string.drawer_enable_swipe_dismiss)
- ListItem.Header(title = showDismissText,
+ ListItem.Header(
+ title = showDismissText,
modifier = Modifier
.toggleable(
value = enableSwipeDismiss,
@@ -274,8 +349,10 @@ private fun CreateActivityUI() {
}
item {
- val maxLandscapeWidthFractionText = stringResource(id = R.string.bottom_drawer_max_width_landscape)
- ListItem.Header(title = maxLandscapeWidthFractionText + if(!isLandscapeOrientation) " (Rotate to landscape Mode to use this)" else "",
+ val maxLandscapeWidthFractionText =
+ stringResource(id = R.string.bottom_drawer_max_width_landscape)
+ ListItem.Header(
+ title = maxLandscapeWidthFractionText + if (!isLandscapeOrientation) " (Rotate to landscape Mode to use this)" else "",
titleMaxLines = 2,
enabled = isLandscapeOrientation,
modifier = Modifier
@@ -311,12 +388,14 @@ private fun CreateActivityUI() {
}
item {
ListItem.Header(title = stringResource(id = R.string.drawer_select_drawer_content))
- ListItem.Item(text = stringResource(id = R.string.drawer_full_screen_size_scrollable_content),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_full_screen_size_scrollable_content),
onClick = {
selectedContent = ContentType.FULL_SCREEN_SCROLLABLE_CONTENT
listContent = true
nestedDrawerContent = false
dynamicSizeContent = false
+ searchableDrawerContent = false
},
trailingAccessoryContent = {
RadioButton(
@@ -325,17 +404,20 @@ private fun CreateActivityUI() {
listContent = true
nestedDrawerContent = false
dynamicSizeContent = false
+ searchableDrawerContent = false
},
selected = selectedContent == ContentType.FULL_SCREEN_SCROLLABLE_CONTENT && listContent
)
}
)
- ListItem.Item(text = stringResource(id = R.string.drawer_more_than_half_screen_content),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_more_than_half_screen_content),
onClick = {
selectedContent = ContentType.EXPANDABLE_SIZE_CONTENT
listContent = true
nestedDrawerContent = false
dynamicSizeContent = false
+ searchableDrawerContent = false
},
trailingAccessoryContent = {
RadioButton(
@@ -344,17 +426,20 @@ private fun CreateActivityUI() {
listContent = true
nestedDrawerContent = false
dynamicSizeContent = false
+ searchableDrawerContent = false
},
selected = selectedContent == ContentType.EXPANDABLE_SIZE_CONTENT && listContent
)
}
)
- ListItem.Item(text = stringResource(id = R.string.drawer_less_than_half_screen_content),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_less_than_half_screen_content),
onClick = {
selectedContent = ContentType.WRAPPED_SIZE_CONTENT
listContent = true
dynamicSizeContent = false
nestedDrawerContent = false
+ searchableDrawerContent = false
},
trailingAccessoryContent = {
RadioButton(
@@ -363,16 +448,19 @@ private fun CreateActivityUI() {
listContent = true
dynamicSizeContent = false
nestedDrawerContent = false
+ searchableDrawerContent = false
},
selected = selectedContent == ContentType.WRAPPED_SIZE_CONTENT && listContent
)
}
)
- ListItem.Item(text = stringResource(id = R.string.drawer_dynamic_size_content),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_dynamic_size_content),
onClick = {
dynamicSizeContent = true
nestedDrawerContent = false
listContent = false
+ searchableDrawerContent = false
},
trailingAccessoryContent = {
RadioButton(
@@ -380,16 +468,19 @@ private fun CreateActivityUI() {
dynamicSizeContent = true
nestedDrawerContent = false
listContent = false
+ searchableDrawerContent = false
},
selected = dynamicSizeContent
)
}
)
- ListItem.Item(text = stringResource(id = R.string.drawer_nested_drawer_content),
+ ListItem.Item(
+ text = stringResource(id = R.string.drawer_nested_drawer_content),
onClick = {
nestedDrawerContent = true
dynamicSizeContent = false
listContent = false
+ searchableDrawerContent = false
},
trailingAccessoryContent = {
RadioButton(
@@ -397,16 +488,363 @@ private fun CreateActivityUI() {
nestedDrawerContent = true
dynamicSizeContent = false
listContent = false
+ searchableDrawerContent = false
},
selected = nestedDrawerContent
)
}
)
+ ListItem.Item(
+ text = stringResource(id = R.string.searchable_drawer_content),
+ onClick = {
+ dynamicSizeContent = false
+ nestedDrawerContent = false
+ listContent = false
+ searchableDrawerContent = true
+ },
+ trailingAccessoryContent = {
+ RadioButton(
+ onClick = {
+ dynamicSizeContent = false
+ nestedDrawerContent = false
+ listContent = false
+ searchableDrawerContent = true
+ },
+ selected = searchableDrawerContent
+ )
+ }
+ )
}
}
}
}
+data class SearchableItem(
+ val title: String,
+ val subTitle: String? = null,
+ val description: String? = null,
+ val footer: String? = null,
+ val leftAccessory: @Composable (() -> Unit)? = null,
+ val rightAccessory: @Composable (() -> Unit)? = null,
+ val status: AvatarStatus? = null,
+ val onClick: () -> Unit = {},
+ val onLongClick: () -> Unit = {},
+ val enabled: Boolean = true,
+ val id: Any = generateUniqueId()
+) : Searchable {
+ override fun getSearchKey(): String = title
+
+ override fun getUniqueId(): Any = id
+}
+
+@Composable
+private fun CreateSearchableDrawerWithButtonOnPrimarySurfaceToInvokeIt(
+ slideOver: Boolean,
+ expandable: Boolean,
+ skipOpenState: Boolean,
+ scrimVisible: Boolean,
+ showHandle: Boolean,
+ preventDismissalOnScrimClick: Boolean,
+ enableSwipeDismiss: Boolean,
+ maxLandscapeWidthFraction: Float
+) {
+ val scope = rememberCoroutineScope()
+ val viewModel: SearchViewModel = viewModel(
+ factory = SearchViewModelFactory(initialItems = List(100) { index ->
+ SearchableItem(
+ title = "Item $index",
+ subTitle = "Subtitle for item $index",
+ description = "Description for item $index",
+ id = index
+ )
+ })
+ )
+ val drawerState = rememberBottomDrawerState(
+ initialValue = DrawerValue.Closed,
+ expandable = expandable,
+ skipOpenState = skipOpenState
+ )
+ val open: () -> Unit = {
+ scope.launch {
+ if(drawerState.currentValue == DrawerValue.Closed) {
+ viewModel.clearSelection()
+ }
+ drawerState.open()
+ }
+ }
+ val expand: () -> Unit = {
+ scope.launch {
+ drawerState.expand()
+ }
+ }
+ val close: () -> Unit = {
+ scope.launch {
+ drawerState.close()
+ viewModel.clearSelection()
+ }
+ }
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+ val toggleItemSelection = { item: Searchable ->
+ viewModel.toggleSelection(item as SearchableItem)
+ }
+ Row {
+ PrimarySurfaceContent(
+ open,
+ text = stringResource(id = R.string.drawer_open)
+ )
+ Spacer(modifier = Modifier.width(10.dp))
+ PrimarySurfaceContent(
+ expand,
+ text = stringResource(id = R.string.drawer_expand)
+ )
+ }
+ BottomDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ KeyboardVisibilityObserver(
+ onKeyboardVisible = {
+ if (drawerState.currentValue == DrawerValue.Open) {
+ expand()
+ }
+ },
+ onKeyboardHidden = {
+ if (drawerState.currentValue == DrawerValue.Expanded) {
+ open()
+ }
+ }
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ SearchableDrawerHeader(
+ onLeftTextClick = {
+ scope.launch {
+ viewModel.clearSelection()
+ }
+ },
+ onRightTextClick = close,
+ onCenterTextClick = {
+ if (drawerState.currentValue == DrawerValue.Open) {
+ expand()
+ } else {
+ open()
+ }
+ }
+ )
+
+ if (uiState.selectionSize <= 0) {
+ SearchBar(
+ onValueChange = { query, selectedPerson ->
+ scope.launch {
+ viewModel.onQueryChanged(query)
+ }
+ }
+ )
+ } else {
+ MultiSelectScreen(uiState.selectionSize)
+ }
+
+ LazyItemsList(
+ filteredSearchItems = uiState.filteredItems,
+ selectedSearchItems = uiState.selectedItems,
+ inSelectionMode = uiState.selectionSize > 0,
+ toggleItemSelection = toggleItemSelection,
+ border = BorderType.NoBorder,
+ modifier = Modifier,
+ )
+ }
+ }
+ },
+ scrimVisible = scrimVisible,
+ slideOver = slideOver,
+ showHandle = showHandle,
+ enableSwipeDismiss = enableSwipeDismiss,
+ maxLandscapeWidthFraction = maxLandscapeWidthFraction,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick
+ )
+}
+
+@Composable
+private fun ClickableTextHeader(
+ text: String,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit = {},
+ textStyle: TextStyle = TextStyle.Default
+) {
+ val interactionSource = remember { MutableInteractionSource() }
+ val animatedFontSizeStart by animateDpAsState(
+ targetValue = if (interactionSource.collectIsPressedAsState().value) 18.5.dp else 17.dp,
+ label = "FontSizeAnimation"
+ )
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .then(modifier)
+ ) {
+ BasicText(
+ text = text,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable(
+ enabled = true,
+ indication = null,
+ interactionSource = interactionSource
+ ) {
+ onClick()
+ },
+ style = textStyle.merge(fontSize = animatedFontSizeStart.value.sp)
+ )
+ }
+}
+
+@Composable
+fun SearchableDrawerHeader(
+ onLeftTextClick: () -> Unit = {},
+ onCenterTextClick: () -> Unit = {},
+ onRightTextClick: () -> Unit = {},
+) {
+ val textColours = listOf(Color(0xFF616161), Color(0xFF242424), Color(0xFF464FEB))
+ val textHeaders = listOf(
+ getStringResource(id = R.string.fluentui_back),
+ getStringResource(id = R.string.fluentui_title),
+ getStringResource(id = R.string.popup_menu_item_share)
+ )
+ val textOnClicks = listOf(onLeftTextClick, onCenterTextClick, onRightTextClick)
+ val textFontStyles = listOf(
+ FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1],
+ FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Title2],
+ FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1]
+ )
+ val textAlignments = listOf(TextAlign.Start, TextAlign.Center, TextAlign.End)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp, vertical = 10.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ for (i in 0..2) {
+ ClickableTextHeader(
+ text = textHeaders[i],
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onClick = textOnClicks.get(i),
+ textStyle = textFontStyles.get(i).copy(
+ color = textColours.get(i),
+ textAlign = textAlignments.get(i)
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun MultiSelectScreen(numSelected: Int) {
+ BasicText(
+ "Selected Items: ${numSelected}",
+ modifier = Modifier.padding(horizontal = 10.dp, vertical = 20.dp),
+ style = TextStyle(
+ color = Color(0xFF242424),
+ fontSize = 17.sp,
+ lineHeight = 22.sp,
+ letterSpacing = -0.43.sp,
+ textAlign = TextAlign.Start,
+ fontWeight = FontWeight(400)
+ )
+ )
+}
+
+@Composable
+fun LazyItemsList(
+ filteredSearchItems: List,
+ selectedSearchItems: Set = setOf(),
+ inSelectionMode: Boolean = false,
+ toggleItemSelection: (SearchableItem) -> Unit = {},
+ border: BorderType = BorderType.NoBorder,
+ modifier: Modifier = Modifier,
+) {
+ val scope = rememberCoroutineScope()
+ var enableStatus by rememberSaveable { mutableStateOf(false) }
+ val lazyListState = rememberLazyListState()
+ val positionString: String =
+ getStringResource(com.microsoft.fluentui.topappbars.R.string.position_string)
+ val statusString: String =
+ getStringResource(com.microsoft.fluentui.topappbars.R.string.status_string)
+ val listItemTokens: ListItemTokens = object : ListItemTokens() {
+ @Composable
+ override fun backgroundBrush(listItemInfo: ListItemInfo): StateBrush {
+ return StateBrush(
+ rest = SolidColor(
+ FluentTheme.aliasTokens.neutralBackgroundColor[Background1].value(
+ themeMode = FluentTheme.themeMode
+ )
+ ),
+ pressed = SolidColor(
+ FluentTheme.aliasTokens.neutralBackgroundColor[Background1Pressed].value(
+ themeMode = FluentTheme.themeMode
+ )
+ ),
+ selected = SolidColor(
+ FluentTheme.aliasTokens.neutralBackgroundColor[Background1Selected].value(
+ themeMode = FluentTheme.themeMode
+ )
+ )
+ )
+ }
+ }
+ LazyColumn(
+ state = lazyListState, modifier = modifier.draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ scope.launch {
+ lazyListState.scrollBy(-delta)
+ }
+ },
+ )
+ ) {
+ itemsIndexed(
+ items = filteredSearchItems,
+ key = { index, item -> item.getUniqueId() }) { index, item -> // ensures stable render updates, will prevent recomps
+ val isSelected = selectedSearchItems.contains(item)
+ ListItem.Item(
+ text = item.title,
+ modifier = Modifier
+ .clearAndSetSemantics {
+ contentDescription =
+ "${item.title}, ${item.subTitle}" + if (enableStatus) statusString.format(
+ item.status
+ ) else ""
+ stateDescription = if (filteredSearchItems.size > 1) positionString.format(
+ index + 1,
+ filteredSearchItems.size
+ ) else ""
+ role = Role.Button
+ },
+ subText = item.subTitle,
+ secondarySubText = item.footer,
+ onClick = {
+ if (inSelectionMode) {
+ toggleItemSelection(item)
+ } else {
+ item.onClick()
+ }
+ },
+ onLongClick = {
+ item.onLongClick()
+ toggleItemSelection(item)
+ },
+ border = border,
+ listItemTokens = listItemTokens,
+ enabled = item.enabled,
+ selected = isSelected,
+ leadingAccessoryContent = item.leftAccessory,
+ trailingAccessoryContent = item.rightAccessory,
+ )
+ }
+ }
+}
+
@Composable
private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
slideOver: Boolean,
@@ -421,7 +859,11 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
) {
val scope = rememberCoroutineScope()
- val drawerState = rememberBottomDrawerState(initialValue = DrawerValue.Closed, expandable = expandable, skipOpenState = skipOpenState)
+ val drawerState = rememberBottomDrawerState(
+ initialValue = DrawerValue.Closed,
+ expandable = expandable,
+ skipOpenState = skipOpenState
+ )
val open: () -> Unit = {
scope.launch { drawerState.open() }
diff --git a/FluentUI.Demo/src/main/res/values-en-rGB/strings.xml b/FluentUI.Demo/src/main/res/values-en-rGB/strings.xml
index fc63f9ef0..d9329d2af 100644
--- a/FluentUI.Demo/src/main/res/values-en-rGB/strings.xml
+++ b/FluentUI.Demo/src/main/res/values-en-rGB/strings.xml
@@ -850,6 +850,8 @@
Less than half screen content
Dynamic size content
+
+ Searchable Drawer Content
Nested Drawer Content
diff --git a/FluentUI.Demo/src/main/res/values/strings.xml b/FluentUI.Demo/src/main/res/values/strings.xml
index 7b64625a3..6e9158ca8 100644
--- a/FluentUI.Demo/src/main/res/values/strings.xml
+++ b/FluentUI.Demo/src/main/res/values/strings.xml
@@ -902,6 +902,8 @@
Less than half screen content
Dynamic size content
+
+ Searchable Drawer Content
Nested Drawer Content
diff --git a/FluentUI/src/main/res/values-night/themes.xml b/FluentUI/src/main/res/values-night/themes.xml
index 480d74b5f..6d8e5a60d 100644
--- a/FluentUI/src/main/res/values-night/themes.xml
+++ b/FluentUI/src/main/res/values-night/themes.xml
@@ -14,6 +14,8 @@
+ - true
+ - @android:color/transparent
- @color/fluentui_black
- @color/fluentui_gray_900
diff --git a/FluentUI/src/main/res/values/themes.xml b/FluentUI/src/main/res/values/themes.xml
index 6671b21df..8640747c6 100644
--- a/FluentUI/src/main/res/values/themes.xml
+++ b/FluentUI/src/main/res/values/themes.xml
@@ -121,6 +121,8 @@
- ?attr/fluentuiBackgroundColor
- ?attr/colorPrimary
- ?attr/fluentuiDialogBackgroundColor
+ - true
+ - @android:color/transparent
- ?attr/fluentuiBackgroundColor
diff --git a/build.gradle b/build.gradle
index 5ee4b18f3..437c8a3b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -57,6 +57,7 @@ allprojects {
junitKtxVersion = '1.1.5'
kotlin_version = '1.8.21'
lifecycleVersion = '2.4.1'
+ lifecycleComposeVersion = '2.6.1'
materialVersion = '1.9.0'
mockitoVersion = '1.10.19'
recyclerViewVersion = '1.3.0'
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AcrylicPaneTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AcrylicPaneTokens.kt
index 3b46f27a9..fb7e33d34 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AcrylicPaneTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AcrylicPaneTokens.kt
@@ -6,45 +6,74 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.ThemeMode
import com.microsoft.fluentui.theme.token.ControlInfo
+import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.theme.token.FluentColor
import com.microsoft.fluentui.theme.token.FluentStyle
import com.microsoft.fluentui.theme.token.IControlToken
import kotlinx.parcelize.Parcelize
+enum class AcrylicPaneOrientation {
+ TOP,
+ BOTTOM,
+ CENTER
+}
+
open class AcrylicPaneInfo(
- val style: FluentStyle = FluentStyle.Neutral
+ val style: FluentStyle = FluentStyle.Neutral,
+ val orientation: AcrylicPaneOrientation = AcrylicPaneOrientation.BOTTOM
) : ControlInfo
@Parcelize
open class AcrylicPaneTokens : IControlToken, Parcelable {
+ companion object {
+ const val DEFAULT_BLUR_RADIUS = 60 // Default value, can be overridden by theme
+ }
@Composable
open fun acrylicPaneGradient(acrylicPaneInfo: AcrylicPaneInfo): Brush {
- if(acrylicPaneInfo.style == FluentStyle.Neutral) {
- val startColor: Color = Color(red = 0xF7, green = 0xF8 , blue = 0xFB, alpha = 0xFF)
- return Brush.verticalGradient(
+ val startColor: Color = FluentColor(
+ light = FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
+ ThemeMode.Light
+ ),
+ dark = FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
+ ThemeMode.Dark
+ )
+ ).value(FluentTheme.themeMode)
+ when (acrylicPaneInfo.orientation) {
+ AcrylicPaneOrientation.TOP -> return Brush.verticalGradient(
colors = listOf(
- startColor,
- startColor,
startColor,
startColor.copy(alpha = 0.5f),
startColor.copy(alpha = 0.0f),
),
- tileMode = TileMode.Decal
+ tileMode = TileMode.Decal
)
- }
- else{
- val startColor: Color = Color(0xFE106cbc)
- return Brush.verticalGradient(
+
+ AcrylicPaneOrientation.CENTER -> return Brush.verticalGradient(
colors = listOf(
+ startColor.copy(alpha = 0.0f),
startColor,
- startColor,
- startColor.copy(alpha = 0.8f),
- startColor.copy(alpha = 0.5f),
startColor.copy(alpha = 0.0f),
),
tileMode = TileMode.Decal
)
+
+ AcrylicPaneOrientation.BOTTOM -> return Brush.verticalGradient(
+ colors = listOf(
+ startColor.copy(alpha = 0.0f),
+ startColor.copy(alpha = 0.5f),
+ startColor
+ ),
+ tileMode = TileMode.Decal
+ )
}
}
+
+ @Composable
+ open fun acrylicPaneBlurRadius(acrylicPaneInfo: AcrylicPaneInfo): Int {
+ return DEFAULT_BLUR_RADIUS // Need blur tokens
+ }
}
\ No newline at end of file
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/util/Utils.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/util/Utils.kt
index f04e82b6b..fb739cd88 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/util/Utils.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/util/Utils.kt
@@ -1,8 +1,15 @@
package com.microsoft.fluentui.util
import android.content.res.Resources
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -18,7 +25,42 @@ fun dpToPx(value: Dp) = (value * Resources
fun getStringResource(id: Int): String {
return LocalContext.current.resources.getString(id)
}
+
@Composable
fun getStringResource(id: Int, vararg formatArgs: Any): String {
return LocalContext.current.resources.getString(id, *formatArgs)
+}
+
+/**
+ * A composable function that observes the visibility of the software keyboard and triggers
+ * callbacks when the keyboard becomes visible or hidden.
+ *
+ * @param onKeyboardVisible A lambda function to be executed when the keyboard becomes visible.
+ * Defaults to an empty lambda.
+ * @param onKeyboardHidden A lambda function to be executed when the keyboard becomes hidden.
+ * Defaults to an empty lambda.
+ * @param content A composable content block to be displayed within this observer.
+ */
+@Composable
+fun KeyboardVisibilityObserver(
+ onKeyboardVisible: () -> Unit = {},
+ onKeyboardHidden: () -> Unit = {},
+ content: @Composable () -> Unit
+) {
+ val imeInsets = WindowInsets.ime
+ val density = LocalDensity.current
+ val isKeyboardVisible by remember {
+ derivedStateOf {
+ imeInsets.getBottom(density) > 0
+ }
+ }
+
+ LaunchedEffect(isKeyboardVisible) {
+ if (isKeyboardVisible) {
+ onKeyboardVisible()
+ } else {
+ onKeyboardHidden()
+ }
+ }
+ content()
}
\ No newline at end of file
diff --git a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/listitem/ListItem.kt b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/listitem/ListItem.kt
index 75b072608..701903067 100644
--- a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/listitem/ListItem.kt
+++ b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/listitem/ListItem.kt
@@ -35,7 +35,6 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.*
import androidx.compose.ui.text.*
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import com.microsoft.fluentui.listitem.R
import com.microsoft.fluentui.icons.ListItemIcons
@@ -212,6 +211,7 @@ object ListItem {
textAlignment: ListItemTextAlignment = ListItemTextAlignment.Regular,
unreadDot: Boolean = false,
enabled: Boolean = true,
+ selected: Boolean = false,
textMaxLines: Int = 1,
subTextMaxLines: Int = 1,
secondarySubTextMaxLines: Int = 1,
@@ -252,7 +252,7 @@ object ListItem {
)
val backgroundColor =
token.backgroundBrush(listItemInfo).getBrushByState(
- enabled = true, selected = false, interactionSource = interactionSource
+ enabled = true, selected = selected, interactionSource = interactionSource
)
val primaryTextTypography = token.primaryTextTypography(listItemInfo)
val subTextTypography = token.subTextTypography(listItemInfo)
@@ -261,17 +261,17 @@ object ListItem {
val primaryTextColor = token.primaryTextColor(
listItemInfo
).getColorByState(
- enabled = enabled, selected = false, interactionSource = interactionSource
+ enabled = enabled, selected = selected, interactionSource = interactionSource
)
val subTextColor = token.subTextColor(
listItemInfo
).getColorByState(
- enabled = enabled, selected = false, interactionSource = interactionSource
+ enabled = enabled, selected = selected, interactionSource = interactionSource
)
val secondarySubTextColor = token.secondarySubTextColor(
listItemInfo
).getColorByState(
- enabled = enabled, selected = false, interactionSource = interactionSource
+ enabled = enabled, selected = selected, interactionSource = interactionSource
)
val rippleColor = token.rippleColor(listItemInfo)
val unreadDotColor = token.unreadDotColor(listItemInfo)
@@ -281,7 +281,7 @@ object ListItem {
token.borderInset(listItemInfo).toPx()
}
val borderColor = token.borderColor(listItemInfo).getColorByState(
- enabled = enabled, selected = false, interactionSource = interactionSource
+ enabled = enabled, selected = selected, interactionSource = interactionSource
)
val textAccessoryContentTextSpacing = token.textAccessoryContentTextSpacing(listItemInfo)
val leadingAccessoryAlignment = when (leadingAccessoryContentAlignment) {
@@ -446,6 +446,7 @@ object ListItem {
* @param textAlignment Optional [ListItemTextAlignment] to align text in the center or start at the lead.
* @param unreadDot Option boolean value that display a dot on leading edge of the accessory Content and makes the primary text bold on true
* @param enabled Optional enable/disable List item
+ * @param selected Optional selected state for List item.
* @param textMaxLines Optional max visible lines for primary text.
* @param subTextMaxLines Optional max visible lines for secondary text.
* @param secondarySubTextMaxLines Optional max visible lines for tertiary text.
@@ -475,6 +476,7 @@ object ListItem {
textAlignment: ListItemTextAlignment = ListItemTextAlignment.Regular,
unreadDot: Boolean = false,
enabled: Boolean = true,
+ selected: Boolean = false,
textMaxLines: Int = 1,
subTextMaxLines: Int = 1,
secondarySubTextMaxLines: Int = 1,
@@ -503,6 +505,7 @@ object ListItem {
textAlignment = textAlignment,
unreadDot = unreadDot,
enabled = enabled,
+ selected = selected,
textMaxLines = textMaxLines,
subTextMaxLines = subTextMaxLines,
secondarySubTextMaxLines = secondarySubTextMaxLines,
@@ -536,6 +539,7 @@ object ListItem {
* @param textAlignment Optional [ListItemTextAlignment] to align text in the center or start at the lead.
* @param unreadDot Option boolean value that display a dot on leading edge of the accessory Content and makes the primary text bold on true
* @param enabled Optional enable/disable List item
+ * @param selected Optional selected state for the list item.
* @param textMaxLines Optional max visible lines for primary text.
* @param subTextMaxLines Optional max visible lines for secondary text.
* @param secondarySubTextMaxLines Optional max visible lines for tertiary text.
@@ -566,6 +570,7 @@ object ListItem {
textAlignment: ListItemTextAlignment = ListItemTextAlignment.Regular,
unreadDot: Boolean = false,
enabled: Boolean = true,
+ selected: Boolean = false,
textMaxLines: Int = 1,
subTextMaxLines: Int = 1,
secondarySubTextMaxLines: Int = 1,
@@ -595,6 +600,7 @@ object ListItem {
textAlignment = textAlignment,
unreadDot = unreadDot,
enabled = enabled,
+ selected = selected,
textMaxLines = textMaxLines,
subTextMaxLines = subTextMaxLines,
secondarySubTextMaxLines = secondarySubTextMaxLines,
diff --git a/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/acrylicpane/AcrylicPane.kt b/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/acrylicpane/AcrylicPane.kt
index 7b4a08a84..f97d9630a 100644
--- a/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/acrylicpane/AcrylicPane.kt
+++ b/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/acrylicpane/AcrylicPane.kt
@@ -1,5 +1,8 @@
package com.microsoft.fluentui.tokenized.acrylicpane
+import android.os.Build
+import android.view.Gravity
+import android.view.WindowManager
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -8,41 +11,87 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import androidx.compose.ui.window.DialogWindowProvider
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
import com.microsoft.fluentui.theme.token.FluentStyle
import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneInfo
+import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneOrientation
import com.microsoft.fluentui.theme.token.controlTokens.AcrylicPaneTokens
-
@Composable
-private fun AcrylicPane(
- modifier: Modifier = Modifier,
- component: @Composable BoxScope.() -> Unit,
- backgroundContent: @Composable () -> Unit,
- triggerRecomposition: Boolean = false
+private fun BlurBehindDialog(
+ orientation: AcrylicPaneOrientation = AcrylicPaneOrientation.BOTTOM,
+ blurRadius: Int = 60,
+ offset: IntOffset = IntOffset(0, 0),
+ content: @Composable () -> Unit
) {
- Box(
- modifier = Modifier.fillMaxSize()
+ val dialogProperties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ decorFitsSystemWindows = false,
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+
+ Dialog(
+ onDismissRequest = {},
+ properties = dialogProperties
) {
- backgroundContent()
+ val window = (LocalView.current.parent as? DialogWindowProvider)?.window
- Box(
- modifier = modifier
- ) {
- component()
+ SideEffect {
+ if (window != null) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL)
+ window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND)
+ window.setBackgroundBlurRadius(blurRadius)
+ }
+ window.setDimAmount(0f)
+ window.setGravity(
+ when (orientation) {
+ AcrylicPaneOrientation.TOP -> Gravity.TOP
+ AcrylicPaneOrientation.BOTTOM -> Gravity.BOTTOM
+ AcrylicPaneOrientation.CENTER -> Gravity.CENTER
+ }
+ )
+ window.attributes.x = offset.x
+ window.attributes.y = offset.y
+ window.decorView.setBackgroundColor(android.graphics.Color.TRANSPARENT)
+ }
}
+ content()
}
}
fun roundToNearestTen(value: Int): Int { // Added for anti-aliasing
return ((value + 5) / 10) * 10
}
+
/**
* A composable function that creates an AcrylicPane with specified properties and content.
+ * This component leverages a real-time, window-level blur to create its acrylic effect.
+ * This behavior is subject to specific system conditions.
+ *
+ * Platform-Specific Behavior & Requirements:
+ * API Level: The background blur is only supported on Android 12 (API 31) and newer.
+ * Device Setting: For the blur to be visible, the "Allow window-level blurs" option must be enabled in the device's Developer Options.
+ *
+ * Fallback Mechanism:
+ * On devices running older Android versions (below API 31) or when the necessary developer
+ * option is disabled, the AcrylicPane will gracefully fall back to a semi-transparent gradient effect.
+ * This ensures the UI remains functional and aesthetically pleasing even when the blur effect is not available.
*
* @param modifier The modifier to be applied to the AcrylicPane.
+ * @param orientation The orientation of the AcrylicPane, default is AcrylicPaneOrientation.BOTTOM.
+ * @param offset The offset of the pane from the top-left corner of the screen, default is IntOffset(0, 0).
* @param paneHeight The height of the pane, default is 300.dp.
* @param acrylicPaneStyle The style of the pane, default is FluentStyle.Neutral.
* @param component The main composable content to be displayed within the pane.
@@ -51,26 +100,45 @@ fun roundToNearestTen(value: Int): Int { // Added for anti-aliasing
*/
@Composable
-public fun AcrylicPane(modifier: Modifier = Modifier, paneHeight: Dp = 300.dp, acrylicPaneStyle:FluentStyle = FluentStyle.Neutral, component: @Composable () -> Unit, backgroundContent: @Composable () -> Unit, acrylicPaneTokens: AcrylicPaneTokens? = null) {
- val paneInfo: AcrylicPaneInfo = AcrylicPaneInfo(style = acrylicPaneStyle)
+fun AcrylicPane(
+ modifier: Modifier = Modifier,
+ orientation: AcrylicPaneOrientation = AcrylicPaneOrientation.BOTTOM,
+ offset: IntOffset = IntOffset(0, 0),
+ paneHeight: Dp = 300.dp,
+ acrylicPaneStyle: FluentStyle = FluentStyle.Neutral,
+ component: @Composable () -> Unit,
+ backgroundContent: @Composable () -> Unit,
+ acrylicPaneTokens: AcrylicPaneTokens? = null
+) {
+ val paneInfo: AcrylicPaneInfo =
+ AcrylicPaneInfo(style = acrylicPaneStyle, orientation = orientation)
val newPaneHeight = roundToNearestTen(paneHeight.value.toInt()).dp
val themeID =
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
val token = acrylicPaneTokens
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AcrylicPaneControlType] as AcrylicPaneTokens
- AcrylicPane(
- modifier = modifier
- .fillMaxWidth()
- .height(newPaneHeight)
- .background(
- token.acrylicPaneGradient(acrylicPaneInfo = paneInfo)
- ),
- component = {
- component()
- },
- backgroundContent = {
- backgroundContent()
- },
- triggerRecomposition = false
- )
-}
+ val backgroundColor: Brush = token.acrylicPaneGradient(acrylicPaneInfo = paneInfo)
+ val blurRadius: Int = token.acrylicPaneBlurRadius(acrylicPaneInfo = paneInfo)
+ Box(
+ modifier = Modifier.fillMaxSize()
+ ) {
+ backgroundContent()
+
+ BlurBehindDialog(
+ orientation = orientation,
+ blurRadius = blurRadius,
+ offset = offset
+ ) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(newPaneHeight)
+ .background(
+ backgroundColor
+ )
+ ) {
+ component()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/fluentui_topappbars/build.gradle b/fluentui_topappbars/build.gradle
index e7f8a6514..0a2fdef00 100644
--- a/fluentui_topappbars/build.gradle
+++ b/fluentui_topappbars/build.gradle
@@ -53,6 +53,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "com.google.android.material:material:$materialVersion"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleComposeVersion"
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-util"
diff --git a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchBar.kt b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchBar.kt
index 37353e033..f8ecafd89 100644
--- a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchBar.kt
+++ b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchBar.kt
@@ -27,6 +27,7 @@ import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.semantics.Role
diff --git a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchViewModel.kt b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchViewModel.kt
new file mode 100644
index 000000000..3a199ec8c
--- /dev/null
+++ b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/SearchViewModel.kt
@@ -0,0 +1,159 @@
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.flow.*
+import java.util.UUID
+
+/**
+ * Remembers a unique identifier for a composable.
+ *
+ * If a non-null [id] is provided, it will be returned.
+ * If [id] is null, a new unique ID will be generated using UUID and
+ * remembered across recompositions and process death.
+ *
+ * @param id An optional existing ID of type [Any].
+ * @return The provided [id] or a newly generated unique ID.
+ */
+fun generateUniqueId(id: Any? = null): Any {
+ return id ?: { UUID.randomUUID().toString() }
+}
+
+/**
+ * An interface for objects that can be searched.
+ * Any class implementing this interface must provide a string key to be used for filtering.
+ */
+interface Searchable {
+ /**
+ * @return The string value that will be checked against the search query.
+ */
+ fun getSearchKey(): String
+ /**
+ * @return A unique identifier for this item, used for stable list updates in Compose.
+ */
+ fun getUniqueId(): Any
+}
+
+/**
+ * A generic UI state holder for the search screen.
+ * @param T The type of item being searched.
+ */
+data class SearchUiState(
+ val searchQuery: String = "",
+ val filteredItems: List = emptyList(),
+ val selectedItems: Set = emptySet(),
+ val selectionSize: Int = 0
+)
+
+
+/**
+ * A generic ViewModel to handle search logic for any list of 'Searchable' items.
+ *
+ * @param T The type of item being searched, constrained to implement [Searchable].
+ * @param initialItems The initial list of items to be displayed and searched.
+ */
+class SearchViewModel(
+ initialItems: List
+) : ViewModel() {
+ private val _searchQuery = MutableStateFlow("")
+ private val _allItems = MutableStateFlow(initialItems)
+ private val _selectedItems = MutableStateFlow>(emptySet())
+
+ val uiState: StateFlow> =
+ combine(_searchQuery, _allItems, _selectedItems) { query, items, selected ->
+ val itemsToShow = if (query.isBlank()) {
+ items
+ } else {
+ items.filter { item ->
+ item.getSearchKey().contains(query, ignoreCase = true)
+ }
+ }
+ SearchUiState(
+ searchQuery = query,
+ filteredItems = itemsToShow,
+ selectedItems = selected,
+ selectionSize = selected.size
+ )
+ }.stateIn(
+ scope = viewModelScope,
+ started = SharingStarted.WhileSubscribed(5000),
+ initialValue = SearchUiState(filteredItems = initialItems)
+ )
+
+ /** Handles the query change event from the UI. */
+ fun onQueryChanged(query: String) {
+ _searchQuery.value = query
+ }
+
+ /** Adds a new item to the master list. */
+ fun addItem(item: T) {
+ _allItems.update { currentList -> currentList + item }
+ }
+
+ /** Removes an item from the master list using its unique ID. */
+ fun removeItem(item: T) {
+ _selectedItems.update { it - item }
+ _allItems.update { currentList ->
+ currentList.filter { it.getUniqueId() != item.getUniqueId() }
+ }
+ }
+
+ /** Clears all items from the master list. */
+ fun clearItems() {
+ _selectedItems.value = emptySet()
+ _allItems.value = emptyList()
+ }
+
+ /**
+ * Adds a single item to the selection set.
+ */
+ fun selectItem(item: T) {
+ _selectedItems.update { currentSet ->
+ // The '+' operator on a set creates a new set with the item added
+ currentSet + item
+ }
+ }
+
+ /**
+ * Removes a single item from the selection set.
+ */
+ fun deselectItem(item: T) {
+ _selectedItems.update { currentSet ->
+ // The '-' operator on a set creates a new set with the item removed
+ currentSet - item
+ }
+ }
+
+ /**
+ * A more convenient function for UI toggles. Selects an item if it's not
+ * selected, and deselects it if it is already selected.
+ */
+ fun toggleSelection(item: T) {
+ _selectedItems.update { currentSet ->
+ if (item in currentSet) {
+ currentSet - item
+ } else {
+ currentSet + item
+ }
+ }
+ }
+
+ /** Clears all selected items. */
+ fun clearSelection() {
+ _selectedItems.value = emptySet()
+ }
+}
+
+/**
+ * A factory for creating instances of [SearchViewModel] with parameters.
+ */
+class SearchViewModelFactory(
+ private val initialItems: List
+) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ if (modelClass.isAssignableFrom(SearchViewModel::class.java)) {
+ @Suppress("UNCHECKED_CAST")
+ return SearchViewModel(initialItems) as T
+ }
+ throw IllegalArgumentException("Unknown ViewModel class")
+ }
+}
\ No newline at end of file