diff --git a/FluentUI.Demo/build.gradle b/FluentUI.Demo/build.gradle
index f089ecacd..29824e6e4 100644
--- a/FluentUI.Demo/build.gradle
+++ b/FluentUI.Demo/build.gradle
@@ -11,10 +11,10 @@ android {
compileSdkVersion constants.compileSdkVersion
defaultConfig {
applicationId 'com.microsoft.fluentuidemo'
- minSdkVersion 21
+ minSdkVersion 23
targetSdkVersion 34
- versionCode 1012
- versionName '0.2.12'
+ versionCode 2005
+ versionName '0.3.5'
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
kotlinOptions {
@@ -33,6 +33,7 @@ android {
}
lintOptions {
lintConfig = file("lint.xml")
+ abortOnError false
}
buildTypes {
release {
@@ -89,7 +90,6 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'com.github.bumptech.glide:glide:4.8.0'
- implementation 'com.jakewharton.threetenabp:threetenabp:1.1.0'
//Compose BOM
implementation platform("androidx.compose:compose-bom:$composeBomVersion")
diff --git a/FluentUI.Demo/src/main/AndroidManifest.xml b/FluentUI.Demo/src/main/AndroidManifest.xml
index 93c6f9cfb..cd261212b 100644
--- a/FluentUI.Demo/src/main/AndroidManifest.xml
+++ b/FluentUI.Demo/src/main/AndroidManifest.xml
@@ -23,6 +23,7 @@
+
@@ -33,7 +34,7 @@
+ android:enableOnBackInvokedCallback="true"/>
@@ -44,8 +45,7 @@
android:windowSoftInputMode="adjustResize" />
-
+
+
+ FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background3].value(
+ themeMode = FluentTheme.themeMode
+ )
+
+ FluentStyle.Brand ->
+ FluentColor(
+ light = FluentTheme.aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground2].value(
+ ThemeMode.Light
+ ),
+ dark = FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
+ ThemeMode.Dark
+ )
+ ).value(themeMode = FluentTheme.themeMode)
+ }
+ )
+ }
}
\ No newline at end of file
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/DemoActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/DemoActivity.kt
index d07bc1d4f..80ae0c305 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/DemoActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/DemoActivity.kt
@@ -10,7 +10,6 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.microsoft.fluentuidemo.databinding.ActivityDemoDetailBinding
import java.util.UUID
@@ -37,11 +36,7 @@ abstract class DemoActivity : AppCompatActivity() {
// Set demo title
val demoID = intent.getSerializableExtra(DEMO_ID) as UUID
- val demo: Demo? = if (DuoSupportUtils.isDualScreenMode(this)) {
- DUO_DEMOS.find { it.id == demoID }
- } else {
- V1DEMO.find { it.id == demoID }
- }
+ val demo: Demo? = V1DEMO.find { it.id == demoID }
if (demo != null)
title = demo.title
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/Demos.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/Demos.kt
index fc2695227..15d8abaf6 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/Demos.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/Demos.kt
@@ -30,6 +30,7 @@ import com.microsoft.fluentuidemo.demos.TabLayoutActivity
import com.microsoft.fluentuidemo.demos.TemplateViewActivity
import com.microsoft.fluentuidemo.demos.TooltipActivity
import com.microsoft.fluentuidemo.demos.TypographyActivity
+import com.microsoft.fluentuidemo.demos.V2ActionBarActivity
import com.microsoft.fluentuidemo.demos.V2AppBarActivity
import com.microsoft.fluentuidemo.demos.V2AvatarActivity
import com.microsoft.fluentuidemo.demos.V2AvatarCarouselActivity
@@ -74,6 +75,7 @@ enum class Badge {
APIBreak
}
+const val V2ACTION_BAR = "V2 ActionBar"
const val V2AVATAR = "V2 Avatar"
const val V2AVATAR_CAROUSEL = "V2 Avatar Carousel"
const val V2AVATAR_GROUP = "V2 Avatar Group"
@@ -146,7 +148,7 @@ val V1DEMO = arrayListOf(
Demo(DATE_TIME_PICKER, DateTimePickerActivity::class),
Demo(DRAWER, DrawerActivity::class),
Demo(LIST_ITEM_VIEW, ListItemViewActivity::class),
- Demo(PEOPLE_PICKER_VIEW, PeoplePickerViewActivity::class),
+ Demo(PEOPLE_PICKER_VIEW, PeoplePickerViewActivity::class, Badge.Modified),
Demo(PERSISTENT_BOTTOM_SHEET, PersistentBottomSheetActivity::class),
Demo(PERSONA_CHIP_VIEW, PersonaChipViewActivity::class),
Demo(PERSONA_LIST_VIEW, PersonaListViewActivity::class),
@@ -161,10 +163,11 @@ val V1DEMO = arrayListOf(
)
val V2DEMO = arrayListOf(
+ Demo(V2ACTION_BAR, V2ActionBarActivity::class, Badge.Modified),
Demo(V2APP_BAR_LAYOUT, V2AppBarActivity::class),
- Demo(V2AVATAR, V2AvatarActivity::class),
+ Demo(V2AVATAR, V2AvatarActivity::class, Badge.Modified),
Demo(V2AVATAR_CAROUSEL, V2AvatarCarouselActivity::class),
- Demo(V2AVATAR_GROUP, V2AvatarGroupActivity::class),
+ Demo(V2AVATAR_GROUP, V2AvatarGroupActivity::class, Badge.Modified),
Demo(V2BADGE, V2BadgeActivity::class),
Demo(V2BANNER, V2BannerActivity::class),
Demo(V2BASIC_CHIP, V2BasicChipActivity::class),
@@ -177,8 +180,8 @@ val V2DEMO = arrayListOf(
Demo(V2CITATION, V2CitationActivity::class),
Demo(V2CONTEXTUAL_COMMAND_BAR, V2ContextualCommandBarActivity::class),
Demo(V2DIALOG, V2DialogActivity::class),
- Demo(V2DRAWER, V2DrawerActivity::class),
- Demo(V2LABEL, V2LabelActivity::class, Badge.Modified),
+ Demo(V2DRAWER, V2DrawerActivity::class, Badge.Modified),
+ Demo(V2LABEL, V2LabelActivity::class),
Demo(V2LIST_ITEM, V2ListItemActivity::class),
Demo(V2MENU, V2MenuActivity::class),
Demo(V2PEOPLE_PICKER, V2PeoplePickerActivity::class),
@@ -187,12 +190,12 @@ val V2DEMO = arrayListOf(
Demo(V2PERSONA_LIST, V2PersonaListActivity::class),
Demo(V2PROGRESS, V2ProgressActivity::class),
Demo(V2SCAFFOLD, V2ScaffoldActivity::class),
- Demo(V2SEARCHBAR, V2SearchBarActivity::class),
+ Demo(V2SEARCHBAR, V2SearchBarActivity::class, Badge.Modified),
Demo(V2SEGMENTED_CONTROL, V2SegmentedControlActivity::class, Badge.Modified),
Demo(V2SHIMMER, V2ShimmerActivity::class),
Demo(V2SIDE_RAIL, V2SideRailActivity::class),
Demo(V2SNACKBAR, V2SnackbarActivity::class),
- Demo(V2TAB_BAR, V2TabBarActivity::class),
+ Demo(V2TAB_BAR, V2TabBarActivity::class, Badge.Modified),
Demo(V2TEXT_FIELD, V2TextFieldActivity::class),
Demo(V2TOOL_TIP, V2ToolTipActivity::class),
)
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/BottomSheetActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/BottomSheetActivity.kt
index 7f6f6e288..1c12cea35 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/BottomSheetActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/BottomSheetActivity.kt
@@ -7,6 +7,7 @@ package com.microsoft.fluentuidemo.demos
import android.os.Bundle
import android.view.LayoutInflater
+import android.widget.Switch
import android.widget.TextView
import com.microsoft.fluentui.bottomsheet.BottomSheet
import com.microsoft.fluentui.bottomsheet.BottomSheetDialog
@@ -67,6 +68,12 @@ class BottomSheetActivity : DemoActivity(), BottomSheetItem.OnClickListener {
R.id.bottom_sheet_item_delete,
R.drawable.ic_delete_24_regular,
getString(R.string.bottom_sheet_item_delete_title)
+ ),
+ BottomSheetItem(
+ R.id.bottom_sheet_item_toggle,
+ R.drawable.ic_fluent_toggle_multiple_24_regular,
+ getString(R.string.bottom_sheet_item_toggle_title),
+ customAccessoryView = Switch(this)
)
)
)
@@ -218,6 +225,7 @@ class BottomSheetActivity : DemoActivity(), BottomSheetItem.OnClickListener {
R.id.bottom_sheet_item_reply -> showSnackbar(resources.getString(R.string.bottom_sheet_item_reply_toast))
R.id.bottom_sheet_item_forward -> showSnackbar(resources.getString(R.string.bottom_sheet_item_forward_toast))
R.id.bottom_sheet_item_delete -> showSnackbar(resources.getString(R.string.bottom_sheet_item_delete_toast))
+ R.id.bottom_sheet_item_toggle -> showSnackbar(resources.getString(R.string.bottom_sheet_item_toggle_toast))
// Double line items
R.id.bottom_sheet_item_camera -> showSnackbar(resources.getString(R.string.bottom_sheet_item_camera_toast))
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/CalendarViewActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/CalendarViewActivity.kt
index 23ad5835e..48baa3bd7 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/CalendarViewActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/CalendarViewActivity.kt
@@ -8,16 +8,13 @@ package com.microsoft.fluentuidemo.demos
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
-import android.view.View
import android.view.View.TEXT_ALIGNMENT_TEXT_START
import com.microsoft.fluentui.calendar.OnDateSelectedListener
import com.microsoft.fluentui.util.DateStringUtils
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.microsoft.fluentuidemo.DemoActivity
-import com.microsoft.fluentuidemo.R
import com.microsoft.fluentuidemo.databinding.ActivityCalendarViewBinding
-import org.threeten.bp.Duration
-import org.threeten.bp.ZonedDateTime
+import java.time.Duration
+import java.time.ZonedDateTime
class CalendarViewActivity : DemoActivity() {
companion object {
@@ -41,9 +38,6 @@ class CalendarViewActivity : DemoActivity() {
true
)
- if (DuoSupportUtils.isDualScreenMode(this)) {
- calenderBinding.exampleDateTitle.textAlignment = TEXT_ALIGNMENT_TEXT_START
- }
calenderBinding.calendarView.onDateSelectedListener = object : OnDateSelectedListener {
override fun onDateSelected(date: ZonedDateTime) {
setExampleDate(date)
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/DateTimePickerActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/DateTimePickerActivity.kt
index 5bb897ec9..054b5c3fc 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/DateTimePickerActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/DateTimePickerActivity.kt
@@ -9,7 +9,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
-import com.jakewharton.threetenabp.AndroidThreeTen
import com.microsoft.fluentui.datetimepicker.DateTimePicker
import com.microsoft.fluentui.datetimepicker.DateTimePickerDialog
import com.microsoft.fluentui.datetimepicker.DateTimePickerDialog.DateRangeMode
@@ -20,8 +19,8 @@ import com.microsoft.fluentui.util.isAccessibilityEnabled
import com.microsoft.fluentuidemo.DemoActivity
import com.microsoft.fluentuidemo.R
import com.microsoft.fluentuidemo.databinding.ActivityDateTimePickerBinding
-import org.threeten.bp.Duration
-import org.threeten.bp.ZonedDateTime
+import java.time.Duration
+import java.time.ZonedDateTime
class DateTimePickerActivity : DemoActivity(), DateTimePickerDialog.OnDateTimePickedListener {
companion object {
@@ -161,11 +160,6 @@ class DateTimePickerActivity : DemoActivity(), DateTimePickerDialog.OnDateTimePi
private var dialogMode: Mode? = null
- init {
- // Initialization of ThreeTenABP required for ZoneDateTime
- AndroidThreeTen.init(this)
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/PersistentBottomSheetActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/PersistentBottomSheetActivity.kt
index 708c4b9d3..7b85dd24b 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/PersistentBottomSheetActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/PersistentBottomSheetActivity.kt
@@ -240,7 +240,7 @@ class PersistentBottomSheetActivity : DemoActivity(), SheetItem.OnClickListener,
ContextCompat.getColor(this, R.color.bottomsheet_horizontal_icon_tint),
disabled = true
)
- ), 0, marginBetweenView
+ ), 0, marginBetweenView, drawerTint = ContextCompat.getColor(this, R.color.bottomsheet_horizontal_icon_tint).toInt()
)
horizontalListAdapter.mOnSheetItemClickListener = this
persistentSheetContentBinding.sheetHorizontalItemList3.layoutManager =
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ActionBarActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ActionBarActivity.kt
new file mode 100644
index 000000000..83f04aaf4
--- /dev/null
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ActionBarActivity.kt
@@ -0,0 +1,147 @@
+package com.microsoft.fluentuidemo.demos
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+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.platform.testTag
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.ThemeMode
+import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.tokenized.controls.Button
+import com.microsoft.fluentui.tokenized.controls.RadioButton
+import com.microsoft.fluentui.tokenized.listitem.ListItem
+import com.microsoft.fluentuidemo.Demo
+import com.microsoft.fluentuidemo.DemoActivity.Companion.DEMO_ID
+import com.microsoft.fluentuidemo.Navigation
+import com.microsoft.fluentuidemo.R
+import com.microsoft.fluentuidemo.V2DemoActivity
+import com.microsoft.fluentuidemo.demos.actionbar.V2ActionBarDemoActivity
+
+const val ACTION_BAR_TOP_RADIO = "actionBarTopRadio"
+const val ACTION_BAR_BOTTOM_RADIO = "actionBarBottomRadio"
+const val ACTION_BAR_BASIC_TYPE_RADIO = "actionBarBasicTypeRadio"
+const val ACTION_BAR_ICON_TYPE_RADIO = "actionBarIconTypeRadio"
+const val ACTION_BAR_CAROUSEL_TYPE_RADIO = "actionBarCarouselTypeRadio"
+
+class V2ActionBarActivity : V2DemoActivity() {
+ init {
+ setupActivity(this)
+ }
+
+ override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-37"
+ override val controlTokensUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-35"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val context = this
+
+ setActivityContent {
+ val actionBarPos = listOf(0, 1)
+ val actionBarType = listOf(0, 1, 2)
+ var selectedActionBarPos by rememberSaveable { mutableStateOf(actionBarPos[0]) }
+ var selectedActionBarType by rememberSaveable { mutableStateOf(actionBarType[0]) }
+
+ Column {
+ ListItem.Header(title = resources.getString(R.string.actionbar_position_heading))
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ ActionBarRow(
+ text = R.string.actionbar_position_top_radio_label,
+ testTag = ACTION_BAR_TOP_RADIO,
+ selected = selectedActionBarPos == actionBarPos[0],
+ onClick = { selectedActionBarPos = actionBarPos[0] }
+ )
+ ActionBarRow(
+ text = R.string.actionbar_position_bottom_radio_label,
+ testTag = ACTION_BAR_BOTTOM_RADIO,
+ selected = selectedActionBarPos == actionBarPos[1],
+ onClick = { selectedActionBarPos = actionBarPos[1] }
+ )
+ }
+ ListItem.Header(title = resources.getString(R.string.actionbar_type_heading))
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ ActionBarRow(
+ text = R.string.actionbar_basic_radio_label,
+ testTag = ACTION_BAR_BASIC_TYPE_RADIO,
+ selected = selectedActionBarType == actionBarType[0],
+ onClick = { selectedActionBarType = actionBarType[0] }
+ )
+ ActionBarRow(
+ text = R.string.actionbar_icon_radio_label,
+ testTag = ACTION_BAR_ICON_TYPE_RADIO,
+ selected = selectedActionBarType == actionBarType[1],
+ onClick = { selectedActionBarType = actionBarType[1] }
+ )
+
+ ActionBarRow(
+ text = R.string.actionbar_carousel_radio_label,
+ testTag = ACTION_BAR_CAROUSEL_TYPE_RADIO,
+ selected = selectedActionBarType == actionBarType[2],
+ onClick = { selectedActionBarType = actionBarType[2] }
+ )
+ }
+
+ Button(
+ text = resources.getString(R.string.actionbar_start_button),
+ onClick = {
+ val demo = Demo("DEMOACTIONBAR", V2ActionBarDemoActivity::class)
+ val packageContext = this@V2ActionBarActivity
+ Navigation.forwardNavigation(
+ packageContext,
+ demo.demoClass.java,
+ Pair(DEMO_ID, demo.id),
+ Pair(DEMO_TITLE, demo.title),
+ Pair("ACTION_BAR_TYPE", selectedActionBarType),
+ Pair("ACTION_BAR_POSITION", selectedActionBarPos)
+ )
+ },
+ modifier = Modifier.padding(16.dp)
+ )
+ }
+ }
+ }
+
+ @Composable
+ fun ActionBarRow(
+ text: Int,
+ testTag: String,
+ selected: Boolean,
+ onClick: () -> Unit
+ ) {
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ BasicText(
+ text = resources.getString(text),
+ modifier = Modifier.weight(1F),
+ style = TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value(
+ themeMode = ThemeMode.Auto
+ )
+ )
+ )
+ RadioButton(
+ modifier = Modifier.testTag(testTag),
+ selected = selected,
+ onClick = onClick
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AppBarActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AppBarActivity.kt
index ad8a1b2a2..199bf67ca 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AppBarActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AppBarActivity.kt
@@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
@@ -56,6 +57,9 @@ const val APP_BAR_SUBTITLE_PARAM = "App Bar Subtitle Param"
const val APP_BAR_STYLE_PARAM = "App Bar AppBar Style Param"
const val APP_BAR_BUTTONBAR_PARAM = "App Bar ButtonBar Param"
const val APP_BAR_SEARCHBAR_PARAM = "App Bar SearchBar Param"
+const val APP_BAR_LOGO_PARAM = "App Bar Logo Param"
+const val APP_BAR_CENTER_ALIGN_PARAM = "App Bar Center Align Param"
+const val APP_BAR_NAVIGATION_ICON_PARAM = "App Bar Navigation Icon Param"
class V2AppBarActivity : V2DemoActivity() {
init {
@@ -79,7 +83,10 @@ class V2AppBarActivity : V2DemoActivity() {
var enableSearchBar: Boolean by rememberSaveable { mutableStateOf(false) }
var enableButtonBar: Boolean by rememberSaveable { mutableStateOf(false) }
var enableBottomBorder: Boolean by rememberSaveable { mutableStateOf(true) }
+ var centerAlignAppBar: Boolean by rememberSaveable { mutableStateOf(false) }
+ var showNavigationIcon: Boolean by rememberSaveable { mutableStateOf(true) }
var yAxisDelta: Float by rememberSaveable { mutableStateOf(1.0F) }
+ var enableLogo: Boolean by rememberSaveable { mutableStateOf(true) }
Column(modifier = Modifier.pointerInput(Unit) {
detectDragGestures { _, distance ->
@@ -97,6 +104,7 @@ class V2AppBarActivity : V2DemoActivity() {
chevronOrientation = ChevronOrientation(90f, 0f),
) {
Column {
+ ListItem.Header(LocalContext.current.resources.getString(R.string.app_bar_size))
PillBar(
mutableListOf(
PillMetaData(
@@ -218,6 +226,56 @@ class V2AppBarActivity : V2DemoActivity() {
)
}
)
+
+ ListItem.Item(
+ text = LocalContext.current.resources.getString(R.string.left_logo),
+ subText = if (enableLogo)
+ LocalContext.current.resources.getString(R.string.fluentui_enabled)
+ else
+ LocalContext.current.resources.getString(R.string.fluentui_disabled),
+ trailingAccessoryContent = {
+ ToggleSwitch(
+ onValueChange = {
+ enableLogo = !enableLogo
+ },
+ modifier = Modifier.testTag(APP_BAR_LOGO_PARAM),
+ checkedState = enableLogo
+ )
+ }
+ )
+
+ ListItem.Item(
+ text = LocalContext.current.resources.getString(R.string.navigation_icon),
+ subText = if (showNavigationIcon)
+ LocalContext.current.resources.getString(R.string.fluentui_enabled)
+ else
+ LocalContext.current.resources.getString(R.string.fluentui_disabled),
+ trailingAccessoryContent = {
+ ToggleSwitch(
+ onValueChange = {
+ showNavigationIcon = !showNavigationIcon
+ },
+ modifier = Modifier.testTag(APP_BAR_NAVIGATION_ICON_PARAM),
+ checkedState = showNavigationIcon
+ )
+ }
+ )
+ ListItem.Item(
+ text = LocalContext.current.resources.getString(R.string.center_title_alignment),
+ subText = if (centerAlignAppBar)
+ LocalContext.current.resources.getString(R.string.fluentui_enabled)
+ else
+ LocalContext.current.resources.getString(R.string.fluentui_disabled),
+ trailingAccessoryContent = {
+ ToggleSwitch(
+ onValueChange = {
+ centerAlignAppBar = !centerAlignAppBar
+ },
+ modifier = Modifier.testTag(APP_BAR_CENTER_ALIGN_PARAM),
+ checkedState = centerAlignAppBar
+ )
+ }
+ )
}
}
@@ -261,31 +319,39 @@ class V2AppBarActivity : V2DemoActivity() {
AppBar(
title = "Fluent UI Demo",
- navigationIcon = FluentIcon(
- SearchBarIcons.Arrowback,
- contentDescription = "Navigate Back",
- onClick = {
- Toast.makeText(
- context,
- "Navigation Icon pressed",
- Toast.LENGTH_SHORT
- ).show()
- },
- flipOnRtl = true
- ),
- subTitle = subtitle,
- logo = {
- Avatar(
- Person(
- "Allan",
- "Munger",
- status = AvatarStatus.DND,
- isActive = true
- ),
- enablePresence = true,
- size = AvatarSize.Size32
+ navigationIcon = if (showNavigationIcon) {
+ FluentIcon(
+ SearchBarIcons.Arrowback,
+ contentDescription = "Navigate Back",
+ onClick = {
+ Toast.makeText(
+ context,
+ "Navigation Icon pressed",
+ Toast.LENGTH_SHORT
+ ).show()
+ },
+ flipOnRtl = true
)
- },
+ } else null,
+ subTitle = subtitle,
+ centerAlignAppBar = centerAlignAppBar,
+ logo = if (enableLogo) {
+ {
+ Avatar(
+ Person(
+ "Allan",
+ "Munger",
+ status = AvatarStatus.DND,
+ isActive = true
+ ),
+ enablePresence = true,
+ size = AvatarSize.Size32,
+ modifier = if (!showNavigationIcon) {
+ Modifier.padding(start = 16.dp)
+ } else Modifier
+ )
+ }
+ } else null,
postTitleIcon = FluentIcon(
ListItemIcons.Chevron,
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_chevron),
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarActivity.kt
index 117dae880..6728bb883 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarActivity.kt
@@ -52,6 +52,7 @@ class V2AvatarActivity : V2DemoActivity() {
) {
var isActive by rememberSaveable { mutableStateOf(true) }
var isOOO by rememberSaveable { mutableStateOf(false) }
+ var isActivityDotPresent by rememberSaveable { mutableStateOf(false) }
BasicText(
modifier = Modifier.padding(start = 16.dp),
@@ -127,8 +128,8 @@ class V2AvatarActivity : V2DemoActivity() {
) {
Button(
onClick = { isActive = !isActive },
- text = "Toggle Activity",
- contentDescription = "Activity Indicator ${if (isActive) "enabled" else "disabled"}"
+ text = "Toggle Activity Ring",
+ contentDescription = "Activity Ring ${if (isActive) "enabled" else "disabled"}"
)
Button(
onClick = { isOOO = !isOOO },
@@ -136,6 +137,19 @@ class V2AvatarActivity : V2DemoActivity() {
contentDescription = "OOO status ${if (isOOO) "enabled" else "disabled"}"
)
}
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(
+ 10.dp,
+ Alignment.CenterHorizontally
+ )
+ ) {
+ Button(
+ onClick = { isActivityDotPresent = !isActivityDotPresent },
+ text = "Toggle Activity Dot",
+ contentDescription = "Activity Dot ${if (isActivityDotPresent) "enabled" else "disabled"}"
+ )
+ }
Divider()
@@ -158,29 +172,32 @@ class V2AvatarActivity : V2DemoActivity() {
status = AvatarStatus.Available, isOOO = isOOO
)
- Avatar(person, size = AvatarSize.Size16, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size20, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size24, enableActivityRings = true)
+ Avatar(person, size = AvatarSize.Size16, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size20, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size24, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
Avatar(
personNoImage,
size = AvatarSize.Size32,
enableActivityRings = true,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size40,
enableActivityRings = true,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoImage,
size = AvatarSize.Size56,
enableActivityRings = true,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
- Avatar(personNoImage, size = AvatarSize.Size72, enableActivityRings = true)
+ Avatar(personNoImage, size = AvatarSize.Size72, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
}
Row(
@@ -197,13 +214,13 @@ class V2AvatarActivity : V2DemoActivity() {
status = AvatarStatus.Away, isOOO = isOOO
)
- Avatar(person, size = AvatarSize.Size16, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size20, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size24, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size32, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size40, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size56, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size72, enableActivityRings = true)
+ Avatar(person, size = AvatarSize.Size16, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size20, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size24, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size32, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size40, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size56, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size72, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
}
Row(
@@ -223,43 +240,50 @@ class V2AvatarActivity : V2DemoActivity() {
person,
size = AvatarSize.Size16,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size20,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size24,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size32,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size40,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size56,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size72,
enableActivityRings = false,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
}
@@ -282,28 +306,31 @@ class V2AvatarActivity : V2DemoActivity() {
)
- Avatar(person, size = AvatarSize.Size16, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size20, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size24, enableActivityRings = true)
- Avatar(person, size = AvatarSize.Size32, enableActivityRings = true)
+ Avatar(person, size = AvatarSize.Size16, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size20, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size24, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
+ Avatar(person, size = AvatarSize.Size32, enableActivityRings = true, enableActivityDot = isActivityDotPresent)
Avatar(
personNoInitial,
size = AvatarSize.Size40,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoInitial,
size = AvatarSize.Size56,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoInitial,
size = AvatarSize.Size72,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
}
@@ -328,44 +355,51 @@ class V2AvatarActivity : V2DemoActivity() {
person,
size = AvatarSize.Size16,
enableActivityRings = false,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size20,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size24,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
person,
size = AvatarSize.Size32,
enableActivityRings = true,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoName,
size = AvatarSize.Size40,
enableActivityRings = false,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoName,
size = AvatarSize.Size56,
enableActivityRings = true,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
Avatar(
personNoName,
size = AvatarSize.Size72,
enableActivityRings = true,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = isActivityDotPresent
)
}
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarGroupActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarGroupActivity.kt
index 8433ceb3b..d58cd5dd5 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarGroupActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2AvatarGroupActivity.kt
@@ -41,7 +41,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
}
override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-3"
- override val controlTokensUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-3"
+ override val controlTokensUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-3"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -54,6 +55,7 @@ class V2AvatarGroupActivity : V2DemoActivity() {
var isActive by rememberSaveable { mutableStateOf(false) }
var enablePresence by rememberSaveable { mutableStateOf(true) }
var maxVisibleAvatar by rememberSaveable { mutableStateOf(1) }
+ var enableActivityDot by rememberSaveable { mutableStateOf(false) }
val group = Group(
listOf(
@@ -110,6 +112,19 @@ class V2AvatarGroupActivity : V2DemoActivity() {
text = "+",
contentDescription = "Max Visible Avatar $maxVisibleAvatar"
)
+ Button(
+ onClick = { enableActivityDot = !enableActivityDot },
+ text = "Show Activity Dot",
+ contentDescription = "Activity Dot ${if (enableActivityDot) "Enabled" else "Disabled"}"
+ )
+ }
+
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(5.dp), horizontalArrangement = Arrangement.SpaceEvenly,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
Button(
onClick = { isActive = !isActive },
text = "Swap Active State",
@@ -146,7 +161,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
group,
size = AvatarSize.Size16,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -165,7 +181,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size20,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -184,7 +201,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size24,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -202,7 +220,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
group,
size = AvatarSize.Size32,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -221,7 +240,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size40,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -239,7 +259,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
group,
size = AvatarSize.Size56,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -258,7 +279,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size72,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -286,7 +308,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size16,
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -306,7 +329,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -326,7 +350,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAvatarTokens()
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -345,7 +370,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size32,
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -365,7 +391,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = AnonymousAccentAvatarTokens()
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
@@ -384,7 +411,8 @@ class V2AvatarGroupActivity : V2DemoActivity() {
size = AvatarSize.Size56,
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
- enablePresence = enablePresence
+ enablePresence = enablePresence,
+ enableActivityDot = enableActivityDot
)
}
}
@@ -402,7 +430,152 @@ class V2AvatarGroupActivity : V2DemoActivity() {
style = AvatarGroupStyle.Pile,
maxVisibleAvatar = maxVisibleAvatar,
enablePresence = enablePresence,
- avatarToken = StandardInvertedAvatarTokens()
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+
+ item {
+ Row(horizontalArrangement = Arrangement.Center) {
+ BasicText(
+ "Pie Group Style",
+ style = aliasTokens.typography[FluentAliasTokens.TypographyTokens.Title2]
+ )
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 16: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size16,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 20: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size20,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 24: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size24,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ avatarToken = AnonymousAvatarTokens(),
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 32: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size32,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 40: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size40,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ avatarToken = AnonymousAccentAvatarTokens(),
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item {
+ BasicText("Size 56: ")
+ }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size56,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ enableActivityDot = enableActivityDot
+ )
+ }
+ }
+ }
+ item {
+ LazyRow(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxSize()
+ ) {
+ item { BasicText("Size 72: ") }
+ item {
+ AvatarGroup(
+ group,
+ size = AvatarSize.Size72,
+ style = AvatarGroupStyle.Pie,
+ maxVisibleAvatar = maxVisibleAvatar,
+ avatarToken = StandardInvertedAvatarTokens(),
+ enableActivityDot = enableActivityDot
)
}
}
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BasicControlsActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BasicControlsActivity.kt
index c090b5823..9e8e437e6 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BasicControlsActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BasicControlsActivity.kt
@@ -170,7 +170,7 @@ class V2BasicControlsActivity : V2DemoActivity() {
selected = (theme == selectedOption.value),
onClick = { },
role = Role.RadioButton,
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
indication = null
)
) {
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 93a3b241b..b69817619 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
@@ -2,6 +2,8 @@ package com.microsoft.fluentuidemo.demos
import android.content.res.Configuration
import android.os.Bundle
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -14,8 +16,8 @@ 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
@@ -30,6 +32,7 @@ import com.microsoft.fluentui.theme.token.FluentAliasTokens
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.fluentuidemo.R
@@ -47,29 +50,35 @@ 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 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
}
}
}
@Composable
private fun CreateActivityUI() {
- var scrimVisible by remember { mutableStateOf(true) }
- var dynamicSizeContent by remember { mutableStateOf(false) }
- var nestedDrawerContent by remember { mutableStateOf(false) }
- var listContent by remember { mutableStateOf(true) }
- var expandable by remember { mutableStateOf(true) }
- var skipOpenState by remember { mutableStateOf(false) }
- var selectedContent by remember { mutableStateOf(ContentType.FULL_SCREEN_SCROLLABLE_CONTENT) }
- var slideOver by remember { mutableStateOf(false) }
- var showHandle by remember { mutableStateOf(true) }
- var enableSwipeDismiss by remember { mutableStateOf(true) }
- var maxLandscapeWidthFraction by remember { mutableFloatStateOf(1F) }
- var preventDismissalOnScrimClick by remember { mutableStateOf(false) }
+ var scrimVisible by rememberSaveable { mutableStateOf(true) }
+ var dynamicSizeContent by rememberSaveable { mutableStateOf(false) }
+ var nestedDrawerContent by rememberSaveable { mutableStateOf(false) }
+ var listContent by rememberSaveable { mutableStateOf(true) }
+ var expandable by rememberSaveable { mutableStateOf(true) }
+ var skipOpenState by rememberSaveable { mutableStateOf(false) }
+ var selectedContent by rememberSaveable { mutableStateOf(ContentType.FULL_SCREEN_SCROLLABLE_CONTENT) }
+ var slideOver by rememberSaveable { mutableStateOf(false) }
+ var showHandle by rememberSaveable { mutableStateOf(true) }
+ 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
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
@@ -412,7 +421,7 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
) {
val scope = rememberCoroutineScope()
- val drawerState = rememberBottomDrawerState(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/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt
index 27fe3f7a1..69b629590 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2BottomSheetActivity.kt
@@ -39,6 +39,7 @@ 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
@@ -75,13 +76,15 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
const val BOTTOM_SHEET_ENABLE_SWIPE_DISMISS_TEST_TAG = "enableSwipeDismiss"
+
class V2BottomSheetActivity : V2DemoActivity() {
init {
setupActivity(this)
}
override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-10"
- override val controlTokensUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-10"
+ override val controlTokensUrl =
+ "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-10"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -94,6 +97,8 @@ class V2BottomSheetActivity : V2DemoActivity() {
@Composable
private fun CreateActivityUI() {
+ var scrimVisible by rememberSaveable { mutableStateOf(false) }
+
var enableSwipeDismiss by remember { mutableStateOf(true) }
var showHandleState by remember { mutableStateOf(true) }
@@ -104,12 +109,14 @@ private fun CreateActivityUI() {
var peekHeightState by remember { mutableStateOf(110.dp) }
+ var preventDismissalOnScrimClick by rememberSaveable { mutableStateOf(false) }
+
var stickyThresholdUpwardDrag: Float by remember { mutableStateOf(56f) }
var stickyThresholdDownwardDrag: Float by remember { mutableStateOf(56f) }
var hidden by remember { mutableStateOf(true) }
- val bottomSheetState = rememberBottomSheetState(BottomSheetValue.Shown)
+ val bottomSheetState = rememberBottomSheetState(BottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
@@ -147,10 +154,12 @@ private fun CreateActivityUI() {
sheetContent = sheetContentState,
expandable = expandableState,
peekHeight = peekHeightState,
+ scrimVisible = scrimVisible,
showHandle = showHandleState,
sheetState = bottomSheetState,
slideOver = slideOverState,
enableSwipeDismiss = enableSwipeDismiss,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
stickyThresholdUpward = stickyThresholdUpwardDrag,
stickyThresholdDownward = stickyThresholdDownwardDrag
) {
@@ -196,28 +205,12 @@ private fun CreateActivityUI() {
}
}
)
- }
- Row(
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth()
- ) {
- Button(
- style = ButtonStyle.OutlinedButton,
- size = ButtonSize.Medium,
- text = "Hide",
- enabled = !hidden,
- onClick = {
- hidden = true
- scope.launch { bottomSheetState.hide() }
- }
- )
Button(
style = ButtonStyle.OutlinedButton,
size = ButtonSize.Medium,
text = "Expand",
- enabled = !hidden && expandableState,
+ enabled = expandableState,
onClick = {
scope.launch { bottomSheetState.expand() }
}
@@ -286,7 +279,7 @@ private fun CreateActivityUI() {
modifier = Modifier.fillMaxWidth()
) {
BasicText(
- text = stringResource(id =R.string.bottom_sheet_text_enable_swipe_dismiss),
+ text = stringResource(id = R.string.bottom_sheet_text_enable_swipe_dismiss),
modifier = Modifier.weight(1F),
style = TextStyle(
color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
@@ -300,6 +293,44 @@ private fun CreateActivityUI() {
onValueChange = { enableSwipeDismiss = it }
)
}
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(30.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ BasicText(
+ text = "Scrim Visible",
+ modifier = Modifier.weight(1F),
+ style = TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ themeMode = ThemeMode.Auto
+ )
+ )
+ )
+ ToggleSwitch(checkedState = scrimVisible,
+ onValueChange = { scrimVisible = it }
+ )
+ }
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(30.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ BasicText(
+ text = "Prevent Dismissal On Scrim Click",
+ modifier = Modifier.weight(1F),
+ style = TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ themeMode = ThemeMode.Auto
+ )
+ )
+ )
+ ToggleSwitch(checkedState = preventDismissalOnScrimClick,
+ onValueChange = { preventDismissalOnScrimClick = it }
+ )
+ }
+
// New Row for Sticky Threshold Downward Drag
Row(
horizontalArrangement = Arrangement.spacedBy(30.dp),
@@ -316,11 +347,15 @@ private fun CreateActivityUI() {
)
)
Slider(
- modifier = Modifier.width(100.dp).height(50.dp).padding(0.dp, 0.dp, 0.dp, 0.dp),
+ modifier = Modifier
+ .width(100.dp)
+ .height(50.dp)
+ .padding(0.dp, 0.dp, 0.dp, 0.dp),
value = stickyThresholdUpwardDrag,
- onValueChange = { stickyThresholdUpwardDrag = it
- peekHeightState+=0.0001.dp
- },
+ onValueChange = {
+ stickyThresholdUpwardDrag = it
+ peekHeightState += 0.0001.dp
+ },
valueRange = 0f..500f,
colors = SliderDefaults.colors(
thumbColor = FluentTheme.aliasTokens.brandColor[FluentAliasTokens.BrandColorTokens.Color100],
@@ -352,11 +387,15 @@ private fun CreateActivityUI() {
)
)
Slider(
- modifier = Modifier.width(100.dp).height(50.dp).padding(0.dp, 0.dp, 0.dp, 0.dp),
+ modifier = Modifier
+ .width(100.dp)
+ .height(50.dp)
+ .padding(0.dp, 0.dp, 0.dp, 0.dp),
value = stickyThresholdDownwardDrag,
- onValueChange = { stickyThresholdDownwardDrag = it
- peekHeightState+=0.0001.dp
- },
+ onValueChange = {
+ stickyThresholdDownwardDrag = it
+ peekHeightState += 0.0001.dp
+ },
valueRange = 0f..500f,
colors = SliderDefaults.colors(
thumbColor = FluentTheme.aliasTokens.brandColor[FluentAliasTokens.BrandColorTokens.Color100],
@@ -390,14 +429,13 @@ private fun CreateActivityUI() {
style = ButtonStyle.Button,
size = ButtonSize.Medium,
text = "+ 8 dp",
- enabled = !hidden,
onClick = { peekHeightState += 8.dp })
Button(
style = ButtonStyle.Button,
size = ButtonSize.Medium,
text = "- 8 dp",
- enabled = !hidden && (peekHeightState > 0.dp),
+ enabled = peekHeightState > 0.dp,
onClick = { peekHeightState -= 8.dp })
}
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ButtonsActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ButtonsActivity.kt
index 3686f7e91..16451ba95 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ButtonsActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ButtonsActivity.kt
@@ -12,14 +12,17 @@ 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.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Divider
+import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.ThumbUp
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
@@ -46,7 +49,9 @@ import com.microsoft.fluentui.theme.token.controlTokens.FABSize
import com.microsoft.fluentui.theme.token.controlTokens.FABState
import com.microsoft.fluentui.tokenized.controls.Button
import com.microsoft.fluentui.tokenized.controls.FloatingActionButton
+import com.microsoft.fluentui.tokenized.controls.RadioButton
import com.microsoft.fluentuidemo.V2DemoActivity
+import kotlinx.coroutines.selects.select
class V2ButtonsActivity : V2DemoActivity() {
init {
@@ -181,8 +186,8 @@ class V2ButtonsActivity : V2DemoActivity() {
}
}
}
-
item {
+ Divider()
FluentTheme {
BasicText(
"Button with selected theme, auto mode and overridden control token",
@@ -195,6 +200,39 @@ class V2ButtonsActivity : V2DemoActivity() {
CreateButtons(MyButtonTokens())
}
}
+ item {
+ Divider()
+ var checkBoxSelectedValues = List(4) { rememberSaveable { mutableStateOf(false) } }
+ FluentTheme {
+ BasicText(
+ "Radio Button Group with selected theme",
+ style = TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ themeMode
+ )
+ )
+ )
+ for(i in 0..3) {
+ Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp, vertical = 3.dp)) {
+ BasicText(
+ "Text",
+ style = TextStyle(
+ color = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ themeMode
+ )
+ )
+ )
+ Spacer(Modifier.width(20.dp))
+ RadioButton(
+ onClick = {
+ selectRadioGroupButton(i, checkBoxSelectedValues)
+ },
+ selected = checkBoxSelectedValues[i].value
+ )
+ }
+ }
+ }
+ }
}
}
FluentTheme {
@@ -311,3 +349,9 @@ class V2ButtonsActivity : V2DemoActivity() {
}
}
}
+
+fun selectRadioGroupButton(buttonNumber: Int, saveableCheckbox: List>){
+ saveableCheckbox.forEachIndexed { index, mutableState ->
+ mutableState.value = index == buttonNumber
+ }
+}
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2CardActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2CardActivity.kt
index ab995d96f..f4822d8fe 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2CardActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2CardActivity.kt
@@ -184,7 +184,7 @@ class V2CardActivity : V2DemoActivity() {
Box(
modifier = Modifier
.clickable(
- interactionSource = MutableInteractionSource(),
+ interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
enabled = true,
onClick = { },
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2DrawerActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2DrawerActivity.kt
index 6338e16d3..b6f0ec5c4 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2DrawerActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2DrawerActivity.kt
@@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
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
@@ -73,20 +73,20 @@ enum class ContentType {
@Composable
private fun CreateActivityUI() {
- var scrimVisible by remember { mutableStateOf(true) }
- var dynamicSizeContent by remember { mutableStateOf(false) }
- var nestedDrawerContent by remember { mutableStateOf(false) }
- var listContent by remember { mutableStateOf(true) }
- var preventDismissalOnScrimClick by remember { mutableStateOf(false) }
- var selectedContent by remember { mutableStateOf(ContentType.FULL_SCREEN_SCROLLABLE_CONTENT) }
- var selectedBehaviorType by remember { mutableStateOf(BehaviorType.BOTTOM_SLIDE_OVER) }
- var relativeToParentAnchor by remember {
+ var scrimVisible by rememberSaveable { mutableStateOf(true) }
+ var dynamicSizeContent by rememberSaveable { mutableStateOf(false) }
+ var nestedDrawerContent by rememberSaveable { mutableStateOf(false) }
+ var listContent by rememberSaveable { mutableStateOf(true) }
+ var preventDismissalOnScrimClick by rememberSaveable { mutableStateOf(false) }
+ var selectedContent by rememberSaveable { mutableStateOf(ContentType.FULL_SCREEN_SCROLLABLE_CONTENT) }
+ var selectedBehaviorType by rememberSaveable { mutableStateOf(BehaviorType.BOTTOM_SLIDE_OVER) }
+ var relativeToParentAnchor by rememberSaveable {
mutableStateOf(
false
)
}
- var offsetX by remember { mutableIntStateOf(0) }
- var offsetY by remember { mutableIntStateOf(0) }
+ var offsetX by rememberSaveable { mutableIntStateOf(0) }
+ var offsetY by rememberSaveable { mutableIntStateOf(0) }
Column {
if (relativeToParentAnchor) {
Row(
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ListItemActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ListItemActivity.kt
index f00359e51..f4684f356 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ListItemActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2ListItemActivity.kt
@@ -8,11 +8,15 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -22,6 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
@@ -33,6 +38,7 @@ import com.microsoft.fluentui.theme.FluentTheme.aliasTokens
import com.microsoft.fluentui.theme.FluentTheme.themeMode
import com.microsoft.fluentui.theme.token.FluentAliasTokens
import com.microsoft.fluentui.theme.token.FluentGlobalTokens
+import com.microsoft.fluentui.theme.token.FluentIcon
import com.microsoft.fluentui.theme.token.Icon
import com.microsoft.fluentui.theme.token.controlTokens.AvatarSize
import com.microsoft.fluentui.theme.token.controlTokens.AvatarSize.Size24
@@ -275,10 +281,30 @@ private fun CreateListActivityUI(context: Context) {
border = BorderType.Bottom
)
ListItem.SectionDescription(description = "Centered action text only supports primary text and ignores any given trailing or leading accessory Contents")
+ GroupedList()
}
}
}
+@Composable
+private fun GroupedList() {
+ ListItem.Header("Grouped List")
+ ListItem.SectionDescription(description = "Grouped List", modifier = Modifier.height(25.dp))
+ Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 10.dp).clip(
+ RoundedCornerShape(10.dp))) {
+ for(i in 0..3) {
+ ListItem.Item(
+ text = "Text",
+ onClick = {},
+ textAlignment = ListItemTextAlignment.Regular,
+ border = BorderType.Bottom,
+ trailingAccessoryContent = { Icon(icon = FluentIcon(Icons.Outlined.KeyboardArrowRight))},
+ )
+ }
+ }
+ ListItem.SectionDescription(description = "Grouped list containing multiple similar elements", modifier = Modifier.wrapContentHeight().padding(0.dp))
+}
+
@Composable
private fun OneLineSimpleList() {
return Column {
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SearchBarActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SearchBarActivity.kt
index 1ca65c256..be34ebb5e 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SearchBarActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SearchBarActivity.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.getValue
@@ -12,11 +13,12 @@ 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.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.icons.SearchBarIcons
import com.microsoft.fluentui.icons.searchbaricons.Office
import com.microsoft.fluentui.theme.token.FluentIcon
@@ -30,6 +32,7 @@ import com.microsoft.fluentui.tokenized.listitem.ListItem
import com.microsoft.fluentui.tokenized.persona.Person
import com.microsoft.fluentui.tokenized.persona.Persona
import com.microsoft.fluentui.tokenized.persona.PersonaList
+import com.microsoft.fluentuidemo.CustomizedSearchBarTokens
import com.microsoft.fluentuidemo.R
import com.microsoft.fluentuidemo.V2DemoActivity
import com.microsoft.fluentuidemo.util.DemoAppStrings
@@ -45,7 +48,6 @@ class V2SearchBarActivity : V2DemoActivity() {
override val paramsUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#params-29"
override val controlTokensUrl = "https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-27"
- @OptIn(ExperimentalComposeUiApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -56,8 +58,8 @@ class V2SearchBarActivity : V2DemoActivity() {
var searchBarStyle: FluentStyle by rememberSaveable { mutableStateOf(FluentStyle.Neutral) }
var displayRightAccessory: Boolean by rememberSaveable { mutableStateOf(true) }
var induceDelay: Boolean by rememberSaveable { mutableStateOf(false) }
-
var selectedPeople: Person? by rememberSaveable { mutableStateOf(null) }
+ var customizedSearchBar: Boolean by rememberSaveable { mutableStateOf(false) }
val listofPeople = listOf(
Person(
@@ -184,6 +186,22 @@ class V2SearchBarActivity : V2DemoActivity() {
)
}
)
+
+ ListItem.Item(
+ text = "Customized Search Bar",
+ subText = if (customizedSearchBar)
+ LocalContext.current.resources.getString(R.string.fluentui_enabled)
+ else
+ LocalContext.current.resources.getString(R.string.fluentui_disabled),
+ trailingAccessoryContent = {
+ ToggleSwitch(
+ onValueChange = {
+ customizedSearchBar = it
+ },
+ checkedState = customizedSearchBar
+ )
+ }
+ )
}
}
@@ -195,6 +213,7 @@ class V2SearchBarActivity : V2DemoActivity() {
val scope = rememberCoroutineScope()
var loading by rememberSaveable { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
+ val showCustomizedAppBar = searchBarStyle == FluentStyle.Neutral && customizedSearchBar
SearchBar(
onValueChange = { query, selectedPerson ->
@@ -251,7 +270,11 @@ class V2SearchBarActivity : V2DemoActivity() {
.show()
}
)
- } else null
+ } else null,
+ searchBarTokens = if (showCustomizedAppBar) {
+ CustomizedSearchBarTokens
+ } else null,
+ modifier = if (showCustomizedAppBar) Modifier.requiredHeight(60.dp) else Modifier
)
val filteredPersona = mutableListOf()
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SegmentedControlActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SegmentedControlActivity.kt
index f1e00772d..3940c548c 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SegmentedControlActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2SegmentedControlActivity.kt
@@ -2,14 +2,19 @@ package com.microsoft.fluentuidemo.demos
import android.os.Bundle
import android.widget.Toast
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.icons.AvatarIcons
@@ -25,20 +30,25 @@ import com.microsoft.fluentui.tokenized.listitem.ChevronOrientation
import com.microsoft.fluentui.tokenized.listitem.ListItem
import com.microsoft.fluentui.tokenized.segmentedcontrols.*
import com.microsoft.fluentuidemo.V2DemoActivity
+import kotlinx.coroutines.launch
+import com.microsoft.fluentui.tokenized.navigation.ViewPager
// Tags used for testing
const val SEGMENTED_CONTROL_PILL_BUTTON = "Segmented Control Pill Button"
const val SEGMENTED_CONTROL_PILL_BAR = "Segmented Control Pill Bar"
const val SEGMENTED_CONTROL_TABS = "Segmented Control Tabs"
const val SEGMENTED_CONTROL_SWITCH = "Segmented Control Switch"
+const val SEGMENTED_CONTROL_VIEW_PAGER = "Segmented Control View Pager"
const val SEGMENTED_CONTROL_PILL_BUTTON_TOGGLE = "Segmented Control Pill Button Toggle"
const val SEGMENTED_CONTROL_PILL_BAR_TOGGLE = "Segmented Control Pill Bar Toggle"
const val SEGMENTED_CONTROL_TABS_TOGGLE = "Segmented Control Tabs Toggle"
const val SEGMENTED_CONTROL_SWITCH_TOGGLE = "Segmented Control Switch Toggle"
+const val SEGMENTED_CONTROL_VIEW_PAGER_TOGGLE = "Segmented Control View Pager Toggle"
const val SEGMENTED_CONTROL_PILL_BUTTON_COMPONENT = "Segmented Control Pill Button Component"
const val SEGMENTED_CONTROL_PILL_BAR_COMPONENT = "Segmented Control Pill Bar Component"
const val SEGMENTED_CONTROL_TABS_COMPONENT = "Segmented Control Tabs Component"
const val SEGMENTED_CONTROL_SWITCH_COMPONENT = "Segmented Control Switch Component"
+const val SEGMENTED_CONTROL_VIEW_PAGER_COMPONENT = "Segmented Control View pager Component"
class V2SegmentedControlActivity : V2DemoActivity() {
@@ -50,6 +60,7 @@ class V2SegmentedControlActivity : V2DemoActivity() {
override val controlTokensUrl =
"https://github.com/microsoft/fluentui-android/wiki/Controls#control-tokens-28"
+ @OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val context = this
@@ -229,8 +240,10 @@ class V2SegmentedControlActivity : V2DemoActivity() {
item {
var enableTabs by rememberSaveable { mutableStateOf(true) }
var selectedTab by rememberSaveable { mutableStateOf(0) }
+ val pagerState = rememberPagerState(pageCount = { 6 })
+ val coroutineScope = rememberCoroutineScope()
- var tabsList: MutableList = mutableListOf()
+ val tabsList: MutableList = mutableListOf()
for (idx in 0..5) {
val label = "Neutral ${idx + 1}"
@@ -245,6 +258,10 @@ class V2SegmentedControlActivity : V2DemoActivity() {
Toast.LENGTH_SHORT
).show()
selectedTab = idx
+ coroutineScope.launch {
+ // Call scroll to on pagerState
+ pagerState.animateScrollToPage(idx)
+ }
},
enabled = enableTabs,
notificationDot = selectedTab != idx
@@ -296,6 +313,66 @@ class V2SegmentedControlActivity : V2DemoActivity() {
}
}
)
+
+ template(
+ "View Pager",
+ testTag = SEGMENTED_CONTROL_VIEW_PAGER,
+ enableSwitch = {
+ ToggleSwitch(
+ Modifier
+ .padding(vertical = 3.dp)
+ .testTag(SEGMENTED_CONTROL_VIEW_PAGER_TOGGLE),
+ onValueChange = { enableTabs = it },
+ checkedState = enableTabs
+ )
+ },
+ neutralContent = {
+ Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
+ PillTabs(
+ modifier = Modifier.testTag(SEGMENTED_CONTROL_VIEW_PAGER_COMPONENT),
+ metadataList = tabsList.subList(0, 4),
+ selectedIndex = selectedTab,
+ scrollable = true
+ )
+ PillTabs(
+ tabsList.subList(0, 4),
+ style = FluentStyle.Brand,
+ selectedIndex = selectedTab,
+ scrollable = false
+ )
+ }
+ },
+ brandContent = {
+ Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
+ PillTabs(
+ tabsList,
+ selectedIndex = selectedTab,
+ scrollable = true
+ )
+ PillTabs(
+ tabsList,
+ style = FluentStyle.Brand,
+ selectedIndex = selectedTab,
+ scrollable = false
+ )
+ ViewPager(pagerState, pageContent = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(
+ color = if (selectedTab % 2 == 0) Color.Cyan else Color.LightGray
+ )
+ ) {
+ BasicText(
+ text = "Page $selectedTab",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }, modifier = Modifier.height(200.dp), userScrollEnabled = true)
+ }
+
+ }
+ )
}
item {
@@ -369,6 +446,7 @@ class V2SegmentedControlActivity : V2DemoActivity() {
}
)
}
+
}
}
}
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2TabBarActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2TabBarActivity.kt
index 088dc5d80..4e1a0d767 100644
--- a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2TabBarActivity.kt
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/V2TabBarActivity.kt
@@ -8,6 +8,7 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -63,7 +64,7 @@ class V2TabBarActivity : V2DemoActivity() {
setActivityContent {
val content = listOf(0, 1, 2)
- var selectedOption by rememberSaveable { mutableStateOf(content[0]) }
+ var selectedOption by rememberSaveable { mutableIntStateOf(content[0]) }
val tabItemsCount = _tabItemsCount.observeAsState(initial = 5)
var showIndicator by rememberSaveable {
mutableStateOf(false)
@@ -205,7 +206,8 @@ class V2TabBarActivity : V2DemoActivity() {
selectedIndex = 0
showHomeBadge = false
},
- badge = { if (selectedIndex == 0 && showHomeBadge) Badge() }
+ badge = { if (selectedIndex == 0 && showHomeBadge) Badge() },
+ accessibilityDescription = resources.getString(R.string.tabBar_home) + ": " + if(selectedIndex == 0) {resources.getString(R.string.Active)} else {resources.getString(R.string.Inactive)}
),
TabData(
title = resources.getString(R.string.tabBar_mail),
@@ -215,7 +217,8 @@ class V2TabBarActivity : V2DemoActivity() {
invokeToast(resources.getString(R.string.tabBar_mail), context)
selectedIndex = 1
},
- badge = { Badge(text = "123+", badgeType = BadgeType.Character) }
+ badge = { Badge(text = "123+", badgeType = BadgeType.Character) },
+ accessibilityDescription = resources.getString(R.string.tabBar_mail) + ": " + if(selectedIndex == 1) {resources.getString(R.string.Active)} else {resources.getString(R.string.Inactive)}
),
TabData(
title = resources.getString(R.string.tabBar_settings),
@@ -224,7 +227,8 @@ class V2TabBarActivity : V2DemoActivity() {
onClick = {
invokeToast(resources.getString(R.string.tabBar_settings), context)
selectedIndex = 2
- }
+ },
+ accessibilityDescription = resources.getString(R.string.tabBar_settings) + ": " + if(selectedIndex == 2) {resources.getString(R.string.Active)} else {resources.getString(R.string.Inactive)}
),
TabData(
title = resources.getString(R.string.tabBar_notification),
@@ -234,7 +238,8 @@ class V2TabBarActivity : V2DemoActivity() {
invokeToast(resources.getString(R.string.tabBar_notification), context)
selectedIndex = 3
},
- badge = { Badge(text = "10", badgeType = BadgeType.Character) }
+ badge = { Badge(text = "10", badgeType = BadgeType.Character) },
+ accessibilityDescription = resources.getString(R.string.tabBar_notification) + ": " + if(selectedIndex == 3) {resources.getString(R.string.Active)} else {resources.getString(R.string.Inactive)}
),
TabData(
title = resources.getString(R.string.tabBar_more),
@@ -245,6 +250,7 @@ class V2TabBarActivity : V2DemoActivity() {
selectedIndex = 4
},
badge = { Badge() },
+ accessibilityDescription = resources.getString(R.string.tabBar_more) + ": " + if(selectedIndex == 4) {resources.getString(R.string.Active)} else {resources.getString(R.string.Inactive)}
)
)
diff --git a/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/actionbar/V2ActionBarDemoActivity.kt b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/actionbar/V2ActionBarDemoActivity.kt
new file mode 100644
index 000000000..8fa1c3020
--- /dev/null
+++ b/FluentUI.Demo/src/main/java/com/microsoft/fluentuidemo/demos/actionbar/V2ActionBarDemoActivity.kt
@@ -0,0 +1,93 @@
+package com.microsoft.fluentuidemo.demos.actionbar
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import com.microsoft.fluentui.compose.Scaffold
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.tokenized.actionbar.ActionBar
+import com.microsoft.fluentui.tokenized.navigation.ViewPager
+import com.microsoft.fluentuidemo.SetStatusBarColor
+import com.microsoft.fluentuidemo.V2DemoActivity
+
+class V2ActionBarDemoActivity : V2DemoActivity() {
+ init {
+ setupActivity(this)
+ }
+
+ @OptIn(ExperimentalFoundationApi::class)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val context = this
+ val selectedActionBarType = intent.getIntExtra("ACTION_BAR_TYPE", 0)
+ val selectedActionBarPosition = intent.getIntExtra("ACTION_BAR_POSITION", 0)
+ setContent {
+ FluentTheme {
+ SetStatusBarColor()
+ val noOfPages = 5
+ val pagerState = rememberPagerState(pageCount = { noOfPages })
+
+ val actionBar = @androidx.compose.runtime.Composable {
+ ActionBar(
+ pagerState = pagerState,
+ startCallback = {
+ this.finish()
+ },
+ type = selectedActionBarType
+ )
+ }
+ Scaffold(
+ contentWindowInsets = WindowInsets.statusBars,
+ topBar = if (selectedActionBarPosition == 0)
+ actionBar
+ else {
+ {}
+ },
+ bottomBar = if (selectedActionBarPosition == 1) {
+ actionBar
+ } else {
+ {}
+ }
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value())
+ .padding(it)
+ ) {
+ ViewPager(
+ pagerState = pagerState,
+ modifier = Modifier.fillMaxSize(),
+ pageContent = {
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(
+ color = if (pagerState.currentPage % 2 == 0) Color.Cyan else Color.LightGray
+ )
+ ) {
+ BasicText(
+ text = "Page ${pagerState.currentPage}",
+ modifier = Modifier.align(Alignment.Center)
+ )
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/FluentUI.Demo/src/main/res/values/ids.xml b/FluentUI.Demo/src/main/res/values/ids.xml
index f1ca268fa..0d8fff24a 100644
--- a/FluentUI.Demo/src/main/res/values/ids.xml
+++ b/FluentUI.Demo/src/main/res/values/ids.xml
@@ -18,6 +18,7 @@
+
diff --git a/FluentUI.Demo/src/main/res/values/strings.xml b/FluentUI.Demo/src/main/res/values/strings.xml
index 026632a7b..aba0eeb5c 100644
--- a/FluentUI.Demo/src/main/res/values/strings.xml
+++ b/FluentUI.Demo/src/main/res/values/strings.xml
@@ -42,6 +42,18 @@
Navigation icon clicked.
+
+ Center align app bar
+
+
+ App Bar size
+
+
+ Left Logo
+
+
+ Navigation Icon
+
Flag
Settings
@@ -248,6 +260,10 @@
Delete
Delete item clicked
+
+ Toggle
+
+ Toggle item clicked
Avatar
@@ -439,9 +455,9 @@
- Expand Persistent BottomSheet
- Hide Persistent BottomSheet
- Show Persistent BottomSheet
+ Expand Persistent Bottom Sheet
+ Hide Persistent Bottom Sheet
+ Show Persistent Bottom Sheet
This is New View
Toggle Bottomsheet Content
Switch to custom Content
diff --git a/FluentUI/build.gradle b/FluentUI/build.gradle
index aefb6fc90..8acde506d 100644
--- a/FluentUI/build.gradle
+++ b/FluentUI/build.gradle
@@ -30,6 +30,12 @@ android {
mavenCentral()
}
}
+ lint {
+ baseline = file("lint-baseline.xml")
+ }
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/FluentUI/lint-baseline.xml b/FluentUI/lint-baseline.xml
new file mode 100644
index 000000000..ce7fbf551
--- /dev/null
+++ b/FluentUI/lint-baseline.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FluentUI/src/main/res/values-night/themes.xml b/FluentUI/src/main/res/values-night/themes.xml
index 323b6cee5..480d74b5f 100644
--- a/FluentUI/src/main/res/values-night/themes.xml
+++ b/FluentUI/src/main/res/values-night/themes.xml
@@ -51,14 +51,6 @@
- @color/fluentui_communication_tint_40
- @color/fluentui_gray_800
-
- - ?attr/fluentuiBackgroundSecondaryColor
- - ?attr/fluentuiBackgroundSecondaryColor
- - @color/fluentui_gray_700
- - ?attr/fluentuiBackgroundSecondaryColor
- - ?attr/fluentuiForegroundColor
- - @color/fluentui_gray_600
-
- @color/fluentui_gray_600
- @color/fluentui_gray_900
diff --git a/FluentUI/src/main/res/values/themes.xml b/FluentUI/src/main/res/values/themes.xml
index 4579b1086..6671b21df 100644
--- a/FluentUI/src/main/res/values/themes.xml
+++ b/FluentUI/src/main/res/values/themes.xml
@@ -95,18 +95,6 @@
- ?attr/fluentuiForegroundSecondaryIconColor
- ?attr/fluentuiForegroundSelectedColor
-
- - ?attr/fluentuiBackgroundColor
- - ?attr/fluentuiBackgroundColor
- - ?attr/fluentuiForegroundSecondaryColor
- - @color/fluentui_gray_400
- - ?attr/fluentuiBackgroundColor
- - ?attr/fluentuiForegroundColor
- - @color/fluentui_gray_25
- - ?attr/fluentuiForegroundSelectedColor
- - ?attr/fluentuiColorPrimaryLighter
- - @color/fluentui_gray_600
-
- @color/fluentui_gray_50
- @color/fluentui_gray_100
diff --git a/NOTICE b/NOTICE
index 66356db6b..7a4736969 100644
--- a/NOTICE
+++ b/NOTICE
@@ -225,213 +225,6 @@ Copyright (C) 2008 The Android Open Source Project
===============================================================================
-ThreeTen Android Backport
-Copyright (C) 2015 Jake Wharton
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-
-===============================================================================
-
TokenAutoComplete
Copyright (c) 2013, 2014 splitwise, Wouter Dullaert
diff --git a/README.md b/README.md
index 60df97774..a27833bd6 100644
--- a/README.md
+++ b/README.md
@@ -147,20 +147,6 @@ dependencies {
More information about contents of each module can be found in [Modularization](#modularization) section
-
-#### a) Develop for Surface-Duo:
-- Please also add the following lines to your repositories section in your gradle script:
-```gradle
-maven {
- url "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1"
-}
-```
-- Also add the SDK dependency to the module-level build.gradle file(current version may be different
-from what's shown here):
-```gradle
-implementation "com.microsoft.device:dualscreen-layout:1.0.0-alpha01"
-```
-
### 2. Using Maven
- Add the FluentUI library as a dependency:
@@ -199,10 +185,7 @@ implementation "com.microsoft.device:dualscreen-layout:1.0.0-alpha01"
```gradle
implementation 'com.splitwise:tokenautocomplete:2.0.8'
```
- - If using **CalendarView** or **DateTimePickerDialog**, include this dependency in your gradle file:
- ```gradle
- implementation 'com.jakewharton.threetenabp:threetenabp:1.1.0'
- ```
+
- Double check that these library versions correspond to the latest versions we implement in the FluentUI [build.gradle](fluentui_others\build.gradle).
### Import and use the library
diff --git a/build.gradle b/build.gradle
index cdc54f62f..8cff2fbf8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -32,7 +32,7 @@ apply plugin: "com.microsoft.hydralab.client-util"
allprojects {
project.ext {
constants = [
- minSdkVersion: 21,
+ minSdkVersion: 23,
targetSdkVersion: 34,
compileSdkVersion: 34
]
@@ -49,7 +49,6 @@ allprojects {
composeCompilerVersion = '1.4.7'
constraintLayoutVersion = '2.1.4'
constraintLayoutComposeVersion = '1.0.1'
- duoVersion = '1.0.0-alpha01'
espressoVersion = '3.5.1'
exifInterfaceVersion = '1.3.6'
extJunitVersion = '1.1.5'
@@ -65,15 +64,12 @@ allprojects {
uiautomatorVersion = '2.2.0'
supportVersion = '28.0.0'
tokenautocompleteVersion = '2.0.8'
- threetenabpVersion = '1.1.0'
universalPkgDir = "universal"
+ composeFoundationVersion = '1.6.0'
}
repositories {
google()
jcenter()
- maven {
- url "https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1"
- }
}
}
diff --git a/config.gradle b/config.gradle
index a66eb86e4..d36d1a102 100644
--- a/config.gradle
+++ b/config.gradle
@@ -11,40 +11,40 @@
* and fluentui_drawer' current version is 0.0.2, both fluentui_listitem and
* fluentui_drawer and FluentUI should increment their respective version ids
*/
-project.ext.fluentui_calendar_versionid = '0.2.1'
-project.ext.fluentui_controls_versionid = '0.2.7'
-project.ext.fluentui_core_versionid = '0.2.8'
-project.ext.fluentui_listitem_versionid = '0.2.3'
-project.ext.fluentui_tablayout_versionid = '0.2.3'
-project.ext.fluentui_drawer_versionid = '0.2.8'
-project.ext.fluentui_ccb_versionid = '0.2.1'
-project.ext.fluentui_others_versionid = '0.2.1'
-project.ext.fluentui_transients_versionid = '0.2.1'
-project.ext.fluentui_topappbars_versionid = '0.2.1'
-project.ext.fluentui_menus_versionid = '0.2.1'
-project.ext.fluentui_peoplepicker_versionid = '0.2.2'
-project.ext.fluentui_persona_versionid = '0.2.2'
-project.ext.fluentui_progress_versionid = '0.2.1'
-project.ext.fluentui_icons_versionid = '0.2.1'
-project.ext.fluentui_notification_versionid = '0.2.3'
-project.ext.FluentUI_versionid = '0.2.12'
-project.ext.fluentui_calendar_version_code = 1001
-project.ext.fluentui_controls_version_code = 1007
-project.ext.fluentui_core_version_code = 1008
-project.ext.fluentui_listitem_version_code = 1003
-project.ext.fluentui_tablayout_version_code = 1003
-project.ext.fluentui_drawer_version_code = 1008
-project.ext.fluentui_ccb_version_code = 1001
-project.ext.fluentui_others_version_code = 1001
-project.ext.fluentui_transients_version_code = 1001
-project.ext.fluentui_topappbars_version_code = 1001
-project.ext.fluentui_menus_version_code = 1001
-project.ext.fluentui_peoplepicker_version_code = 1002
-project.ext.fluentui_persona_version_code = 1002
-project.ext.fluentui_progress_version_code = 1001
-project.ext.fluentui_icons_version_code = 1001
-project.ext.fluentui_notification_version_code = 1003
-project.ext.FluentUI_version_code = 1012
+project.ext.fluentui_calendar_versionid = '0.3.2'
+project.ext.fluentui_controls_versionid = '0.3.1'
+project.ext.fluentui_core_versionid = '0.3.5'
+project.ext.fluentui_listitem_versionid = '0.3.4'
+project.ext.fluentui_tablayout_versionid = '0.3.3'
+project.ext.fluentui_drawer_versionid = '0.3.4'
+project.ext.fluentui_ccb_versionid = '0.3.2'
+project.ext.fluentui_others_versionid = '0.3.4'
+project.ext.fluentui_transients_versionid = '0.3.3'
+project.ext.fluentui_topappbars_versionid = '0.3.4'
+project.ext.fluentui_menus_versionid = '0.3.3'
+project.ext.fluentui_peoplepicker_versionid = '0.3.3'
+project.ext.fluentui_persona_versionid = '0.3.3'
+project.ext.fluentui_progress_versionid = '0.3.2'
+project.ext.fluentui_icons_versionid = '0.3.2'
+project.ext.fluentui_notification_versionid = '0.3.2'
+project.ext.FluentUI_versionid = '0.3.5'
+project.ext.fluentui_calendar_version_code = 2002
+project.ext.fluentui_controls_version_code = 2001
+project.ext.fluentui_core_version_code = 2005
+project.ext.fluentui_listitem_version_code = 2004
+project.ext.fluentui_tablayout_version_code = 2003
+project.ext.fluentui_drawer_version_code = 2004
+project.ext.fluentui_ccb_version_code = 2002
+project.ext.fluentui_others_version_code = 2004
+project.ext.fluentui_transients_version_code = 2003
+project.ext.fluentui_topappbars_version_code = 2004
+project.ext.fluentui_menus_version_code = 2003
+project.ext.fluentui_peoplepicker_version_code = 2003
+project.ext.fluentui_persona_version_code = 2003
+project.ext.fluentui_progress_version_code = 2002
+project.ext.fluentui_icons_version_code = 2002
+project.ext.fluentui_notification_version_code = 2002
+project.ext.FluentUI_version_code = 2005
project.ext.license_type = 'MIT License'
project.ext.license_url = 'https://github.com/microsoft/fluentui-android/blob/master/LICENSE'
project.ext.github_url = 'https://github.com/microsoft/fluentui-android'
diff --git a/fluentui-android-release.yml b/fluentui-android-release.yml
new file mode 100644
index 000000000..dff843f40
--- /dev/null
+++ b/fluentui-android-release.yml
@@ -0,0 +1,92 @@
+trigger: none
+name: $(Date:yyyyMMdd).$(Rev:r)
+resources:
+ pipelines:
+ - pipeline: 'fluentui-android-maven-publish'
+ project: 'fluentui-native'
+ source: 'fluentui-maven-central-publish [1es-pt]'
+ repositories:
+ - repository: 1ESPipelineTemplates
+ type: git
+ name: 1ESPipelineTemplates/1ESPipelineTemplates
+ ref: refs/tags/release
+extends:
+ template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
+ parameters:
+ pool:
+ name: Azure-Pipelines-1ESPT-ExDShared
+ image: windows-2022
+ os: windows
+ customBuildTags:
+ - ES365AIMigrationTooling-Release
+ stages:
+ - stage: Stage_1
+ displayName: ESRP Release
+ jobs:
+ - job: Job_1
+ displayName: Agent job
+ condition: succeeded()
+ timeoutInMinutes: 0
+ templateContext:
+ type: releaseJob
+ isProduction: true
+ inputs:
+ - input: pipelineArtifact
+ buildType: 'specific'
+ project: '$(projectName)'
+ definition: '$(pipelineDefinition)'
+ buildVersionToDownload: 'specific'
+ pipelineId: '$(buildId)'
+ artifactName: 'Build'
+ targetPath: '$(Pipeline.Workspace)/fluentui-android-maven-publish/Build'
+ steps:
+ - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@7
+ displayName: 'ESRP Release'
+ inputs:
+ connectedservicename: '$(connectedServiceName)'
+ keyvaultname: $(keyVaultName)
+ authcertname: $(authCertName)
+ signcertname: '$(signCertName) '
+ clientid: '$(clientId)'
+ folderlocation: '$(Pipeline.Workspace)/fluentui-android-maven-publish/Build'
+ owners: '$(owners)'
+ approvers: '$(approvers)'
+ mainpublisher: fluentuiandroidrelease
+ - stage: Stage_2
+ displayName: AppCenter Release
+ jobs:
+ - job: Job_1
+ displayName: Agent job
+ condition: succeeded()
+ timeoutInMinutes: 0
+ templateContext:
+ type: releaseJob
+ isProduction: true
+ inputs:
+ - input: pipelineArtifact
+ buildType: 'specific'
+ project: '$(projectName)'
+ definition: '$(pipelineDefinition)'
+ buildVersionToDownload: 'specific'
+ pipelineId: '$(buildId)'
+ artifactName: 'dogfood'
+ targetPath: '$(Pipeline.Workspace)/fluentui-android-maven-publish/dogfood'
+ - input: pipelineArtifact
+ buildType: 'specific'
+ project: '$(projectName)'
+ definition: '$(pipelineDefinition)'
+ buildVersionToDownload: 'specific'
+ pipelineId: '$(buildId)'
+ artifactName: 'notes'
+ targetPath: '$(Pipeline.Workspace)/fluentui-android-maven-publish/notes'
+ steps:
+ - task: AppCenterDistribute@3
+ displayName: Deploy $(Pipeline.Workspace)/fluentui-android-maven-publish/dogfood/FluentUI.Demo-dogfood-release.apk to Visual Studio App Center
+ inputs:
+ serverEndpoint: $(serverEndpoint)
+ appSlug: $(appSlug)
+ app: $(Pipeline.Workspace)/fluentui-android-maven-publish/dogfood/FluentUI.Demo-dogfood-release.apk
+ symbolsType: Android
+ releaseNotesSelection: file
+ releaseNotesFile: $(Pipeline.Workspace)/fluentui-android-maven-publish/notes/dogfood-release-notes.txt
+ isSilent: false
diff --git a/fluentui-office-build-universal-publish-1espt.yml b/fluentui-office-build-universal-publish-1espt.yml
index b67cec018..63932b747 100644
--- a/fluentui-office-build-universal-publish-1espt.yml
+++ b/fluentui-office-build-universal-publish-1espt.yml
@@ -62,6 +62,6 @@ extends:
vstsFeedPublish: 'Office'
vstsFeedPackagePublish: 'fluentuiandroid'
versionOption: 'custom'
- versionPublish: '0.2.12'
+ versionPublish: '0.3.4'
packagePublishDescription: 'Fluent Universal Package'
publishedPackageVar: 'fluent package'
\ No newline at end of file
diff --git a/fluentui_calendar/build.gradle b/fluentui_calendar/build.gradle
index a1782c5a4..339947952 100644
--- a/fluentui_calendar/build.gradle
+++ b/fluentui_calendar/build.gradle
@@ -57,8 +57,6 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
- implementation "com.microsoft.device:dualscreen-layout:$duoVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarAdapter.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarAdapter.kt
index 37d2c7b98..ce59231a8 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarAdapter.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarAdapter.kt
@@ -18,9 +18,15 @@ import android.view.ViewGroup
import com.microsoft.fluentui.calendar.CalendarDaySelectionDrawable.Mode
import com.microsoft.fluentui.managers.PreferencesManager
import com.microsoft.fluentui.util.DateTimeUtils
-import org.threeten.bp.*
-import org.threeten.bp.temporal.ChronoUnit
import java.lang.StringBuilder
+import java.time.DayOfWeek
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
/**
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarDayView.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarDayView.kt
index a3e603f35..83c60c346 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarDayView.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarDayView.kt
@@ -30,10 +30,10 @@ import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
import com.microsoft.fluentui.util.DateStringUtils
import com.microsoft.fluentui.util.DateTimeUtils
import com.microsoft.fluentui.util.isAccessibilityEnabled
-import org.threeten.bp.LocalDate
-import org.threeten.bp.ZonedDateTime
-import org.threeten.bp.format.DateTimeFormatter
-import org.threeten.bp.temporal.ChronoUnit
+import java.time.LocalDate
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
import java.util.*
/**
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarView.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarView.kt
index cd8063c4f..380a8fa26 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarView.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/CalendarView.kt
@@ -16,10 +16,13 @@ import android.util.AttributeSet
import android.util.Property
import android.view.View
import android.widget.LinearLayout
-import com.jakewharton.threetenabp.AndroidThreeTen
import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
import com.microsoft.fluentui.util.ThemeUtil
-import org.threeten.bp.*
+import java.time.Duration
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZonedDateTime
// TODO: Convert to TemplateView along with other things that extend LinearLayout
// TODO: implement ability to add icon to CalendarDayView
@@ -108,10 +111,6 @@ class CalendarView : LinearLayout, OnDateSelectedListener {
}
}
- init {
- AndroidThreeTen.init(context)
- }
-
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : super(FluentUIContextThemeWrapper(context,R.style.Theme_FluentUI_Calendar), attrs, defStyleAttr) {
dividerHeight = Math.round(resources.getDimension(R.dimen.fluentui_divider_height))
@@ -192,7 +191,7 @@ class CalendarView : LinearLayout, OnDateSelectedListener {
}
}
- super.onMeasure(widthMeasureSpec, View.MeasureSpec.makeMeasureSpec(computeHeight(displayMode), View.MeasureSpec.EXACTLY))
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(computeHeight(displayMode), View.MeasureSpec.EXACTLY))
}
override fun onDateSelected(dateTime: ZonedDateTime) {
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeekHeadingView.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeekHeadingView.kt
index 681aa4c7e..425a46c93 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeekHeadingView.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeekHeadingView.kt
@@ -15,9 +15,8 @@ import android.widget.LinearLayout
import android.widget.TextView
import com.microsoft.fluentui.calendar.CalendarView.Companion.WEEK_MID
import com.microsoft.fluentui.managers.PreferencesManager
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.microsoft.fluentui.util.activity
-import org.threeten.bp.DayOfWeek
+import java.time.DayOfWeek
/**
* [WeekHeadingView] is a LinearLayout holding the [CalendarView] header with views for
@@ -51,23 +50,7 @@ internal class WeekHeadingView : LinearLayout {
textView.gravity = Gravity.CENTER
post {
context.activity?.let {
- if (DuoSupportUtils.intersectHinge(it, this)) {
- when {
- currentDay < WEEK_MID -> {
- addView(textView, LayoutParams(0, LayoutParams.MATCH_PARENT, (DuoSupportUtils.getHalfScreenWidth(it) / DuoSupportUtils.COLUMNS_IN_START_DUO_MODE).toFloat()))
- }
- currentDay == WEEK_MID -> {
- addView(textView, LayoutParams(0, LayoutParams.MATCH_PARENT, (DuoSupportUtils.getHalfScreenWidth(it) / DuoSupportUtils.COLUMNS_IN_START_DUO_MODE).toFloat()))
- addView(View(context), LayoutParams(0, LayoutParams.MATCH_PARENT, (DuoSupportUtils.getHingeWidth(it).toFloat())))
- }
- else -> {
- addView(textView, LayoutParams(0, LayoutParams.MATCH_PARENT, (DuoSupportUtils.getHalfScreenWidth(it) / DuoSupportUtils.COLUMNS_IN_END_DUO_MODE).toFloat()))
-
- }
- }
- } else {
- addView(textView, LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f))
- }
+ addView(textView, LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f))
}
}
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeeksView.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeeksView.kt
index 67a549904..f335f2010 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeeksView.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/calendar/WeeksView.kt
@@ -29,16 +29,13 @@ import android.view.View
import com.microsoft.fluentui.util.ColorProperty
import com.microsoft.fluentui.util.DateTimeUtils
-import com.microsoft.fluentui.util.activity
-import com.microsoft.fluentui.util.DuoSupportUtils
-import com.microsoft.fluentui.util.displaySize
import com.microsoft.fluentui.view.MSRecyclerView
-import org.threeten.bp.Duration
-import org.threeten.bp.LocalDate
-import org.threeten.bp.Month
-import org.threeten.bp.ZonedDateTime
-import org.threeten.bp.chrono.IsoChronology
-import org.threeten.bp.temporal.ChronoUnit
+import java.time.Duration
+import java.time.LocalDate
+import java.time.Month
+import java.time.ZonedDateTime
+import java.time.chrono.IsoChronology
+import java.time.temporal.ChronoUnit
import java.util.*
/**
@@ -118,15 +115,6 @@ internal class WeeksView : MSRecyclerView {
setHasFixedSize(true)
layoutManager = GridLayoutManager(context, DAYS_IN_WEEK, LinearLayoutManager.VERTICAL, false)
layoutManager?.scrollToPosition(pickerAdapter.todayPosition)
- post {
- context.activity?.let {
- if (DuoSupportUtils.intersectHinge(it, this)) {
- (layoutManager as GridLayoutManager).spanCount = context.displaySize.x
- addItemDecoration(HingeItemDecoration(DuoSupportUtils.getHingeWidth(it)))
- (layoutManager as GridLayoutManager).spanSizeLookup = DuoSupportUtils.getSpanSizeLookup(it)
- }
- }
- }
itemAnimator = null
@@ -197,32 +185,7 @@ internal class WeeksView : MSRecyclerView {
paint.getTextBounds(text, 0, text.length, textBounds)
paint.color = overlayFontColorProperty.color
-
- context.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it)) {
- // For duo mode we show month name both on left and right screen
- // This shows on start 1/4th screen position
- canvas.drawText(text,
- (measuredWidth/4 - textBounds.width()/2).toFloat(),
- (((monthDescriptor.bottom + monthDescriptor.top)- textBounds.height()) / 2).toFloat(),
- paint
- )
- // This shows on 3/4th screen position
- canvas.drawText(text,
- ((3*measuredWidth)/4 - textBounds.width()/2).toFloat(),
- (((monthDescriptor.bottom + monthDescriptor.top)- textBounds.height()) / 2).toFloat(),
- paint
- )
- }
- else {
- // Show on 1/2 screen position
- canvas.drawText(text,
- ((measuredWidth - textBounds.width()) / 2).toFloat(),
- (((monthDescriptor.bottom + monthDescriptor.top)- textBounds.height()) / 2).toFloat(),
- paint
- )
- }
- } ?: canvas.drawText(text,
+ canvas.drawText(text,
((measuredWidth - textBounds.width()) / 2).toFloat(),
(((monthDescriptor.bottom + monthDescriptor.top)- textBounds.height()) / 2).toFloat(),
paint
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePicker.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePicker.kt
index 4df5294e7..95243b9eb 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePicker.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePicker.kt
@@ -9,12 +9,11 @@ import android.app.Dialog
import android.content.Context
import android.os.Bundle
import androidx.appcompat.app.AppCompatDialogFragment
-import com.jakewharton.threetenabp.AndroidThreeTen
import com.microsoft.fluentui.datetimepicker.DateTimePickerDialog.*
import com.microsoft.fluentui.util.DateTimeUtils
import com.microsoft.fluentui.util.isAccessibilityEnabled
-import org.threeten.bp.Duration
-import org.threeten.bp.ZonedDateTime
+import java.time.Duration
+import java.time.ZonedDateTime
/**
* [DateTimePicker] houses a [DateTimePickerDialog] and provides state management for the dialog.
@@ -56,10 +55,6 @@ class DateTimePicker : AppCompatDialogFragment(), OnDateTimeSelectedListener, On
}
}
- init {
- AndroidThreeTen.init(context)
- }
-
private lateinit var displayMode: DisplayMode
private lateinit var dateRangeMode: DateRangeMode
private lateinit var dateTime: ZonedDateTime
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePickerDialog.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePickerDialog.kt
index 54b7cbbb1..0f1582426 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePickerDialog.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/DateTimePickerDialog.kt
@@ -16,16 +16,15 @@ import android.view.*
import android.view.accessibility.AccessibilityEvent
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
-import com.jakewharton.threetenabp.AndroidThreeTen
import com.microsoft.fluentui.calendar.R
import com.microsoft.fluentui.calendar.CalendarView
import com.microsoft.fluentui.calendar.OnDateSelectedListener
import com.microsoft.fluentui.calendar.databinding.DialogDateTimePickerBinding
import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
import com.microsoft.fluentui.util.*
-import org.threeten.bp.Duration
-import org.threeten.bp.ZonedDateTime
import com.microsoft.fluentui.calendar.databinding.DialogResizableBinding
+import java.time.Duration
+import java.time.ZonedDateTime
// TODO consider merging PickerMode and DateRangeMode since not all combinations will work
/**
@@ -118,10 +117,6 @@ class DateTimePickerDialog : AppCompatDialog, Toolbar.OnMenuItemClickListener, O
private lateinit var dialogContainerBinding: DialogDateTimePickerBinding
private lateinit var pagerAdapter: DateTimePagerAdapter
- init {
- AndroidThreeTen.init(context)
- }
-
@JvmOverloads
constructor(
context: Context,
@@ -217,14 +212,7 @@ class DateTimePickerDialog : AppCompatDialog, Toolbar.OnMenuItemClickListener, O
override fun onStart() {
super.onStart()
- context.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it)) {
- window?.setLayout(DuoSupportUtils.getSingleScreenWidthPixels(it),WindowManager.LayoutParams.MATCH_PARENT)
- }
- else {
- window?.setLayout(context.desiredDialogSize[0], WindowManager.LayoutParams.MATCH_PARENT)
- }
- } ?: window?.setLayout(context.desiredDialogSize[0], WindowManager.LayoutParams.MATCH_PARENT)
+ window?.setLayout(context.desiredDialogSize[0], WindowManager.LayoutParams.MATCH_PARENT)
}
override fun dismiss() {
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimePicker.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimePicker.kt
index aef6a0e0d..723e0a8ee 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimePicker.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimePicker.kt
@@ -22,9 +22,13 @@ import com.microsoft.fluentui.managers.PreferencesManager
import com.microsoft.fluentui.util.DateStringUtils
import com.microsoft.fluentui.util.DateTimeUtils
import com.microsoft.fluentui.view.NumberPicker
-import org.threeten.bp.*
-import org.threeten.bp.temporal.ChronoUnit
import java.text.DateFormatSymbols
+import java.time.Duration
+import java.time.LocalDate
+import java.time.YearMonth
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.temporal.ChronoUnit
/**
* [TimePicker] houses [NumberPicker]s that allow users to pick dates, times and periods (12 hour clocks).
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimeSlot.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimeSlot.kt
index ed8b7c386..6745b6866 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimeSlot.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/datetimepicker/TimeSlot.kt
@@ -5,9 +5,9 @@
package com.microsoft.fluentui.datetimepicker
-import org.threeten.bp.Duration
-import org.threeten.bp.ZonedDateTime
import java.io.Serializable
+import java.time.Duration
+import java.time.ZonedDateTime
// TODO PBI #668220 investigate whether it's feasible to replace dateTime + duration with this data class
data class TimeSlot(val start: ZonedDateTime, val duration: Duration) : Serializable
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/managers/PreferencesManager.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/managers/PreferencesManager.kt
index 3f65b10e4..4614014cb 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/managers/PreferencesManager.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/managers/PreferencesManager.kt
@@ -6,7 +6,7 @@
package com.microsoft.fluentui.managers
import android.content.Context
-import org.threeten.bp.DayOfWeek
+import java.time.DayOfWeek
/**
* [PreferencesManager] helper methods dealing with device SharedPreferences
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateStringUtils.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateStringUtils.kt
index 5fed57911..d5c7d997d 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateStringUtils.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateStringUtils.kt
@@ -6,15 +6,14 @@
package com.microsoft.fluentui.util
import android.content.Context
-import android.text.format.DateUtils
import android.text.format.DateUtils.*
import com.microsoft.fluentui.calendar.R
-import org.threeten.bp.LocalDate
-import org.threeten.bp.LocalDateTime
-import org.threeten.bp.ZoneId
-import org.threeten.bp.ZonedDateTime
-import org.threeten.bp.temporal.TemporalAccessor
import java.text.SimpleDateFormat
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.temporal.TemporalAccessor
import java.util.*
/**
@@ -54,7 +53,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatDateWithWeekDay(context: Context, date: Long): String =
- DateUtils.formatDateTime(context, date,FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY)
+ formatDateTime(context, date,FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY)
/**
* Formats a date with the abbreviated weekday + month + day
@@ -73,7 +72,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatDateAbbrevAll(context: Context, time: Long): String =
- DateUtils.formatDateTime(context, time, FORMAT_ABBREV_ALL or FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY)
+ formatDateTime(context, time, FORMAT_ABBREV_ALL or FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY)
/**
* Formats the month day and year
@@ -83,7 +82,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatMonthDayYear(context: Context, date: TemporalAccessor): String =
- DateUtils.formatDateTime(context, date.epochMillis, 0)
+ formatDateTime(context, date.epochMillis, 0)
/**
* Formats a date with the weekday + month + day + Time. The year is optionally formatted if it
@@ -97,7 +96,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatFullDateTime(context: Context, time: Long): String =
- DateUtils.formatDateTime(context, time, FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY or FORMAT_SHOW_TIME)
+ formatDateTime(context, time, FORMAT_SHOW_DATE or FORMAT_SHOW_WEEKDAY or FORMAT_SHOW_TIME)
/**
* @see .formatFullDateTime
@@ -131,7 +130,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatAbbrevTime(context: Context, dateTime: TemporalAccessor): String =
- DateUtils.formatDateTime(context, dateTime.epochMillis, FORMAT_SHOW_TIME or FORMAT_ABBREV_TIME)
+ formatDateTime(context, dateTime.epochMillis, FORMAT_SHOW_TIME or FORMAT_ABBREV_TIME)
/**
* Formats a date with abbreviated Weekday + Date + Year
@@ -143,7 +142,7 @@ object DateStringUtils {
*/
@JvmStatic
fun formatWeekdayDateYearAbbrev(context: Context, date: TemporalAccessor): String =
- DateUtils.formatDateTime(
+ formatDateTime(
context,
date.epochMillis,
FORMAT_ABBREV_WEEKDAY or FORMAT_ABBREV_MONTH or FORMAT_SHOW_WEEKDAY or FORMAT_SHOW_DATE or FORMAT_SHOW_YEAR
@@ -159,8 +158,8 @@ object DateStringUtils {
if (calendar.get(Calendar.YEAR) != currentYear)
flags = flags or FORMAT_SHOW_YEAR
- val date = DateUtils.formatDateTime(context, timestamp, flags)
- val time = DateUtils.formatDateTime(context, timestamp, FORMAT_SHOW_TIME)
+ val date = formatDateTime(context, timestamp, flags)
+ val time = formatDateTime(context, timestamp, FORMAT_SHOW_TIME)
return context.getString(stringResource, date, time)
}
diff --git a/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateTimeUtils.kt b/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateTimeUtils.kt
index 915cff76b..ba1b919e1 100644
--- a/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateTimeUtils.kt
+++ b/fluentui_calendar/src/main/java/com/microsoft/fluentui/util/DateTimeUtils.kt
@@ -5,9 +5,14 @@
package com.microsoft.fluentui.util
-import org.threeten.bp.*
-import org.threeten.bp.format.DateTimeFormatter
-import org.threeten.bp.format.DateTimeParseException
+import java.time.DayOfWeek
+import java.time.Duration
+import java.time.Instant
+import java.time.LocalDate
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
/**
* [DateTimeUtils] contains helper methods for manipulating and parsing dates
diff --git a/fluentui_calendar/src/main/res/values-night/themes.xml b/fluentui_calendar/src/main/res/values-night/themes.xml
new file mode 100644
index 000000000..e2ceb95a5
--- /dev/null
+++ b/fluentui_calendar/src/main/res/values-night/themes.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/fluentui_calendar/src/main/res/values/themes.xml b/fluentui_calendar/src/main/res/values/themes.xml
index df5643eea..fb7d6bd34 100644
--- a/fluentui_calendar/src/main/res/values/themes.xml
+++ b/fluentui_calendar/src/main/res/values/themes.xml
@@ -28,6 +28,7 @@
- ?attr/fluentuiForegroundOnPrimaryColor
- ?attr/fluentuiForegroundOnPrimaryColor
- ?attr/fluentuiForegroundSecondaryColor
+ - @color/fluentui_gray_600
- ?attr/colorPrimary
diff --git a/fluentui_ccb/build.gradle b/fluentui_ccb/build.gradle
index 77889b480..9475fdd3a 100644
--- a/fluentui_ccb/build.gradle
+++ b/fluentui_ccb/build.gradle
@@ -33,6 +33,9 @@ android {
buildFeatures {
compose true
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_core/build.gradle b/fluentui_core/build.gradle
index bf28b4255..89293d399 100644
--- a/fluentui_core/build.gradle
+++ b/fluentui_core/build.gradle
@@ -46,6 +46,12 @@ android {
}
productFlavors {
}
+ lint {
+ baseline = file("lint-baseline.xml")
+ }
+ lintOptions {
+ abortOnError false
+ }
}
gradle.taskGraph.whenReady { taskGraph ->
@@ -66,8 +72,8 @@ dependencies {
implementation "androidx.cardview:cardview:1.0.0"
implementation "com.google.android.material:material:$materialVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation "com.microsoft.device:dualscreen-layout:$duoVersion"
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
+ implementation "androidx.compose.foundation:foundation:$composeFoundationVersion"
implementation "androidx.compose.material:material"
testImplementation "junit:junit:$junitVersion"
diff --git a/fluentui_core/lint-baseline.xml b/fluentui_core/lint-baseline.xml
new file mode 100644
index 000000000..ea9aef2e5
--- /dev/null
+++ b/fluentui_core/lint-baseline.xml
@@ -0,0 +1,1301 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/AnchoredDraggable.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/AnchoredDraggable.kt
new file mode 100644
index 000000000..41513ef48
--- /dev/null
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/AnchoredDraggable.kt
@@ -0,0 +1,912 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.fluentui.compose
+
+import android.annotation.SuppressLint
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.gestures.DragScope
+import androidx.compose.foundation.gestures.DraggableState
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.unit.Velocity
+import kotlin.math.abs
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Structure that represents the anchors of a [AnchoredDraggableState].
+ *
+ * See the DraggableAnchors factory method to construct drag anchors using a default implementation.
+ */
+interface DraggableAnchors {
+
+ /**
+ * Get the anchor position for an associated [value]
+ *
+ * @param value The value to look up
+ *
+ * @return The position of the anchor, or [Float.NaN] if the anchor does not exist
+ */
+ fun positionOf(value: T): Float
+
+ /**
+ * Whether there is an anchor position associated with the [value]
+ *
+ * @param value The value to look up
+ *
+ * @return true if there is an anchor for this value, false if there is no anchor for this value
+ */
+ fun hasAnchorFor(value: T): Boolean
+
+ /**
+ * Find the closest anchor to the [position].
+ *
+ * @param position The position to start searching from
+ *
+ * @return The closest anchor or null if the anchors are empty
+ */
+ fun closestAnchor(position: Float): T?
+
+ /**
+ * Find the closest anchor to the [position], in the specified direction.
+ *
+ * @param position The position to start searching from
+ * @param searchUpwards Whether to search upwards from the current position or downwards
+ *
+ * @return The closest anchor or null if the anchors are empty
+ */
+ fun closestAnchor(position: Float, searchUpwards: Boolean): T?
+
+ /**
+ * The smallest anchor, or [Float.NEGATIVE_INFINITY] if the anchors are empty.
+ */
+ fun minAnchor(): Float
+
+ /**
+ * The biggest anchor, or [Float.POSITIVE_INFINITY] if the anchors are empty.
+ */
+ fun maxAnchor(): Float
+
+ /**
+ * The amount of anchors
+ */
+ val size: Int
+}
+
+/**
+ * [DraggableAnchorsConfig] stores a mutable configuration anchors, comprised of values of [T] and
+ * corresponding [Float] positions. This [DraggableAnchorsConfig] is used to construct an immutable
+ * [DraggableAnchors] instance later on.
+ */
+class DraggableAnchorsConfig {
+
+ internal val anchors = mutableMapOf()
+
+ /**
+ * Set the anchor position for [this] anchor.
+ *
+ * @param position The anchor position.
+ */
+ @Suppress("BuilderSetStyle")
+ infix fun T.at(position: Float) {
+ anchors[this] = position
+ }
+}
+
+/**
+ * Create a new [DraggableAnchors] instance using a builder function.
+ *
+ * @param builder A function with a [DraggableAnchorsConfig] that offers APIs to configure anchors
+ * @return A new [DraggableAnchors] instance with the anchor positions set by the `builder`
+ * function.
+ */
+fun DraggableAnchors(
+ builder: DraggableAnchorsConfig.() -> Unit
+): DraggableAnchors = MapDraggableAnchors(DraggableAnchorsConfig().apply(builder).anchors)
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
+ * When the drag ends, the offset will be animated to one of the anchors and when that anchor is
+ * reached, the value of the [AnchoredDraggableState] will also be updated to the value
+ * corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom
+ * drag will behave like bottom to top, and a left to right drag will behave like right to left.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to
+ * the internal [Modifier.draggable].
+ */
+@Suppress("ModifierFactoryUnreferencedReceiver")
+fun Modifier.anchoredDraggable(
+ state: AnchoredDraggableState,
+ orientation: Orientation,
+ enabled: Boolean = true,
+ reverseDirection: Boolean = false,
+ interactionSource: MutableInteractionSource? = null
+) = draggable(
+ state = state.draggableState,
+ orientation = orientation,
+ enabled = enabled,
+ interactionSource = interactionSource,
+ reverseDirection = reverseDirection,
+ startDragImmediately = state.isAnimationRunning,
+ onDragStopped = { velocity -> launch { state.settle(velocity) } }
+)
+
+/**
+ * Scope used for suspending anchored drag blocks. Allows to set [AnchoredDraggableState.offset] to
+ * a new value.
+ *
+ * @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
+ * access to this scope.
+ */
+interface AnchoredDragScope {
+ /**
+ * Assign a new value for an offset value for [AnchoredDraggableState].
+ *
+ * @param newOffset new value for [AnchoredDraggableState.offset].
+ * @param lastKnownVelocity last known velocity (if known)
+ */
+ fun dragTo(
+ newOffset: Float,
+ lastKnownVelocity: Float = 0f
+ )
+}
+
+/**
+ * State of the [anchoredDraggable] modifier.
+ * Use the constructor overload with anchors if the anchors are defined in composition, or update
+ * the anchors using [updateAnchors].
+ *
+ * This contains necessary information about any ongoing drag or animation and provides methods
+ * to change the state either immediately or by starting an animation.
+ *
+ * @param initialValue The initial value of the state.
+ * @param positionalThreshold The positional threshold, in px, to be used when calculating the
+ * target state while a drag is in progress and when settling after the drag ends. This is the
+ * distance from the start of a transition. It will be, depending on the direction of the
+ * interaction, added or subtracted from/to the origin offset. It should always be a positive value.
+ * @param velocityThreshold The velocity threshold (in px per second) that the end velocity has to
+ * exceed in order to animate to the next state, even if the [positionalThreshold] has not been
+ * reached.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
+ */
+@Stable
+class AnchoredDraggableState(
+ initialValue: T,
+ internal val positionalThreshold: (totalDistance: Float) -> Float,
+ internal val velocityThreshold: () -> Float,
+ val animationSpec: AnimationSpec,
+ internal val confirmValueChange: (newValue: T) -> Boolean = { true }
+) {
+
+ /**
+ * Construct an [AnchoredDraggableState] instance with anchors.
+ *
+ * @param initialValue The initial value of the state.
+ * @param anchors The anchors of the state. Use [updateAnchors] to update the anchors later.
+ * @param animationSpec The default animation that will be used to animate to a new state.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state
+ * change.
+ * @param positionalThreshold The positional threshold, in px, to be used when calculating the
+ * target state while a drag is in progress and when settling after the drag ends. This is the
+ * distance from the start of a transition. It will be, depending on the direction of the
+ * interaction, added or subtracted from/to the origin offset. It should always be a positive
+ * value.
+ * @param velocityThreshold The velocity threshold (in px per second) that the end velocity has
+ * to exceed in order to animate to the next state, even if the [positionalThreshold] has not
+ * been reached.
+ */
+ constructor(
+ initialValue: T,
+ anchors: DraggableAnchors,
+ positionalThreshold: (totalDistance: Float) -> Float,
+ velocityThreshold: () -> Float,
+ animationSpec: AnimationSpec,
+ confirmValueChange: (newValue: T) -> Boolean = { true }
+ ) : this(
+ initialValue,
+ positionalThreshold,
+ velocityThreshold,
+ animationSpec,
+ confirmValueChange
+ ) {
+ this.anchors = anchors
+ trySnapTo(initialValue)
+ }
+
+ private val dragMutex = MutatorMutex()
+ internal var minBound = Float.NEGATIVE_INFINITY
+
+ internal val draggableState = object : DraggableState {
+
+ private val dragScope = object : DragScope {
+ override fun dragBy(pixels: Float) {
+ with(anchoredDragScope) {
+ dragTo(newOffsetForDelta(pixels))
+ }
+ }
+ }
+
+ override suspend fun drag(
+ dragPriority: MutatePriority,
+ block: suspend DragScope.() -> Unit
+ ) {
+ this@AnchoredDraggableState.anchoredDrag(dragPriority) {
+ with(dragScope) { block() }
+ }
+ }
+
+ override fun dispatchRawDelta(delta: Float) {
+ this@AnchoredDraggableState.dispatchRawDelta(delta)
+ }
+ }
+
+ /**
+ * The current value of the [AnchoredDraggableState].
+ */
+ var currentValue: T by mutableStateOf(initialValue)
+ private set
+
+ /**
+ * The target value. This is the closest value to the current offset, taking into account
+ * positional thresholds. If no interactions like animations or drags are in progress, this
+ * will be the current value.
+ */
+ val targetValue: T by derivedStateOf {
+ dragTarget ?: run {
+ val currentOffset = offset
+ if (!currentOffset.isNaN()) {
+ computeTarget(currentOffset, currentValue, velocity = 0f)
+ } else currentValue
+ }
+ }
+
+ /**
+ * The closest value in the swipe direction from the current offset, not considering thresholds.
+ * If an [anchoredDrag] is in progress, this will be the target of that anchoredDrag (if
+ * specified).
+ */
+ internal val closestValue: T by derivedStateOf {
+ dragTarget ?: run {
+ val currentOffset = offset
+ if (!currentOffset.isNaN()) {
+ computeTargetWithoutThresholds(currentOffset, currentValue)
+ } else currentValue
+ }
+ }
+
+ /**
+ * The current offset, or [Float.NaN] if it has not been initialized yet.
+ *
+ * The offset will be initialized when the anchors are first set through [updateAnchors].
+ *
+ * Strongly consider using [requireOffset] which will throw if the offset is read before it is
+ * initialized. This helps catch issues early in your workflow.
+ */
+ var offset: Float by mutableFloatStateOf(Float.NaN)
+ private set
+
+ /**
+ * Require the current offset.
+ *
+ * @see offset
+ *
+ * @throws IllegalStateException If the offset has not been initialized yet
+ */
+ fun requireOffset(): Float {
+ check(!offset.isNaN()) {
+ "The offset was read before being initialized. Did you access the offset in a phase " +
+ "before layout, like effects or composition?"
+ }
+ return offset
+ }
+
+ /*
+It's a flag to indicate whether anchors are filled or not.
+Useful as a flag to let expand(), open() to get to know whether anchors are filled or not
+when launched for the very first time
+ */
+ var anchorsFilled: Boolean by mutableStateOf(false)
+
+ /**
+ * Whether an animation is currently in progress.
+ */
+ val isAnimationRunning: Boolean get() = dragTarget != null
+
+ /**
+ * The fraction of the progress going from [currentValue] to [closestValue], within [0f..1f]
+ * bounds, or 1f if the [AnchoredDraggableState] is in a settled state.
+ */
+ /*@FloatRange(from = 0f, to = 1f)*/
+ val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
+ val a = anchors.positionOf(currentValue)
+ val b = anchors.positionOf(closestValue)
+ val distance = abs(b - a)
+ if (!distance.isNaN() && distance > 1e-6f) {
+ val progress = (this.requireOffset() - a) / (b - a)
+ // If we are very close to 0f or 1f, we round to the closest
+ if (progress < 1e-6f) 0f else if (progress > 1 - 1e-6f) 1f else progress
+ } else 1f
+ }
+
+ /**
+ * The velocity of the last known animation. Gets reset to 0f when an animation completes
+ * successfully, but does not get reset when an animation gets interrupted.
+ * You can use this value to provide smooth reconciliation behavior when re-targeting an
+ * animation.
+ */
+ var lastVelocity: Float by mutableFloatStateOf(0f)
+ private set
+
+ private var dragTarget: T? by mutableStateOf(null)
+
+ var anchors: DraggableAnchors by mutableStateOf(emptyDraggableAnchors())
+ private set
+
+ /**
+ * Update the anchors. If there is no ongoing [anchoredDrag] operation, snap to the [newTarget],
+ * otherwise restart the ongoing [anchoredDrag] operation (e.g. an animation) with the new
+ * anchors.
+ *
+ * If your anchors depend on the size of the layout, updateAnchors should be called in the
+ * layout (placement) phase, e.g. through Modifier.onSizeChanged. This ensures that the
+ * state is set up within the same frame.
+ * For static anchors, or anchors with different data dependencies, [updateAnchors] is safe to
+ * be called from side effects or layout.
+ *
+ * @param newAnchors The new anchors.
+ * @param newTarget The new target, by default the closest anchor or the current target if there
+ * are no anchors.
+ */
+ fun updateAnchors(
+ newAnchors: DraggableAnchors,
+ newTarget: T = if (!offset.isNaN()) {
+ newAnchors.closestAnchor(offset) ?: targetValue
+ } else targetValue
+ ) {
+ if (anchors != newAnchors) {
+ anchors = newAnchors
+ // Attempt to snap. If nobody is holding the lock, we can immediately update the offset.
+ // If anybody is holding the lock, we send a signal to restart the ongoing work with the
+ // updated anchors.
+ val snapSuccessful = trySnapTo(newTarget)
+ if (!snapSuccessful) {
+ dragTarget = newTarget
+ }
+ }
+ anchorsFilled = true
+ }
+
+ /**
+ * Find the closest anchor, taking into account the [velocityThreshold] and
+ * [positionalThreshold], and settle at it with an animation.
+ *
+ * If the [velocity] is lower than the [velocityThreshold], the closest anchor by distance and
+ * [positionalThreshold] will be the target. If the [velocity] is higher than the
+ * [velocityThreshold], the [positionalThreshold] will not be considered and the next
+ * anchor in the direction indicated by the sign of the [velocity] will be the target.
+ */
+ suspend fun settle(velocity: Float) {
+ val previousValue = this.currentValue
+ val targetValue = computeTarget(
+ offset = requireOffset(),
+ currentValue = previousValue,
+ velocity = velocity
+ )
+ if (confirmValueChange(targetValue)) {
+ animateTo(targetValue, velocity)
+ } else {
+ // If the user vetoed the state change, rollback to the previous state.
+ animateTo(previousValue, velocity)
+ }
+ }
+
+ private fun computeTarget(
+ offset: Float,
+ currentValue: T,
+ velocity: Float
+ ): T {
+ val currentAnchors = anchors
+ val currentAnchorPosition = currentAnchors.positionOf(currentValue)
+ val velocityThresholdPx = velocityThreshold()
+ return if (currentAnchorPosition == offset || currentAnchorPosition.isNaN()) {
+ currentValue
+ } else if (currentAnchorPosition < offset) {
+ // Swiping from lower to upper (positive).
+ if (velocity >= velocityThresholdPx) {
+ currentAnchors.closestAnchor(offset, true)!!
+ } else {
+ val upper = currentAnchors.closestAnchor(offset, true)!!
+ val distance = abs(currentAnchors.positionOf(upper) - currentAnchorPosition)
+ val relativeThreshold = abs(positionalThreshold(distance))
+ val absoluteThreshold = abs(currentAnchorPosition + relativeThreshold)
+ if (offset < absoluteThreshold) currentValue else upper
+ }
+ } else {
+ // Swiping from upper to lower (negative).
+ if (velocity <= -velocityThresholdPx) {
+ currentAnchors.closestAnchor(offset, false)!!
+ } else {
+ val lower = currentAnchors.closestAnchor(offset, false)!!
+ val distance = abs(currentAnchorPosition - currentAnchors.positionOf(lower))
+ val relativeThreshold = abs(positionalThreshold(distance))
+ val absoluteThreshold = abs(currentAnchorPosition - relativeThreshold)
+ if (offset < 0) {
+ // For negative offsets, larger absolute thresholds are closer to lower anchors
+ // than smaller ones.
+ if (abs(offset) < absoluteThreshold) currentValue else lower
+ } else {
+ if (offset > absoluteThreshold) currentValue else lower
+ }
+ }
+ }
+ }
+
+ private fun computeTargetWithoutThresholds(
+ offset: Float,
+ currentValue: T,
+ ): T {
+ val currentAnchors = anchors
+ val currentAnchor = currentAnchors.positionOf(currentValue)
+ return if (currentAnchor == offset || currentAnchor.isNaN()) {
+ currentValue
+ } else if (currentAnchor < offset) {
+ currentAnchors.closestAnchor(offset, true) ?: currentValue
+ } else {
+ currentAnchors.closestAnchor(offset, false) ?: currentValue
+ }
+ }
+
+ private val anchoredDragScope: AnchoredDragScope = object : AnchoredDragScope {
+ override fun dragTo(newOffset: Float, lastKnownVelocity: Float) {
+ offset = newOffset
+ lastVelocity = lastKnownVelocity
+ }
+ }
+
+ /**
+ * Call this function to take control of drag logic and perform anchored drag with the latest
+ * anchors.
+ *
+ * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+ * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+ * in order to guarantee that mutual exclusion is enforced.
+ *
+ * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+ * drag, the ongoing drag will be cancelled.
+ *
+ * If the [anchors] change while the [block] is being executed, it will be cancelled and
+ * re-executed with the latest anchors and target. This allows you to target the correct
+ * state.
+ *
+ * @param dragPriority of the drag operation
+ * @param block perform anchored drag given the current anchor provided
+ */
+ suspend fun anchoredDrag(
+ dragPriority: MutatePriority = MutatePriority.Default,
+ block: suspend AnchoredDragScope.(anchors: DraggableAnchors) -> Unit
+ ) {
+ try {
+ dragMutex.mutate(dragPriority) {
+ restartable(inputs = { anchors }) { latestAnchors ->
+ anchoredDragScope.block(latestAnchors)
+ }
+ }
+ } finally {
+ val closest = anchors.closestAnchor(offset)
+ if (closest != null && abs(offset - anchors.positionOf(closest)) <= 0.5f) {
+ currentValue = closest
+ }
+ }
+ }
+
+ /**
+ * Call this function to take control of drag logic and perform anchored drag with the latest
+ * anchors and target.
+ *
+ * All actions that change the [offset] of this [AnchoredDraggableState] must be performed
+ * within an [anchoredDrag] block (even if they don't call any other methods on this object)
+ * in order to guarantee that mutual exclusion is enforced.
+ *
+ * This overload allows the caller to hint the target value that this [anchoredDrag] is intended
+ * to arrive to. This will set [AnchoredDraggableState.targetValue] to provided value so
+ * consumers can reflect it in their UIs.
+ *
+ * If the [anchors] or [AnchoredDraggableState.targetValue] change while the [block] is being
+ * executed, it will be cancelled and re-executed with the latest anchors and target. This
+ * allows you to target the correct state.
+ *
+ * If [anchoredDrag] is called from elsewhere with the [dragPriority] higher or equal to ongoing
+ * drag, the ongoing drag will be cancelled.
+ *
+ * @param targetValue hint the target value that this [anchoredDrag] is intended to arrive to
+ * @param dragPriority of the drag operation
+ * @param block perform anchored drag given the current anchor provided
+ */
+ suspend fun anchoredDrag(
+ targetValue: T,
+ dragPriority: MutatePriority = MutatePriority.Default,
+ block: suspend AnchoredDragScope.(anchors: DraggableAnchors, targetValue: T) -> Unit
+ ) {
+ if (anchors.hasAnchorFor(targetValue)) {
+ try {
+ dragMutex.mutate(dragPriority) {
+ dragTarget = targetValue
+ restartable(
+ inputs = { anchors to this@AnchoredDraggableState.targetValue }
+ ) { (latestAnchors, latestTarget) ->
+ anchoredDragScope.block(latestAnchors, latestTarget)
+ }
+ }
+ } finally {
+ dragTarget = null
+ val closest = anchors.closestAnchor(offset)
+ if (closest != null && abs(offset - anchors.positionOf(closest)) <= 0.5f) {
+ currentValue = closest
+ }
+ }
+ } else {
+ // Todo: b/283467401, revisit this behavior
+ currentValue = targetValue
+ }
+ }
+
+ internal fun newOffsetForDelta(delta: Float) =
+ ((if (offset.isNaN()) 0f else offset) + delta)
+ .coerceIn(anchors.minAnchor(), anchors.maxAnchor())
+
+ /**
+ * Drag by the [delta], coerce it in the bounds and dispatch it to the [AnchoredDraggableState].
+ *
+ * @return The delta the consumed by the [AnchoredDraggableState]
+ */
+ fun dispatchRawDelta(delta: Float): Float {
+ val newOffset = newOffsetForDelta(delta)
+ val oldOffset = if (offset.isNaN()) 0f else offset
+ offset = newOffset
+ return newOffset - oldOffset
+ }
+
+
+ /**
+ * Attempt to snap synchronously. Snapping can happen synchronously when there is no other drag
+ * transaction like a drag or an animation is progress. If there is another interaction in
+ * progress, the suspending [snapTo] overload needs to be used.
+ *
+ * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
+ */
+ private fun trySnapTo(targetValue: T): Boolean = dragMutex.tryMutate {
+ with(anchoredDragScope) {
+ val targetOffset = anchors.positionOf(targetValue)
+ if (!targetOffset.isNaN()) {
+ dragTo(targetOffset)
+ dragTarget = null
+ }
+ currentValue = targetValue
+ }
+ }
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [AnchoredDraggableState].
+ */
+ fun Saver(
+ animationSpec: AnimationSpec,
+ positionalThreshold: (distance: Float) -> Float,
+ velocityThreshold: () -> Float,
+ confirmValueChange: (T) -> Boolean = { true },
+ ) = Saver, T>(
+ save = { it.currentValue },
+ restore = {
+ AnchoredDraggableState(
+ initialValue = it,
+ animationSpec = animationSpec,
+ confirmValueChange = confirmValueChange,
+ positionalThreshold = positionalThreshold,
+ velocityThreshold = velocityThreshold
+ )
+ }
+ )
+ }
+}
+
+/**
+ * Snap to a [targetValue] without any animation.
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ */
+suspend fun AnchoredDraggableState.snapTo(targetValue: T) {
+ anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
+ val targetOffset = anchors.positionOf(latestTarget)
+ if (!targetOffset.isNaN()) dragTo(targetOffset)
+ }
+}
+
+/**
+ * Animate to a [targetValue].
+ * If the [targetValue] is not in the set of anchors, the [AnchoredDraggableState.currentValue] will
+ * be updated to the [targetValue] without updating the offset.
+ *
+ * @throws CancellationException if the interaction interrupted by another interaction like a
+ * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
+ *
+ * @param targetValue The target value of the animation
+ * @param velocity The velocity the animation should start with
+ */
+suspend fun AnchoredDraggableState.animateTo(
+ targetValue: T,
+ velocity: Float = this.lastVelocity,
+) {
+ anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
+ val targetOffset = anchors.positionOf(latestTarget)
+ if (!targetOffset.isNaN()) {
+ var prev = if (offset.isNaN()) 0f else offset
+ animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
+ // Our onDrag coerces the value within the bounds, but an animation may
+ // overshoot, for example a spring animation or an overshooting interpolator
+ // We respect the user's intention and allow the overshoot, but still use
+ // DraggableState's drag for its mutex.
+ dragTo(value, velocity)
+ prev = value
+ }
+ }
+ }
+}
+
+private class AnchoredDragFinishedSignal : CancellationException() {
+ override fun fillInStackTrace(): Throwable {
+ stackTrace = emptyArray()
+ return this
+ }
+}
+
+private suspend fun restartable(inputs: () -> I, block: suspend (I) -> Unit) {
+ try {
+ coroutineScope {
+ var previousDrag: Job? = null
+ snapshotFlow(inputs)
+ .collect { latestInputs ->
+ previousDrag?.apply {
+ cancel(AnchoredDragFinishedSignal())
+ join()
+ }
+ previousDrag = launch(start = CoroutineStart.UNDISPATCHED) {
+ block(latestInputs)
+ this@coroutineScope.cancel(AnchoredDragFinishedSignal())
+ }
+ }
+ }
+ } catch (anchoredDragFinished: AnchoredDragFinishedSignal) {
+ // Ignored
+ }
+}
+
+val AnchoredDraggableState.PreUpPostDownNestedScrollConnection: NestedScrollConnection
+ get() = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ dispatchRawDelta(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag) {
+ dispatchRawDelta(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val toFling = Offset(available.x, available.y).toFloat()
+ return if (toFling < 0 && offset > minBound) {
+ settle(velocity = toFling)
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ settle(velocity = Offset(available.x, available.y).toFloat())
+ return available
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
+
+val AnchoredDraggableState.PostDownNestedScrollConnection: NestedScrollConnection
+ get() = object : NestedScrollConnection {
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag && available.toFloat() > 0) {
+ dispatchRawDelta(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ settle(velocity = Offset(available.x, available.y).toFloat())
+ return available
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
+val AnchoredDraggableState.NonDismissiblePostDownNestedScrollConnection: NestedScrollConnection
+ get() = object : NestedScrollConnection {
+
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ dispatchRawDelta(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag && available.toFloat() < 0) {
+ dispatchRawDelta(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ settle(velocity = Offset(available.x, available.y).toFloat())
+ return available
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
+
+val AnchoredDraggableState.NonDismissiblePreUpPostDownNestedScrollConnection: NestedScrollConnection
+ get() = object : NestedScrollConnection {
+
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ dispatchRawDelta(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag && available.toFloat() < 0) {
+ dispatchRawDelta(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val toFling = Offset(available.x, available.y).toFloat()
+ return if (toFling < 0 && offset > minBound) {
+ settle(velocity = toFling)
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
+
+ private fun Float.toOffset(): Offset = Offset(0f, this)
+
+ private fun Offset.toFloat(): Float = this.y
+ }
+
+private fun emptyDraggableAnchors() = MapDraggableAnchors(emptyMap())
+
+private class MapDraggableAnchors(private val anchors: Map) : DraggableAnchors {
+
+ override fun positionOf(value: T): Float = anchors[value] ?: Float.NaN
+ override fun hasAnchorFor(value: T) = anchors.containsKey(value)
+
+ override fun closestAnchor(position: Float): T? = anchors.minByOrNull {
+ abs(position - it.value)
+ }?.key
+
+ override fun closestAnchor(
+ position: Float,
+ searchUpwards: Boolean
+ ): T? {
+ return anchors.minByOrNull { (_, anchor) ->
+ val delta = if (searchUpwards) anchor - position else position - anchor
+ if (delta < 0) Float.POSITIVE_INFINITY else delta
+ }?.key
+ }
+
+ override fun minAnchor() = anchors.values.minOrNull() ?: Float.NaN
+
+ override fun maxAnchor() = anchors.values.maxOrNull() ?: Float.NaN
+
+ override val size: Int
+ get() = anchors.size
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is MapDraggableAnchors<*>) return false
+
+ return anchors == other.anchors
+ }
+
+ override fun hashCode() = 31 * anchors.hashCode()
+
+ override fun toString() = "MapDraggableAnchors($anchors)"
+}
\ No newline at end of file
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/ModalPopup.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/ModalPopup.kt
index 043664e91..55e6c2526 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/ModalPopup.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/ModalPopup.kt
@@ -1,12 +1,16 @@
package com.microsoft.fluentui.compose
import android.content.Context
+import android.graphics.Outline
import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.os.Build
import android.view.Gravity
-import android.view.KeyEvent
import android.view.View
-import android.view.ViewTreeObserver
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
import android.view.WindowManager
+import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.captionBar
@@ -15,7 +19,6 @@ import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.mandatorySystemGestures
import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.tappableElement
@@ -33,15 +36,22 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.AbstractComposeView
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewRootForInspector
import androidx.compose.ui.semantics.popup
import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.PopupProperties
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.lifecycle.findViewTreeViewModelStoreOwner
@@ -56,54 +66,74 @@ import java.util.UUID
*/
@Composable
fun ModalPopup(
- onDismissRequest: () -> Unit,
+ onDismissRequest:(() -> Unit)? = null,
windowInsetsType: Int = WindowInsetsCompat.Type.systemBars(),
content: @Composable () -> Unit,
) {
+ val properties = PopupProperties()
val view = LocalView.current
- val id = rememberSaveable { UUID.randomUUID() }
+ val density = LocalDensity.current
+ val layoutDirection = LocalLayoutDirection.current
val parentComposition = rememberCompositionContext()
val currentContent by rememberUpdatedState(content)
- val layoutDirection = LocalLayoutDirection.current
+ val id = rememberSaveable { UUID.randomUUID() }
val modalWindow = remember {
ModalWindow(
onDismissRequest = onDismissRequest,
+ properties = properties,
composeView = view,
+ density = density,
saveId = id
).apply {
- setCustomContent(
- parent = parentComposition,
- content = {
- Box(
- Modifier
- .semantics { this.popup() }
- // Get the size of the content
- .onSizeChanged {
- popupContentSize = it
- }
- // Hide the popup while we can't position it correctly
- .alpha(if (canCalculatePosition) 1f else 0f)
- .windowInsetsPadding(
- convertWindowInsetsCompatTypeToWindowInsets(windowInsetsType)
- )
- .imePadding()
- ) {
- currentContent()
- }
+ setCustomContent(parentComposition) {
+ Box(
+ Modifier
+ .semantics { this.popup() }
+ // Get the size of the content
+ .onSizeChanged {
+ popupContentSize = it
+ }
+ // Hide the popup while we can't position it correctly
+ .alpha(if (canCalculatePosition) 1f else 0f)
+ .windowInsetsPadding(
+ convertWindowInsetsCompatTypeToWindowInsets(windowInsetsType)
+ )
+ .imePadding()
+ ) {
+ currentContent()
}
- )
+ }
}
}
DisposableEffect(modalWindow) {
modalWindow.show()
- modalWindow.superSetLayoutDirection(layoutDirection)
+ modalWindow.updateParameters(
+ onDismissRequest = onDismissRequest,
+ properties = properties,
+ layoutDirection = layoutDirection
+ )
onDispose {
modalWindow.disposeComposition()
modalWindow.dismiss()
}
}
+
+ Layout(
+ content = {},
+ modifier = Modifier
+ .onGloballyPositioned { childCoordinates ->
+ val parentCoordinates = childCoordinates.parentLayoutCoordinates
+ if (parentCoordinates != null) {
+ modalWindow.updateParentLayoutCoordinates(parentCoordinates)
+ }
+ }
+ ) { _, _ ->
+ modalWindow.parentLayoutDirection = layoutDirection
+ layout(0, 0) {}
+ }
}
+
@Composable
fun convertWindowInsetsCompatTypeToWindowInsets(windowInsetsCompatType: Int): WindowInsets {
return when (windowInsetsCompatType) {
@@ -121,17 +151,32 @@ fun convertWindowInsetsCompatTypeToWindowInsets(windowInsetsCompatType: Int): Wi
/** Custom compose view for [BottomDrawer] */
private class ModalWindow(
- private var onDismissRequest: () -> Unit,
+ private var onDismissRequest: (() -> Unit)? = null,
+ private var properties: PopupProperties,
private val composeView: View,
+ density: Density,
saveId: UUID,
+ private val popupLayoutHelper: PopupLayoutHelperImpl = if (Build.VERSION.SDK_INT >= 29) {
+ PopupLayoutHelperImpl29()
+ } else {
+ PopupLayoutHelperImpl()
+ }
) :
AbstractComposeView(composeView.context),
- ViewTreeObserver.OnGlobalLayoutListener,
ViewRootForInspector {
+ private val windowManager =
+ composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+
+ private val params: WindowManager.LayoutParams = createLayoutParams()
+ var parentLayoutDirection: LayoutDirection = LayoutDirection.Ltr
var popupContentSize: IntSize? by mutableStateOf(null)
+ private var parentLayoutCoordinates: LayoutCoordinates? by mutableStateOf(null)
val canCalculatePosition by derivedStateOf {
- popupContentSize != null
+ parentLayoutCoordinates != null && popupContentSize != null
}
+
+ override val subCompositionView: AbstractComposeView get() = this
+
init {
id = android.R.id.content
// Set up view owners
@@ -141,104 +186,81 @@ private class ModalWindow(
setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Popup:$saveId")
// Enable children to draw their shadow by not clipping them
clipChildren = false
- }
-
- private val windowManager =
- composeView.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
-
-
- private val params: WindowManager.LayoutParams =
- WindowManager.LayoutParams().apply {
- // Position bottom sheet from the bottom of the screen
- gravity = Gravity.BOTTOM or Gravity.START
- // Application panel window
- type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
- // Fill up the entire app view
- width = WindowManager.LayoutParams.MATCH_PARENT
- height = WindowManager.LayoutParams.MATCH_PARENT
-
- // Format of screen pixels
- format = PixelFormat.TRANSLUCENT
- // Title used as fallback for a11y services
- // TODO: Provide bottom sheet window resource
- title = composeView.context.resources.getString(
- androidx.compose.ui.R.string.default_popup_window_title
- )
- // Get the Window token from the parent view
- token = composeView.applicationWindowToken
-
- // Flags specific to modal bottom sheet.
- flags = flags and (
- WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
- WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
- ).inv()
-
- flags = flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ with(density) { elevation = 8.dp.toPx() }
+ outlineProvider = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, result: Outline) {
+ result.setRect(0, 0, view.width, view.height)
+ result.alpha = 0f
+ }
}
+ }
private var content: @Composable () -> Unit by mutableStateOf({})
override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
private set
- @Composable
- override fun Content() {
- content()
+ fun show() {
+ windowManager.addView(this, params)
}
fun setCustomContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
- parent?.let { setParentCompositionContext(it) }
+ setParentCompositionContext(parent)
this.content = content
shouldCreateCompositionOnAttachedToWindow = true
}
- fun show() {
- windowManager.addView(this, params)
+ @Composable
+ override fun Content() {
+ content()
}
- fun dismiss() {
- this.setViewTreeLifecycleOwner(null)
- setViewTreeSavedStateRegistryOwner(null)
- composeView.viewTreeObserver.removeOnGlobalLayoutListener(this)
- windowManager.removeViewImmediate(this)
+ private fun focusable(isFocusable: Boolean) = applyNewFlags(
+ if (!isFocusable) {
+ params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ } else {
+ params.flags and (WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE.inv())
+ }
+ )
+
+ private fun applyNewFlags(flags: Int) {
+ params.flags = flags
+ popupLayoutHelper.updateViewLayout(windowManager, this, params)
}
- /**
- * Taken from PopupWindow. Calls [onDismissRequest] when back button is pressed.
- */
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- if (event.keyCode == KeyEvent.KEYCODE_BACK) {
- if (keyDispatcherState == null) {
- return super.dispatchKeyEvent(event)
- }
- if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
- val state = keyDispatcherState
- state?.startTracking(event, this)
- return true
- } else if (event.action == KeyEvent.ACTION_UP) {
- val state = keyDispatcherState
- if (state != null && state.isTracking(event) && !event.isCanceled) {
- onDismissRequest()
- return true
- }
- }
+ fun updateParameters(
+ onDismissRequest: (() -> Unit)?,
+ properties: PopupProperties,
+ layoutDirection: LayoutDirection
+ ) {
+ this.onDismissRequest = onDismissRequest
+ if (properties.usePlatformDefaultWidth && !this.properties.usePlatformDefaultWidth) {
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT
+ popupLayoutHelper.updateViewLayout(windowManager, this, params)
}
- return super.dispatchKeyEvent(event)
+ this.properties = properties
+ focusable(properties.focusable)
+ superSetLayoutDirection(layoutDirection)
+ }
+
+ fun updateParentLayoutCoordinates(parentLayoutCoordinates: LayoutCoordinates) {
+ this.parentLayoutCoordinates = parentLayoutCoordinates
}
- override fun onGlobalLayout() {
- // No-op
+ fun dismiss() {
+ this.setViewTreeLifecycleOwner(null)
+ setViewTreeSavedStateRegistryOwner(null)
+ windowManager.removeViewImmediate(this)
}
override fun setLayoutDirection(layoutDirection: Int) {
- // Do nothing. ViewRootImpl will call this method attempting to set the layout direction
- // from the context's locale, but we have one already from the parent composition.
+ // Do nothing.
}
- // Sets the "real" layout direction for our content that we obtain from the parent composition.
fun superSetLayoutDirection(layoutDirection: LayoutDirection) {
val direction = when (layoutDirection) {
LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
@@ -246,4 +268,70 @@ private class ModalWindow(
}
super.setLayoutDirection(direction)
}
-}
\ No newline at end of file
+
+ private fun createLayoutParams(): WindowManager.LayoutParams{
+ return WindowManager.LayoutParams().apply {
+ // Position bottom sheet from the bottom of the screen
+ gravity = Gravity.BOTTOM or Gravity.START
+ // Application panel window
+ type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL
+ // Fill up the entire app view
+ width = WindowManager.LayoutParams.MATCH_PARENT
+ // for build versions less than or equal to S_V2, set the height to wrap content
+ height = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2)
+ WindowManager.LayoutParams.WRAP_CONTENT
+ else
+ WindowManager.LayoutParams.MATCH_PARENT
+
+ // Format of screen pixels
+ format = PixelFormat.TRANSLUCENT
+ // Title used as fallback for a11y services
+ // TODO: Provide bottom sheet window resource
+ title = composeView.context.resources.getString(
+ androidx.compose.ui.R.string.default_popup_window_title
+ )
+ // Get the Window token from the parent view
+ token = composeView.applicationWindowToken
+
+ // Flags specific to modal bottom sheet.
+ flags = flags and (
+ WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES or
+ WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ ).inv()
+
+ flags = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
+ flags
+ } else flags or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
+ }
+ }
+}
+
+private open class PopupLayoutHelperImpl {
+
+ open fun setGestureExclusionRects(composeView: View, width: Int, height: Int) {
+ //For Android versions below API 29, it’s not necessary to explicitly exclude the entire screen from system gestures.
+ // The skeleton method is defined to keep consistency in the two objects.
+ }
+
+ fun updateViewLayout(
+ windowManager: WindowManager,
+ popupView: View,
+ params: ViewGroup.LayoutParams
+ ) {
+ windowManager.updateViewLayout(popupView, params)
+ }
+}
+
+@RequiresApi(29) // android.view.View#setSystemGestureExclusionRects call requires API 29 and above
+private class PopupLayoutHelperImpl29 : PopupLayoutHelperImpl() {
+ override fun setGestureExclusionRects(composeView: View, width: Int, height: Int) { // We need to explicitly specify to exclude the entire screen from system gestures
+ composeView.systemGestureExclusionRects = mutableListOf(
+ Rect(
+ 0,
+ 0,
+ width,
+ height
+ )
+ )
+ }
+}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt
index 141b8d949..ad3d97561 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/compose/Swipeable.kt
@@ -16,6 +16,7 @@
package com.microsoft.fluentui.compose
+import android.annotation.SuppressLint
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
@@ -544,6 +545,7 @@ internal fun rememberSwipeableStateFor(
* @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed
* in order to animate to the next state, even if the positional [thresholds] have not been reached.
*/
+@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.swipeable(
state: SwipeableState,
anchors: Map,
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/ControlTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/ControlTokens.kt
index 16ba30df7..c6de06a60 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/ControlTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/ControlTokens.kt
@@ -33,6 +33,7 @@ object UndefinedControlToken: IControlToken
*/
open class ControlTokens : IControlTokens {
enum class ControlType : IType {
+ ActionBarControlType,
AnnouncementCardControlType,
AppBarControlType,
AvatarControlType,
@@ -76,11 +77,13 @@ open class ControlTokens : IControlTokens {
TextFieldControlType,
ToggleSwitchControlType,
TooltipControlType,
+ ViewPagerControlType
}
override val tokens: TokenSet by lazy {
TokenSet { type ->
when (type) {
+ ControlType.ActionBarControlType -> ActionBarTokens()
ControlType.AnnouncementCardControlType -> AnnouncementCardTokens()
ControlType.AppBarControlType -> AppBarTokens()
ControlType.AvatarControlType -> AvatarTokens()
@@ -124,6 +127,7 @@ open class ControlTokens : IControlTokens {
ControlType.TextFieldControlType -> TextFieldTokens()
ControlType.ToggleSwitchControlType -> ToggleSwitchTokens()
ControlType.TooltipControlType -> TooltipTokens()
+ ControlType.ViewPagerControlType -> ViewPagerTokens()
else -> {
UndefinedControlToken
}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/FluentGlobalTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/FluentGlobalTokens.kt
index 02fbbcf2c..c8c7cf0f9 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/FluentGlobalTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/FluentGlobalTokens.kt
@@ -224,6 +224,7 @@ object FluentGlobalTokens {
CornerRadius40(4.dp),
CornerRadius80(8.dp),
CornerRadius120(12.dp),
+ CornerRadius160(16.dp),
CornerRadiusCircle(9999.dp)
}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ActionBarTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ActionBarTokens.kt
new file mode 100644
index 000000000..5ae11108f
--- /dev/null
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ActionBarTokens.kt
@@ -0,0 +1,38 @@
+package com.microsoft.fluentui.theme.token.controlTokens
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import com.microsoft.fluentui.theme.FluentTheme.aliasTokens
+import com.microsoft.fluentui.theme.FluentTheme.themeMode
+import com.microsoft.fluentui.theme.token.ControlInfo
+import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.theme.token.FluentGlobalTokens
+import com.microsoft.fluentui.theme.token.IControlToken
+
+import kotlinx.parcelize.Parcelize
+
+enum class ACTIONBARTYPE {
+ BASIC,
+ ICON,
+ CAROUSEL
+}
+
+open class ActionBarInfo: ControlInfo
+
+@Parcelize
+open class ActionBarTokens : IControlToken, Parcelable {
+
+ @Composable
+ open fun actionBarHeight(actionBarInfo: ActionBarInfo): Dp {
+ return FluentGlobalTokens.SizeTokens.Size480.value
+ }
+
+ @Composable
+ open fun actionBarColor(actionBarInfo: ActionBarInfo): Color {
+ return aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
+ themeMode = themeMode
+ )
+ }
+}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AppBarTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AppBarTokens.kt
index 032f80f43..5c6c3ae11 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AppBarTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AppBarTokens.kt
@@ -169,7 +169,6 @@ open class AppBarTokens : IControlToken, Parcelable {
AppBarSize.Large -> FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Title1]
AppBarSize.Medium -> FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Title2]
AppBarSize.Small -> FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Body1Strong]
- else -> TextStyle(fontSize = 0.sp)
}
}
@@ -203,7 +202,7 @@ open class AppBarTokens : IControlToken, Parcelable {
@Composable
open fun navigationIconPadding(info: AppBarInfo): PaddingValues {
return when (info.appBarSize) {
- AppBarSize.Large -> PaddingValues()
+ AppBarSize.Large -> PaddingValues(16.dp)
AppBarSize.Medium -> PaddingValues(16.dp)
AppBarSize.Small -> PaddingValues(16.dp)
}
@@ -213,7 +212,7 @@ open class AppBarTokens : IControlToken, Parcelable {
open fun textPadding(info: AppBarInfo): PaddingValues {
return when (info.appBarSize) {
AppBarSize.Large -> PaddingValues(start = 12.dp)
- AppBarSize.Medium -> PaddingValues()
+ AppBarSize.Medium -> PaddingValues(start = 8.dp)
AppBarSize.Small -> PaddingValues(start = 8.dp)
}
}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarGroupTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarGroupTokens.kt
index 7e3fa5f13..b206f65b7 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarGroupTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarGroupTokens.kt
@@ -13,7 +13,8 @@ import kotlinx.parcelize.Parcelize
enum class AvatarGroupStyle {
Stack,
- Pile
+ Pile,
+ Pie
}
open class AvatarGroupInfo(
@@ -107,6 +108,8 @@ open class AvatarGroupTokens : IControlToken, Parcelable {
AvatarSize.Size72 -> FluentGlobalTokens.SizeTokens.Size80
.value
}
+
+ AvatarGroupStyle.Pie -> 0.dp
}
}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarTokens.kt
index 2932c4dc2..0f52b3dbf 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/AvatarTokens.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
@@ -128,6 +129,7 @@ import com.microsoft.fluentui.icons.avataricons.presence.unknown.medium.Dark
import com.microsoft.fluentui.icons.avataricons.presence.unknown.medium.Light
import com.microsoft.fluentui.icons.avataricons.presence.unknown.small.Dark
import com.microsoft.fluentui.icons.avataricons.presence.unknown.small.Light
+import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.FluentTheme.aliasTokens
import com.microsoft.fluentui.theme.FluentTheme.themeMode
import com.microsoft.fluentui.theme.token.*
@@ -211,31 +213,37 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
lineHeight = 12.sp,
fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
)
+
AvatarSize.Size20 -> TextStyle(
fontSize = 9.sp,
lineHeight = 12.sp,
fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
)
+
AvatarSize.Size24 -> TextStyle(
fontSize = FluentGlobalTokens.FontSizeTokens.Size100.value,
lineHeight = FluentGlobalTokens.LineHeightTokens.Size100.value,
fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
)
+
AvatarSize.Size32 -> TextStyle(
fontSize = FluentGlobalTokens.FontSizeTokens.Size200.value,
lineHeight = FluentGlobalTokens.LineHeightTokens.Size200.value,
fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
)
+
AvatarSize.Size40 -> TextStyle(
fontSize = FluentGlobalTokens.FontSizeTokens.Size300.value,
lineHeight = FluentGlobalTokens.LineHeightTokens.Size300.value,
fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
)
+
AvatarSize.Size56 -> TextStyle(
fontSize = FluentGlobalTokens.FontSizeTokens.Size500.value,
lineHeight = FluentGlobalTokens.LineHeightTokens.Size500.value,
fontWeight = FluentGlobalTokens.FontWeightTokens.Medium.value
)
+
AvatarSize.Size72 -> TextStyle(
fontSize = FluentGlobalTokens.FontSizeTokens.Size700.value,
lineHeight = FluentGlobalTokens.LineHeightTokens.Size700.value,
@@ -260,27 +268,25 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
@Composable
open fun icon(avatarInfo: AvatarInfo): ImageVector {
return when (avatarStyle(avatarInfo)) {
- AvatarStyle.Standard, AvatarStyle.StandardInverted ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> AvatarIcons.Icon.Standard.Xsmall
- AvatarSize.Size20 -> AvatarIcons.Icon.Standard.Small
- AvatarSize.Size24 -> AvatarIcons.Icon.Standard.Small
- AvatarSize.Size32 -> AvatarIcons.Icon.Standard.Medium
- AvatarSize.Size40 -> AvatarIcons.Icon.Standard.Large
- AvatarSize.Size56 -> AvatarIcons.Icon.Standard.Xlarge
- AvatarSize.Size72 -> AvatarIcons.Icon.Standard.Xxlarge
- }
+ AvatarStyle.Standard, AvatarStyle.StandardInverted -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> AvatarIcons.Icon.Standard.Xsmall
+ AvatarSize.Size20 -> AvatarIcons.Icon.Standard.Small
+ AvatarSize.Size24 -> AvatarIcons.Icon.Standard.Small
+ AvatarSize.Size32 -> AvatarIcons.Icon.Standard.Medium
+ AvatarSize.Size40 -> AvatarIcons.Icon.Standard.Large
+ AvatarSize.Size56 -> AvatarIcons.Icon.Standard.Xlarge
+ AvatarSize.Size72 -> AvatarIcons.Icon.Standard.Xxlarge
+ }
- AvatarStyle.Anonymous, AvatarStyle.AnonymousAccent ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> AvatarIcons.Icon.Anonymous.Xsmall
- AvatarSize.Size20 -> AvatarIcons.Icon.Anonymous.Small
- AvatarSize.Size24 -> AvatarIcons.Icon.Anonymous.Small
- AvatarSize.Size32 -> AvatarIcons.Icon.Anonymous.Medium
- AvatarSize.Size40 -> AvatarIcons.Icon.Anonymous.Large
- AvatarSize.Size56 -> AvatarIcons.Icon.Anonymous.Xlarge
- AvatarSize.Size72 -> AvatarIcons.Icon.Anonymous.Xxlarge
- }
+ AvatarStyle.Anonymous, AvatarStyle.AnonymousAccent -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> AvatarIcons.Icon.Anonymous.Xsmall
+ AvatarSize.Size20 -> AvatarIcons.Icon.Anonymous.Small
+ AvatarSize.Size24 -> AvatarIcons.Icon.Anonymous.Small
+ AvatarSize.Size32 -> AvatarIcons.Icon.Anonymous.Medium
+ AvatarSize.Size40 -> AvatarIcons.Icon.Anonymous.Large
+ AvatarSize.Size56 -> AvatarIcons.Icon.Anonymous.Xlarge
+ AvatarSize.Size72 -> AvatarIcons.Icon.Anonymous.Xxlarge
+ }
}
}
@@ -289,12 +295,9 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
return if (avatarInfo.isImageAvailable || avatarInfo.hasValidInitials) {
FluentColor(
light = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Shade30
- ),
- dark = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Tint40
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Shade30
+ ), dark = calculatedColor(
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Tint40
)
).value(
themeMode = themeMode
@@ -305,22 +308,21 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
)
} else {
when (avatarStyle(avatarInfo)) {
- AvatarStyle.Standard ->
- aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
- themeMode = themeMode
- )
- AvatarStyle.StandardInverted ->
- aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
- themeMode = themeMode
- )
- AvatarStyle.Anonymous ->
- aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value(
- themeMode = themeMode
- )
- AvatarStyle.AnonymousAccent ->
- aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
- themeMode = themeMode
- )
+ AvatarStyle.Standard -> aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.StandardInverted -> aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.Anonymous -> aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.AnonymousAccent -> aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
+ themeMode = themeMode
+ )
}
}
}
@@ -331,12 +333,9 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
if (avatarInfo.isImageAvailable || avatarInfo.hasValidInitials) {
FluentColor(
light = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Tint40
- ),
- dark = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Shade30
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Tint40
+ ), dark = calculatedColor(
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Shade30
)
).value(
themeMode = themeMode
@@ -347,22 +346,21 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
)
} else {
when (avatarStyle(avatarInfo)) {
- AvatarStyle.Standard ->
- aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
- themeMode = themeMode
- )
- AvatarStyle.StandardInverted ->
- aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
- themeMode = themeMode
- )
- AvatarStyle.Anonymous ->
- aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
- themeMode = themeMode
- )
- AvatarStyle.AnonymousAccent ->
- aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(
- themeMode = themeMode
- )
+ AvatarStyle.Standard -> aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.StandardInverted -> aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.Anonymous -> aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
+ themeMode = themeMode
+ )
+
+ AvatarStyle.AnonymousAccent -> aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(
+ themeMode = themeMode
+ )
}
}
)
@@ -371,245 +369,284 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
@Composable
open fun presenceIcon(avatarInfo: AvatarInfo): FluentIcon {
return when (avatarInfo.status) {
- AvatarStatus.Available ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
- )
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
- )
+ AvatarStatus.Available -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Light else AvatarIcons.Presence.Available.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Dark else AvatarIcons.Presence.Available.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Light else AvatarIcons.Presence.Available.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Small.Dark else AvatarIcons.Presence.Available.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Light else AvatarIcons.Presence.Available.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Dark else AvatarIcons.Presence.Available.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Light else AvatarIcons.Presence.Available.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Dark else AvatarIcons.Presence.Available.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Large.Light else AvatarIcons.Presence.Available.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Large.Dark else AvatarIcons.Presence.Available.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Light else AvatarIcons.Presence.Available.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Medium.Dark else AvatarIcons.Presence.Available.Medium.Dark
+ )
- AvatarStatus.Busy ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Large.Light else AvatarIcons.Presence.Available.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Availableoof.Large.Dark else AvatarIcons.Presence.Available.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
- )
+ AvatarStatus.Busy -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Light else AvatarIcons.Presence.Busy.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Dark else AvatarIcons.Presence.Busy.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Light else AvatarIcons.Presence.Busy.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Small.Dark else AvatarIcons.Presence.Busy.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Light else AvatarIcons.Presence.Busy.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Dark else AvatarIcons.Presence.Busy.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Light else AvatarIcons.Presence.Busy.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Dark else AvatarIcons.Presence.Busy.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Large.Light else AvatarIcons.Presence.Busy.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Large.Dark else AvatarIcons.Presence.Busy.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Light else AvatarIcons.Presence.Busy.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Medium.Dark else AvatarIcons.Presence.Busy.Medium.Dark
+ )
- AvatarStatus.Away ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Large.Light else AvatarIcons.Presence.Busy.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Busyoof.Large.Dark else AvatarIcons.Presence.Busy.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
- )
+ AvatarStatus.Away -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Light else AvatarIcons.Presence.Away.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Dark else AvatarIcons.Presence.Away.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Light else AvatarIcons.Presence.Away.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Small.Dark else AvatarIcons.Presence.Away.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Light else AvatarIcons.Presence.Away.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Dark else AvatarIcons.Presence.Away.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Light else AvatarIcons.Presence.Away.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Dark else AvatarIcons.Presence.Away.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Large.Light else AvatarIcons.Presence.Away.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Large.Dark else AvatarIcons.Presence.Away.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Light else AvatarIcons.Presence.Away.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Medium.Dark else AvatarIcons.Presence.Away.Medium.Dark
+ )
- AvatarStatus.DND ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Large.Light else AvatarIcons.Presence.Away.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Awayoof.Large.Dark else AvatarIcons.Presence.Away.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
- )
+ AvatarStatus.DND -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Light else AvatarIcons.Presence.Dnd.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Dark else AvatarIcons.Presence.Dnd.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Light else AvatarIcons.Presence.Dnd.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Small.Dark else AvatarIcons.Presence.Dnd.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Light else AvatarIcons.Presence.Dnd.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Dark else AvatarIcons.Presence.Dnd.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Light else AvatarIcons.Presence.Dnd.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Dark else AvatarIcons.Presence.Dnd.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Large.Light else AvatarIcons.Presence.Dnd.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Large.Dark else AvatarIcons.Presence.Dnd.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Light else AvatarIcons.Presence.Dnd.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Medium.Dark else AvatarIcons.Presence.Dnd.Medium.Dark
+ )
- AvatarStatus.Unknown ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Large.Light else AvatarIcons.Presence.Dnd.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Dndoof.Large.Dark else AvatarIcons.Presence.Dnd.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
- )
+ AvatarStatus.Unknown -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Unknown.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Unknown.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Unknown.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Unknown.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Unknown.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Unknown.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Unknown.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Unknown.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Unknown.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Unknown.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Unknown.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Unknown.Medium.Dark
+ )
- AvatarStatus.Blocked ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Unknown.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Unknown.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
- )
+ AvatarStatus.Blocked -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Blocked.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Blocked.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Blocked.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Blocked.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Blocked.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Blocked.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Blocked.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Blocked.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Blocked.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Blocked.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Blocked.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Blocked.Medium.Dark
+ )
- AvatarStatus.Offline ->
- when (avatarInfo.size) {
- AvatarSize.Size16 -> FluentIcon()
- AvatarSize.Size20 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
- )
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Blocked.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Blocked.Large.Dark
+ )
+ }
- AvatarSize.Size24 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
- )
+ AvatarStatus.Offline -> when (avatarInfo.size) {
+ AvatarSize.Size16 -> FluentIcon()
+ AvatarSize.Size20 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
+ )
- AvatarSize.Size32 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
- )
+ AvatarSize.Size24 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
+ )
- AvatarSize.Size40 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Offline.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Offline.Medium.Dark
- )
+ AvatarSize.Size32 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Light else AvatarIcons.Presence.Offline.Small.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Small.Dark else AvatarIcons.Presence.Offline.Small.Dark
+ )
- AvatarSize.Size56 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Offline.Medium.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Offline.Medium.Dark
- )
+ AvatarSize.Size40 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Offline.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Offline.Medium.Dark
+ )
- AvatarSize.Size72 -> FluentIcon(
- light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Offline.Large.Light,
- dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Offline.Large.Dark
- )
- }
+ AvatarSize.Size56 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Light else AvatarIcons.Presence.Offline.Medium.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Medium.Dark else AvatarIcons.Presence.Offline.Medium.Dark
+ )
+
+ AvatarSize.Size72 -> FluentIcon(
+ light = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Light else AvatarIcons.Presence.Offline.Large.Light,
+ dark = if (avatarInfo.isOOO) AvatarIcons.Presence.Oof.Large.Dark else AvatarIcons.Presence.Offline.Large.Dark
+ )
+ }
+ }
+ }
+
+ @Composable
+ open fun unreadDotBorderStroke(avatarInfo: AvatarInfo): BorderStroke {
+ return BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
+ aliasTokens.neutralStrokeColor[FluentAliasTokens.NeutralStrokeColorTokens.StrokeFocus1].value(
+ themeMode = themeMode
+ )
+ )
+ }
+
+ @Composable
+ open fun unreadDotBackgroundBrush(avatarInfo: AvatarInfo): Brush {
+ return SolidColor(
+ aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
+ themeMode = themeMode
+ )
+ )
+ }
+
+ @Composable
+ open fun unreadDotSize(avatarInfo: AvatarInfo): Dp {
+ return when (avatarInfo.size) {
+ AvatarSize.Size16 -> 8.dp
+ AvatarSize.Size20 -> 8.dp
+ AvatarSize.Size24 -> 8.dp
+ AvatarSize.Size32 -> 10.dp
+ AvatarSize.Size40 -> 12.dp
+ AvatarSize.Size56 -> 14.dp
+ AvatarSize.Size72 -> 16.dp
}
}
+ @Composable
+ open fun unreadDotOffset(avatarInfo: AvatarInfo): DpOffset {
+ return when(avatarInfo.size) {
+ AvatarSize.Size16 -> DpOffset(4.dp, (0).dp)
+ AvatarSize.Size20 -> DpOffset(4.dp, (-2).dp)
+ AvatarSize.Size24 -> DpOffset(4.dp, (-3).dp)
+ AvatarSize.Size32 -> DpOffset(4.dp, (-3).dp)
+ AvatarSize.Size40 -> DpOffset(4.dp, (-3).dp)
+ AvatarSize.Size56 -> DpOffset(4.dp, (-4).dp)
+ AvatarSize.Size72 -> DpOffset(4.dp, (-5).dp)
+ }
+ }
+
@Composable
open fun presenceOffset(avatarInfo: AvatarInfo): DpOffset {
return when (avatarInfo.size) {
@@ -641,12 +678,9 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
val glowColor: Color = if (avatarInfo.isImageAvailable || avatarInfo.hasValidInitials) {
FluentColor(
light = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Primary
- ),
- dark = calculatedColor(
- avatarInfo.calculatedColorKey,
- FluentGlobalTokens.SharedColorsTokens.Tint30
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Primary
+ ), dark = calculatedColor(
+ avatarInfo.calculatedColorKey, FluentGlobalTokens.SharedColorsTokens.Tint30
)
).value(
themeMode = themeMode
@@ -660,55 +694,58 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
AvatarStyle.Standard, AvatarStyle.StandardInverted, AvatarStyle.AnonymousAccent -> aliasTokens.brandStroke[FluentAliasTokens.BrandStrokeColorTokens.BrandStroke1].value(
themeMode = themeMode
)
+
AvatarStyle.Anonymous -> aliasTokens.neutralStrokeColor[FluentAliasTokens.NeutralStrokeColorTokens.Stroke1].value(
themeMode = themeMode
)
}
}
- return if (avatarInfo.active)
- when (avatarInfo.size) {
- AvatarSize.Size16 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size16,
- glowColor
- )
- AvatarSize.Size20 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size20,
- glowColor
- )
- AvatarSize.Size24 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size24,
- glowColor
- )
- AvatarSize.Size32 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size32,
- glowColor
- )
- AvatarSize.Size40 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size40,
- glowColor
- )
- AvatarSize.Size56 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size56,
- glowColor
- )
- AvatarSize.Size72 -> activityRingToken.activeBorderStroke(
- ActivityRingSize.Size72,
- glowColor
- )
- }
- else
- when (avatarInfo.size) {
- AvatarSize.Size16 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size16)
- AvatarSize.Size20 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size20)
- AvatarSize.Size24 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size24)
- AvatarSize.Size32 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size32)
- AvatarSize.Size40 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size40)
- AvatarSize.Size56 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size56)
- AvatarSize.Size72 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size72)
- }
+ return if (avatarInfo.active) when (avatarInfo.size) {
+ AvatarSize.Size16 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size16, glowColor
+ )
+
+ AvatarSize.Size20 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size20, glowColor
+ )
+
+ AvatarSize.Size24 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size24, glowColor
+ )
+
+ AvatarSize.Size32 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size32, glowColor
+ )
+
+ AvatarSize.Size40 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size40, glowColor
+ )
+
+ AvatarSize.Size56 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size56, glowColor
+ )
+
+ AvatarSize.Size72 -> activityRingToken.activeBorderStroke(
+ ActivityRingSize.Size72, glowColor
+ )
+ }
+ else when (avatarInfo.size) {
+ AvatarSize.Size16 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size16)
+ AvatarSize.Size20 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size20)
+ AvatarSize.Size24 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size24)
+ AvatarSize.Size32 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size32)
+ AvatarSize.Size40 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size40)
+ AvatarSize.Size56 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size56)
+ AvatarSize.Size72 -> activityRingToken.inactiveBorderStroke(ActivityRingSize.Size72)
+ }
}
+ @Composable
+ open fun cutoutColorFilter(avatarInfo: AvatarInfo): ColorFilter? {
+ return null
+ }
+
@Composable
open fun cutoutCornerRadius(avatarInfo: AvatarInfo): Dp {
return when (avatarInfo.cutoutStyle) {
@@ -742,8 +779,7 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
@Composable
private fun calculatedColor(
- avatarString: String,
- token: FluentGlobalTokens.SharedColorsTokens
+ avatarString: String, token: FluentGlobalTokens.SharedColorsTokens
): Color {
val colors = listOf(
FluentGlobalTokens.SharedColorSets.DarkRed,
@@ -778,40 +814,51 @@ open class AvatarTokens(private val activityRingToken: ActivityRingsToken = Acti
FluentGlobalTokens.SharedColorSets.Anchor
)
- when(token){
+ when (token) {
FluentGlobalTokens.SharedColorsTokens.Primary -> {
return colors[abs(avatarString.hashCode()) % colors.size].primary
}
+
FluentGlobalTokens.SharedColorsTokens.Tint10 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint10
}
+
FluentGlobalTokens.SharedColorsTokens.Tint20 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint20
}
+
FluentGlobalTokens.SharedColorsTokens.Tint30 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint30
}
+
FluentGlobalTokens.SharedColorsTokens.Tint40 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint40
}
+
FluentGlobalTokens.SharedColorsTokens.Tint50 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint50
}
+
FluentGlobalTokens.SharedColorsTokens.Tint60 -> {
return colors[abs(avatarString.hashCode()) % colors.size].tint60
}
+
FluentGlobalTokens.SharedColorsTokens.Shade10 -> {
return colors[abs(avatarString.hashCode()) % colors.size].shade10
}
+
FluentGlobalTokens.SharedColorsTokens.Shade20 -> {
return colors[abs(avatarString.hashCode()) % colors.size].shade20
}
+
FluentGlobalTokens.SharedColorsTokens.Shade30 -> {
return colors[abs(avatarString.hashCode()) % colors.size].shade30
}
+
FluentGlobalTokens.SharedColorsTokens.Shade40 -> {
return colors[abs(avatarString.hashCode()) % colors.size].shade40
}
+
FluentGlobalTokens.SharedColorsTokens.Shade50 -> {
return colors[abs(avatarString.hashCode()) % colors.size].shade50
}
@@ -832,6 +879,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size20 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
@@ -840,6 +888,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size24 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
@@ -848,6 +897,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size32 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
@@ -856,6 +906,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size40 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
@@ -864,6 +915,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size56 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
@@ -872,6 +924,7 @@ open class ActivityRingsToken : Parcelable {
)
)
)
+
ActivityRingSize.Size72 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth40.value,
@@ -885,8 +938,7 @@ open class ActivityRingsToken : Parcelable {
@Composable
open fun activeBorderStroke(
- activityRingSize: ActivityRingSize,
- glowColor: Color
+ activityRingSize: ActivityRingSize, glowColor: Color
): List {
return when (activityRingSize) {
ActivityRingSize.Size16 -> listOf(
@@ -895,120 +947,105 @@ open class ActivityRingsToken : Parcelable {
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth15.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth15.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth15.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size20 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth15.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth15.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size24 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size32 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size40 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size56 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth20.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
)
)
+
ActivityRingSize.Size72 -> listOf(
BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth40.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
)
- ),
- BorderStroke(
- FluentGlobalTokens.StrokeWidthTokens.StrokeWidth40.value,
- glowColor
- ),
- BorderStroke(
+ ), BorderStroke(
+ FluentGlobalTokens.StrokeWidthTokens.StrokeWidth40.value, glowColor
+ ), BorderStroke(
FluentGlobalTokens.StrokeWidthTokens.StrokeWidth40.value,
aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background1].value(
themeMode = themeMode
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/SearchBarTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/SearchBarTokens.kt
index 7c1ef43af..651c17340 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/SearchBarTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/SearchBarTokens.kt
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.DefaultShadowColor
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
@@ -29,6 +30,7 @@ open class SearchBarTokens : IControlToken, Parcelable {
FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background5].value(
themeMode = FluentTheme.themeMode
)
+
FluentStyle.Brand ->
FluentColor(
light = FluentTheme.aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground2].value(
@@ -50,6 +52,7 @@ open class SearchBarTokens : IControlToken, Parcelable {
FluentTheme.aliasTokens.neutralBackgroundColor[FluentAliasTokens.NeutralBackgroundColorTokens.Background3].value(
themeMode = FluentTheme.themeMode
)
+
FluentStyle.Brand ->
FluentColor(
light = FluentTheme.aliasTokens.brandBackgroundColor[FluentAliasTokens.BrandBackgroundColorTokens.BrandBackground1].value(
@@ -89,6 +92,7 @@ open class SearchBarTokens : IControlToken, Parcelable {
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value(
themeMode = FluentTheme.themeMode
)
+
FluentStyle.Brand ->
FluentColor(
light = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
@@ -107,6 +111,7 @@ open class SearchBarTokens : IControlToken, Parcelable {
when (searchBarInfo.style) {
FluentStyle.Neutral ->
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value()
+
FluentStyle.Brand ->
FluentColor(
light = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
@@ -127,6 +132,7 @@ open class SearchBarTokens : IControlToken, Parcelable {
FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value(
themeMode = FluentTheme.themeMode
)
+
FluentStyle.Brand ->
FluentColor(
light = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundOnColor].value(
@@ -168,4 +174,21 @@ open class SearchBarTokens : IControlToken, Parcelable {
open fun height(searchBarInfo: SearchBarInfo): Dp {
return 40.dp
}
+
+ @Composable
+ open fun cornerRadius(searchBarInfo: SearchBarInfo): Dp =
+ FluentGlobalTokens.CornerRadiusTokens.CornerRadius80.value
+
+ @Composable
+ open fun elevation(searchBarInfo: SearchBarInfo): Dp = 0.dp
+
+ @Composable
+ open fun borderWidth(searchBarInfo: SearchBarInfo): Dp = 0.dp
+
+ @Composable
+ open fun borderColor(searchBarInfo: SearchBarInfo): Color =
+ FluentTheme.aliasTokens.neutralStrokeColor[FluentAliasTokens.NeutralStrokeColorTokens.Stroke2].value()
+
+ @Composable
+ open fun shadowColor(searchBarInfo: SearchBarInfo): Color = DefaultShadowColor
}
\ No newline at end of file
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/TabItemTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/TabItemTokens.kt
index ab8784965..a429d013e 100644
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/TabItemTokens.kt
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/TabItemTokens.kt
@@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.ThemeMode
@@ -63,49 +66,53 @@ open class TabItemTokens : IControlToken, Parcelable {
}
@Composable
- open fun iconColor(tabItemInfo: TabItemInfo): StateColor {
+ open fun iconColor(tabItemInfo: TabItemInfo): StateBrush {
return when (tabItemInfo.fluentStyle) {
- FluentStyle.Neutral -> StateColor(
- rest = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value(
+ FluentStyle.Neutral -> StateBrush(
+ rest = SolidColor( FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground3].value(
themeMode = FluentTheme.themeMode
+ )
),
- pressed = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
+ pressed = SolidColor( FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
themeMode = FluentTheme.themeMode
+ )
),
- focused= FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ focused= SolidColor( FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
themeMode = FluentTheme.themeMode
+ )
),
- disabled = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ disabled = SolidColor( FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
themeMode = FluentTheme.themeMode
)
+ )
)
- FluentStyle.Brand -> StateColor(
- rest = FluentColor(
+ FluentStyle.Brand -> StateBrush(
+ rest = SolidColor( FluentColor(
light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
ThemeMode.Light
),
dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
ThemeMode.Dark
)
- ).value(FluentTheme.themeMode),
- pressed = FluentColor(
+ ).value(FluentTheme.themeMode)),
+ pressed = SolidColor( FluentColor(
light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1Pressed].value(
ThemeMode.Light
),
dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
ThemeMode.Dark
)
- ).value(FluentTheme.themeMode),
- selected = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(),
- disabled = FluentColor(
+ ).value(FluentTheme.themeMode)),
+ selected = SolidColor( FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value()),
+ disabled = SolidColor( FluentColor(
light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForegroundDisabled2].value(
ThemeMode.Light
),
dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
ThemeMode.Dark
)
- ).value(FluentTheme.themeMode)
+ ).value(FluentTheme.themeMode))
)
}
}
@@ -155,6 +162,51 @@ open class TabItemTokens : IControlToken, Parcelable {
}
}
+ @Composable
+ open fun indicatorColor(tabItemInfo: TabItemInfo): StateBrush {
+ return when (tabItemInfo.fluentStyle) {
+ FluentStyle.Neutral -> StateBrush(
+ rest = SolidColor(FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground2].value(
+ themeMode = FluentTheme.themeMode
+ )),
+ pressed = SolidColor(FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1Pressed].value(
+ themeMode = FluentTheme.themeMode
+ )),
+ disabled = SolidColor(FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ themeMode = FluentTheme.themeMode
+ ))
+ )
+
+ FluentStyle.Brand -> StateBrush(
+ rest = SolidColor(FluentColor(
+ light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value(
+ ThemeMode.Light
+ ),
+ dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ ThemeMode.Dark
+ )
+ ).value(FluentTheme.themeMode)),
+ pressed = SolidColor(FluentColor(
+ light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1Pressed].value(
+ ThemeMode.Light
+ ),
+ dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.Foreground1].value(
+ ThemeMode.Dark
+ )
+ ).value(FluentTheme.themeMode)),
+ selected = SolidColor(FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForeground1].value()),
+ disabled = SolidColor(FluentColor(
+ light = FluentTheme.aliasTokens.brandForegroundColor[FluentAliasTokens.BrandForegroundColorTokens.BrandForegroundDisabled2].value(
+ ThemeMode.Light
+ ),
+ dark = FluentTheme.aliasTokens.neutralForegroundColor[FluentAliasTokens.NeutralForegroundColorTokens.ForegroundDisable1].value(
+ ThemeMode.Dark
+ )
+ ).value(FluentTheme.themeMode))
+ )
+ }
+ }
+
@Composable
open fun padding(tabItemInfo: TabItemInfo): PaddingValues {
return when(tabItemInfo.tabTextAlignment){
@@ -163,4 +215,9 @@ open class TabItemTokens : IControlToken, Parcelable {
TabTextAlignment.NO_TEXT -> PaddingValues(top = 8.dp, start = 8.dp, bottom = 4.dp, end = 8.dp)
}
}
+
+ @Composable
+ open fun textTypography(tabItemInfo: TabItemInfo): TextStyle {
+ return FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Caption2]
+ }
}
\ No newline at end of file
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ViewPagerTokens.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ViewPagerTokens.kt
new file mode 100644
index 000000000..a40930b7c
--- /dev/null
+++ b/fluentui_core/src/main/java/com/microsoft/fluentui/theme/token/controlTokens/ViewPagerTokens.kt
@@ -0,0 +1,32 @@
+package com.microsoft.fluentui.theme.token.controlTokens
+
+import android.os.Parcelable
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.token.ControlInfo
+import com.microsoft.fluentui.theme.token.FluentAliasTokens
+import com.microsoft.fluentui.theme.token.FluentGlobalTokens
+import com.microsoft.fluentui.theme.token.IControlToken
+import kotlinx.parcelize.Parcelize
+
+
+open class ViewPagerInfo: ControlInfo
+@Parcelize
+open class ViewPagerTokens : IControlToken, Parcelable {
+
+ @Composable
+ open fun contentPadding(viewPagerInfo: ViewPagerInfo): PaddingValues {
+ return PaddingValues(0.dp)
+ }
+
+ @Composable
+ open fun pageSpacing(viewPagerInfo: ViewPagerInfo): Dp {
+ return 0.dp
+ }
+}
diff --git a/fluentui_core/src/main/java/com/microsoft/fluentui/util/DuoSupportUtils.kt b/fluentui_core/src/main/java/com/microsoft/fluentui/util/DuoSupportUtils.kt
deleted file mode 100644
index 170e549cb..000000000
--- a/fluentui_core/src/main/java/com/microsoft/fluentui/util/DuoSupportUtils.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.microsoft.fluentui.util
-
-import android.app.Activity
-import android.content.Context
-import android.graphics.Rect
-import androidx.recyclerview.widget.GridLayoutManager
-import android.view.View
-import com.microsoft.device.dualscreen.layout.ScreenHelper
-import java.lang.Exception
-
-/**
- * [DuoSupportUtils] is helper object for Surface Duo Device Support
- */
-object DuoSupportUtils {
- const val DUO_HINGE_WIDTH = 84
- const val COLUMNS_IN_START_DUO_MODE = 3
- const val COLUMNS_IN_END_DUO_MODE = 4
-
- @JvmStatic
- fun isDeviceSurfaceDuo(activity: Activity) = ScreenHelper.isDeviceSurfaceDuo(activity)
-
- @JvmStatic
- fun isDualScreenMode(activity: Activity) = ScreenHelper.isDualMode(activity)
-
- @JvmStatic
- fun getRotation(activity: Activity) = ScreenHelper.getCurrentRotation(activity)
-
- @JvmStatic
- fun getHinge(activity: Activity) = ScreenHelper.getHinge(activity)
-
- @JvmStatic
- fun getScreenRectangles(activity: Activity) = ScreenHelper.getScreenRectangles(activity)
-
- /**
- * Use [isWindowDoublePortrait] to check if the device is in landscape mode and app is spanned.
- */
- @JvmStatic
- fun isWindowDoublePortrait(activity: Activity): Boolean {
- return activity.isLandscape && isDualScreenMode(activity)
- }
-
- /**
- * Use [isWindowDoubleLandscape] to check if the device is in portrait mode and app is spanned.
- */
- @JvmStatic
- fun isWindowDoubleLandscape(activity: Activity): Boolean {
- return activity.isPortrait && isDualScreenMode(activity)
- }
-
- private fun getRect(view: View): Rect {
- val screenPos = IntArray(2)
- view.getLocationOnScreen(screenPos)
- return Rect(screenPos[0], screenPos[1], view.width, view.height)
- }
-
- /**
- * Use [moreOnLeft] to check if a given [View] or [Rect] is more to the left of the Surface Duo hinge or is more to the right.
- * @return true if the View or Rect is more on left of the hinge, false otherwise. (false implies more to the right of the hinge)
- */
- @JvmStatic
- fun moreOnLeft(activity: Activity, rect: Rect) = isWindowDoublePortrait(activity) && ((getHinge(activity)!!.left - rect.left) >= (rect.right - getHinge(activity)!!.right))
-
- @JvmStatic
- fun moreOnLeft(activity: Activity, view: View) = moreOnLeft(activity, getRect(view))
-
-
- /**
- * Use [moreOnTop] to check if a given [View] or [Rect] is more on the top of Surface Duo hinge or is more on the bottom.
- * @return true if the View or Rect is more on top of the hinge, false otherwise. (false implies more on the bottom of the hinge)
- */
- @JvmStatic
- fun moreOnTop(activity: Activity, rect: Rect) = isWindowDoubleLandscape(activity) && ((getHinge(activity)!!.top - rect.top) >= (rect.bottom - getHinge(activity)!!.bottom))
-
- @JvmStatic
- fun moreOnTop(activity: Activity, view: View) = moreOnTop(activity, getRect(view))
-
- /**
- * Use [intersectHinge] to check if a given [View] or [Rect] intersects with the Surface Duo hinge.
- * @return true if the View or Rect intersects with the hinge, false otherwise.
- */
- @JvmStatic
- fun intersectHinge(activity: Activity, anchorRect: Rect): Boolean {
- return isDeviceSurfaceDuo(activity) && getHinge(activity)!!.intersect(anchorRect)
- }
-
- @JvmStatic
- fun intersectHinge(activity: Activity, anchor: View) = intersectHinge(activity, getRect(anchor))
-
- /**
- * Returns the width of hinge/display mask.
- */
- @JvmStatic
- fun getHingeWidth(activity: Activity): Int {
- if (!isDeviceSurfaceDuo(activity)) return 0
- return if (activity.isLandscape)
- getHinge(activity)!!.width()
- else
- getHinge(activity)!!.height()
- }
-
- /**
- * Returns the width of hinge/display mask.
- */
- @JvmStatic
- fun getHalfScreenWidth(activity: Activity): Int {
- if (!isDeviceSurfaceDuo(activity)) return activity.baseContext.displaySize.x/2
- return (activity.baseContext.displaySize.x - getHingeWidth(activity))/2
- }
-
- fun getSpanSizeLookup(activity: Activity): GridLayoutManager.SpanSizeLookup {
- val span: Int = getHalfScreenWidth(activity) / COLUMNS_IN_START_DUO_MODE
- val spanMid: Int = getHalfScreenWidth(activity) / COLUMNS_IN_START_DUO_MODE + getHingeWidth(activity)
- val spanEnd: Int = getHalfScreenWidth(activity) / COLUMNS_IN_END_DUO_MODE
-
- return object : GridLayoutManager.SpanSizeLookup() {
- override fun getSpanSize(position: Int): Int {
- if (position % (COLUMNS_IN_START_DUO_MODE+ COLUMNS_IN_END_DUO_MODE) < 2) {
- return span
- } else if (position % (COLUMNS_IN_START_DUO_MODE+ COLUMNS_IN_END_DUO_MODE) == 2) {
- return spanMid
- } else {
- return spanEnd
- }
- }
- }
- }
-
- /**
- * Use [getSingleScreenWidthPixels] to get the pixels of a single screen on Surface Duo device
- */
- @JvmStatic
- fun getSingleScreenWidthPixels(activity: Activity) = if (isWindowDoublePortrait(activity)) (activity.displaySize.x - getHingeWidth(activity)) / 2 else activity.displaySize.x
-
- /**
- * Exception thrown when the [Context] is not activity context.
- */
- class ActivityContextNotFoundException : Exception("Activity Context is required.")
-}
\ No newline at end of file
diff --git a/fluentui_drawer/build.gradle b/fluentui_drawer/build.gradle
index 8acaf3ab1..abe58f744 100644
--- a/fluentui_drawer/build.gradle
+++ b/fluentui_drawer/build.gradle
@@ -38,6 +38,9 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
@@ -49,6 +52,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "com.google.android.material:material:$materialVersion"
+ implementation "androidx.activity:activity-compose:$composeActivityVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetAdapter.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetAdapter.kt
index 3912c7a75..bc0578592 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetAdapter.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetAdapter.kt
@@ -103,15 +103,22 @@ class BottomSheetAdapter : RecyclerView.Adapter {
image.imageAlpha = ThemeUtil.getThemeAttrColor(FluentUIContextThemeWrapper(context, R.style.Theme_FluentUI_Drawer), R.attr.fluentuiBottomSheetDisabledIconColor)
listItemView.customView = image
- var accessoryImage: ImageView ?= null
- if (item.accessoryBitmap != null) {
- accessoryImage = context.createImageView(item.accessoryBitmap)
+ var accessoryView: View ?= null
+ var accessoryImageView: ImageView ?= null
+ if (item.customAccessoryView != null) {
+ accessoryView = item.customAccessoryView
+ } else if (item.accessoryBitmap != null) {
+ accessoryImageView = context.createImageView(item.accessoryBitmap)
} else if (item.accessoryImageId != NO_ID) {
- accessoryImage = context.createImageView(item.accessoryImageId, item.getImageTint(context))
+ accessoryImageView = context.createImageView(item.accessoryImageId, item.getImageTint(context))
}
- if (accessoryImage != null && item.disabled)
- accessoryImage.imageAlpha = ThemeUtil.getThemeAttrColor(FluentUIContextThemeWrapper(context, R.style.Theme_FluentUI_Drawer), R.attr.fluentuiBottomSheetDisabledIconColor)
- listItemView.customAccessoryView = accessoryImage
+ if (accessoryView != null) {
+ accessoryView.isEnabled = !item.disabled
+ } else if (accessoryImageView != null && item.disabled) {
+ accessoryImageView.imageAlpha =
+ ThemeUtil.getThemeAttrColor(FluentUIContextThemeWrapper(context, R.style.Theme_FluentUI_Drawer), R.attr.fluentuiBottomSheetDisabledIconColor)
+ }
+ listItemView.customAccessoryView = accessoryView ?: accessoryImageView
listItemView.setOnClickListener {
onBottomSheetItemClickListener?.onBottomSheetItemClick(item)
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetItem.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetItem.kt
index 17bfa6b47..a4cdabb71 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetItem.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/bottomsheet/BottomSheetItem.kt
@@ -44,6 +44,7 @@ class BottomSheetItem : Parcelable {
val accessoryBitmap: Bitmap?
val roleDescription: String
+ val customAccessoryView: View?
@JvmOverloads
constructor(
@@ -59,6 +60,7 @@ class BottomSheetItem : Parcelable {
@DrawableRes accessoryImageId: Int = NO_ID,
accessoryBitmap: Bitmap? = null,
roleDescription: String = "",
+ customAccessoryView: View? = null
) {
this.id = id
this.imageId = imageId
@@ -72,6 +74,7 @@ class BottomSheetItem : Parcelable {
this.accessoryImageId = accessoryImageId
this.accessoryBitmap = accessoryBitmap
this.roleDescription = roleDescription
+ this.customAccessoryView = customAccessoryView
}
private constructor(parcel: Parcel) : this(
@@ -123,6 +126,7 @@ class BottomSheetItem : Parcelable {
if (accessoryImageId != other.accessoryImageId) return false
if (accessoryBitmap != other.accessoryBitmap) return false
if (roleDescription != other.roleDescription) return false
+ if (customAccessoryView != other.customAccessoryView) return false
return true
}
@@ -140,6 +144,7 @@ class BottomSheetItem : Parcelable {
result = 31 * result + accessoryImageId.hashCode()
result = 31 * result + (accessoryBitmap?.hashCode() ?: 0)
result = 31 * result + roleDescription.hashCode()
+ result = 31 * result + (customAccessoryView?.hashCode() ?: 0)
return result
}
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/drawer/DrawerDialog.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/drawer/DrawerDialog.kt
index a8b2c86b2..2c7dc9997 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/drawer/DrawerDialog.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/drawer/DrawerDialog.kt
@@ -194,7 +194,11 @@ open class DrawerDialog @JvmOverloads constructor(context: Context, val behavior
val displaySize: Point = context.displaySize
val layoutParams: WindowManager.LayoutParams? = window?.attributes
- layoutParams?.gravity = Gravity.TOP
+ if(behaviorType == BehaviorType.TOP) {
+ layoutParams?.gravity = Gravity.TOP
+ } else {
+ layoutParams?.gravity = Gravity.BOTTOM
+ }
layoutParams?.y = topMargin
layoutParams?.dimAmount = this.dimValue
window?.attributes = layoutParams
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/persistentbottomsheet/SheetHorizontalItemAdapter.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/persistentbottomsheet/SheetHorizontalItemAdapter.kt
index a1ce00be6..59f13cae0 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/persistentbottomsheet/SheetHorizontalItemAdapter.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/persistentbottomsheet/SheetHorizontalItemAdapter.kt
@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.annotation.ColorInt
import com.microsoft.fluentui.drawer.R
import com.microsoft.fluentui.util.createImageView
@@ -18,7 +19,7 @@ import com.microsoft.fluentui.util.createImageView
/**
* [SheetHorizontalItemAdapter] is used for horizontal list in bottomSheet
*/
-class SheetHorizontalItemAdapter(private val context: Context, items: ArrayList, @StyleRes private val themeId: Int = R.style.Theme_FluentUI_Drawer, private val marginBetweenView: Int = 0) : RecyclerView.Adapter() {
+class SheetHorizontalItemAdapter(private val context: Context, items: ArrayList, @StyleRes private val themeId: Int = R.style.Theme_FluentUI_Drawer, private val marginBetweenView: Int = 0, @ColorInt private val drawerTint: Int? = null) : RecyclerView.Adapter() {
var mOnSheetItemClickListener: SheetItem.OnClickListener? = null
private val mItems: ArrayList = items
@@ -49,7 +50,7 @@ class SheetHorizontalItemAdapter(private val context: Context, items: ArrayList<
if (it != null) {
listItemView.update(item.title, context.createImageView(it), item.disabled)
} else {
- listItemView.update(item.title, context.createImageView(item.drawable), item.disabled)
+ listItemView.update(item.title, context.createImageView(item.drawable, imageTint = drawerTint), item.disabled)
}
}
listItemView.setOnClickListener {
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/Utils.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/Utils.kt
index 3f4f73980..5352f1198 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/Utils.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/Utils.kt
@@ -1,4 +1,47 @@
package com.microsoft.fluentui.tokenized
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.testTag
+
internal fun calculateFraction(a: Float, b: Float, pos: Float) =
((pos - a) / (b - a)).coerceIn(0f, 1f)
+
+@Composable
+internal fun Scrim(
+ open: Boolean,
+ color: Color,
+ onClose: () -> Unit,
+ fraction: () -> Float,
+ preventDismissalOnScrimClick: Boolean = false,
+ onScrimClick: () -> Unit = {},
+ tag: String
+) {
+ val dismissDrawer = if (open) {
+ Modifier.pointerInput(onClose) {
+ detectTapGestures {
+ if (!preventDismissalOnScrimClick) {
+ onClose()
+ }
+ onScrimClick() //this function runs post onClose() so that the drawer is closed before the callback is invoked
+ }
+ }
+ } else {
+ Modifier
+ }
+
+ Canvas(
+ Modifier
+ .fillMaxSize()
+ .then(dismissDrawer)
+ .testTag(tag)
+
+ ) {
+ drawRect(color = color, alpha = fraction())
+ }
+}
\ No newline at end of file
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt
index d566415a7..d7a5470e9 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/bottomsheet/BottomSheet.kt
@@ -13,7 +13,6 @@ import android.view.accessibility.AccessibilityManager
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.*
@@ -31,7 +30,6 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.*
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@@ -58,6 +56,7 @@ import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
+import com.microsoft.fluentui.tokenized.Scrim
/**
* Possible values of [BottomSheetState].
@@ -101,7 +100,7 @@ class BottomSheetState(
/**
* Whether the bottom sheet is visible.
*/
- val isVisible: Boolean
+ var isVisible: Boolean = false
get() = currentValue != BottomSheetValue.Hidden
internal val hasExpandedState: Boolean
@@ -135,25 +134,27 @@ class BottomSheetState(
*
* @throws [CancellationException] if the animation is interrupted
*/
- suspend fun hide() = animateTo(BottomSheetValue.Hidden)
+ suspend fun hide() {
+ try {
+ animateTo(BottomSheetValue.Hidden)
+ } finally {
+ isVisible = false
+ }
+ }
companion object {
/**
* The default [Saver] implementation for [BottomSheetState].
*/
fun Saver(
- animationSpec: AnimationSpec,
- confirmStateChange: (BottomSheetValue) -> Boolean
- ): Saver = Saver(
- save = { it.currentValue },
- restore = {
- BottomSheetState(
- initialValue = it,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
- )
- }
- )
+ animationSpec: AnimationSpec, confirmStateChange: (BottomSheetValue) -> Boolean
+ ): Saver = Saver(save = { it.currentValue }, restore = {
+ BottomSheetState(
+ initialValue = it,
+ animationSpec = animationSpec,
+ confirmStateChange = confirmStateChange
+ )
+ })
}
}
@@ -171,10 +172,8 @@ fun rememberBottomSheetState(
confirmStateChange: (BottomSheetValue) -> Boolean = { true }
): BottomSheetState {
return rememberSaveable(
- initialValue, animationSpec, confirmStateChange,
- saver = BottomSheetState.Saver(
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ initialValue, animationSpec, confirmStateChange, saver = BottomSheetState.Saver(
+ animationSpec = animationSpec, confirmStateChange = confirmStateChange
)
) {
BottomSheetState(
@@ -229,13 +228,15 @@ fun BottomSheet(
sheetState: BottomSheetState = rememberBottomSheetState(BottomSheetValue.Hidden),
expandable: Boolean = true,
peekHeight: Dp = 110.dp,
- scrimVisible: Boolean = true,
+ scrimVisible: Boolean = false,
showHandle: Boolean = true,
slideOver: Boolean = true,
enableSwipeDismiss: Boolean = false,
+ preventDismissalOnScrimClick: Boolean = false, // if true, the sheet will not be dismissed when the scrim is clicked
stickyThresholdUpward: Float = 56f,
stickyThresholdDownward: Float = 56f,
bottomSheetTokens: BottomSheetTokens? = null,
+ onDismiss: () -> Unit = {}, // callback to be invoked after the sheet is closed
content: @Composable () -> Unit
) {
val themeID =
@@ -252,16 +253,14 @@ fun BottomSheet(
val sheetBackgroundColor: Brush = tokens.backgroundBrush(bottomSheetInfo)
val sheetHandleColor: Color = tokens.handleColor(bottomSheetInfo)
val scrimOpacity: Float = tokens.scrimOpacity(bottomSheetInfo)
- val scrimColor: Color =
- tokens.scrimColor(bottomSheetInfo).copy(alpha = scrimOpacity)
+ val scrimColor: Color = tokens.scrimColor(bottomSheetInfo).copy(alpha = scrimOpacity)
val scope = rememberCoroutineScope()
- val maxLandscapeWidth :Float= tokens.maxLandscapeWidth(bottomSheetInfo)
+ val maxLandscapeWidth: Float = tokens.maxLandscapeWidth(bottomSheetInfo)
BoxWithConstraints(modifier) {
val fullHeight = constraints.maxHeight.toFloat()
- val sheetHeightState =
- remember(sheetContent.hashCode()) { mutableStateOf(null) }
+ val sheetHeightState = remember(sheetContent.hashCode()) { mutableStateOf(null) }
Box(
Modifier
@@ -276,35 +275,42 @@ fun BottomSheet(
true
}
}
- }
- ) {
+ }) {
content()
- if (slideOver) {
+ if (scrimVisible) {
Scrim(
- color = if (scrimVisible) scrimColor else Color.Transparent,
- onDismiss = {
+ color = scrimColor,
+ onClose = {
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
- scope.launch { sheetState.show() }
+ scope.launch { sheetState.hide() }
}
},
fraction = {
- if (sheetState.anchors.isEmpty()
- || !sheetState.anchors.containsValue(BottomSheetValue.Expanded)
- || (sheetHeightState.value != null && sheetHeightState.value == 0f)
- ) {
+ if (sheetState.anchors.isEmpty() || (sheetHeightState.value != null && sheetHeightState.value == 0f)) {
0.toFloat()
} else {
+ val targetValue: BottomSheetValue = if (slideOver) {
+ if (sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded } != null) {
+ BottomSheetValue.Expanded
+ } else if (sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Shown } != null) {
+ BottomSheetValue.Shown
+ } else {
+ BottomSheetValue.Hidden
+ }
+ } else {
+ BottomSheetValue.Shown
+ }
calculateFraction(
- sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Shown }?.key!!,
- sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded }?.key!!,
+ sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Hidden }?.key!!,
+ sheetState.anchors.entries.firstOrNull { it.value == targetValue }?.key!!,
sheetState.offset.value
)
}
},
- visible = (sheetState.targetValue == BottomSheetValue.Expanded
- || (sheetState.targetValue == BottomSheetValue.Shown
- && sheetState.currentValue == BottomSheetValue.Expanded)
- )
+ open = sheetState.isVisible,
+ onScrimClick = onDismiss,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ tag = BOTTOMSHEET_SCRIM_TAG
)
}
}
@@ -314,15 +320,14 @@ fun BottomSheet(
Modifier
.align(Alignment.TopCenter)
.fillMaxWidth(
- if(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)maxLandscapeWidth
+ if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) maxLandscapeWidth
else 1F
)
.nestedScroll(
if (!enableSwipeDismiss && sheetState.offset.value >= (fullHeight - dpToPx(
peekHeight
))
- )
- sheetState.NonDismissiblePostDownNestedScrollConnection
+ ) sheetState.NonDismissiblePostDownNestedScrollConnection
else if (slideOver) sheetState.PreUpPostDownNestedScrollConnection
else sheetState.PostDownNestedScrollConnection
)
@@ -360,11 +365,7 @@ fun BottomSheet(
}
}
.sheetHeight(
- expandable,
- slideOver,
- fullHeight,
- peekHeight,
- sheetState
+ expandable, slideOver, fullHeight, peekHeight, sheetState
)
.clip(sheetShape)
.shadow(sheetElevation)
@@ -376,6 +377,7 @@ fun BottomSheet(
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
scope.launch { sheetState.hide() }
}
+ onDismiss()
true
}
}
@@ -423,6 +425,7 @@ fun BottomSheet(
if (!sheetState.isVisible) {
if (enableSwipeDismiss) {
scope.launch { sheetState.hide() }
+ onDismiss()
} else {
scope.launch { sheetState.show() }
}
@@ -434,67 +437,65 @@ fun BottomSheet(
) {
val collapsed = LocalContext.current.resources.getString(R.string.collapsed)
val expanded = LocalContext.current.resources.getString(R.string.expanded)
- val accessibilityManager = LocalContext.current.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
- Icon(
- painterResource(id = R.drawable.ic_drawer_handle),
- contentDescription =
- if (sheetState.currentValue == BottomSheetValue.Expanded || (sheetState.hasExpandedState && sheetState.isVisible)) {
+ val accessibilityManager =
+ LocalContext.current.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
+ Icon(painterResource(id = R.drawable.ic_drawer_handle),
+ contentDescription = if (sheetState.currentValue == BottomSheetValue.Expanded || (sheetState.hasExpandedState && sheetState.isVisible)) {
LocalContext.current.resources.getString(R.string.drag_handle)
} else {
null
},
tint = sheetHandleColor,
- modifier = Modifier
- .clickable(
- enabled = sheetState.hasExpandedState,
- role = Role.Button,
- onClickLabel =
- if (sheetState.currentValue == BottomSheetValue.Expanded) {
- LocalContext.current.resources.getString(R.string.collapse)
- } else {
- if (sheetState.hasExpandedState && sheetState.isVisible) LocalContext.current.resources.getString(
- R.string.expand
- ) else null
- }
- ) {
- if (sheetState.currentValue == BottomSheetValue.Expanded) {
- if (sheetState.confirmStateChange(BottomSheetValue.Shown)) {
- scope.launch { sheetState.show() }
- accessibilityManager?.let { manager ->
- if(manager.isEnabled){
- val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
- text.add(collapsed)
- }
- manager.sendAccessibilityEvent(event)
- }
+ modifier = Modifier.clickable(
+ enabled = sheetState.hasExpandedState,
+ role = Role.Button,
+ onClickLabel = if (sheetState.currentValue == BottomSheetValue.Expanded) {
+ LocalContext.current.resources.getString(R.string.collapse)
+ } else {
+ if (sheetState.hasExpandedState && sheetState.isVisible) LocalContext.current.resources.getString(
+ R.string.expand
+ ) else null
+ }
+ ) {
+ if (sheetState.currentValue == BottomSheetValue.Expanded) {
+ if (sheetState.confirmStateChange(BottomSheetValue.Shown)) {
+ scope.launch { sheetState.show() }
+ accessibilityManager?.let { manager ->
+ if (manager.isEnabled) {
+ val event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ .apply {
+ text.add(collapsed)
+ }
+ manager.sendAccessibilityEvent(event)
}
}
- } else if (sheetState.hasExpandedState) {
- if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
- scope.launch { sheetState.expand() }
- accessibilityManager?.let { manager ->
- if(manager.isEnabled){
- val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
- text.add(expanded)
- }
- manager.sendAccessibilityEvent(event)
- }
+ }
+ } else if (sheetState.hasExpandedState) {
+ if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
+ scope.launch { sheetState.expand() }
+ accessibilityManager?.let { manager ->
+ if (manager.isEnabled) {
+ val event =
+ AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ .apply {
+ text.add(expanded)
+ }
+ manager.sendAccessibilityEvent(event)
}
}
}
}
- )
+ })
}
}
Column(modifier = Modifier
.testTag(BOTTOMSHEET_CONTENT_TAG)
- .then(if (slideOver) Modifier
- .onFocusChanged { focusState ->
- if (focusState.hasFocus && sheetState.currentValue != BottomSheetValue.Expanded) { // this expands the sheet when the content is focused
- scope.launch { sheetState.expand() }
- }
- } else Modifier.fillMaxSize()),
- content = { sheetContent() })
+ .then(if (slideOver) Modifier.onFocusChanged { focusState ->
+ if (focusState.hasFocus && sheetState.currentValue != BottomSheetValue.Expanded) { // this expands the sheet when the content is focused
+ scope.launch { sheetState.expand() }
+ }
+ } else Modifier.fillMaxSize()), content = { sheetContent() })
}
}
}
@@ -517,27 +518,27 @@ private fun Modifier.bottomSheetSwipeable(
if (sheetHeight != null && sheetHeight != 0f) {
val anchors = if (!expandable) {
mapOf(
- fullHeight to BottomSheetValue.Hidden,
- (fullHeight - min(sheetHeight, peekHeightPx))+keyCorrection to BottomSheetValue.Shown
+ fullHeight to BottomSheetValue.Hidden, (fullHeight - min(
+ sheetHeight, peekHeightPx
+ )) + keyCorrection to BottomSheetValue.Shown
)
} else if (sheetHeight <= peekHeightPx) {
mapOf(
fullHeight to BottomSheetValue.Hidden,
- (fullHeight - sheetHeight)+keyCorrection to BottomSheetValue.Shown
+ (fullHeight - sheetHeight) + keyCorrection to BottomSheetValue.Shown
)
} else {
mapOf(
fullHeight to BottomSheetValue.Hidden,
- (fullHeight - peekHeightPx)+keyCorrection to BottomSheetValue.Shown,
- (max(0f, fullHeight - sheetHeight))+(keyCorrection*2) to BottomSheetValue.Expanded
+ (fullHeight - peekHeightPx) + keyCorrection to BottomSheetValue.Shown,
+ (max(
+ 0f, fullHeight - sheetHeight
+ )) + (keyCorrection * 2) to BottomSheetValue.Expanded
)
}
- if (sheetState.initialValue == BottomSheetValue.Expanded
- && anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded } == null
- ) {
+ if (sheetState.initialValue == BottomSheetValue.Expanded && anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded } == null) {
throw IllegalArgumentException(
- "BottomSheet initial value must not be set to Expanded " +
- "if the whole content is visible in Shown state itself"
+ "BottomSheet initial value must not be set to Expanded " + "if the whole content is visible in Shown state itself"
)
}
Modifier.swipeable(
@@ -549,9 +550,15 @@ private fun Modifier.bottomSheetSwipeable(
val fromKey = anchors.entries.firstOrNull { it.value == from }?.key
val toKey = anchors.entries.firstOrNull { it.value == to }?.key
- if(fromKey == null || toKey == null) { FixedThreshold(56.dp) } //in case of null defaulting to 56.dp threshold
- else if (fromKey < toKey) { FixedThreshold(stickyThresholdDownward.dp) } // Threshold for drag down
- else{ FixedThreshold(stickyThresholdUpward.dp) } // Threshold for drag up
+ if (fromKey == null || toKey == null) {
+ FixedThreshold(56.dp)
+ } //in case of null defaulting to 56.dp threshold
+ else if (fromKey < toKey) {
+ FixedThreshold(stickyThresholdDownward.dp)
+ } // Threshold for drag down
+ else {
+ FixedThreshold(stickyThresholdUpward.dp)
+ } // Threshold for drag up
},
resistance = null
)
@@ -582,9 +589,15 @@ private fun Modifier.bottomSheetSwipeable(
val fromKey = anchors.entries.firstOrNull { it.value == from }?.key
val toKey = anchors.entries.firstOrNull { it.value == to }?.key
- if(fromKey == null || toKey == null) { FixedThreshold(56.dp) } //in case of null defaulting to 56 as a fallback
- else if (fromKey < toKey) { FixedThreshold(stickyThresholdDownward.dp) } // Threshold for drag down
- else{ FixedThreshold(stickyThresholdUpward.dp) } // Threshold for drag up
+ if (fromKey == null || toKey == null) {
+ FixedThreshold(56.dp)
+ } //in case of null defaulting to 56 as a fallback
+ else if (fromKey < toKey) {
+ FixedThreshold(stickyThresholdDownward.dp)
+ } // Threshold for drag down
+ else {
+ FixedThreshold(stickyThresholdUpward.dp)
+ } // Threshold for drag up
},
resistance = null
)
@@ -601,47 +614,16 @@ private fun Modifier.sheetHeight(
peekHeight: Dp,
sheetState: BottomSheetState
): Modifier {
- val modifier =
- if (slideOver) {
- if (expandable) {
- Modifier
- } else {
- Modifier.heightIn(
- 0.dp,
- pxToDp(min(fullHeight * BottomSheetOpenFraction, dpToPx(peekHeight)))
- )
- }
- } else {
- Modifier.heightIn(0.dp, pxToDp(fullHeight - sheetState.offset.value))
- }
- return this.then(modifier)
-}
-
-//TODO : Revisit Scrim usage across module to check single scrim implementation across module.
-@Composable
-private fun Scrim(
- color: Color,
- onDismiss: () -> Unit,
- fraction: () -> Float,
- visible: Boolean
-) {
- if (visible) {
- val closeSheet = LocalContext.current.resources.getString(R.string.fluentui_close_sheet)
- val dismissModifier = Modifier
- .pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
- .semantics(mergeDescendants = true) {
- contentDescription = closeSheet
- onClick { onDismiss(); true }
- }
-
- Canvas(
+ val modifier = if (slideOver) {
+ if (expandable) {
Modifier
- .fillMaxSize()
- .then(dismissModifier)
- .testTag(BOTTOMSHEET_SCRIM_TAG)
-
- ) {
- drawRect(color = color, alpha = fraction())
+ } else {
+ Modifier.heightIn(
+ 0.dp, pxToDp(min(fullHeight * BottomSheetOpenFraction, dpToPx(peekHeight)))
+ )
}
+ } else {
+ Modifier.heightIn(0.dp, pxToDp(fullHeight - sheetState.offset.value))
}
+ return this.then(modifier)
}
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/BottomDrawer.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/BottomDrawer.kt
new file mode 100644
index 000000000..debf37dac
--- /dev/null
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/BottomDrawer.kt
@@ -0,0 +1,426 @@
+package com.microsoft.fluentui.tokenized.drawer
+
+import android.content.Context
+import android.content.res.Configuration
+import android.view.accessibility.AccessibilityEvent
+import android.view.accessibility.AccessibilityManager
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.collapse
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.expand
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.compose.NonDismissiblePreUpPostDownNestedScrollConnection
+import com.microsoft.fluentui.compose.PostDownNestedScrollConnection
+import com.microsoft.fluentui.compose.swipeable
+import com.microsoft.fluentui.drawer.R
+import com.microsoft.fluentui.theme.token.Icon
+import com.microsoft.fluentui.tokenized.calculateFraction
+import com.microsoft.fluentui.util.pxToDp
+import kotlinx.coroutines.launch
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+
+@Composable
+internal fun BottomDrawer(
+ modifier: Modifier,
+ drawerState: DrawerState,
+ drawerShape: Shape,
+ drawerElevation: Dp,
+ drawerBackground: Brush,
+ drawerHandleColor: Color,
+ scrimColor: Color,
+ scrimVisible: Boolean,
+ slideOver: Boolean,
+ enableSwipeDismiss: Boolean = true,
+ showHandle: Boolean,
+ onDismiss: () -> Unit,
+ drawerContent: @Composable () -> Unit,
+ maxLandscapeWidthFraction : Float = 1F,
+ preventDismissalOnScrimClick: Boolean = false,
+ onScrimClick: () -> Unit = {}
+) {
+ BoxWithConstraints(modifier.fillMaxSize()) {
+ val fullHeight = constraints.maxHeight.toFloat()
+ val drawerHeight =
+ remember(drawerContent.hashCode()) { mutableStateOf(null) }
+ val maxOpenHeight = fullHeight * DrawerOpenFraction
+
+ val drawerConstraints = with(LocalDensity.current) {
+ Modifier
+ .sizeIn(
+ maxWidth = constraints.maxWidth.toDp(),
+ maxHeight = constraints.maxHeight.toDp()
+ )
+ }
+ val scope = rememberCoroutineScope()
+
+ Scrim(
+ open = !drawerState.isClosed || (drawerHeight != null && drawerHeight.value == 0f),
+ onClose = onDismiss,
+ fraction = {
+ if (drawerState.anchors.isEmpty() || (drawerHeight != null && drawerHeight.value == 0f)) {
+ 0.toFloat()
+ } else {
+ var targetValue: DrawerValue = if (slideOver) {
+ drawerState.anchors.maxBy { it.value }.value
+ } else if (drawerState.skipOpenState) {
+ DrawerValue.Expanded
+ } else {
+ DrawerValue.Open
+ }
+ calculateFraction(
+ drawerState.anchors.entries.firstOrNull { it.value == DrawerValue.Closed }?.key!!,
+ drawerState.anchors.entries.firstOrNull { it.value == targetValue }?.key!!,
+ drawerState.offset.value
+ )
+ }
+ },
+ color = if (scrimVisible) scrimColor else Color.Transparent,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ onScrimClick = onScrimClick
+ )
+ val configuration = LocalConfiguration.current
+ Box(
+ drawerConstraints
+ .fillMaxWidth(
+ if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) maxLandscapeWidthFraction
+ else 1F
+ )
+ .nestedScroll(
+ if (!enableSwipeDismiss && drawerState.offset.value >= maxOpenHeight) drawerState.NonDismissiblePreUpPostDownNestedScrollConnection else
+ if (slideOver) drawerState.nestedScrollConnection else drawerState.PostDownNestedScrollConnection
+ )
+ .offset {
+ val y = if (drawerState.anchors == null) {
+ fullHeight.roundToInt()
+ } else {
+ drawerState.offset.value.roundToInt()
+ }
+ IntOffset(x = 0, y = y)
+ }
+ .then(
+ if (maxLandscapeWidthFraction != 1F
+ && configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+ ) Modifier.align(Alignment.TopCenter)
+ else Modifier
+ )
+ .onGloballyPositioned { layoutCoordinates ->
+ if (!drawerState.animationInProgress
+ && drawerState.currentValue == DrawerValue.Closed
+ && drawerState.targetValue == DrawerValue.Closed
+ ) {
+ onDismiss()
+ }
+
+ if (slideOver) {
+ val originalSize = layoutCoordinates.size.height.toFloat()
+ drawerHeight.value = if (drawerState.expandable) {
+ originalSize
+ } else {
+ min(
+ originalSize,
+ maxOpenHeight
+ )
+ }
+ }
+ }
+ .bottomDrawerSwipeable(
+ drawerState,
+ slideOver,
+ maxOpenHeight,
+ fullHeight,
+ drawerHeight.value
+ )
+ .drawerHeight(
+ slideOver,
+ maxOpenHeight,
+ fullHeight,
+ drawerState
+ )
+ .shadow(drawerElevation)
+ .clip(drawerShape)
+ .background(drawerBackground)
+ .semantics {
+ if (!drawerState.isClosed) {
+ dismiss {
+ onDismiss()
+ true
+ }
+ if (drawerState.currentValue == DrawerValue.Open && drawerState.hasExpandedState) {
+ expand {
+ if (drawerState.confirmStateChange(DrawerValue.Expanded)) {
+ scope.launch { drawerState.expand() }
+ }
+ true
+ }
+ } else if (drawerState.hasExpandedState && drawerState.hasOpenedState) {
+ collapse {
+ if (drawerState.confirmStateChange(DrawerValue.Open)) {
+ scope.launch { drawerState.open() }
+ }
+ true
+ }
+ }
+ }
+ }
+ .focusable(false),
+ ) {
+ Column {
+ if (showHandle) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .padding(vertical = 8.dp)
+ .fillMaxWidth()
+ .draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ if (!enableSwipeDismiss && drawerState.offset.value >= maxOpenHeight) {
+ if (delta < 0) {
+ drawerState.performDrag(delta)
+ }
+ } else {
+ drawerState.performDrag(delta)
+ }
+ },
+ onDragStopped = { velocity ->
+ launch {
+ drawerState.performFling(
+ velocity
+ )
+ if (drawerState.isClosed) {
+ if (enableSwipeDismiss)
+ onDismiss()
+ else
+ scope.launch { drawerState.open() }
+ }
+ }
+ },
+ )
+ .testTag(DRAWER_HANDLE_TAG)
+ ) {
+ val collapsed = LocalContext.current.resources.getString(R.string.collapsed)
+ val expanded = LocalContext.current.resources.getString(R.string.expanded)
+ val accessibilityManager = LocalContext.current.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
+ Icon(
+ painterResource(id = R.drawable.ic_drawer_handle),
+ contentDescription = LocalContext.current.resources.getString(R.string.drag_handle),
+ tint = drawerHandleColor,
+ modifier = Modifier
+ .clickable(
+ enabled = drawerState.hasExpandedState,
+ role = Role.Button,
+ onClickLabel =
+ if (drawerState.currentValue == DrawerValue.Expanded) {
+ LocalContext.current.resources.getString(R.string.collapse)
+ } else {
+ if (drawerState.hasExpandedState && !drawerState.isClosed) LocalContext.current.resources.getString(
+ R.string.expand
+ ) else null
+ }
+ ) {
+ if (drawerState.currentValue == DrawerValue.Expanded) {
+ if (drawerState.hasOpenedState && drawerState.confirmStateChange(
+ DrawerValue.Open
+ )
+ ) {
+ scope.launch { drawerState.open() }
+ accessibilityManager?.let { manager ->
+ if(manager.isEnabled){
+ val event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
+ text.add(collapsed)
+ }
+ manager.sendAccessibilityEvent(event)
+ }
+ }
+ }
+ } else if (drawerState.hasExpandedState) {
+ if (drawerState.confirmStateChange(DrawerValue.Expanded)) {
+ scope.launch { drawerState.expand() }
+ accessibilityManager?.let { manager ->
+ if(manager.isEnabled){
+ val event = AccessibilityEvent.obtain(
+ AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
+ text.add(expanded)
+ }
+ manager.sendAccessibilityEvent(event)
+ }
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+ Column(modifier = Modifier
+ .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
+ }
+ }
+ }
+}
+
+
+private fun Modifier.bottomDrawerSwipeable(
+ drawerState: DrawerState,
+ slideOver: Boolean,
+ maxOpenHeight: Float,
+ fullHeight: Float,
+ drawerHeight: Float?
+): Modifier {
+ val modifier = if (slideOver) {
+ if (drawerHeight != null) {
+ val minHeight = 0f
+ val bottomOpenStateY = max(maxOpenHeight, fullHeight - drawerHeight)
+ val bottomExpandedStateY = max(minHeight, fullHeight - drawerHeight)
+ val anchors =
+ if (drawerHeight <= maxOpenHeight) { // when contentHeight is less than maxOpenHeight
+ if (drawerState.anchors.containsValue(DrawerValue.Expanded)) {
+ /*
+ *For dynamic content when drawerHeight was previously greater than maxOpenHeight and now less than maxOpenHEight
+ *The old anchors won't have Open state, so we need to continue with Expanded state.
+ */
+ mapOf(
+ bottomOpenStateY to DrawerValue.Expanded,
+ fullHeight to DrawerValue.Closed,
+ )
+ } else {
+ mapOf(
+ bottomOpenStateY to DrawerValue.Open,
+ fullHeight to DrawerValue.Closed
+ )
+ }
+ } else {
+ if (drawerState.expandable) {
+ if (drawerState.skipOpenState) {
+ if (drawerState.anchors.containsValue(DrawerValue.Open)) {
+ /*
+ *For dynamic content when drawerHeight was previously less than maxOpenHeight and now greater than maxOpenHEight
+ *The old anchors won't have Expanded state, so we need to continue with Open state.
+ */
+ mapOf(
+ bottomExpandedStateY to DrawerValue.Open, // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
+ fullHeight to DrawerValue.Closed
+ )
+ } else {
+ mapOf(
+ bottomExpandedStateY to DrawerValue.Expanded, // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
+ fullHeight to DrawerValue.Closed,
+ )
+ }
+ } else {
+ mapOf(
+ maxOpenHeight to DrawerValue.Open,
+ bottomExpandedStateY to DrawerValue.Expanded,
+ fullHeight to DrawerValue.Closed
+ )
+ }
+ } else {
+ mapOf(
+ maxOpenHeight to DrawerValue.Open,
+ fullHeight to DrawerValue.Closed
+ )
+ }
+ }
+ Modifier.swipeable(
+ state = drawerState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = false,
+ resistance = null
+ )
+ } else {
+ Modifier
+ }
+ } else {
+ val anchors = if (drawerState.expandable) {
+ if (drawerState.skipOpenState) {
+ mapOf(
+ 0F to DrawerValue.Expanded,
+ fullHeight to DrawerValue.Closed,
+ )
+ } else {
+ mapOf(
+ maxOpenHeight to DrawerValue.Open,
+ 0F to DrawerValue.Expanded,
+ fullHeight to DrawerValue.Closed
+ )
+ }
+ } else {
+ mapOf(
+ maxOpenHeight to DrawerValue.Open,
+ fullHeight to DrawerValue.Closed
+ )
+ }
+ Modifier.swipeable(
+ state = drawerState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = false,
+ resistance = null
+ )
+ }
+ return this.then(modifier)
+}
+
+
+private fun Modifier.drawerHeight(
+ slideOver: Boolean,
+ fixedHeight: Float,
+ fullHeight: Float,
+ drawerState: DrawerState
+): Modifier {
+ val modifier = if (slideOver) {
+ if (drawerState.expandable) {
+ Modifier
+ } else {
+ Modifier.heightIn(
+ 0.dp,
+ pxToDp(fixedHeight)
+ )
+ }
+ } else {
+ Modifier.height(pxToDp(fullHeight - drawerState.offset.value))
+ }
+
+ return this.then(modifier)
+}
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/Drawer.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/Drawer.kt
index 496c0c685..a0808a438 100644
--- a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/Drawer.kt
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/Drawer.kt
@@ -1,28 +1,9 @@
package com.microsoft.fluentui.tokenized.drawer
-import android.content.Context
-import android.content.res.Configuration
-import android.view.accessibility.AccessibilityEvent
-import android.view.accessibility.AccessibilityManager
-import androidx.compose.animation.core.TweenSpec
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.gestures.draggable
-import androidx.compose.foundation.gestures.rememberDraggableState
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,85 +13,32 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
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.draw.alpha
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.focusTarget
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.layout
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.collapse
-import androidx.compose.ui.semantics.dismiss
-import androidx.compose.ui.semantics.expand
-import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
-import androidx.constraintlayout.compose.ConstraintLayout
import androidx.core.view.WindowInsetsCompat
-import com.microsoft.fluentui.compose.FixedThreshold
import com.microsoft.fluentui.compose.ModalPopup
-import com.microsoft.fluentui.compose.NonDismissiblePreUpPostDownNestedScrollConnection
-import com.microsoft.fluentui.compose.PostDownNestedScrollConnection
import com.microsoft.fluentui.compose.PreUpPostDownNestedScrollConnection
import com.microsoft.fluentui.compose.SwipeableState
-import com.microsoft.fluentui.compose.swipeable
-import com.microsoft.fluentui.drawer.R
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
-import com.microsoft.fluentui.theme.token.Icon
import com.microsoft.fluentui.theme.token.controlTokens.BehaviorType
import com.microsoft.fluentui.theme.token.controlTokens.DrawerInfo
import com.microsoft.fluentui.theme.token.controlTokens.DrawerTokens
-import com.microsoft.fluentui.tokenized.calculateFraction
-import com.microsoft.fluentui.util.dpToPx
-import com.microsoft.fluentui.util.pxToDp
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.math.roundToInt
-
-
-/**
- * Possible values of [DrawerState].
- */
-enum class DrawerValue {
- /**
- * The state of the drawer when it is closed.
- */
- Closed,
-
- /**
- * The state of the drawer when it is open.
- */
- Open,
-
- /**
- * The state of the bottom drawer when it is expanded (i.e. at 100% height).
- */
- Expanded
-}
/**
* State of the [Drawer] composable.
@@ -152,7 +80,7 @@ class DrawerState(
}
}
- var enable: Boolean by mutableStateOf(false)
+ var enable: Boolean by mutableStateOf(initialValue != DrawerValue.Closed)
/**
* Whether drawer has Open state.
@@ -336,15 +264,16 @@ fun rememberDrawerState(confirmStateChange: (DrawerValue) -> Boolean = { true })
@Composable
fun rememberBottomDrawerState(
+ initialValue: DrawerValue = DrawerValue.Closed,
expandable: Boolean = true,
skipOpenState: Boolean = false,
confirmStateChange: (DrawerValue) -> Boolean = { true }
): DrawerState {
return rememberSaveable(
- confirmStateChange, expandable, skipOpenState,
+ initialValue, confirmStateChange, expandable, skipOpenState,
saver = DrawerState.Saver(expandable, skipOpenState, confirmStateChange)
) {
- DrawerState(DrawerValue.Closed, expandable, skipOpenState, confirmStateChange)
+ DrawerState(initialValue, expandable, skipOpenState, confirmStateChange)
}
}
@@ -363,7 +292,7 @@ private class DrawerPositionProvider(val offset: IntOffset?) : PopupPositionProv
}
@Composable
-private fun Scrim(
+internal fun Scrim(
open: Boolean,
onClose: () -> Unit,
fraction: () -> Float,
@@ -394,664 +323,6 @@ private fun Scrim(
}
}
-private val EndDrawerPadding = 56.dp
-private val DrawerVelocityThreshold = 400.dp
-
-private val AnimationSpec = TweenSpec(durationMillis = 256)
-
-private const val DrawerOpenFraction = 0.5f
-
-//Tag use for testing
-const val DRAWER_HANDLE_TAG = "Fluent Drawer Handle"
-const val DRAWER_CONTENT_TAG = "Fluent Drawer Content"
-const val DRAWER_SCRIM_TAG = "Fluent Drawer Scrim"
-
-//Drawer Handle height + padding
-private val DrawerHandleHeightOffset = 20.dp
-
-private fun Modifier.drawerHeight(
- slideOver: Boolean,
- fixedHeight: Float,
- fullHeight: Float,
- drawerState: DrawerState
-): Modifier {
- val modifier = if (slideOver) {
- if (drawerState.expandable) {
- Modifier
- } else {
- Modifier.heightIn(
- 0.dp,
- pxToDp(fixedHeight)
- )
- }
- } else {
- Modifier.height(pxToDp(fullHeight - drawerState.offset.value))
- }
-
- return this.then(modifier)
-}
-
-private fun Modifier.bottomDrawerSwipeable(
- drawerState: DrawerState,
- slideOver: Boolean,
- maxOpenHeight: Float,
- fullHeight: Float,
- drawerHeight: Float?
-): Modifier {
- val modifier = if (slideOver) {
- if (drawerHeight != null) {
- val minHeight = 0f
- val bottomOpenStateY = max(maxOpenHeight, fullHeight - drawerHeight)
- val bottomExpandedStateY = max(minHeight, fullHeight - drawerHeight)
- val anchors =
- if (drawerHeight <= maxOpenHeight) { // when contentHeight is less than maxOpenHeight
- if (drawerState.anchors.containsValue(DrawerValue.Expanded)) {
- /*
- *For dynamic content when drawerHeight was previously greater than maxOpenHeight and now less than maxOpenHEight
- *The old anchors won't have Open state, so we need to continue with Expanded state.
- */
- mapOf(
- bottomOpenStateY to DrawerValue.Expanded,
- fullHeight to DrawerValue.Closed,
- )
- } else {
- mapOf(
- bottomOpenStateY to DrawerValue.Open,
- fullHeight to DrawerValue.Closed
- )
- }
- } else {
- if (drawerState.expandable) {
- if (drawerState.skipOpenState) {
- if (drawerState.anchors.containsValue(DrawerValue.Open)) {
- /*
- *For dynamic content when drawerHeight was previously less than maxOpenHeight and now greater than maxOpenHEight
- *The old anchors won't have Expanded state, so we need to continue with Open state.
- */
- mapOf(
- bottomExpandedStateY to DrawerValue.Open, // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
- fullHeight to DrawerValue.Closed
- )
- } else {
- mapOf(
- bottomExpandedStateY to DrawerValue.Expanded, // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
- fullHeight to DrawerValue.Closed,
- )
- }
- } else {
- mapOf(
- maxOpenHeight to DrawerValue.Open,
- bottomExpandedStateY to DrawerValue.Expanded,
- fullHeight to DrawerValue.Closed
- )
- }
- } else {
- mapOf(
- maxOpenHeight to DrawerValue.Open,
- fullHeight to DrawerValue.Closed
- )
- }
- }
- Modifier.swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = false,
- resistance = null
- )
- } else {
- Modifier
- }
- } else {
- val anchors = if (drawerState.expandable) {
- if (drawerState.skipOpenState) {
- mapOf(
- 0F to DrawerValue.Expanded,
- fullHeight to DrawerValue.Closed,
- )
- } else {
- mapOf(
- maxOpenHeight to DrawerValue.Open,
- 0F to DrawerValue.Expanded,
- fullHeight to DrawerValue.Closed
- )
- }
- } else {
- mapOf(
- maxOpenHeight to DrawerValue.Open,
- fullHeight to DrawerValue.Closed
- )
- }
- Modifier.swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = false,
- resistance = null
- )
- }
- return this.then(modifier)
-}
-
-/**
- *
- *
- * Side drawers block interaction with the rest of an app’s content with a scrim.
- * They are elevated above most of the app’s UI and don’t affect the screen’s layout grid.
- *
- * @param drawerContent composable that represents content inside the drawer
- * @param modifier optional modifier for the drawer
- * @param drawerState state of the drawer
- * @param drawerShape shape of the drawer sheet
- * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
- * drawer sheet
- * @param drawerBackground background color to be used for the drawer sheet
- * @param scrimColor color of the scrim that obscures content when the drawer is open
- * @param preventDismissalOnScrimClick when true, the drawer will not be dismissed when the scrim is clicked
- * @param onScrimClick callback to be invoked when the scrim is clicked
- *
- * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] width
- */
-@Composable
-private fun HorizontalDrawer(
- modifier: Modifier,
- behaviorType: BehaviorType,
- drawerState: DrawerState,
- drawerShape: Shape,
- drawerElevation: Dp,
- drawerBackground: Brush,
- scrimColor: Color,
- scrimVisible: Boolean,
- onDismiss: () -> Unit,
- drawerContent: @Composable () -> Unit,
- preventDismissalOnScrimClick: Boolean = false,
- onScrimClick: () -> Unit = {}
-) {
- BoxWithConstraints(modifier.fillMaxSize()) {
- val modalDrawerConstraints = constraints
-
- // TODO : think about Infinite max bounds case
- if (!modalDrawerConstraints.hasBoundedWidth) {
- throw IllegalStateException("Drawer shouldn't have infinite width")
- }
-
- val fullWidth = modalDrawerConstraints.maxWidth.toFloat()
- var drawerWidth by remember(fullWidth) { mutableStateOf(fullWidth) }
- //Hack to get exact drawerHeight wrt to content.
- val visible = remember { mutableStateOf(true) }
- if (visible.value) {
- Box(
- modifier = Modifier
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- drawerWidth = placeable.width.toFloat()
- visible.value = false
- }
- }
- ) {
- drawerContent()
- }
- } else {
- val paddingPx = pxToDp(max(dpToPx(EndDrawerPadding), (fullWidth - drawerWidth)))
- val leftSlide = behaviorType == BehaviorType.LEFT_SLIDE_OVER
-
- val minValue =
- modalDrawerConstraints.maxWidth.toFloat() * (if (leftSlide) (-1F) else (1F))
- val maxValue = 0f
-
- val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
- val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
- Scrim(
- open = !drawerState.isClosed,
- onClose = onDismiss,
- fraction = {
- calculateFraction(minValue, maxValue, drawerState.offset.value)
- },
- color = if (scrimVisible) scrimColor else Color.Transparent,
- preventDismissalOnScrimClick = preventDismissalOnScrimClick,
- onScrimClick = onScrimClick
- )
-
- Box(
- modifier = with(LocalDensity.current) {
- Modifier
- .sizeIn(
- minWidth = modalDrawerConstraints.minWidth.toDp(),
- minHeight = modalDrawerConstraints.minHeight.toDp(),
- maxWidth = modalDrawerConstraints.maxWidth.toDp(),
- maxHeight = modalDrawerConstraints.maxHeight.toDp()
- )
- }
- .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
- .padding(
- start = if (leftSlide) 0.dp else paddingPx,
- end = if (leftSlide) paddingPx else 0.dp
- )
- .semantics {
- if (!drawerState.isClosed) {
- dismiss {
- onDismiss()
- true
- }
- }
- }
- .shadow(drawerElevation)
- .clip(drawerShape)
- .background(drawerBackground)
- .swipeable(
- state = drawerState,
- anchors = anchors,
- thresholds = { _, _ -> FixedThreshold(pxToDp(value = drawerWidth / 2)) },
- orientation = Orientation.Horizontal,
- enabled = false,
- reverseDirection = isRtl,
- velocityThreshold = DrawerVelocityThreshold,
- resistance = null
- ),
- ) {
- Column(
- Modifier
- .draggable(
- orientation = Orientation.Horizontal,
- state = rememberDraggableState { delta ->
- drawerState.performDrag(delta)
- },
- onDragStopped = { velocity ->
- launch {
- drawerState.performFling(
- velocity
- )
- if (drawerState.isClosed) {
- onDismiss()
- }
- }
- },
- )
- .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
- }
- }
- }
-}
-
-@Composable
-private fun TopDrawer(
- modifier: Modifier,
- drawerState: DrawerState,
- drawerShape: Shape,
- drawerElevation: Dp,
- drawerBackground: Brush,
- drawerHandleColor: Color,
- scrimColor: Color,
- scrimVisible: Boolean,
- onDismiss: () -> Unit,
- drawerContent: @Composable () -> Unit,
- preventDismissalOnScrimClick: Boolean = false,
- onScrimClick: () -> Unit = {}
-) {
- BoxWithConstraints(modifier.fillMaxSize()) {
- val fullHeight = constraints.maxHeight.toFloat()
- var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }
-
- Box(
- modifier = Modifier
- .alpha(0f)
- .layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) {
- drawerHeight =
- placeable.height.toFloat() + dpToPx(DrawerHandleHeightOffset)
- }
- }
- ) {
- drawerContent()
- }
- val maxOpenHeight = fullHeight * DrawerOpenFraction
- val minHeight = 0f
- val topCloseHeight = minHeight
- val topOpenHeight = min(maxOpenHeight, drawerHeight)
-
- val minValue: Float = topCloseHeight
- val maxValue: Float = topOpenHeight
-
- val anchors = mapOf(
- topCloseHeight to DrawerValue.Closed,
- topOpenHeight to DrawerValue.Open
- )
-
- val drawerConstraints = with(LocalDensity.current) {
- Modifier
- .sizeIn(
- maxWidth = constraints.maxWidth.toDp(),
- maxHeight = constraints.maxHeight.toDp()
- )
- }
-
- Scrim(
- open = !drawerState.isClosed,
- onClose = onDismiss,
- fraction = {
- calculateFraction(minValue, maxValue, drawerState.offset.value)
- },
- color = if (scrimVisible) scrimColor else Color.Transparent,
- preventDismissalOnScrimClick = preventDismissalOnScrimClick,
- onScrimClick = onScrimClick
- )
-
- Box(
- drawerConstraints
- .offset { IntOffset(0, 0) }
- .semantics {
- if (!drawerState.isClosed) {
- dismiss {
- onDismiss()
- true
- }
- }
- }
- .height(
- pxToDp(drawerState.offset.value)
- )
- .shadow(drawerElevation)
- .clip(drawerShape)
- .background(drawerBackground)
- .swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = false,
- resistance = null
- )
- .focusable(false),
- ) {
- ConstraintLayout(modifier = Modifier.padding(bottom = 8.dp)) {
- val (drawerContentConstrain, drawerHandleConstrain) = createRefs()
- Column(modifier = Modifier
- .offset { IntOffset(0, 0) }
- .padding(bottom = 8.dp)
- .constrainAs(drawerContentConstrain) {
- top.linkTo(parent.top)
- bottom.linkTo(drawerHandleConstrain.top)
- }
- .focusTarget()
- .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() }
- )
- Column(horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier
- .constrainAs(drawerHandleConstrain) {
- top.linkTo(drawerContentConstrain.bottom)
- bottom.linkTo(parent.bottom)
- }
- .fillMaxWidth()
- .draggable(
- orientation = Orientation.Vertical,
- state = rememberDraggableState { delta ->
- drawerState.performDrag(delta)
- },
- onDragStopped = { velocity ->
- launch {
- drawerState.performFling(
- velocity
- )
- if (drawerState.isClosed) {
- onDismiss()
- }
- }
- },
- )
- .testTag(DRAWER_HANDLE_TAG)
- ) {
- Icon(
- painterResource(id = R.drawable.ic_drawer_handle),
- contentDescription = null,
- tint = drawerHandleColor
- )
- }
- }
- }
- }
-}
-
-@Composable
-private fun BottomDrawer(
- modifier: Modifier,
- drawerState: DrawerState,
- drawerShape: Shape,
- drawerElevation: Dp,
- drawerBackground: Brush,
- drawerHandleColor: Color,
- scrimColor: Color,
- scrimVisible: Boolean,
- slideOver: Boolean,
- enableSwipeDismiss: Boolean = true,
- showHandle: Boolean,
- onDismiss: () -> Unit,
- drawerContent: @Composable () -> Unit,
- maxLandscapeWidthFraction : Float = 1F,
- preventDismissalOnScrimClick: Boolean = false,
- onScrimClick: () -> Unit = {}
-) {
- BoxWithConstraints(modifier.fillMaxSize()) {
- val fullHeight = constraints.maxHeight.toFloat()
- val drawerHeight =
- remember(drawerContent.hashCode()) { mutableStateOf(null) }
- val maxOpenHeight = fullHeight * DrawerOpenFraction
-
- val drawerConstraints = with(LocalDensity.current) {
- Modifier
- .sizeIn(
- maxWidth = constraints.maxWidth.toDp(),
- maxHeight = constraints.maxHeight.toDp()
- )
- }
- val scope = rememberCoroutineScope()
-
- Scrim(
- open = !drawerState.isClosed || (drawerHeight != null && drawerHeight.value == 0f),
- onClose = onDismiss,
- fraction = {
- if (drawerState.anchors.isEmpty() || (drawerHeight != null && drawerHeight.value == 0f)) {
- 0.toFloat()
- } else {
- var targetValue: DrawerValue = if (slideOver) {
- drawerState.anchors.maxBy { it.value }.value
- } else if (drawerState.skipOpenState) {
- DrawerValue.Expanded
- } else {
- DrawerValue.Open
- }
- calculateFraction(
- drawerState.anchors.entries.firstOrNull { it.value == DrawerValue.Closed }?.key!!,
- drawerState.anchors.entries.firstOrNull { it.value == targetValue }?.key!!,
- drawerState.offset.value
- )
- }
- },
- color = if (scrimVisible) scrimColor else Color.Transparent,
- preventDismissalOnScrimClick = preventDismissalOnScrimClick,
- onScrimClick = onScrimClick
- )
- val configuration = LocalConfiguration.current
- Box(
- drawerConstraints
- .fillMaxWidth(
- if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) maxLandscapeWidthFraction
- else 1F
- )
- .nestedScroll(
- if (!enableSwipeDismiss && drawerState.offset.value >= maxOpenHeight) drawerState.NonDismissiblePreUpPostDownNestedScrollConnection else
- if (slideOver) drawerState.nestedScrollConnection else drawerState.PostDownNestedScrollConnection
- )
- .offset {
- val y = if (drawerState.anchors == null) {
- fullHeight.roundToInt()
- } else {
- drawerState.offset.value.roundToInt()
- }
- IntOffset(x = 0, y = y)
- }
- .then(
- if (maxLandscapeWidthFraction != 1F
- && configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
- ) Modifier.align(Alignment.TopCenter)
- else Modifier
- )
- .onGloballyPositioned { layoutCoordinates ->
- if (!drawerState.animationInProgress
- && drawerState.currentValue == DrawerValue.Closed
- && drawerState.targetValue == DrawerValue.Closed
- ) {
- onDismiss()
- }
-
- if (slideOver) {
- val originalSize = layoutCoordinates.size.height.toFloat()
- drawerHeight.value = if (drawerState.expandable) {
- originalSize
- } else {
- min(
- originalSize,
- maxOpenHeight
- )
- }
- }
- }
- .bottomDrawerSwipeable(
- drawerState,
- slideOver,
- maxOpenHeight,
- fullHeight,
- drawerHeight.value
- )
- .drawerHeight(
- slideOver,
- maxOpenHeight,
- fullHeight,
- drawerState
- )
- .shadow(drawerElevation)
- .clip(drawerShape)
- .background(drawerBackground)
- .semantics {
- if (!drawerState.isClosed) {
- dismiss {
- onDismiss()
- true
- }
- if (drawerState.currentValue == DrawerValue.Open && drawerState.hasExpandedState) {
- expand {
- if (drawerState.confirmStateChange(DrawerValue.Expanded)) {
- scope.launch { drawerState.expand() }
- }
- true
- }
- } else if (drawerState.hasExpandedState && drawerState.hasOpenedState) {
- collapse {
- if (drawerState.confirmStateChange(DrawerValue.Open)) {
- scope.launch { drawerState.open() }
- }
- true
- }
- }
- }
- }
- .focusable(false),
- ) {
- Column {
- if (showHandle) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier
- .padding(vertical = 8.dp)
- .fillMaxWidth()
- .draggable(
- orientation = Orientation.Vertical,
- state = rememberDraggableState { delta ->
- if (!enableSwipeDismiss && drawerState.offset.value >= maxOpenHeight) {
- if (delta < 0) {
- drawerState.performDrag(delta)
- }
- } else {
- drawerState.performDrag(delta)
- }
- },
- onDragStopped = { velocity ->
- launch {
- drawerState.performFling(
- velocity
- )
- if (drawerState.isClosed) {
- if (enableSwipeDismiss)
- onDismiss()
- else
- scope.launch { drawerState.open() }
- }
- }
- },
- )
- .testTag(DRAWER_HANDLE_TAG)
- ) {
- val collapsed = LocalContext.current.resources.getString(R.string.collapsed)
- val expanded = LocalContext.current.resources.getString(R.string.expanded)
- val accessibilityManager = LocalContext.current.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
- Icon(
- painterResource(id = R.drawable.ic_drawer_handle),
- contentDescription = LocalContext.current.resources.getString(R.string.drag_handle),
- tint = drawerHandleColor,
- modifier = Modifier
- .clickable(
- enabled = drawerState.hasExpandedState,
- role = Role.Button,
- onClickLabel =
- if (drawerState.currentValue == DrawerValue.Expanded) {
- LocalContext.current.resources.getString(R.string.collapse)
- } else {
- if (drawerState.hasExpandedState && !drawerState.isClosed) LocalContext.current.resources.getString(
- R.string.expand
- ) else null
- }
- ) {
- if (drawerState.currentValue == DrawerValue.Expanded) {
- if (drawerState.hasOpenedState && drawerState.confirmStateChange(
- DrawerValue.Open
- )
- ) {
- scope.launch { drawerState.open() }
- accessibilityManager?.let { manager ->
- if(manager.isEnabled){
- val event = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
- text.add(collapsed)
- }
- manager.sendAccessibilityEvent(event)
- }
- }
- }
- } else if (drawerState.hasExpandedState) {
- if (drawerState.confirmStateChange(DrawerValue.Expanded)) {
- scope.launch { drawerState.expand() }
- accessibilityManager?.let { manager ->
- if(manager.isEnabled){
- val event = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
- text.add(expanded)
- }
- manager.sendAccessibilityEvent(event)
- }
- }
- }
- }
- }
- )
- }
- }
- Column(modifier = Modifier
- .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
- }
- }
- }
-}
-
/**
*
* Drawer block interaction with the rest of an app’s content with a scrim.
@@ -1213,6 +484,7 @@ fun BottomDrawer(
preventDismissalOnScrimClick: Boolean = false,
onScrimClick: () -> Unit = {},
) {
+
if (drawerState.enable) {
val themeID =
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
@@ -1227,9 +499,12 @@ fun BottomDrawer(
val behaviorType =
if (slideOver) BehaviorType.BOTTOM_SLIDE_OVER else BehaviorType.BOTTOM
val drawerInfo = DrawerInfo(type = behaviorType)
+ BackHandler { //TODO: Add pull down animation with predictive back
+ close()
+ }
ModalPopup(
+ windowInsetsType = windowInsetsType,
onDismissRequest = close,
- windowInsetsType = windowInsetsType
)
{
val drawerShape: Shape =
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/DrawerUtils.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/DrawerUtils.kt
new file mode 100644
index 000000000..c62d905fd
--- /dev/null
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/DrawerUtils.kt
@@ -0,0 +1,39 @@
+package com.microsoft.fluentui.tokenized.drawer
+
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.ui.unit.dp
+
+val EndDrawerPadding = 56.dp
+val DrawerVelocityThreshold = 400.dp
+
+val AnimationSpec = TweenSpec(durationMillis = 256)
+
+const val DrawerOpenFraction = 0.5f
+
+//Tag use for testing
+const val DRAWER_HANDLE_TAG = "Fluent Drawer Handle"
+const val DRAWER_CONTENT_TAG = "Fluent Drawer Content"
+const val DRAWER_SCRIM_TAG = "Fluent Drawer Scrim"
+
+//Drawer Handle height + padding
+val DrawerHandleHeightOffset = 20.dp
+
+/**
+ * Possible values of [DrawerState].
+ */
+enum class DrawerValue {
+ /**
+ * The state of the drawer when it is closed.
+ */
+ Closed,
+
+ /**
+ * The state of the drawer when it is open.
+ */
+ Open,
+
+ /**
+ * The state of the bottom drawer when it is expanded (i.e. at 100% height).
+ */
+ Expanded
+}
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/HorizontalDrawer.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/HorizontalDrawer.kt
new file mode 100644
index 000000000..c742fd729
--- /dev/null
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/HorizontalDrawer.kt
@@ -0,0 +1,186 @@
+package com.microsoft.fluentui.tokenized.drawer
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.compose.FixedThreshold
+import com.microsoft.fluentui.compose.swipeable
+import com.microsoft.fluentui.theme.token.controlTokens.BehaviorType
+import com.microsoft.fluentui.tokenized.calculateFraction
+import com.microsoft.fluentui.util.dpToPx
+import com.microsoft.fluentui.util.pxToDp
+import kotlinx.coroutines.launch
+import kotlin.math.max
+import kotlin.math.roundToInt
+
+/**
+ *
+ *
+ * Side drawers block interaction with the rest of an app’s content with a scrim.
+ * They are elevated above most of the app’s UI and don’t affect the screen’s layout grid.
+ *
+ * @param drawerContent composable that represents content inside the drawer
+ * @param modifier optional modifier for the drawer
+ * @param drawerState state of the drawer
+ * @param drawerShape shape of the drawer sheet
+ * @param drawerElevation drawer sheet elevation. This controls the size of the shadow below the
+ * drawer sheet
+ * @param drawerBackground background color to be used for the drawer sheet
+ * @param scrimColor color of the scrim that obscures content when the drawer is open
+ * @param preventDismissalOnScrimClick when true, the drawer will not be dismissed when the scrim is clicked
+ * @param onScrimClick callback to be invoked when the scrim is clicked
+ *
+ * @throws IllegalStateException when parent has [Float.POSITIVE_INFINITY] width
+ */
+
+
+@Composable
+internal fun HorizontalDrawer(
+ modifier: Modifier,
+ behaviorType: BehaviorType,
+ drawerState: DrawerState,
+ drawerShape: Shape,
+ drawerElevation: Dp,
+ drawerBackground: Brush,
+ scrimColor: Color,
+ scrimVisible: Boolean,
+ onDismiss: () -> Unit,
+ drawerContent: @Composable () -> Unit,
+ preventDismissalOnScrimClick: Boolean = false,
+ onScrimClick: () -> Unit = {}
+) {
+ BoxWithConstraints(modifier.fillMaxSize()) {
+ val modalDrawerConstraints = constraints
+
+ // TODO : think about Infinite max bounds case
+ if (!modalDrawerConstraints.hasBoundedWidth) {
+ throw IllegalStateException("Drawer shouldn't have infinite width")
+ }
+
+ val fullWidth = modalDrawerConstraints.maxWidth.toFloat()
+ var drawerWidth by remember(fullWidth) { mutableStateOf(fullWidth) }
+ //Hack to get exact drawerHeight wrt to content.
+ val visible = remember { mutableStateOf(true) }
+ if (visible.value) {
+ Box(
+ modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ drawerWidth = placeable.width.toFloat()
+ visible.value = false
+ }
+ }
+ ) {
+ drawerContent()
+ }
+ } else {
+ val paddingPx = pxToDp(max(dpToPx(EndDrawerPadding), (fullWidth - drawerWidth)))
+ val leftSlide = behaviorType == BehaviorType.LEFT_SLIDE_OVER
+
+ val minValue =
+ modalDrawerConstraints.maxWidth.toFloat() * (if (leftSlide) (-1F) else (1F))
+ val maxValue = 0f
+
+ val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
+ val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+ Scrim(
+ open = !drawerState.isClosed,
+ onClose = onDismiss,
+ fraction = {
+ calculateFraction(minValue, maxValue, drawerState.offset.value)
+ },
+ color = if (scrimVisible) scrimColor else Color.Transparent,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ onScrimClick = onScrimClick
+ )
+
+ Box(
+ modifier = with(LocalDensity.current) {
+ Modifier
+ .sizeIn(
+ minWidth = modalDrawerConstraints.minWidth.toDp(),
+ minHeight = modalDrawerConstraints.minHeight.toDp(),
+ maxWidth = modalDrawerConstraints.maxWidth.toDp(),
+ maxHeight = modalDrawerConstraints.maxHeight.toDp()
+ )
+ }
+ .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
+ .padding(
+ start = if (leftSlide) 0.dp else paddingPx,
+ end = if (leftSlide) paddingPx else 0.dp
+ )
+ .semantics {
+ if (!drawerState.isClosed) {
+ dismiss {
+ onDismiss()
+ true
+ }
+ }
+ }
+ .shadow(drawerElevation)
+ .clip(drawerShape)
+ .background(drawerBackground)
+ .swipeable(
+ state = drawerState,
+ anchors = anchors,
+ thresholds = { _, _ -> FixedThreshold(pxToDp(value = drawerWidth / 2)) },
+ orientation = Orientation.Horizontal,
+ enabled = false,
+ reverseDirection = isRtl,
+ velocityThreshold = DrawerVelocityThreshold,
+ resistance = null
+ ),
+ ) {
+ Column(
+ Modifier
+ .draggable(
+ orientation = Orientation.Horizontal,
+ state = rememberDraggableState { delta ->
+ drawerState.performDrag(delta)
+ },
+ onDragStopped = { velocity ->
+ launch {
+ drawerState.performFling(
+ velocity
+ )
+ if (drawerState.isClosed) {
+ onDismiss()
+ }
+ }
+ },
+ )
+ .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/TopDrawer.kt b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/TopDrawer.kt
new file mode 100644
index 000000000..e3154ecc2
--- /dev/null
+++ b/fluentui_drawer/src/main/java/com/microsoft/fluentui/tokenized/drawer/TopDrawer.kt
@@ -0,0 +1,187 @@
+package com.microsoft.fluentui.tokenized.drawer
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import com.microsoft.fluentui.compose.swipeable
+import com.microsoft.fluentui.drawer.R
+import com.microsoft.fluentui.theme.token.Icon
+import com.microsoft.fluentui.tokenized.calculateFraction
+import com.microsoft.fluentui.util.dpToPx
+import com.microsoft.fluentui.util.pxToDp
+import kotlinx.coroutines.launch
+import kotlin.math.min
+
+
+@Composable
+internal fun TopDrawer(
+ modifier: Modifier,
+ drawerState: DrawerState,
+ drawerShape: Shape,
+ drawerElevation: Dp,
+ drawerBackground: Brush,
+ drawerHandleColor: Color,
+ scrimColor: Color,
+ scrimVisible: Boolean,
+ onDismiss: () -> Unit,
+ drawerContent: @Composable () -> Unit,
+ preventDismissalOnScrimClick: Boolean = false,
+ onScrimClick: () -> Unit = {}
+) {
+ BoxWithConstraints(modifier.fillMaxSize()) {
+ val fullHeight = constraints.maxHeight.toFloat()
+ var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }
+
+ Box(
+ modifier = Modifier
+ .alpha(0f)
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ drawerHeight =
+ placeable.height.toFloat() + dpToPx(DrawerHandleHeightOffset)
+ }
+ }
+ ) {
+ drawerContent()
+ }
+ val maxOpenHeight = fullHeight * DrawerOpenFraction
+ val minHeight = 0f
+ val topCloseHeight = minHeight
+ val topOpenHeight = min(maxOpenHeight, drawerHeight)
+
+ val minValue: Float = topCloseHeight
+ val maxValue: Float = topOpenHeight
+
+ val anchors = mapOf(
+ topCloseHeight to DrawerValue.Closed,
+ topOpenHeight to DrawerValue.Open
+ )
+
+ val drawerConstraints = with(LocalDensity.current) {
+ Modifier
+ .sizeIn(
+ maxWidth = constraints.maxWidth.toDp(),
+ maxHeight = constraints.maxHeight.toDp()
+ )
+ }
+
+ Scrim(
+ open = !drawerState.isClosed,
+ onClose = onDismiss,
+ fraction = {
+ calculateFraction(minValue, maxValue, drawerState.offset.value)
+ },
+ color = if (scrimVisible) scrimColor else Color.Transparent,
+ preventDismissalOnScrimClick = preventDismissalOnScrimClick,
+ onScrimClick = onScrimClick
+ )
+
+ Box(
+ drawerConstraints
+ .offset { IntOffset(0, 0) }
+ .semantics {
+ if (!drawerState.isClosed) {
+ dismiss {
+ onDismiss()
+ true
+ }
+ }
+ }
+ .height(
+ pxToDp(drawerState.offset.value)
+ )
+ .shadow(drawerElevation)
+ .clip(drawerShape)
+ .background(drawerBackground)
+ .swipeable(
+ state = drawerState,
+ anchors = anchors,
+ orientation = Orientation.Vertical,
+ enabled = false,
+ resistance = null
+ )
+ .focusable(false),
+ ) {
+ ConstraintLayout(modifier = Modifier.padding(bottom = 8.dp)) {
+ val (drawerContentConstrain, drawerHandleConstrain) = createRefs()
+ Column(modifier = Modifier
+ .offset { IntOffset(0, 0) }
+ .padding(bottom = 8.dp)
+ .constrainAs(drawerContentConstrain) {
+ top.linkTo(parent.top)
+ bottom.linkTo(drawerHandleConstrain.top)
+ }
+ .focusTarget()
+ .testTag(DRAWER_CONTENT_TAG), content = { drawerContent() }
+ )
+ Column(horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .constrainAs(drawerHandleConstrain) {
+ top.linkTo(drawerContentConstrain.bottom)
+ bottom.linkTo(parent.bottom)
+ }
+ .fillMaxWidth()
+ .draggable(
+ orientation = Orientation.Vertical,
+ state = rememberDraggableState { delta ->
+ drawerState.performDrag(delta)
+ },
+ onDragStopped = { velocity ->
+ launch {
+ drawerState.performFling(
+ velocity
+ )
+ if (drawerState.isClosed) {
+ onDismiss()
+ }
+ }
+ },
+ )
+ .testTag(DRAWER_HANDLE_TAG)
+ ) {
+ Icon(
+ painterResource(id = R.drawable.ic_drawer_handle),
+ contentDescription = null,
+ tint = drawerHandleColor
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/__ActionBarIcons.kt b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/__ActionBarIcons.kt
new file mode 100644
index 000000000..9b50d0aad
--- /dev/null
+++ b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/__ActionBarIcons.kt
@@ -0,0 +1,19 @@
+package com.microsoft.fluentui.icons
+
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.microsoft.fluentui.icons.actionbaricons.Arrowright
+import com.microsoft.fluentui.icons.actionbaricons.Chevron
+import kotlin.collections.List as ____KtList
+
+object ActionBarIcons
+
+private var __AllIcons: ____KtList? = null
+
+val ActionBarIcons.AllIcons: ____KtList
+ get() {
+ if (__AllIcons != null) {
+ return __AllIcons!!
+ }
+ __AllIcons= listOf(Arrowright, Chevron)
+ return __AllIcons!!
+ }
diff --git a/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Arrowright.kt b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Arrowright.kt
new file mode 100644
index 000000000..28ecf87cf
--- /dev/null
+++ b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Arrowright.kt
@@ -0,0 +1,48 @@
+package com.microsoft.fluentui.icons.actionbaricons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.icons.ActionBarIcons
+
+val ActionBarIcons.Arrowright: ImageVector
+ get() {
+ if (_arrowright != null) {
+ return _arrowright!!
+ }
+ _arrowright = Builder(name = "Arrowright", defaultWidth = 20.0.dp, defaultHeight = 20.0.dp,
+ viewportWidth = 20.0f, viewportHeight = 20.0f).apply {
+ path(fill = SolidColor(Color(0xFF212121)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(10.8371f, 3.1307f)
+ curveTo(10.6332f, 2.9446f, 10.3169f, 2.959f, 10.1307f, 3.1629f)
+ curveTo(9.9446f, 3.3668f, 9.959f, 3.6831f, 10.1629f, 3.8693f)
+ lineTo(16.3307f, 9.5f)
+ horizontalLineTo(2.5f)
+ curveTo(2.2239f, 9.5f, 2.0f, 9.7239f, 2.0f, 10.0f)
+ curveTo(2.0f, 10.2761f, 2.2239f, 10.5f, 2.5f, 10.5f)
+ horizontalLineTo(16.3279f)
+ lineTo(10.1629f, 16.1281f)
+ curveTo(9.959f, 16.3143f, 9.9446f, 16.6305f, 10.1307f, 16.8345f)
+ curveTo(10.3169f, 17.0384f, 10.6332f, 17.0528f, 10.8371f, 16.8666f)
+ lineTo(17.7535f, 10.5526f)
+ curveTo(17.8934f, 10.4248f, 17.9732f, 10.2573f, 17.993f, 10.0841f)
+ curveTo(17.9976f, 10.0568f, 18.0f, 10.0287f, 18.0f, 10.0f)
+ curveTo(18.0f, 9.9731f, 17.9979f, 9.9467f, 17.9938f, 9.921f)
+ curveTo(17.9756f, 9.7451f, 17.8955f, 9.5745f, 17.7535f, 9.4448f)
+ lineTo(10.8371f, 3.1307f)
+ close()
+ }
+ }
+ .build()
+ return _arrowright!!
+ }
+
+private var _arrowright: ImageVector? = null
diff --git a/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Chevron.kt b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Chevron.kt
new file mode 100644
index 000000000..f5e20e70e
--- /dev/null
+++ b/fluentui_icons/src/main/java/com/microsoft/fluentui/icons/actionbaricons/Chevron.kt
@@ -0,0 +1,42 @@
+package com.microsoft.fluentui.icons.actionbaricons
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.icons.ActionBarIcons
+import com.microsoft.fluentui.icons.ListItemIcons
+
+val ActionBarIcons.Chevron: ImageVector
+ get() {
+ if (_chevron != null) {
+ return _chevron!!
+ }
+ _chevron = Builder(name = "Chevron", defaultWidth = 12.0.dp, defaultHeight = 12.0.dp,
+ viewportWidth = 12.0f, viewportHeight = 12.0f).apply {
+ path(fill = SolidColor(Color(0xFF808080)), stroke = null, strokeLineWidth = 0.0f,
+ strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
+ pathFillType = NonZero) {
+ moveTo(4.6465f, 2.1465f)
+ curveTo(4.4512f, 2.3417f, 4.4512f, 2.6583f, 4.6465f, 2.8535f)
+ lineTo(7.7929f, 6.0f)
+ lineTo(4.6465f, 9.1465f)
+ curveTo(4.4512f, 9.3417f, 4.4512f, 9.6583f, 4.6465f, 9.8535f)
+ curveTo(4.8417f, 10.0488f, 5.1583f, 10.0488f, 5.3535f, 9.8535f)
+ lineTo(8.8535f, 6.3535f)
+ curveTo(9.0488f, 6.1583f, 9.0488f, 5.8417f, 8.8535f, 5.6465f)
+ lineTo(5.3535f, 2.1465f)
+ curveTo(5.1583f, 1.9512f, 4.8417f, 1.9512f, 4.6465f, 2.1465f)
+ close()
+ }
+ }
+ .build()
+ return _chevron!!
+ }
+
+private var _chevron: ImageVector? = null
diff --git a/fluentui_listitem/build.gradle b/fluentui_listitem/build.gradle
index c70f5c461..89b9693fd 100644
--- a/fluentui_listitem/build.gradle
+++ b/fluentui_listitem/build.gradle
@@ -37,6 +37,9 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/contentBuilder/ListContentBuilder.kt b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/contentBuilder/ListContentBuilder.kt
index 173d4976e..841af2ba0 100644
--- a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/contentBuilder/ListContentBuilder.kt
+++ b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/contentBuilder/ListContentBuilder.kt
@@ -185,7 +185,7 @@ class ListContentBuilder {
content = {
var col = 0
val widthRatio = if ((row + 1) * itemsInRow <= size || !equidistant)
- 1.0f / itemsInRow
+ 1.0f / maxItemsInRow
else
1.0f / min(itemsInRow, (size - (row * itemsInRow)))
while (col < itemsInRow && (row * itemsInRow + col) < size) {
diff --git a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/tabItem/TabItem.kt b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/tabItem/TabItem.kt
index 44f6f1b95..9ef610e6b 100644
--- a/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/tabItem/TabItem.kt
+++ b/fluentui_listitem/src/main/java/com/microsoft/fluentui/tokenized/tabItem/TabItem.kt
@@ -27,12 +27,19 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -42,7 +49,6 @@ import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
-import com.microsoft.fluentui.theme.token.FluentAliasTokens
import com.microsoft.fluentui.theme.token.FluentGlobalTokens
import com.microsoft.fluentui.theme.token.FluentStyle
import com.microsoft.fluentui.theme.token.Icon
@@ -50,6 +56,7 @@ import com.microsoft.fluentui.theme.token.controlTokens.TabItemInfo
import com.microsoft.fluentui.theme.token.controlTokens.TabItemTokens
import com.microsoft.fluentui.theme.token.controlTokens.TabTextAlignment
+@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun TabItem(
title: String,
@@ -81,14 +88,16 @@ fun TabItem(
),
animationSpec = tween(durationMillis = 300)
)
- val iconColor by animateColorAsState (
- targetValue = token.iconColor(tabItemInfo = tabItemInfo).getColorByState(
- enabled = enabled,
- selected = selected,
- interactionSource = interactionSource
- ),
- animationSpec = tween(durationMillis = 300)
+ val iconColorBrush: Brush = token.iconColor(tabItemInfo = tabItemInfo).getBrushByState(
+ enabled = enabled,
+ selected = selected,
+ interactionSource = interactionSource
)
+
+ val indicatorColor: Brush = token.indicatorColor(tabItemInfo = tabItemInfo).getBrushByState(
+ enabled = enabled, selected = selected, interactionSource = interactionSource
+ )
+
val padding = token.padding(tabItemInfo = tabItemInfo)
val backgroundColor = token.backgroundBrush(tabItemInfo = tabItemInfo).getBrushByState(
enabled = enabled, selected = selected, interactionSource = interactionSource
@@ -112,9 +121,19 @@ fun TabItem(
val iconContent: @Composable () -> Unit = {
Icon(
imageVector = icon,
- modifier = Modifier.size(if (textAlignment == TabTextAlignment.NO_TEXT) 28.dp else 24.dp),
+ modifier = Modifier
+ .semantics {
+ invisibleToUser()
+ }
+ .size(if (textAlignment == TabTextAlignment.NO_TEXT) 28.dp else 24.dp)
+ .graphicsLayer(alpha = 0.99f)
+ .drawWithCache {
+ onDrawWithContent {
+ drawContent()
+ drawRect(brush = iconColorBrush, blendMode = BlendMode.SrcAtop)
+ }
+ },
contentDescription = if (textAlignment == TabTextAlignment.NO_TEXT) title else null,
- tint = iconColor
)
}
@@ -145,6 +164,9 @@ fun TabItem(
BasicText(
text = title,
modifier = Modifier
+ .semantics {
+ invisibleToUser()
+ }
.constrainAs(textConstrain) {
start.linkTo(iconConstrain.end)
end.linkTo(badgeConstrain.start)
@@ -214,7 +236,7 @@ fun TabItem(
totalWidth,
totalHeight
) {
-
+
anchorPlaceable.placeRelative(0, 0)
val badgeX = anchorPlaceable.width + badgeHorizontalOffset
val badgeY = badgeVerticalOffset.roundToPx()
@@ -241,11 +263,11 @@ fun TabItem(
) {
badgeWithIcon()
- val fontStyle = FluentTheme.aliasTokens.typography[FluentAliasTokens.TypographyTokens.Caption2]
- var fontSize = remember { mutableStateOf(fontStyle.fontSize) }
+ val textTypography = token.textTypography(tabItemInfo = tabItemInfo)
+ var fontSize = remember { mutableStateOf(textTypography.fontSize) }
var textStyle by remember(textColor) {
mutableStateOf(
- fontStyle.merge(TextStyle(color = textColor, fontSize = fontSize.value))
+ textTypography.merge(TextStyle(color = textColor, fontSize = fontSize.value))
)
}
@@ -256,12 +278,8 @@ fun TabItem(
style = textStyle,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
- onTextLayout = { textLayoutResult ->
- if (textLayoutResult.didOverflowHeight) {
- textStyle.fontSize
- fontSize.value *= 0.9
- textStyle = textStyle.copy(fontSize = fontSize.value)
- }
+ modifier = Modifier.semantics {
+ invisibleToUser()
}
)
}
@@ -277,7 +295,7 @@ fun TabItem(
modifier = Modifier
.height(3.dp)
.width(indicatorWidth)
- .background(shape = RoundedCornerShape(indicatorCornerRadiusSize), color = textColor)
+ .background(shape = RoundedCornerShape(indicatorCornerRadiusSize), brush = indicatorColor)
.clip(RoundedCornerShape(indicatorCornerRadiusSize))
)
}
diff --git a/fluentui_menus/build.gradle b/fluentui_menus/build.gradle
index 1a71e473d..cf6f0b9d8 100644
--- a/fluentui_menus/build.gradle
+++ b/fluentui_menus/build.gradle
@@ -30,6 +30,9 @@ android {
buildFeatures {
compose true
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenu.kt b/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenu.kt
index 57b03ed00..2e540edfa 100644
--- a/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenu.kt
+++ b/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenu.kt
@@ -13,8 +13,6 @@ import android.view.View
import com.microsoft.fluentui.menus.R
import com.microsoft.fluentui.popupmenu.PopupMenu.ItemCheckableBehavior
import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
-import com.microsoft.fluentui.util.DuoSupportUtils
-import com.microsoft.fluentui.util.activity
/**
* [PopupMenu] is a transient UI that displays a list of options. The popup appears from a view that
@@ -76,12 +74,6 @@ class PopupMenu : ListPopupWindow, PopupMenuItem.OnClickListener {
isModal = true
width = adapter.calculateWidth()
- context.activity?.let {
- if (DuoSupportUtils.isWindowDoublePortrait(it) && anchorView.x < DuoSupportUtils.getSingleScreenWidthPixels(it) &&
- anchorView.x + width > DuoSupportUtils.getSingleScreenWidthPixels(it)) {
- width = DuoSupportUtils.getSingleScreenWidthPixels(it) - anchorView.x.toInt()
- }
- }
}
override fun onPopupMenuItemClicked(popupMenuItem: PopupMenuItem) {
diff --git a/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenuAdapter.kt b/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenuAdapter.kt
index 133b33658..1244180b1 100644
--- a/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenuAdapter.kt
+++ b/fluentui_menus/src/main/java/com/microsoft/fluentui/popupmenu/PopupMenuAdapter.kt
@@ -11,10 +11,7 @@ import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import com.microsoft.fluentui.menus.R
-import com.microsoft.fluentui.util.DuoSupportUtils
-import com.microsoft.fluentui.util.activity
import kotlin.math.max
-import kotlin.math.min
internal class PopupMenuAdapter : BaseAdapter {
private val context: Context
@@ -68,12 +65,6 @@ internal class PopupMenuAdapter : BaseAdapter {
itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
maxWidth = max(maxWidth, itemView.measuredWidth)
- context.activity?.let {
- if (DuoSupportUtils.isWindowDoublePortrait(it)) {
- val singleScreenDisplayPixels = DuoSupportUtils.getSingleScreenWidthPixels(it)
- maxWidth = min(maxWidth, singleScreenDisplayPixels - DuoSupportUtils.DUO_HINGE_WIDTH)
- }
- }
}
return max(minWidth, maxWidth)
diff --git a/fluentui_notification/src/main/java/com/microsoft/fluentui/tokenized/notification/Badge.kt b/fluentui_notification/src/main/java/com/microsoft/fluentui/tokenized/notification/Badge.kt
index 9e90b5f79..c8e617bf5 100644
--- a/fluentui_notification/src/main/java/com/microsoft/fluentui/tokenized/notification/Badge.kt
+++ b/fluentui_notification/src/main/java/com/microsoft/fluentui/tokenized/notification/Badge.kt
@@ -7,6 +7,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -69,7 +73,15 @@ fun Badge(
}
} else {
val textColor = token.textColor(badgeInfo = badgeInfo)
- val typography = token.typography(badgeInfo = badgeInfo)
+ var typography = token.typography(badgeInfo = badgeInfo)
+ val fontSize = remember { mutableStateOf(typography.fontSize) }
+ var textStyle by remember(textColor) {
+ mutableStateOf(
+ typography.merge(TextStyle(color = textColor, fontSize = fontSize.value, platformStyle = PlatformTextStyle(
+ includeFontPadding = false
+ )))
+ )
+ }
val paddingValues = token.padding(badgeInfo = badgeInfo)
val shape = RoundedCornerShape(token.cornerRadius(badgeInfo = badgeInfo))
@@ -86,14 +98,14 @@ fun Badge(
BasicText(
text,
modifier = Modifier.padding(paddingValues),
- style = typography.merge(
- TextStyle(
- color = textColor,
- platformStyle = PlatformTextStyle(
- includeFontPadding = false
- )
- )
- )
+ style = textStyle,
+ onTextLayout = { textLayoutResult ->
+ if (textLayoutResult.didOverflowHeight) {
+ textStyle.fontSize
+ fontSize.value *= 0.9
+ textStyle = textStyle.copy(fontSize = fontSize.value)
+ }
+ }
)
}
}
diff --git a/fluentui_others/build.gradle b/fluentui_others/build.gradle
index f8607f7ce..cac516032 100644
--- a/fluentui_others/build.gradle
+++ b/fluentui_others/build.gradle
@@ -27,6 +27,12 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion composeCompilerVersion
+ }
testOptions {
unitTests {
includeAndroidResources = true
@@ -47,15 +53,15 @@ gradle.taskGraph.whenReady { taskGraph ->
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':fluentui_core')
+ implementation project(':fluentui_controls')
+ implementation project(':fluentui_icons')
implementation "androidx.appcompat:appcompat:$appCompatVersion"
implementation "androidx.exifinterface:exifinterface:$exifInterfaceVersion"
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
implementation "androidx.cardview:cardview:1.0.0"
implementation "com.google.android.material:material:$materialVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
- implementation "com.microsoft.device:dualscreen-layout:$duoVersion"
-
+ implementation "androidx.compose.foundation:foundation"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$extJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
diff --git a/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/actionbar/ActionBar.kt b/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/actionbar/ActionBar.kt
new file mode 100644
index 000000000..1d79b0b64
--- /dev/null
+++ b/fluentui_others/src/main/java/com/microsoft/fluentui/tokenized/actionbar/ActionBar.kt
@@ -0,0 +1,131 @@
+package com.microsoft.fluentui.tokenized.actionbar
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+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.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.icons.ActionBarIcons
+import com.microsoft.fluentui.icons.actionbaricons.Arrowright
+import com.microsoft.fluentui.icons.actionbaricons.Chevron
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.token.ControlTokens
+import com.microsoft.fluentui.theme.token.controlTokens.ACTIONBARTYPE
+import com.microsoft.fluentui.theme.token.controlTokens.ActionBarInfo
+import com.microsoft.fluentui.theme.token.controlTokens.ActionBarTokens
+import com.microsoft.fluentui.theme.token.controlTokens.ButtonStyle
+import com.microsoft.fluentui.tokenized.controls.Button
+import kotlinx.coroutines.launch
+
+/**
+ * ActionBar is a composable that provides a way to navigate between pages.
+ *
+ * @param pagerState: PagerState
+ * @param modifier: Modifier
+ * @param type: Int
+ * @param startCallback: () -> Unit
+ * @param actionBarTokens: ActionBarTokens?
+ */
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+fun ActionBar(
+ pagerState: PagerState,
+ modifier: Modifier = Modifier,
+ type: Int = ACTIONBARTYPE.BASIC.ordinal,
+ startCallback: () -> Unit,
+ actionBarTokens: ActionBarTokens? = null
+) {
+ val token =
+ actionBarTokens
+ ?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.ActionBarControlType] as ActionBarTokens
+ val noOfPages = pagerState.pageCount
+ val actionBarInfo = ActionBarInfo()
+ val height = token.actionBarHeight(actionBarInfo)
+ Box(
+ modifier = modifier.fillMaxWidth().height(height).background(
+ token.actionBarColor(actionBarInfo)
+ )
+ ) {
+ val scope = rememberCoroutineScope()
+ var selectedPage by rememberSaveable { mutableStateOf(0) }
+
+ // carousel indicator
+ if (type == ACTIONBARTYPE.CAROUSEL.ordinal) {
+ Row(
+ Modifier
+ .wrapContentHeight()
+ .fillMaxWidth()
+ .align(Alignment.Center),
+ horizontalArrangement = Arrangement.Center
+ ) {
+ repeat(pagerState.pageCount) { iteration ->
+ val color =
+ if (pagerState.currentPage == iteration) Color.DarkGray else Color.LightGray
+ Box(
+ modifier = Modifier
+ .padding(2.dp)
+ .clip(CircleShape)
+ .background(color)
+ .size(8.dp)
+ )
+ }
+ }
+ }
+
+ // left action
+ if (selectedPage < noOfPages - 1) {
+ Button(
+ style = ButtonStyle.TextButton,
+ onClick = {
+ scope.launch {
+ selectedPage = noOfPages - 1
+ pagerState.animateScrollToPage(noOfPages - 1)
+ }
+ },
+ modifier = Modifier.align(Alignment.CenterStart),
+ text = "Skip"
+ )
+ }
+
+ // right action
+ val rightActionText =
+ if (type == ACTIONBARTYPE.CAROUSEL.ordinal) "" else if (selectedPage == noOfPages - 1) "Start" else "Next"
+ val trailingIcon =
+ if (type == ACTIONBARTYPE.ICON.ordinal) {
+ ActionBarIcons.Chevron
+ } else if (type == ACTIONBARTYPE.CAROUSEL.ordinal) {
+ ActionBarIcons.Arrowright
+ } else {
+ null
+ }
+
+ Button(
+ style = ButtonStyle.TextButton,
+ trailingIcon = trailingIcon,
+ onClick = {
+ if (selectedPage < noOfPages - 1) {
+ selectedPage += 1
+ scope.launch {
+ pagerState.animateScrollToPage(selectedPage)
+ }
+ } else {
+ startCallback()
+ }
+ },
+ modifier = Modifier.align(Alignment.CenterEnd),
+ text = rightActionText
+ )
+
+ }
+}
diff --git a/fluentui_peoplepicker/build.gradle b/fluentui_peoplepicker/build.gradle
index a2f33355a..7572f41ae 100644
--- a/fluentui_peoplepicker/build.gradle
+++ b/fluentui_peoplepicker/build.gradle
@@ -73,7 +73,6 @@ dependencies {
implementation "androidx.exifinterface:exifinterface:$exifInterfaceVersion"
implementation "com.google.android.material:material:$materialVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation "com.jakewharton.threetenabp:threetenabp:$threetenabpVersion"
implementation "com.splitwise:tokenautocomplete:$tokenautocompleteVersion"
implementation("androidx.compose.foundation:foundation")
diff --git a/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextView.kt b/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextView.kt
index 743b4f5d3..271b71a68 100644
--- a/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextView.kt
+++ b/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextView.kt
@@ -34,15 +34,13 @@ import android.view.accessibility.AccessibilityNodeInfo
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.MultiAutoCompleteTextView
+import androidx.core.content.ContextCompat
import com.microsoft.fluentui.persona.IPersona
import com.microsoft.fluentui.persona.PersonaChipView
import com.microsoft.fluentui.persona.setPersona
import com.microsoft.fluentui.util.ThemeUtil
import com.microsoft.fluentui.util.getTextSize
import com.microsoft.fluentui.util.inputMethodManager
-import com.microsoft.fluentui.util.activity
-import com.microsoft.fluentui.util.displaySize
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.tokenautocomplete.CountSpan
import com.tokenautocomplete.TokenCompleteTextView
import kotlin.math.max
@@ -252,9 +250,14 @@ internal class PeoplePickerTextView : TokenCompleteTextView {
// Soft keyboard does not always show up when the view first loads without this
if (hasFocus) {
+ // add bottom border
+ this.background = ContextCompat.getDrawable(context, R.drawable.people_picker_textview_focusable_background)
post {
context.inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
+ } else {
+ // remove bottom border
+ this.background = null
}
/**
@@ -304,11 +307,6 @@ internal class PeoplePickerTextView : TokenCompleteTextView {
return
super.replaceText(text)
- context.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it) && lastSpan != null) {
- checkForIntersectionWithHinge(lastSpan!!)
- }
- }
}
override fun canDeleteSelection(beforeLength: Int): Boolean {
@@ -475,11 +473,6 @@ internal class PeoplePickerTextView : TokenCompleteTextView {
val personaSpan = buildSpanForObject(persona)
text.insert(offset, spannableStringBuilder)
text.setSpan(personaSpan, offset, offset + spannableStringBuilder.length - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- context.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it)) {
- checkForIntersectionWithHinge(personaSpan)
- }
- }
}
private fun checkForIntersectionWithHinge(tokenImageSpan: TokenImageSpan) {
@@ -495,15 +488,6 @@ internal class PeoplePickerTextView : TokenCompleteTextView {
personaBound.right += parentTextViewLocation[0]
personaBound.top += parentTextViewLocation[1]
personaBound.bottom += parentTextViewLocation[1]
- context.activity?.let {
- if (DuoSupportUtils.intersectHinge(it, personaBound)) {
- text.removeSpan(tokenImageSpan)
- val spanWithEmptySpace = getViewForObjectWithSpace(tokenImageSpan.token, (context.displaySize.x + DuoSupportUtils.getHingeWidth((it))) / 2 - personaBound.left + resources.getDimension(R.dimen.fluentui_people_picker_horizontal_margin).toInt())
- val countSpanWidth = resources.getDimension(R.dimen.fluentui_people_picker_count_span_width).toInt() + DuoSupportUtils.getHingeWidth(it)
-
- text.setSpan(TokenImageSpan(spanWithEmptySpace, tokenImageSpan.token, maxTextWidth().toInt() - countSpanWidth), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- }
- }
}
// Persona spans don't always fit their new space so we rebuild the spans in available space.
@@ -517,11 +501,6 @@ internal class PeoplePickerTextView : TokenCompleteTextView {
val spanEnd = text.getSpanEnd(personaSpan)
text.removeSpan(personaSpan)
text.setSpan(rebuiltSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
- context.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it)) {
- checkForIntersectionWithHinge(rebuiltSpan)
- }
- }
}
}
diff --git a/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextViewAdapter.kt b/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextViewAdapter.kt
index 2cbb54cab..aae3ab510 100644
--- a/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextViewAdapter.kt
+++ b/fluentui_peoplepicker/src/main/java/com/microsoft/fluentui/peoplepicker/PeoplePickerTextViewAdapter.kt
@@ -8,8 +8,6 @@ package com.microsoft.fluentui.peoplepicker
import android.content.Context
import android.graphics.drawable.InsetDrawable
import androidx.core.content.ContextCompat
-import android.view.Gravity.CENTER
-import android.view.Gravity.START
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -17,8 +15,6 @@ import android.widget.*
import com.microsoft.fluentui.listitem.ListItemView
import com.microsoft.fluentui.peoplepicker.databinding.PeoplePickerSearchDirectoryBinding
import com.microsoft.fluentui.persona.*
-import com.microsoft.fluentui.util.DuoSupportUtils
-import com.microsoft.fluentui.util.activity
import java.util.*
/**
@@ -123,11 +119,6 @@ internal class PeoplePickerTextViewAdapter : ArrayAdapter, Filterable
// Need to use the convertView, otherwise accessibility focus breaks. Also more efficient.
val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.people_picker_search_directory, parent, false)
searchDirectoryBinding = PeoplePickerSearchDirectoryBinding.bind(view)
- convertView?.context?.activity?.let {
- if (DuoSupportUtils.isDualScreenMode(it)) {
- searchDirectoryBinding?.peoplePickerSearchDirectoryText?.gravity = START or CENTER
- }
- }
return view
}
diff --git a/fluentui_peoplepicker/src/main/res/drawable/people_picker_textview_focusable_background.xml b/fluentui_peoplepicker/src/main/res/drawable/people_picker_textview_focusable_background.xml
new file mode 100644
index 000000000..1c494cbf9
--- /dev/null
+++ b/fluentui_peoplepicker/src/main/res/drawable/people_picker_textview_focusable_background.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/fluentui_persona/build.gradle b/fluentui_persona/build.gradle
index dcc0777a9..97a409aaf 100644
--- a/fluentui_persona/build.gradle
+++ b/fluentui_persona/build.gradle
@@ -59,6 +59,9 @@ android {
}
productFlavors {
}
+ lintOptions {
+ abortOnError false
+ }
}
diff --git a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Avatar.kt b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Avatar.kt
index 1eac0e067..4aa4253cf 100644
--- a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Avatar.kt
+++ b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Avatar.kt
@@ -10,9 +10,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.clearAndSetSemantics
@@ -29,6 +32,7 @@ import com.microsoft.fluentui.theme.token.ControlTokens
import com.microsoft.fluentui.theme.token.FluentIcon
import com.microsoft.fluentui.theme.token.Icon
import com.microsoft.fluentui.theme.token.controlTokens.*
+import com.microsoft.fluentui.util.dpToPx
// Tags used for testing
const val AVATAR_IMAGE = "Fluent Avatar Image"
@@ -44,6 +48,7 @@ const val AVATAR_ICON = "Fluent Avatar Icon"
* @param size Set Size of Avatar. Default: [AvatarSize.Size32]
* @param enableActivityRings Enable/Disable Activity Rings on Avatar
* @param enablePresence Enable/Disable Presence Indicator on Avatar, if cutout is provided then presence indicator is not displayed
+ * @param enableActivityDot Enable/Disable Activity Dot on Avatar.
* @param cutoutIconDrawable cutout drawable
* @param cutoutIconImageVector cutout image vector
* @param cutoutStyle shape of the cutout. Default: [CutoutStyle.Circle]
@@ -57,6 +62,7 @@ fun Avatar(
size: AvatarSize = AvatarSize.Size32,
enableActivityRings: Boolean = false,
enablePresence: Boolean = true,
+ enableActivityDot: Boolean = false,
@DrawableRes cutoutIconDrawable: Int? = null,
cutoutIconImageVector: ImageVector? = null,
cutoutStyle: CutoutStyle = CutoutStyle.Circle,
@@ -71,10 +77,15 @@ fun Avatar(
val personInitials = person.getInitials()
val avatarInfo = AvatarInfo(
- size, AvatarType.Person, person.isActive,
-
- person.status, person.isOOO, person.isImageAvailable(),
- personInitials.isNotEmpty(), person.getName(), cutoutStyle
+ size,
+ AvatarType.Person,
+ person.isActive,
+ person.status,
+ person.isOOO,
+ person.isImageAvailable(),
+ personInitials.isNotEmpty(),
+ person.getName(),
+ cutoutStyle
)
val avatarSize = token.avatarSize(avatarInfo)
val backgroundColor = token.backgroundBrush(avatarInfo)
@@ -82,23 +93,18 @@ fun Avatar(
val borders = token.borderStroke(avatarInfo)
val fontTextStyle = token.fontTypography(avatarInfo)
val cutoutCornerRadius = token.cutoutCornerRadius(avatarInfo)
- val cutoutBackgroundColor =
- token.cutoutBackgroundColor(avatarInfo = avatarInfo)
+ val cutoutBackgroundColor = token.cutoutBackgroundColor(avatarInfo = avatarInfo)
val cutoutBorderColor = token.cutoutBorderColor(avatarInfo = avatarInfo)
val cutoutIconSize = token.cutoutIconSize(avatarInfo = avatarInfo)
val isCutoutEnabled = (cutoutIconDrawable != null || cutoutIconImageVector != null)
var isImageOrInitialsAvailable = true
- Box(modifier = Modifier
- .semantics(mergeDescendants = true) {
- contentDescription = "${person.getName()}. " +
- "${if (enablePresence) "Status, ${person.status}," else ""} " +
- "${if (enablePresence && person.isOOO) "Out Of Office," else ""} " +
- if (enableActivityRings) {
- if (person.isActive) "Active" else "Inactive"
- } else ""
- }
- ) {
+ Box(modifier = Modifier.semantics(mergeDescendants = true) {
+ contentDescription =
+ "${person.getName()}. " + "${if (enablePresence) "Status, ${person.status}," else ""} " + "${if (enablePresence && person.isOOO) "Out Of Office," else ""} " + if (enableActivityRings) {
+ if (person.isActive) "Active" else "Inactive"
+ } else ""
+ }) {
Box(
Modifier
.then(modifier)
@@ -107,35 +113,35 @@ fun Avatar(
) {
when {
person.image != null -> {
- Image(
- painter = painterResource(person.image), null,
+ Image(painter = painterResource(person.image),
+ null,
+ contentScale = ContentScale.Crop,
modifier = Modifier
.size(avatarSize)
.clip(CircleShape)
.semantics {
testTag = AVATAR_IMAGE
- }
- )
+ })
}
+
person.bitmap != null -> {
- Image(
- bitmap = person.bitmap.asImageBitmap(), null,
+ Image(bitmap = person.bitmap.asImageBitmap(),
+ null,
+ contentScale = ContentScale.Crop,
modifier = Modifier
.size(avatarSize)
.clip(CircleShape)
.semantics {
testTag = AVATAR_IMAGE
- }
- )
+ })
}
+
personInitials.isNotEmpty() -> {
- BasicText(personInitials,
- style = fontTextStyle.merge(
- TextStyle(color = foregroundColor)
- ),
- modifier = Modifier
- .clearAndSetSemantics { })
+ BasicText(personInitials, style = fontTextStyle.merge(
+ TextStyle(color = foregroundColor)
+ ), modifier = Modifier.clearAndSetSemantics { })
}
+
else -> {
isImageOrInitialsAvailable = false
Icon(
@@ -151,8 +157,7 @@ fun Avatar(
}
}
- if (enableActivityRings)
- ActivityRing(radius = avatarSize / 2, borders)
+ if (enableActivityRings) ActivityRing(radius = avatarSize / 2, borders)
if (isCutoutEnabled && isImageOrInitialsAvailable && cutoutIconSize > 0.dp) {
Box(
@@ -164,30 +169,30 @@ fun Avatar(
if (cutoutIconDrawable != null) {
Image(
painter = painterResource(cutoutIconDrawable),
+ contentScale = ContentScale.Crop,
modifier = Modifier
.background(cutoutBackgroundColor)
.border(
- 2.dp,
- cutoutBorderColor,
- RoundedCornerShape(cutoutCornerRadius)
+ 2.dp, cutoutBorderColor, RoundedCornerShape(cutoutCornerRadius)
)
.padding(4.dp)
.size(cutoutIconSize),
- contentDescription = cutoutContentDescription
+ contentDescription = cutoutContentDescription,
+ colorFilter = token.cutoutColorFilter(avatarInfo = avatarInfo)
)
} else if (cutoutIconImageVector != null) {
Image(
imageVector = cutoutIconImageVector,
+ contentScale = ContentScale.Crop,
modifier = Modifier
.background(cutoutBackgroundColor)
.border(
- 2.dp,
- cutoutBorderColor,
- RoundedCornerShape(cutoutCornerRadius)
+ 2.dp, cutoutBorderColor, RoundedCornerShape(cutoutCornerRadius)
)
.padding(4.dp)
.size(cutoutIconSize),
- contentDescription = cutoutContentDescription
+ contentDescription = cutoutContentDescription,
+ colorFilter = token.cutoutColorFilter(avatarInfo = avatarInfo)
)
}
}
@@ -202,7 +207,89 @@ fun Avatar(
Modifier
.align(Alignment.BottomEnd)
// Adding 2.dp to both side to incorporate border which is an image in Fluent Android.
- .offset(presenceOffset.x + 2.dp, -presenceOffset.y + 2.dp)
+ .offset(presenceOffset.x + 2.dp, -presenceOffset.y + 2.dp),
+ contentScale = ContentScale.Crop
+ )
+ }
+
+ if (enableActivityDot) {
+ ActivityDot(token, avatarInfo, modifier.align(Alignment.TopEnd))
+ }
+ }
+ }
+}
+
+@Composable
+internal fun SlicedAvatar(
+ person: Person,
+ modifier: Modifier = Modifier,
+ width: Dp = 32.dp,
+ avatarToken: AvatarTokens? = null,
+ slicedAvatarSize: Dp = 32.dp,
+ size: AvatarSize = AvatarSize.Size32
+) {
+ val personInitials = person.getInitials()
+ // if less than 19dp, show only first initial
+ val personInitialsToDisplay =
+ if (personInitials.length >= 2 && width < 19.dp) personInitials[0].toString() else personInitials
+ val token = avatarToken
+ ?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AvatarControlType] as AvatarTokens
+ val avatarInfo = AvatarInfo(
+ size = size,
+ type = AvatarType.Person,
+ isImageAvailable = person.isImageAvailable(),
+ hasValidInitials = personInitials.isNotEmpty(),
+ calculatedColorKey = person.getName()
+ )
+ val foregroundColor = token.foregroundColor(avatarInfo)
+ val fontTextStyle = fontTypographyForSlicedAvatar(slicedAvatarSize)
+ val backgroundBrush = token.backgroundBrush(avatarInfo)
+ when {
+ person.image != null -> {
+ Image(
+ painter = painterResource(person.image),
+ null,
+ contentScale = ContentScale.Crop,
+ modifier = modifier
+ )
+ }
+
+ person.bitmap != null -> {
+ Image(
+ bitmap = person.bitmap.asImageBitmap(),
+ null,
+ contentScale = ContentScale.Crop,
+ modifier = modifier
+ )
+ }
+
+ personInitialsToDisplay.isNotEmpty() -> {
+ Box(
+ modifier = modifier.background(
+ brush = backgroundBrush
+ ), contentAlignment = Alignment.Center
+ ) {
+ BasicText(personInitialsToDisplay, style = fontTextStyle.merge(
+ TextStyle(color = foregroundColor)
+ ), modifier = Modifier.clearAndSetSemantics { })
+ }
+ }
+
+ else -> {
+ Box(
+ modifier = modifier.background(
+ brush = backgroundBrush
+ ), contentAlignment = Alignment.Center
+ ) {
+ Icon(
+ token.icon(avatarInfo),
+ null,
+ modifier = Modifier
+ .background(backgroundBrush, CircleShape)
+ .semantics {
+ testTag = AVATAR_ICON
+ },
+ tint = foregroundColor,
)
}
}
@@ -224,7 +311,7 @@ fun Avatar(
group: Group,
modifier: Modifier = Modifier,
size: AvatarSize = AvatarSize.Size32,
- avatarToken: AvatarTokens? = null,
+ avatarToken: AvatarTokens? = null
) {
val themeID =
@@ -233,7 +320,8 @@ fun Avatar(
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AvatarControlType] as AvatarTokens
val avatarInfo = AvatarInfo(
- size, AvatarType.Group,
+ size,
+ AvatarType.Group,
isImageAvailable = group.isImageAvailable(),
hasValidInitials = group.getInitials().isNotEmpty(),
calculatedColorKey = group.groupName
@@ -245,8 +333,7 @@ fun Avatar(
val foregroundColor = token.foregroundColor(avatarInfo)
var membersList = ""
- for (person in group.members)
- membersList += (person.firstName + person.lastName + "\n")
+ for (person in group.members) membersList += (person.firstName + person.lastName + "\n")
Box(
modifier
@@ -260,43 +347,37 @@ fun Avatar(
Modifier
.clip(RoundedCornerShape(cornerRadius))
.background(backgroundColor)
- .fillMaxSize(),
- contentAlignment = Alignment.Center
+ .fillMaxSize(), contentAlignment = Alignment.Center
) {
if (group.image != null) {
- Image(
- painter = painterResource(group.image),
+ Image(painter = painterResource(group.image),
+ contentScale = ContentScale.Crop,
contentDescription = null,
modifier = Modifier
.size(avatarSize)
.clip(RoundedCornerShape(cornerRadius))
.semantics {
testTag = AVATAR_IMAGE
- }
- )
+ })
} else if (group.bitmap != null) {
- Image(
- bitmap = group.bitmap.asImageBitmap(),
+ Image(bitmap = group.bitmap.asImageBitmap(),
+ contentScale = ContentScale.Crop,
contentDescription = null,
modifier = Modifier
.size(avatarSize)
.clip(RoundedCornerShape(cornerRadius))
.semantics {
testTag = AVATAR_IMAGE
- }
- )
+ })
} else if (group.groupName.isNotEmpty()) {
BasicText(group.getInitials(),
style = fontTextStyle.merge(TextStyle(color = foregroundColor)),
modifier = Modifier.clearAndSetSemantics { })
} else {
Icon(
- token.icon(avatarInfo),
- null,
- modifier = Modifier.semantics {
+ token.icon(avatarInfo), null, modifier = Modifier.semantics {
testTag = AVATAR_ICON
- },
- tint = foregroundColor
+ }, tint = foregroundColor
)
}
}
@@ -311,6 +392,7 @@ fun Avatar(
* @param size Set Size of Avatar. Default: [AvatarSize. Medium]
* @param enableActivityRings Enable/Disable Activity Rings on Avatar
* @param avatarToken Token to provide appearance values to Avatar
+ * @param enableActivityDot Enable/Disable Activity Dot on Avatar.
*/
@Composable
fun Avatar(
@@ -318,7 +400,8 @@ fun Avatar(
modifier: Modifier = Modifier,
size: AvatarSize = AvatarSize.Size32,
enableActivityRings: Boolean = false,
- avatarToken: AvatarTokens? = null
+ avatarToken: AvatarTokens? = null,
+ enableActivityDot: Boolean = false
) {
val themeID =
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
@@ -349,8 +432,10 @@ fun Avatar(
modifier = Modifier.clearAndSetSemantics { })
}
- if (enableActivityRings)
- ActivityRing(radius = avatarSize / 2, borders)
+ if (enableActivityRings) ActivityRing(radius = avatarSize / 2, borders)
+ if (enableActivityDot) {
+ ActivityDot(token, avatarInfo, modifier.align(Alignment.TopEnd))
+ }
}
}
@@ -368,3 +453,29 @@ fun ActivityRing(radius: Dp, borders: List) {
}
}
}
+
+@Composable
+fun ActivityDot(token: AvatarTokens, avatarInfo: AvatarInfo, modifier: Modifier) {
+ val unreadDotOffset: DpOffset = token.unreadDotOffset(avatarInfo)
+ val unreadDotSize: Dp = token.unreadDotSize(avatarInfo)
+ val unreadDotBackground: Brush = token.unreadDotBackgroundBrush(avatarInfo)
+ val unreadDotBorderStroke = token.unreadDotBorderStroke(avatarInfo)
+ Box(
+ modifier = modifier
+ .size(unreadDotSize)
+ .offset(unreadDotOffset.x + unreadDotBorderStroke.width , -unreadDotOffset.y + unreadDotBorderStroke.width)
+ ) {
+ Canvas(Modifier) {
+ drawCircle(
+ brush = unreadDotBorderStroke.brush,
+ radius = dpToPx(unreadDotBorderStroke.width + unreadDotSize / 2)
+ )
+ drawCircle(
+ brush = unreadDotBackground,
+ style = Fill,
+ radius = dpToPx(unreadDotSize / 2)
+ )
+ }
+ }
+
+}
diff --git a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarGroup.kt b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarGroup.kt
index c6e1c0368..7f2c6bbb1 100644
--- a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarGroup.kt
+++ b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarGroup.kt
@@ -37,6 +37,7 @@ fun AvatarGroup(
style: AvatarGroupStyle = AvatarGroupStyle.Stack,
maxVisibleAvatar: Int = DEFAULT_MAX_AVATAR,
enablePresence: Boolean = false,
+ enableActivityDot: Boolean = false,
avatarToken: AvatarTokens? = null,
avatarGroupToken: AvatarGroupTokens? = null
) {
@@ -52,6 +53,8 @@ fun AvatarGroup(
else
maxVisibleAvatar
+ val showActivityDot: Boolean = enableActivityDot && style == AvatarGroupStyle.Stack
+
var enablePresence: Boolean = enablePresence
if (style == AvatarGroupStyle.Stack)
enablePresence = false
@@ -81,29 +84,52 @@ fun AvatarGroup(
Layout(modifier = modifier
.padding(8.dp)
.then(semanticModifier), content = {
- for (i in 0 until visibleAvatar) {
- val person = group.members[i]
+ if (group.members.size > 0) {
+ if (style == AvatarGroupStyle.Pie) {
+ if (visibleAvatar > 1) {
+ AvatarPie(
+ group = group,
+ size = size,
+ noOfVisibleAvatars = visibleAvatar,
+ avatarTokens = avatarToken
+ )
+ } else {
+ Avatar(
+ group.members[0],
+ size = size,
+ enableActivityRings = true,
+ enablePresence = enablePresence,
+ avatarToken = avatarToken
+ )
+ }
- var paddingModifier: Modifier = Modifier
- if (style == AvatarGroupStyle.Pile && person.isActive) {
- val padding = token.pilePadding(avatarGroupInfo)
- paddingModifier = paddingModifier.padding(start = padding, end = padding)
- }
+ } else {
+ for (i in 0 until visibleAvatar) {
+ val person = group.members[i]
- Avatar(
- person,
- modifier = paddingModifier,
- size = size,
- enableActivityRings = true,
- enablePresence = enablePresence,
- avatarToken = avatarToken
- )
- }
- if (group.members.size > visibleAvatar || group.members.isEmpty()) {
- Avatar(
- group.members.size - visibleAvatar, size = size,
- enableActivityRings = true, avatarToken = avatarToken
- )
+ var paddingModifier: Modifier = Modifier
+ if (style == AvatarGroupStyle.Pile && person.isActive) {
+ val padding = token.pilePadding(avatarGroupInfo)
+ paddingModifier = paddingModifier.padding(start = padding, end = padding)
+ }
+
+ Avatar(
+ person,
+ modifier = paddingModifier,
+ size = size,
+ enableActivityRings = true,
+ enablePresence = enablePresence,
+ avatarToken = avatarToken,
+ enableActivityDot = group.members.size == visibleAvatar && i == visibleAvatar - 1 && showActivityDot
+ )
+ }
+ if (group.members.size > visibleAvatar || group.members.isEmpty()) {
+ Avatar(
+ group.members.size - visibleAvatar, size = size,
+ enableActivityRings = true, avatarToken = avatarToken, enableActivityDot = showActivityDot
+ )
+ }
+ }
}
}) { measurables, constraints ->
val placeables = measurables.map { measurable ->
@@ -127,4 +153,5 @@ fun AvatarGroup(
}
}
}
-}
\ No newline at end of file
+}
+
diff --git a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarPie.kt b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarPie.kt
new file mode 100644
index 000000000..95a640b8d
--- /dev/null
+++ b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/AvatarPie.kt
@@ -0,0 +1,155 @@
+package com.microsoft.fluentui.tokenized.persona
+
+import androidx.compose.foundation.background
+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.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.token.ControlTokens
+import com.microsoft.fluentui.theme.token.controlTokens.AvatarInfo
+import com.microsoft.fluentui.theme.token.controlTokens.AvatarTokens
+import com.microsoft.fluentui.theme.token.controlTokens.AvatarSize
+
+private val SPACER_SIZE = 2.dp
+
+@Composable
+fun AvatarPie(
+ group: Group, size: AvatarSize, noOfVisibleAvatars: Int = 2, avatarTokens: AvatarTokens? = null
+) {
+ val avatarInfo = AvatarInfo(
+ size
+ )
+ val token = avatarTokens
+ ?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AvatarControlType] as AvatarTokens
+ val avatarSize = token.avatarSize(avatarInfo)
+
+ Box(
+ modifier = Modifier
+ .requiredSize(avatarSize)
+ .background(
+ color = Color.White, shape = CircleShape
+ ), contentAlignment = Alignment.Center
+ ) {
+ val slicedAvatarDimen = avatarSize / 2 - SPACER_SIZE / 2
+ if (noOfVisibleAvatars == 2) {
+ RenderTwoSlices(avatarSize, slicedAvatarDimen, group, size)
+ } else if (noOfVisibleAvatars >= 3) {
+ RenderThreeSlices(avatarSize, slicedAvatarDimen, group, size)
+ }
+ }
+}
+
+@Composable
+private fun RenderTwoSlices(
+ avatarSize: Dp, slicedAvatarDimen: Dp, group: Group, size: AvatarSize
+) {
+ Row(
+ modifier = Modifier
+ .requiredSize(avatarSize)
+ .clip(CircleShape)
+ ) {
+ SlicedAvatar(
+ group.members[0],
+ slicedAvatarSize = avatarSize,
+ width = slicedAvatarDimen,
+ modifier = Modifier
+ .height(avatarSize)
+ .width(slicedAvatarDimen),
+ size = size
+ )
+ AddVerticalSpacer()
+ SlicedAvatar(
+ group.members[1],
+ slicedAvatarSize = avatarSize,
+ width = slicedAvatarDimen,
+ modifier = Modifier
+ .height(avatarSize)
+ .width(slicedAvatarDimen),
+ size = size
+ )
+ }
+}
+
+@Composable
+private fun RenderThreeSlices(
+ avatarSize: Dp, slicedAvatarDimen: Dp, group: Group, size: AvatarSize
+) {
+ Row(
+ modifier = Modifier
+ .requiredSize(avatarSize)
+ .clip(CircleShape)
+ ) {
+ SlicedAvatar(
+ group.members[0],
+ slicedAvatarSize = avatarSize,
+ width = slicedAvatarDimen,
+ modifier = Modifier
+ .height(avatarSize)
+ .width(slicedAvatarDimen)
+ .align(Alignment.CenterVertically),
+ size = size
+ )
+ AddVerticalSpacer()
+ Column(
+ modifier = Modifier
+ .height(avatarSize)
+ .width(slicedAvatarDimen),
+ ) {
+ SlicedAvatar(
+ group.members[1],
+ slicedAvatarSize = slicedAvatarDimen,
+ width = slicedAvatarDimen,
+ modifier = Modifier
+ .height(slicedAvatarDimen)
+ .width(slicedAvatarDimen),
+ size = size
+ )
+ AddHorizontalSpacer()
+ SlicedAvatar(
+ group.members[2],
+ slicedAvatarSize = slicedAvatarDimen,
+ width = slicedAvatarDimen,
+ modifier = Modifier
+ .height(slicedAvatarDimen)
+ .width(slicedAvatarDimen),
+ size = size
+ )
+ }
+
+ }
+}
+
+@Composable
+private fun AddVerticalSpacer() {
+ Spacer(
+ modifier = Modifier
+ .background(color = Color.White)
+ .fillMaxHeight()
+ .width(SPACER_SIZE)
+ )
+}
+
+@Composable
+private fun AddHorizontalSpacer() {
+ Spacer(
+ modifier = Modifier
+ .background(color = Color.White)
+ .fillMaxWidth()
+ .height(SPACER_SIZE)
+ )
+}
diff --git a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Utils.kt b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Utils.kt
index ffde47e45..d34510553 100644
--- a/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Utils.kt
+++ b/fluentui_persona/src/main/java/com/microsoft/fluentui/tokenized/persona/Utils.kt
@@ -4,7 +4,11 @@ import android.graphics.Bitmap
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import com.microsoft.fluentui.theme.token.FluentGlobalTokens
import com.microsoft.fluentui.theme.token.controlTokens.AvatarSize
import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
import kotlinx.parcelize.Parcelize
@@ -152,4 +156,89 @@ fun getAvatarSize(secondaryText: String?, tertiaryText: String?): AvatarSize {
return AvatarSize.Size40
}
return AvatarSize.Size56
+}
+
+
+@Composable
+fun fontTypographyForSlicedAvatar(slicedAvatarSize: Dp): TextStyle {
+ return when (slicedAvatarSize) {
+ 7.dp -> TextStyle(
+ fontSize = 4.sp,
+ lineHeight = 4.69.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 9.dp -> TextStyle(
+ fontSize = 5.sp,
+ lineHeight = 9.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 11.dp -> TextStyle(
+ fontSize = 6.sp,
+ lineHeight = 7.5.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 15.dp -> TextStyle(
+ fontSize = 10.sp,
+ lineHeight = 13.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 16.dp -> TextStyle(
+ fontSize = 6.sp,
+ lineHeight = 7.03.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 19.dp -> TextStyle(
+ fontSize = 8.sp,
+ lineHeight = 15.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 20.dp -> TextStyle(
+ fontSize = 8.sp,
+ lineHeight = 9.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 24.dp -> TextStyle(
+ fontSize = 10.sp,
+ lineHeight = 9.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 27.dp -> TextStyle(
+ fontSize = 11.sp,
+ lineHeight = 12.89.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 32.dp -> TextStyle(
+ fontSize = 13.sp,
+ lineHeight = 13.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 35.dp-> TextStyle(
+ fontSize = 13.sp,
+ lineHeight = 28.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 40.dp -> TextStyle(
+ fontSize = 10.sp,
+ lineHeight = 15.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 56.dp -> TextStyle(
+ fontSize = 14.sp,
+ lineHeight = 18.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ 72.dp -> TextStyle(
+ fontSize = FluentGlobalTokens.FontSizeTokens.Size400.value,
+ lineHeight = FluentGlobalTokens.LineHeightTokens.Size700.value,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+
+ else -> {
+ TextStyle(
+ fontSize = 13.sp,
+ lineHeight = 13.sp,
+ fontWeight = FluentGlobalTokens.FontWeightTokens.Regular.value
+ )
+ }
+ }
}
\ No newline at end of file
diff --git a/fluentui_progress/build.gradle b/fluentui_progress/build.gradle
index 50e9e2eda..9bdb0b224 100644
--- a/fluentui_progress/build.gradle
+++ b/fluentui_progress/build.gradle
@@ -37,6 +37,9 @@ android {
buildFeatures {
compose true
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_tablayout/build.gradle b/fluentui_tablayout/build.gradle
index f808986ad..b62186ccb 100644
--- a/fluentui_tablayout/build.gradle
+++ b/fluentui_tablayout/build.gradle
@@ -33,6 +33,9 @@ android {
composeOptions {
kotlinCompilerExtensionVersion composeCompilerVersion
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/TabBar.kt b/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/TabBar.kt
index 5d7fa7b84..c3fe509ba 100644
--- a/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/TabBar.kt
+++ b/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/TabBar.kt
@@ -5,8 +5,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.ControlTokens
import com.microsoft.fluentui.theme.token.FluentStyle
@@ -15,7 +16,7 @@ import com.microsoft.fluentui.theme.token.controlTokens.TabBarTokens
import com.microsoft.fluentui.theme.token.controlTokens.TabItemTokens
import com.microsoft.fluentui.theme.token.controlTokens.TabTextAlignment
import com.microsoft.fluentui.tokenized.tabItem.TabItem
-
+import com.microsoft.fluentui.tablayout.R
data class TabData(
var title: String,
@@ -23,7 +24,8 @@ data class TabData(
var selectedIcon: ImageVector = icon,
var selected: Boolean = false,
var onClick: () -> Unit,
- var badge: @Composable (() -> Unit)? = null
+ var badge: @Composable (() -> Unit)? = null,
+ var accessibilityDescription: String? = null, //Custom announcement for Talkback
)
/**
@@ -52,6 +54,7 @@ fun TabBar(
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
val token = tabBarTokens
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.TabBarControlType] as TabBarTokens
+ val resources = LocalContext.current.resources
Column(modifier.fillMaxWidth()) {
Box(
@@ -65,9 +68,16 @@ fun TabBar(
) {
tabDataList.forEachIndexed { index, tabData ->
tabData.selected = index == selectedIndex
+ var accessibilityDescriptionValue = if(tabData.accessibilityDescription != null) { tabData.accessibilityDescription }
+ else{ tabData.title + if(tabData.selected) resources.getString(R.string.tab_active).prependIndent(": ") else resources.getString(R.string.tab_inactive).prependIndent(": ") }
TabItem(
title = tabData.title,
modifier = Modifier
+ .semantics {
+ if (accessibilityDescriptionValue != null) {
+ contentDescription = accessibilityDescriptionValue
+ }
+ }
.fillMaxWidth()
.weight(1F),
icon = if (tabData.selected) tabData.selectedIcon else tabData.icon,
diff --git a/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/ViewPager.kt b/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/ViewPager.kt
new file mode 100644
index 000000000..b44c226f3
--- /dev/null
+++ b/fluentui_tablayout/src/main/java/com/microsoft/fluentui/tokenized/navigation/ViewPager.kt
@@ -0,0 +1,55 @@
+package com.microsoft.fluentui.tokenized.navigation
+
+import android.graphics.Paint.Align
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PageSize
+import androidx.compose.foundation.pager.PagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.microsoft.fluentui.theme.FluentTheme
+import com.microsoft.fluentui.theme.token.ControlTokens
+import com.microsoft.fluentui.theme.token.controlTokens.ViewPagerInfo
+import com.microsoft.fluentui.theme.token.controlTokens.ViewPagerTokens
+
+/**
+ * API to create a ViewPager.
+ *
+ * @param pagerState PagerState to manage the state of ViewPager
+ * @param pageContent Content to be displayed in ViewPager
+ * @param modifier Optional modifier for ViewPager
+ * @param pageSize Size of the page. Default: [PageSize.Fill]
+ * @param userScrollEnabled Boolean for enabling/disabling user scroll. Default: [false]
+ * @param verticalAlignment Alignment of content in ViewPager. Default: [Alignment.CenterVertically]
+ * @param viewPagerTokens Tokens to customize appearance of ViewPager. Default: [null]
+ */
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun ViewPager(
+ pagerState: PagerState,
+ pageContent: @Composable () -> Unit,
+ modifier: Modifier = Modifier,
+ pageSize: PageSize = PageSize.Fill,
+ userScrollEnabled: Boolean = false,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+ viewPagerTokens: ViewPagerTokens? = null
+) {
+ val token =
+ viewPagerTokens
+ ?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.ViewPagerControlType] as ViewPagerTokens
+
+ val viewPagerInfo = ViewPagerInfo()
+ // HorizontalPager is a horizontally scrolling pager using the provided pagerState
+ HorizontalPager(
+ state = pagerState,
+ modifier = modifier,
+ contentPadding = token.contentPadding(viewPagerInfo),
+ pageSpacing = token.pageSpacing(viewPagerInfo),
+ pageSize = pageSize,
+ userScrollEnabled = userScrollEnabled,
+ verticalAlignment = verticalAlignment
+ ) {
+ pageContent()
+ }
+}
diff --git a/fluentui_tablayout/src/main/res/values/strings.xml b/fluentui_tablayout/src/main/res/values/strings.xml
index f3d06bb1f..2de3c71d6 100644
--- a/fluentui_tablayout/src/main/res/values/strings.xml
+++ b/fluentui_tablayout/src/main/res/values/strings.xml
@@ -4,5 +4,11 @@
\u0020 tab %1$d of %2$d
- Item %d in list of %d
+ Item %1$d in list of %2$d
+
+
+ Active
+
+
+ Inactive
\ No newline at end of file
diff --git a/fluentui_topappbars/build.gradle b/fluentui_topappbars/build.gradle
index a2dc541a8..e7f8a6514 100644
--- a/fluentui_topappbars/build.gradle
+++ b/fluentui_topappbars/build.gradle
@@ -39,6 +39,9 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/search/Searchbar.kt b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/search/Searchbar.kt
index 423a5bb37..2fa7e0461 100644
--- a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/search/Searchbar.kt
+++ b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/search/Searchbar.kt
@@ -14,7 +14,6 @@ import android.util.AttributeSet
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
-import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
@@ -22,10 +21,8 @@ import android.widget.RelativeLayout
import com.microsoft.fluentui.topappbars.R
import com.microsoft.fluentui.appbarlayout.AppBarLayout
import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.microsoft.fluentui.util.inputMethodManager
import com.microsoft.fluentui.util.isVisible
-import com.microsoft.fluentui.util.activity
import com.microsoft.fluentui.util.toggleKeyboardVisibility
import com.microsoft.fluentui.view.TemplateView
import com.microsoft.fluentui.progress.ProgressBar
@@ -156,8 +153,6 @@ open class Searchbar : TemplateView, SearchView.OnQueryTextListener {
private var searchView: SearchView? = null
private var searchCloseButton: ImageButton? = null
private var searchProgress: ProgressBar? = null
- private var singleScreenDisplayPixels = 0
- private var screenPos = IntArray(2)
override fun onTemplateLoaded() {
super.onTemplateLoaded()
@@ -169,9 +164,6 @@ open class Searchbar : TemplateView, SearchView.OnQueryTextListener {
searchView = findViewInTemplateById(R.id.search_view)
searchCloseButton = findViewInTemplateById(R.id.search_close)
searchProgress = findViewInTemplateById(R.id.search_progress)
- context.activity?.let {
- singleScreenDisplayPixels = DuoSupportUtils.getSingleScreenWidthPixels(it)
- }
// Hide the default search view close button from TalkBack and get rid of the space it takes up.
val closeButton = searchView?.findViewById(R.id.search_close_btn)
@@ -183,21 +175,6 @@ open class Searchbar : TemplateView, SearchView.OnQueryTextListener {
setUnfocusedState()
}
- override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- var widthMeasureSpec = widthMeasureSpec
- val viewWidth = MeasureSpec.getSize(widthMeasureSpec)
- this.getLocationOnScreen(screenPos)
-
- // Adjust x coordinate for second screen on Duo
- if (screenPos[0] > singleScreenDisplayPixels)
- screenPos[0] -= singleScreenDisplayPixels + DuoSupportUtils.DUO_HINGE_WIDTH
-
- // Adjust for hinge
- if (screenPos[0] + viewWidth > singleScreenDisplayPixels)
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(singleScreenDisplayPixels - screenPos[0], MeasureSpec.EXACTLY)
- super.onMeasure(widthMeasureSpec, heightMeasureSpec)
- }
-
private fun updateViews() {
searchView?.queryHint = queryHint
searchView?.setSearchableInfo(searchableInfo)
diff --git a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/AppBar.kt b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/AppBar.kt
index 679378edf..8eb71fc65 100644
--- a/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/AppBar.kt
+++ b/fluentui_topappbars/src/main/java/com/microsoft/fluentui/tokenized/AppBar.kt
@@ -20,15 +20,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.microsoft.fluentui.core.R
import com.microsoft.fluentui.icons.ListItemIcons
-import com.microsoft.fluentui.icons.SearchBarIcons
-import com.microsoft.fluentui.icons.appbaricons.AppBarIcons
-import com.microsoft.fluentui.icons.appbaricons.appbaricons.Arrowback
import com.microsoft.fluentui.icons.listitemicons.Chevron
import com.microsoft.fluentui.theme.FluentTheme
import com.microsoft.fluentui.theme.token.*
@@ -54,7 +50,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.AppBarTokens
* @param subTitle Subtitle to be displayed. Default: [null]
* @param logo Composable to be placed at left of Title. Guideline is to not increase a size of 32x32. Default: [null]
* @param searchMode Boolean to enable/disable searchMode. Default: [false]
- * @param navigationIcon Navigate Back Icon to be placed at extreme left. Default: [SearchBarIcons.Arrowback]
* @param postTitleIcon Icon to be placed after title making the title clickable. Default: Empty [FluentIcon]
* @param preSubtitleIcon Icon to be placed before subtitle. Default: Empty [FluentIcon]
* @param postSubtitleIcon Icon to be placed after subtitle. Default: [ListItemIcons.Chevron]
@@ -64,6 +59,8 @@ import com.microsoft.fluentui.theme.token.controlTokens.AppBarTokens
* @param bottomBorder Boolean to place a bottom border on AppBar. Applies only when searchBar and bottomBar are empty. Default: [true]
* @param appTitleDelta Ratio of opening of appTitle. Used for Shychrome and other animations. Default: [1.0F]
* @param accessoryDelta Ratio of opening of accessory View. Used for Shychrome and other animations. Default: [1.0F]
+ * @param centerAlignAppBar boolean indicating if the app bar should be center aligned. Default: [false]
+ * @param navigationIcon Navigate Back Icon to be placed at extreme left. Default: [null]
* @param appBarTokens Optional Tokens for App Bar to customize it. Default: [null]
*/
@@ -72,7 +69,7 @@ const val APP_BAR = "Fluent App bar"
const val APP_BAR_SUBTITLE = "Fluent App bar Subtitle"
const val APP_BAR_BOTTOM_BAR = "Fluent App bar Bottom bar"
const val APP_BAR_SEARCH_BAR = "Fluent App bar Search bar"
-@OptIn(ExperimentalTextApi::class)
+
@Composable
fun AppBar(
title: String,
@@ -82,7 +79,6 @@ fun AppBar(
subTitle: String? = null,
logo: @Composable (() -> Unit)? = null,
searchMode: Boolean = false,
- navigationIcon: FluentIcon = FluentIcon(AppBarIcons.Arrowback, flipOnRtl = true),
postTitleIcon: FluentIcon = FluentIcon(),
preSubtitleIcon: FluentIcon = FluentIcon(),
postSubtitleIcon: FluentIcon = FluentIcon(
@@ -95,7 +91,9 @@ fun AppBar(
bottomBorder: Boolean = true,
appTitleDelta: Float = 1.0F,
accessoryDelta: Float = 1.0F,
- appBarTokens: AppBarTokens? = null
+ centerAlignAppBar: Boolean = false,
+ navigationIcon: FluentIcon? = null,
+ appBarTokens: AppBarTokens? = null,
) {
val themeID =
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
@@ -140,52 +138,43 @@ fun AppBar(
.fillMaxWidth()
.scale(scaleX = 1.0F, scaleY = appTitleDelta)
.alpha(if (appTitleDelta != 1.0F) appTitleDelta / 3 else 1.0F),
- horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
- if (appBarSize != AppBarSize.Large && navigationIcon.isIconAvailable()) {
+ if (navigationIcon !== null && navigationIcon.isIconAvailable()) {
Icon(
navigationIcon,
modifier =
- Modifier.then(
- if(navigationIcon.onClick != null)
- Modifier.clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(color = token.navigationIconRippleColor()),
- enabled = true,
- onClick = navigationIcon.onClick ?: {}
+ Modifier
+ .then(
+ if (navigationIcon.onClick != null)
+ Modifier.clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(color = token.navigationIconRippleColor()),
+ enabled = true,
+ onClick = navigationIcon.onClick ?: {}
+ )
+ else Modifier
)
- else Modifier
- )
.padding(token.navigationIconPadding(appBarInfo))
.size(token.leftIconSize(appBarInfo)),
tint = token.navigationIconColor(appBarInfo)
)
}
- if (appBarSize != AppBarSize.Medium) {
- Box(
- modifier = Modifier
- .then(
- if (appBarSize == AppBarSize.Large)
- Modifier.padding(start = 16.dp)
- else
- Modifier
- )
- ) {
- logo?.invoke()
- }
- }
+ logo?.invoke()
val titleTextStyle = token.titleTypography(appBarInfo)
val subtitleTextStyle = token.subtitleTypography(appBarInfo)
+ val titleAlignment: Alignment.Horizontal =
+ if (centerAlignAppBar) Alignment.CenterHorizontally else Alignment.Start
if (appBarSize != AppBarSize.Large && !subTitle.isNullOrBlank()) {
Column(
modifier = Modifier
.weight(1F)
.padding(token.textPadding(appBarInfo))
- .testTag(APP_BAR_SUBTITLE)
+ .testTag(APP_BAR_SUBTITLE),
+ horizontalAlignment = titleAlignment
) {
Row(
modifier = Modifier
@@ -262,20 +251,26 @@ fun AppBar(
}
}
} else {
- BasicText(
- text = title,
+ Column(
modifier = Modifier
.padding(token.textPadding(appBarInfo))
.weight(1F)
.semantics { heading() },
- style = titleTextStyle.merge(
- TextStyle(
- color = token.titleTextColor(appBarInfo)
- )
- ),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
+ horizontalAlignment = titleAlignment
+ ) {
+
+ BasicText(
+ text = title,
+ style = titleTextStyle.merge(
+ TextStyle(
+ color = token.titleTextColor(appBarInfo)
+ )
+ ),
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+
+ }
}
if (rightAccessoryView != null) {
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 286d30692..37353e033 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
@@ -1,8 +1,8 @@
package com.microsoft.fluentui.tokenized
import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
@@ -15,10 +15,10 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.draw.shadow
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
@@ -73,8 +73,6 @@ import com.microsoft.fluentui.topappbars.R
* @param rightAccessoryIcon [FluentIcon] Object which is displayed on the right side of microphone. Default: [null]
* @param searchBarTokens Tokens which help in customizing appearance of search bar. Default: [null]
*/
-// AnimatedContent Backspace Key
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalAnimationApi::class)
@Composable
fun SearchBar(
onValueChange: (String, Person?) -> Unit,
@@ -90,6 +88,7 @@ fun SearchBar(
personaChipOnClick: (() -> Unit)? = null,
microphoneCallback: (() -> Unit)? = null,
navigationIconCallback: (() -> Unit)? = null,
+ leftAccessoryIcon: ImageVector? = SearchBarIcons.Search,
rightAccessoryIcon: FluentIcon? = null,
searchBarTokens: SearchBarTokens? = null
) {
@@ -106,8 +105,23 @@ fun SearchBar(
var personaChipSelected by rememberSaveable { mutableStateOf(false) }
var selectedPerson: Person? = selectedPerson
+ val borderWidth = token.borderWidth(searchBarInfo)
+ val elevation = token.elevation(searchBarInfo)
+ val height = token.height(searchBarInfo)
val scope = rememberCoroutineScope()
+ val borderModifier = if (borderWidth > 0.dp) {
+ Modifier.border(
+ width = borderWidth,
+ color = token.borderColor(searchBarInfo),
+ shape = RoundedCornerShape(token.cornerRadius(searchBarInfo))
+ )
+ } else Modifier
+ val shadowModifier = if (elevation > 0.dp) Modifier.shadow(
+ elevation = token.elevation(searchBarInfo),
+ shape = RoundedCornerShape(token.cornerRadius(searchBarInfo)),
+ spotColor = token.shadowColor(searchBarInfo)
+ ) else Modifier
Row(
modifier = modifier
@@ -116,12 +130,14 @@ fun SearchBar(
) {
Row(
Modifier
- .requiredHeightIn(min = token.height(searchBarInfo))
+ .requiredHeightIn(min = height)
+ .then(borderModifier)
+ .then(shadowModifier)
.fillMaxWidth()
- .clip(RoundedCornerShape(8.dp))
+ .clip(RoundedCornerShape(token.cornerRadius(searchBarInfo)))
.background(
token.inputBackgroundBrush(searchBarInfo),
- RoundedCornerShape(8.dp)
+ RoundedCornerShape(token.cornerRadius(searchBarInfo))
),
verticalAlignment = Alignment.CenterVertically
) {
@@ -152,11 +168,12 @@ fun SearchBar(
if (LocalLayoutDirection.current == LayoutDirection.Rtl)
mirrorImage = true
}
+
false -> {
onClick = {
focusRequester.requestFocus()
}
- icon = SearchBarIcons.Search
+ icon = leftAccessoryIcon ?: SearchBarIcons.Search
contentDescription =
LocalContext.current.resources.getString(R.string.fluentui_search)
mirrorImage = false
@@ -306,6 +323,7 @@ fun SearchBar(
onClick = microphoneCallback
)
}
+
false ->
Box(
modifier = Modifier
diff --git a/fluentui_transients/build.gradle b/fluentui_transients/build.gradle
index a5b071dd5..065d18d43 100644
--- a/fluentui_transients/build.gradle
+++ b/fluentui_transients/build.gradle
@@ -27,6 +27,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
+ lintOptions {
+ abortOnError false
+ }
}
dependencies {
diff --git a/fluentui_transients/src/main/java/com/microsoft/fluentui/snackbar/Snackbar.kt b/fluentui_transients/src/main/java/com/microsoft/fluentui/snackbar/Snackbar.kt
index 136752391..8163cf643 100644
--- a/fluentui_transients/src/main/java/com/microsoft/fluentui/snackbar/Snackbar.kt
+++ b/fluentui_transients/src/main/java/com/microsoft/fluentui/snackbar/Snackbar.kt
@@ -11,7 +11,6 @@ import com.google.android.material.snackbar.BaseTransientBottomBar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.appcompat.widget.AppCompatButton
-import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -22,9 +21,7 @@ import com.microsoft.fluentui.transients.R
import com.microsoft.fluentui.transients.R.id.*
import com.microsoft.fluentui.theming.FluentUIContextThemeWrapper
import com.microsoft.fluentui.transients.databinding.ViewSnackbarBinding
-import com.microsoft.fluentui.util.DuoSupportUtils
import com.microsoft.fluentui.util.ThemeUtil
-import com.microsoft.fluentui.util.activity
/**
* Snackbars provide lightweight feedback about an operation by showing a brief message at the bottom of the screen.
@@ -137,55 +134,6 @@ class Snackbar : BaseTransientBottomBar {
actionButtonView = binding.snackbarAction
updateBackground()
- // Set the margin on the FrameLayout (SnackbarLayout) instead of the content because the content's bottom margin is buggy in some APIs.
- if (content.parent is FrameLayout) {
- context.activity?.let {
- if(DuoSupportUtils.isWindowDoublePortrait(it)) {
- val singleScreenDisplayPixels = DuoSupportUtils.getSingleScreenWidthPixels(it)
- val snackbarLP = getView().layoutParams
- snackbarLP.width = singleScreenDisplayPixels
- getView().layoutParams = snackbarLP
- alignLeft(parent)
- }
- }
- }
- }
-
- /**
- * This is adapted from android.support.design.widget.Snackbar
- * It ensures we can use Snackbars in complex ViewGroups like RecyclerView.
- */
- private fun alignLeft(view: View) {
- var currentView: View? = view
- var fallbackParent: ViewGroup? = null
-
- do {
- if (currentView is CoordinatorLayout) {
- // We've found a CoordinatorLayout, use it
- val params = getView().layoutParams as CoordinatorLayout.LayoutParams
- params.gravity = Gravity.BOTTOM
- getView().layoutParams = params
- return
- }
-
- if (currentView is FrameLayout)
- if (currentView.id == android.R.id.content) {
- // If we've hit the decor content view, then we didn't find a CoL in the
- // hierarchy, so use it.
- val params = getView().layoutParams as FrameLayout.LayoutParams
- params.gravity = Gravity.BOTTOM
- view.layoutParams = params
- return
- } else
- // It's not the content view but we'll use it as our fallback
- fallbackParent = currentView
-
- // Else, we will loop and crawl up the view hierarchy and try to find a parent
- currentView = currentView?.parent as? View
- } while (currentView != null)
-
- // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
- return
}
/**
@@ -216,7 +164,8 @@ class Snackbar : BaseTransientBottomBar {
actionButtonView.visibility = View.VISIBLE
actionButtonView.setOnClickListener { view ->
listener.onClick(view)
- dismiss()
+ // dismiss the Snackbar
+ dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION)
}
updateStyle()
diff --git a/fluentui_transients/src/main/java/com/microsoft/fluentui/tooltip/Tooltip.kt b/fluentui_transients/src/main/java/com/microsoft/fluentui/tooltip/Tooltip.kt
index 468b70cb1..639ffb5ed 100644
--- a/fluentui_transients/src/main/java/com/microsoft/fluentui/tooltip/Tooltip.kt
+++ b/fluentui_transients/src/main/java/com/microsoft/fluentui/tooltip/Tooltip.kt
@@ -162,7 +162,6 @@ class Tooltip {
initTooltipArrow(anchorRect, anchor.layoutIsRtl, config.offsetX)
checkEdgeCase(anchorRect)
- hingeSupport(anchorRect, config.touchDismissLocation)
if (requireReinit) initTooltipArrow(anchorRect, anchor.layoutIsRtl, config.offsetX)
if (requireReadjustment) readjustTooltip(anchorRect, anchor.layoutIsRtl, config)
@@ -226,12 +225,6 @@ class Tooltip {
private fun setPositionX(anchorCenter: Int, offsetX: Int) {
positionX = anchorCenter - contentWidth / 2 + offsetX
- // Duo Second Screen Support
- val secondScreen = anchorCenter > displayWidth && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } ?: false
- if (secondScreen) positionX -= displayWidth + DuoSupportUtils.DUO_HINGE_WIDTH
-
// Navigation Bar in Nougat+ can appear on the left on phones at 270 rotation and adds
// its height to the left of the display creating an offset that needs to be corrected to get
// accurate horizontal position.
@@ -249,18 +242,11 @@ class Tooltip {
private fun setPositionY(anchor: Rect, offsetY: Int, dismissLocation: TouchDismissLocation) {
positionY = anchor.bottom
- // Duo Second Screen Support
- val secondScreen = anchor.bottom > displayHeight && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } ?: false
- if (secondScreen) positionY -= displayHeight + DuoSupportUtils.DUO_HINGE_WIDTH
-
isAboveAnchor = context.activity?.let {
positionY + contentHeight + margin > displayHeight
} ?: false
if (isAboveAnchor) {
positionY = anchor.top - contentHeight - offsetY
- if (secondScreen) positionY -= displayHeight + DuoSupportUtils.DUO_HINGE_WIDTH
}
}
@@ -302,10 +288,7 @@ class Tooltip {
val layoutParams = toolTipArrow.layoutParams as LinearLayout.LayoutParams
val cornerRadius = context.resources.getDimensionPixelSize(R.dimen.fluentui_tooltip_radius)
if (!isSideAnchor) { // Normal Top/Bottom arrow
- val anchorCenterX = if (anchorRect.centerX() > displayWidth && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } == true) anchorRect.centerX() - displayWidth - DuoSupportUtils.DUO_HINGE_WIDTH
- else anchorRect.centerX()
+ val anchorCenterX = anchorRect.centerX()
val offset = if (isRTL)
positionX + contentWidth - anchorCenterX - tooltipArrowWidth
@@ -316,10 +299,6 @@ class Tooltip {
} else {// Edge Case Left/Right arrow
layoutParams.gravity = Gravity.TOP
var topMargin = anchorRect.centerY() - positionY - tooltipArrowWidth
- val secondScreen = anchorRect.top > displayHeight && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } ?: false
- topMargin -= if (secondScreen) displayHeight else 0
if (positionY + contentHeight >= displayHeight) topMargin -= cornerRadius
layoutParams.topMargin = topMargin
}
@@ -344,63 +323,19 @@ class Tooltip {
val leftEdge =
(startPosition - cornerRadius - margin - context.softNavBarOffsetX < 0) || (doesNotFitAboveOrBelow && anchorRect.left < rightSpace)
- // Duo Support
- val secondScreen = anchorRect.left > displayWidth && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } ?: false
if (leftEdge) { // checks if the arrow is cut by the left edge of the screen and sets positionX to the left of the anchor with proper width.
positionX = anchorRect.right
- if (secondScreen) positionX -= displayWidth + DuoSupportUtils.DUO_HINGE_WIDTH
}
if (rightEdge) { // checks if the arrow is cut by the right edge of the screen and sets positionX to the left of the anchor with proper width.
isAboveAnchor = true // Enables right arrow
positionX = anchorRect.left - contentWidth - upArrowWidth / 2
- if (secondScreen) positionX -= displayWidth + DuoSupportUtils.DUO_HINGE_WIDTH
}
if (leftEdge || rightEdge)
requireReadjustment = true
}
- private fun hingeSupport(anchorRect: Rect, dismissLocation: TouchDismissLocation) {
- context.activity?.let {
- val upArrowWidth =
- context.resources.getDimensionPixelSize(R.dimen.fluentui_tooltip_arrow_width)
- val tooltipRect = Rect(
- positionX,
- positionY,
- positionX + contentWidth,
- positionY + contentHeight - upArrowWidth / 2
- )
- val anchorIntersects = DuoSupportUtils.intersectHinge(it, anchorRect)
- val tooltipIntersects = DuoSupportUtils.intersectHinge(it, tooltipRect)
-
- if (anchorIntersects || tooltipIntersects) {
- if (DuoSupportUtils.isWindowDoublePortrait(it)) {
- isAboveAnchor = false // Enables left arrow
- if (DuoSupportUtils.moreOnLeft(it, anchorRect)) {
- isAboveAnchor = true // Enables right arrow
- positionX = anchorRect.left - contentWidth - upArrowWidth / 2
- } else {
- positionX = anchorRect.right
- }
- requireReadjustment = true
- } else { // Device is in vertical orientation
- // Usually the tooltip will occur below the anchor, so the tooltip will intersect only in case its in top screen
- // In such case, we make tooltip on the top of the anchor.
- if (tooltipIntersects) {
- isAboveAnchor = true
- isSideAnchor = false
- positionY = anchorRect.top - contentHeight
- if (dismissLocation == TouchDismissLocation.INSIDE) positionY -= context.statusBarHeight
- requireReinit = true
- }
- }
- }
- }
- }
-
private fun readjustTooltip(anchorRect: Rect, isRTL: Boolean, config: Config) {
val upArrowWidth =
context.resources.getDimensionPixelSize(R.dimen.fluentui_tooltip_arrow_width)
@@ -427,10 +362,6 @@ class Tooltip {
// Otherwise sets positionY such that the content ends at the bottom of anchor
else anchorRect.bottom - contentHeight
- val secondScreen = anchorRect.top > displayHeight && context.activity?.let {
- DuoSupportUtils.isDeviceSurfaceDuo(it)
- } ?: false
- positionY -= if (secondScreen) displayHeight else 0
// Readjusts positionY if it crosses AppBar on the top
if (positionY < topBarHeight + context.statusBarHeight)
@@ -438,20 +369,6 @@ class Tooltip {
if (config.touchDismissLocation == TouchDismissLocation.INSIDE)
positionY -= context.statusBarHeight
- // Readjustment for Duo hinge
- val tooltipRect =
- Rect(positionX, positionY, positionX + contentWidth, positionY + contentHeight)
- context.activity?.let {
- if (DuoSupportUtils.intersectHinge(it, tooltipRect)) {
- positionY = if (DuoSupportUtils.moreOnTop(it, anchorRect)) {
- DuoSupportUtils.getHinge(it)!!.top - contentHeight - margin + cornerRadius
- } else {
- DuoSupportUtils.getHinge(it)!!.bottom + margin - cornerRadius
- }
- isAboveAnchor = tooltipRect.left < anchorRect.left
- }
- }
-
// Reinitialize tooltip with side arrow
initTooltipArrow(anchorRect, isRTL, config.offsetX)
if (config.touchDismissLocation == TouchDismissLocation.INSIDE)
diff --git a/publish.gradle b/publish.gradle
index f6c0363fa..07fd246e5 100644
--- a/publish.gradle
+++ b/publish.gradle
@@ -36,9 +36,9 @@ project.ext.publishingFunc = { artifactIdName ->
url rootProject.buildDir.path + '/artifacts'
}
}
- tasks.withType(PublishToMavenRepository) {
+ tasks.withType(PublishToMavenRepository).configureEach {
onlyIf {
- (repository == publishing.repositories.local && !(artifactExists("central", artifactIdName, android.defaultConfig.versionName))) || (repository == publishing.repositories.feed && !(artifactExists("feed", artifactIdName, android.defaultConfig.versionName)))
+ (repository == publishing.repositories.local) || (repository == publishing.repositories.feed && !(artifactExists("feed", artifactIdName, android.defaultConfig.versionName)))
}
}
publications {