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 {