removed compositionProviders (#397)
* removed compositionProviders * updating test --------- Co-authored-by: PraveenKumar <pyeruva@microsoft.com>
This commit is contained in:
Родитель
ce6cf2ddb3
Коммит
831e1d6c75
|
@ -17,7 +17,7 @@ import com.microsoft.fluentui.theme.token.controlTokens.CircularProgressIndicato
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.ShimmerShape
|
||||
import com.microsoft.fluentui.tokenized.progress.CircularProgressIndicator
|
||||
import com.microsoft.fluentui.tokenized.progress.LinearProgressIndicator
|
||||
import com.microsoft.fluentui.tokenized.progress.Shimmer
|
||||
import com.microsoft.fluentui.tokenized.shimmer.Shimmer
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -76,11 +76,11 @@ class V2ProgressIndicatorUITest {
|
|||
fun testShimmerBounds(){
|
||||
composeTestRule.setContent {
|
||||
FluentTheme {
|
||||
Shimmer(ShimmerShape.Box, modifier = Modifier
|
||||
Shimmer(shape = ShimmerShape.Box, modifier = Modifier
|
||||
.height(50.dp)
|
||||
.width(50.dp)
|
||||
.testTag("shimmer"))
|
||||
Shimmer(ShimmerShape.Circle, modifier = Modifier.size(50.dp).testTag("circleShimmer"))
|
||||
Shimmer(shape= ShimmerShape.Circle, modifier = Modifier.size(50.dp).testTag("circleShimmer"))
|
||||
}
|
||||
}
|
||||
composeTestRule.onNodeWithTag("shimmer").assertHeightIsEqualTo(50.dp).assertWidthIsEqualTo(50.dp)
|
||||
|
|
|
@ -61,31 +61,31 @@ class V2BadgeActivity : DemoActivity() {
|
|||
LazyRow {
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("1", badgeType = BadgeType.Character)
|
||||
Badge(text = "1", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("2", badgeType = BadgeType.Character)
|
||||
Badge(text = "2", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("8", badgeType = BadgeType.Character)
|
||||
Badge(text = "8", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("12", badgeType = BadgeType.Character)
|
||||
Badge(text = "12", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("123", badgeType = BadgeType.Character)
|
||||
Badge(text = "123", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("12345678910", badgeType = BadgeType.Character)
|
||||
Badge(text = "12345678910", badgeType = BadgeType.Character)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("Badge", badgeType = BadgeType.Character)
|
||||
Badge(text = "Badge", badgeType = BadgeType.Character)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -101,31 +101,31 @@ class V2BadgeActivity : DemoActivity() {
|
|||
LazyRow {
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("1", badgeType = BadgeType.List)
|
||||
Badge(text = "1", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("2", badgeType = BadgeType.List)
|
||||
Badge(text = "2", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("8", badgeType = BadgeType.List)
|
||||
Badge(text = "8", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("12", badgeType = BadgeType.List)
|
||||
Badge(text = "12", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("123", badgeType = BadgeType.List)
|
||||
Badge(text = "123", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("12345678910", badgeType = BadgeType.List)
|
||||
Badge(text = "12345678910", badgeType = BadgeType.List)
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Badge("Badge", badgeType = BadgeType.List)
|
||||
Badge(text = "Badge", badgeType = BadgeType.List)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import com.microsoft.fluentui.theme.token.controlTokens.CircularProgressIndicato
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.ShimmerShape
|
||||
import com.microsoft.fluentui.tokenized.progress.CircularProgressIndicator
|
||||
import com.microsoft.fluentui.tokenized.progress.LinearProgressIndicator
|
||||
import com.microsoft.fluentui.tokenized.progress.Shimmer
|
||||
import com.microsoft.fluentui.tokenized.shimmer.Shimmer
|
||||
import com.microsoft.fluentuidemo.DemoActivity
|
||||
import com.microsoft.fluentuidemo.databinding.V2ActivityComposeBinding
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
|
@ -87,7 +87,7 @@ class V2ScaffoldActivity : DemoActivity() {
|
|||
invokeToast(resources.getString(R.string.tabBar_mail), context)
|
||||
selectedIndex = 1
|
||||
},
|
||||
badge = { Badge("123+", badgeType = BadgeType.Character) }
|
||||
badge = { Badge(text = "123+", badgeType = BadgeType.Character) }
|
||||
),
|
||||
TabData(
|
||||
title = resources.getString(R.string.tabBar_settings),
|
||||
|
|
|
@ -65,7 +65,7 @@ class V2TabBarActivity : DemoActivity() {
|
|||
invokeToast(resources.getString(R.string.tabBar_mail), context)
|
||||
selectedIndex = 1
|
||||
},
|
||||
badge = { Badge("123+", badgeType = BadgeType.Character) }
|
||||
badge = { Badge(text = "123+", badgeType = BadgeType.Character) }
|
||||
),
|
||||
TabData(
|
||||
title = resources.getString(R.string.tabBar_settings),
|
||||
|
@ -84,7 +84,7 @@ class V2TabBarActivity : DemoActivity() {
|
|||
invokeToast(resources.getString(R.string.tabBar_notification), context)
|
||||
selectedIndex = 3
|
||||
},
|
||||
badge = { Badge("10", badgeType = BadgeType.Character) }
|
||||
badge = { Badge(text = "10", badgeType = BadgeType.Character) }
|
||||
),
|
||||
TabData(
|
||||
title = resources.getString(R.string.tabBar_more),
|
||||
|
|
|
@ -10,7 +10,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -41,9 +43,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.ContextualCommandBarToke
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
private val LocalContextualCommandBarTokens = compositionLocalOf { ContextualCommandBarTokens() }
|
||||
private val LocalContextualCommandBarInfo = compositionLocalOf { ContextualCommandBarInfo() }
|
||||
|
||||
enum class ActionButtonPosition {
|
||||
None,
|
||||
Start,
|
||||
|
@ -86,228 +85,90 @@ fun ContextualCommandBar(
|
|||
val token = contextualCommandBarToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.ContextualCommandBar] as ContextualCommandBarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalContextualCommandBarTokens provides token,
|
||||
LocalContextualCommandBarInfo provides ContextualCommandBarInfo()
|
||||
) {
|
||||
val groupBorderRadius =
|
||||
getContextualCommandBarTokens().groupBorderRadius(getContextualCommandBarInfo())
|
||||
val itemBorderRadius =
|
||||
getContextualCommandBarTokens().itemBorderRadius(getContextualCommandBarInfo())
|
||||
val contextualCommandBarInfo = ContextualCommandBarInfo()
|
||||
val groupBorderRadius =
|
||||
token.groupBorderRadius(contextualCommandBarInfo)
|
||||
val itemBorderRadius =
|
||||
token.itemBorderRadius(contextualCommandBarInfo)
|
||||
|
||||
val soloItemShape = RoundedCornerShape(groupBorderRadius)
|
||||
val startShape = RoundedCornerShape(
|
||||
topStart = groupBorderRadius,
|
||||
bottomStart = groupBorderRadius,
|
||||
topEnd = itemBorderRadius,
|
||||
bottomEnd = itemBorderRadius
|
||||
)
|
||||
val defaultShape = RoundedCornerShape(itemBorderRadius)
|
||||
val endShape = RoundedCornerShape(
|
||||
topEnd = groupBorderRadius,
|
||||
bottomEnd = groupBorderRadius,
|
||||
topStart = itemBorderRadius,
|
||||
bottomStart = itemBorderRadius
|
||||
)
|
||||
val focusStroke = getContextualCommandBarTokens().focusStroke(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
var focusedBorderModifier: Modifier = Modifier
|
||||
val soloItemShape = RoundedCornerShape(groupBorderRadius)
|
||||
val startShape = RoundedCornerShape(
|
||||
topStart = groupBorderRadius,
|
||||
bottomStart = groupBorderRadius,
|
||||
topEnd = itemBorderRadius,
|
||||
bottomEnd = itemBorderRadius
|
||||
)
|
||||
val defaultShape = RoundedCornerShape(itemBorderRadius)
|
||||
val endShape = RoundedCornerShape(
|
||||
topEnd = groupBorderRadius,
|
||||
bottomEnd = groupBorderRadius,
|
||||
topStart = itemBorderRadius,
|
||||
bottomStart = itemBorderRadius
|
||||
)
|
||||
val focusStroke = token.focusStroke(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
var focusedBorderModifier: Modifier = Modifier
|
||||
|
||||
val selectedString = LocalContext.current.resources.getString(R.string.fluentui_selected)
|
||||
val selectedString = LocalContext.current.resources.getString(R.string.fluentui_selected)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.focusable(enabled = false)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
getContextualCommandBarTokens().contextualCommandBarBackgroundColor(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.focusable(enabled = false)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
token.contextualCommandBarBackgroundColor(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
) {
|
||||
val (KeyboardDismiss, Content) = createRefs()
|
||||
val contentPaddingWithActionButton =
|
||||
-(getContextualCommandBarTokens().actionButtonGradientWidth(
|
||||
getContextualCommandBarInfo()
|
||||
) + getContextualCommandBarTokens().buttonPadding(getContextualCommandBarInfo()))
|
||||
if (scrollable) {
|
||||
LazyRow(modifier = Modifier
|
||||
.focusable(enabled = false)
|
||||
.constrainAs(Content) {
|
||||
when (actionButtonPosition) {
|
||||
ActionButtonPosition.Start -> {
|
||||
start.linkTo(
|
||||
KeyboardDismiss.end,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
ActionButtonPosition.End -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(
|
||||
KeyboardDismiss.start,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
}
|
||||
ActionButtonPosition.None -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
)
|
||||
) {
|
||||
val (KeyboardDismiss, Content) = createRefs()
|
||||
val contentPaddingWithActionButton =
|
||||
-(token.actionButtonGradientWidth(
|
||||
contextualCommandBarInfo
|
||||
) + token.buttonPadding(contextualCommandBarInfo))
|
||||
if (scrollable) {
|
||||
LazyRow(modifier = Modifier
|
||||
.focusable(enabled = false)
|
||||
.constrainAs(Content) {
|
||||
when (actionButtonPosition) {
|
||||
ActionButtonPosition.Start -> {
|
||||
start.linkTo(
|
||||
KeyboardDismiss.end,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.padding(
|
||||
getContextualCommandBarTokens().buttonPadding(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
), state = lazyListState) {
|
||||
|
||||
var itemKey = 0
|
||||
for ((index, commandGroup) in groups.withIndex()) {
|
||||
for ((itemIndex, item) in commandGroup.items.withIndex()) {
|
||||
val key = itemKey.toString()
|
||||
item(key) {
|
||||
val shape = if (commandGroup.items.size == 1) soloItemShape
|
||||
else if (item == commandGroup.items.first()) startShape
|
||||
else if (item == commandGroup.items.last()) endShape
|
||||
else defaultShape
|
||||
|
||||
val interactionSource: MutableInteractionSource =
|
||||
remember { MutableInteractionSource() }
|
||||
val clickableModifier = Modifier.combinedClickable(
|
||||
enabled = item.enabled,
|
||||
onClick = item.onClick,
|
||||
onClickLabel = null,
|
||||
onLongClick = item.onLongClick,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
|
||||
focusedBorderModifier = Modifier
|
||||
for (borderStroke in focusStroke) {
|
||||
focusedBorderModifier =
|
||||
focusedBorderModifier.border(borderStroke, shape)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(
|
||||
0, key.toInt() - 2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.defaultMinSize(
|
||||
minWidth = getContextualCommandBarTokens().buttonMinWidth(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.height(IntrinsicSize.Min)
|
||||
.clip(shape)
|
||||
.background(
|
||||
getContextualCommandBarTokens()
|
||||
.buttonBackgroundColor(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
.getColorByState(
|
||||
enabled = item.enabled,
|
||||
selected = item.selected,
|
||||
interactionSource = interactionSource
|
||||
), shape = shape
|
||||
)
|
||||
.then(clickableModifier)
|
||||
.then(if (interactionSource.collectIsFocusedAsState().value || interactionSource.collectIsHoveredAsState().value) focusedBorderModifier else Modifier)
|
||||
.semantics {
|
||||
contentDescription =
|
||||
item.label +
|
||||
if (item.selected)
|
||||
selectedString
|
||||
else ""
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
CommandItemComposable(item, interactionSource, commandGroup)
|
||||
}
|
||||
if (itemIndex != commandGroup.items.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().buttonSpacing(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
} else if (index != groups.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().groupSpacing(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
itemKey += 1
|
||||
ActionButtonPosition.End -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(
|
||||
KeyboardDismiss.start,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
}
|
||||
ActionButtonPosition.None -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
} else {
|
||||
Row(modifier = Modifier
|
||||
.constrainAs(Content) {
|
||||
when (actionButtonPosition) {
|
||||
ActionButtonPosition.Start -> {
|
||||
start.linkTo(
|
||||
KeyboardDismiss.end,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
ActionButtonPosition.End -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(
|
||||
KeyboardDismiss.start,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
}
|
||||
ActionButtonPosition.None -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
getContextualCommandBarTokens().buttonPadding(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)) {
|
||||
for ((index, commandGroup) in groups.withIndex()) {
|
||||
for ((itemIndex, item) in commandGroup.items.withIndex()) {
|
||||
.padding(
|
||||
token.buttonPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
), state = lazyListState) {
|
||||
|
||||
var itemKey = 0
|
||||
for ((index, commandGroup) in groups.withIndex()) {
|
||||
for ((itemIndex, item) in commandGroup.items.withIndex()) {
|
||||
val key = itemKey.toString()
|
||||
item(key) {
|
||||
val shape = if (commandGroup.items.size == 1) soloItemShape
|
||||
else if (item == commandGroup.items.first()) startShape
|
||||
else if (item == commandGroup.items.last()) endShape
|
||||
|
@ -330,21 +191,35 @@ fun ContextualCommandBar(
|
|||
focusedBorderModifier =
|
||||
focusedBorderModifier.border(borderStroke, shape)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.weight(commandGroup.weight),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
modifier = Modifier.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(
|
||||
0, key.toInt() - 2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.defaultMinSize(
|
||||
minWidth = token.buttonMinWidth(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(
|
||||
getContextualCommandBarTokens()
|
||||
token
|
||||
.buttonBackgroundColor(
|
||||
getContextualCommandBarInfo()
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
.getColorByState(
|
||||
enabled = item.enabled,
|
||||
|
@ -356,124 +231,252 @@ fun ContextualCommandBar(
|
|||
.then(if (interactionSource.collectIsFocusedAsState().value || interactionSource.collectIsHoveredAsState().value) focusedBorderModifier else Modifier)
|
||||
.semantics {
|
||||
contentDescription =
|
||||
item.label + if (item.selected) selectedString else ""
|
||||
item.label +
|
||||
if (item.selected)
|
||||
selectedString
|
||||
else ""
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
CommandItemComposable(
|
||||
item = item,
|
||||
interactionSource = interactionSource,
|
||||
commandGroup = commandGroup
|
||||
token,
|
||||
contextualCommandBarInfo,
|
||||
item,
|
||||
interactionSource,
|
||||
commandGroup
|
||||
)
|
||||
}
|
||||
if (itemIndex != commandGroup.items.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
token.buttonSpacing(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
} else if (index != groups.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
token.groupSpacing(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if (itemIndex != commandGroup.items.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().buttonSpacing(
|
||||
getContextualCommandBarInfo()
|
||||
}
|
||||
itemKey += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Row(modifier = Modifier
|
||||
.constrainAs(Content) {
|
||||
when (actionButtonPosition) {
|
||||
ActionButtonPosition.Start -> {
|
||||
start.linkTo(
|
||||
KeyboardDismiss.end,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
ActionButtonPosition.End -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(
|
||||
KeyboardDismiss.start,
|
||||
margin = contentPaddingWithActionButton
|
||||
)
|
||||
}
|
||||
ActionButtonPosition.None -> {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
token.buttonPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)) {
|
||||
for ((index, commandGroup) in groups.withIndex()) {
|
||||
for ((itemIndex, item) in commandGroup.items.withIndex()) {
|
||||
val shape = if (commandGroup.items.size == 1) soloItemShape
|
||||
else if (item == commandGroup.items.first()) startShape
|
||||
else if (item == commandGroup.items.last()) endShape
|
||||
else defaultShape
|
||||
|
||||
val interactionSource: MutableInteractionSource =
|
||||
remember { MutableInteractionSource() }
|
||||
val clickableModifier = Modifier.combinedClickable(
|
||||
enabled = item.enabled,
|
||||
onClick = item.onClick,
|
||||
onClickLabel = null,
|
||||
onLongClick = item.onLongClick,
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
|
||||
focusedBorderModifier = Modifier
|
||||
for (borderStroke in focusStroke) {
|
||||
focusedBorderModifier =
|
||||
focusedBorderModifier.border(borderStroke, shape)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.weight(commandGroup.weight),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth()
|
||||
.clip(shape)
|
||||
.background(
|
||||
token
|
||||
.buttonBackgroundColor(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
} else if (index != groups.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().groupSpacing(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.background(Color.Transparent)
|
||||
.getColorByState(
|
||||
enabled = item.enabled,
|
||||
selected = item.selected,
|
||||
interactionSource = interactionSource
|
||||
), shape = shape
|
||||
)
|
||||
.then(clickableModifier)
|
||||
.then(if (interactionSource.collectIsFocusedAsState().value || interactionSource.collectIsHoveredAsState().value) focusedBorderModifier else Modifier)
|
||||
.semantics {
|
||||
contentDescription =
|
||||
item.label + if (item.selected) selectedString else ""
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
CommandItemComposable(
|
||||
token,
|
||||
contextualCommandBarInfo,
|
||||
item = item,
|
||||
interactionSource = interactionSource,
|
||||
commandGroup = commandGroup
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
if (itemIndex != commandGroup.items.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
token.buttonSpacing(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
} else if (index != groups.size - 1) {
|
||||
Spacer(
|
||||
Modifier
|
||||
.requiredWidth(
|
||||
token.groupSpacing(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.background(Color.Transparent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (actionButtonPosition != ActionButtonPosition.None) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val keyboardDismiss: (() -> Unit) = { keyboardController?.hide() }
|
||||
val actionButtonClickable = Modifier.clickable(enabled = true,
|
||||
onClick = actionButtonIcon.onClick ?: keyboardDismiss,
|
||||
role = Role.Button,
|
||||
onClickLabel = actionButtonIcon.contentDescription,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() })
|
||||
}
|
||||
if (actionButtonPosition != ActionButtonPosition.None) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val keyboardDismiss: (() -> Unit) = { keyboardController?.hide() }
|
||||
val actionButtonClickable = Modifier.clickable(enabled = true,
|
||||
onClick = actionButtonIcon.onClick ?: keyboardDismiss,
|
||||
role = Role.Button,
|
||||
onClickLabel = actionButtonIcon.contentDescription,
|
||||
indication = rememberRipple(),
|
||||
interactionSource = remember { MutableInteractionSource() })
|
||||
|
||||
val isRtl: Boolean = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val isRtl: Boolean = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.constrainAs(KeyboardDismiss) {
|
||||
if (actionButtonPosition == ActionButtonPosition.Start) {
|
||||
start.linkTo(parent.start)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (actionButtonPosition == ActionButtonPosition.End)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().actionButtonGradientWidth(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
getContextualCommandBarTokens().actionButtonGradient(
|
||||
getContextualCommandBarInfo()
|
||||
),
|
||||
startX = if (!isRtl) 0.0F else Float.POSITIVE_INFINITY,
|
||||
endX = if (!isRtl) Float.POSITIVE_INFINITY else 0.0F
|
||||
)
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
actionButtonIcon,
|
||||
Row(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.constrainAs(KeyboardDismiss) {
|
||||
if (actionButtonPosition == ActionButtonPosition.Start) {
|
||||
start.linkTo(parent.start)
|
||||
} else {
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (actionButtonPosition == ActionButtonPosition.End)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.then(actionButtonClickable)
|
||||
.background(
|
||||
getContextualCommandBarTokens().actionButtonBackgroundColor(
|
||||
getContextualCommandBarInfo()
|
||||
.requiredWidth(
|
||||
token.actionButtonGradientWidth(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.padding(
|
||||
getContextualCommandBarTokens().actionButtonIconPadding(
|
||||
getContextualCommandBarInfo()
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
token.actionButtonGradient(
|
||||
contextualCommandBarInfo
|
||||
),
|
||||
startX = if (!isRtl) 0.0F else Float.POSITIVE_INFINITY,
|
||||
endX = if (!isRtl) Float.POSITIVE_INFINITY else 0.0F
|
||||
)
|
||||
),
|
||||
tint = getContextualCommandBarTokens().actionButtonIconColor(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
)
|
||||
if (actionButtonPosition == ActionButtonPosition.Start)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.requiredWidth(
|
||||
getContextualCommandBarTokens().actionButtonGradientWidth(
|
||||
getContextualCommandBarInfo()
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
getContextualCommandBarTokens().actionButtonGradient(
|
||||
getContextualCommandBarInfo()
|
||||
),
|
||||
startX = if (!isRtl) Float.POSITIVE_INFINITY else 0F,
|
||||
endX = if (!isRtl) 0F else Float.POSITIVE_INFINITY
|
||||
)
|
||||
)
|
||||
Icon(
|
||||
actionButtonIcon,
|
||||
modifier = Modifier
|
||||
.then(actionButtonClickable)
|
||||
.background(
|
||||
token.actionButtonBackgroundColor(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
.padding(
|
||||
token.actionButtonIconPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
),
|
||||
tint = token.actionButtonIconColor(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
if (actionButtonPosition == ActionButtonPosition.Start)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.requiredWidth(
|
||||
token.actionButtonGradientWidth(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.background(
|
||||
Brush.horizontalGradient(
|
||||
token.actionButtonGradient(
|
||||
contextualCommandBarInfo
|
||||
),
|
||||
startX = if (!isRtl) Float.POSITIVE_INFINITY else 0F,
|
||||
endX = if (!isRtl) 0F else Float.POSITIVE_INFINITY
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -481,60 +484,64 @@ fun ContextualCommandBar(
|
|||
|
||||
@Composable
|
||||
private fun CommandItemComposable(
|
||||
item: CommandItem, interactionSource: MutableInteractionSource, commandGroup: CommandGroup
|
||||
token: ContextualCommandBarTokens,
|
||||
contextualCommandBarInfo: ContextualCommandBarInfo,
|
||||
item: CommandItem,
|
||||
interactionSource: MutableInteractionSource,
|
||||
commandGroup: CommandGroup
|
||||
) {
|
||||
val foregroundColor = getContextualCommandBarTokens().iconColor(
|
||||
getContextualCommandBarInfo()
|
||||
val foregroundColor = token.iconColor(
|
||||
contextualCommandBarInfo
|
||||
).getColorByState(
|
||||
enabled = item.enabled, selected = item.selected, interactionSource = interactionSource
|
||||
)
|
||||
|
||||
val contentPadding: PaddingValues = if (commandGroup.items.size == 1) {
|
||||
PaddingValues(
|
||||
top = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), bottom = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), start = getContextualCommandBarTokens().groupIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), end = getContextualCommandBarTokens().groupIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
top = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), bottom = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), start = token.groupIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
), end = token.groupIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
} else if (item == commandGroup.items.first()) {
|
||||
PaddingValues(
|
||||
top = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), bottom = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), start = getContextualCommandBarTokens().groupIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), end = getContextualCommandBarTokens().itemIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
top = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), bottom = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), start = token.groupIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
), end = token.itemIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
} else if (item == commandGroup.items.last()) {
|
||||
PaddingValues(
|
||||
top = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), bottom = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), start = getContextualCommandBarTokens().itemIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), end = getContextualCommandBarTokens().groupIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
top = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), bottom = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), start = token.itemIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
), end = token.groupIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
} else {
|
||||
PaddingValues(
|
||||
top = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), bottom = getContextualCommandBarTokens().iconVerticalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), start = getContextualCommandBarTokens().itemIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
), end = getContextualCommandBarTokens().itemIconHorizontalPadding(
|
||||
getContextualCommandBarInfo()
|
||||
top = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), bottom = token.iconVerticalPadding(
|
||||
contextualCommandBarInfo
|
||||
), start = token.itemIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
), end = token.itemIconHorizontalPadding(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -544,21 +551,21 @@ private fun CommandItemComposable(
|
|||
Modifier
|
||||
.padding(contentPadding)
|
||||
.requiredSize(
|
||||
getContextualCommandBarTokens().iconSize(
|
||||
getContextualCommandBarInfo()
|
||||
token.iconSize(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
), tint = foregroundColor
|
||||
)
|
||||
} else {
|
||||
val fontTypography = getContextualCommandBarTokens().typography(
|
||||
getContextualCommandBarInfo()
|
||||
val fontTypography = token.typography(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.requiredHeight(
|
||||
getContextualCommandBarTokens().iconSize(
|
||||
getContextualCommandBarInfo()
|
||||
token.iconSize(
|
||||
contextualCommandBarInfo
|
||||
)
|
||||
), contentAlignment = Alignment.Center
|
||||
) {
|
||||
|
@ -574,16 +581,6 @@ private fun CommandItemComposable(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getContextualCommandBarTokens(): ContextualCommandBarTokens {
|
||||
return LocalContextualCommandBarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getContextualCommandBarInfo(): ContextualCommandBarInfo {
|
||||
return LocalContextualCommandBarInfo.current
|
||||
}
|
||||
|
||||
data class CommandGroup(
|
||||
val groupName: String,
|
||||
val items: List<CommandItem>,
|
||||
|
|
|
@ -11,8 +11,6 @@ import androidx.compose.material.Icon
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -32,9 +30,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.ButtonSize
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.ButtonStyle
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.ButtonTokens
|
||||
|
||||
val LocalButtonTokens = compositionLocalOf { ButtonTokens() }
|
||||
val LocalButtonInfo = compositionLocalOf { ButtonInfo() }
|
||||
|
||||
/**
|
||||
* API to create a button, containing icon as well as text.
|
||||
*
|
||||
|
@ -64,112 +59,97 @@ fun Button(
|
|||
buttonTokens: ButtonTokens? = null
|
||||
) {
|
||||
val token = buttonTokens ?: FluentTheme.controlTokens.tokens[ControlType.Button] as ButtonTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalButtonTokens provides token,
|
||||
LocalButtonInfo provides ButtonInfo(style, size)
|
||||
) {
|
||||
val clickAndSemanticsModifier = Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(),
|
||||
val buttonInfo = ButtonInfo(style, size)
|
||||
val clickAndSemanticsModifier = Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(buttonInfo = buttonInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val backgroundColor =
|
||||
getButtonToken().backgroundColor(buttonInfo = getButtonInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
val contentPadding = token.padding(buttonInfo)
|
||||
val iconSpacing = token.spacing(buttonInfo)
|
||||
val shape = RoundedCornerShape(token.cornerRadius(buttonInfo))
|
||||
val borders: List<BorderStroke> =
|
||||
token.borderStroke(buttonInfo = buttonInfo).getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.height(token.fixedHeight(buttonInfo))
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
val contentPadding = getButtonToken().padding(getButtonInfo())
|
||||
val iconSpacing = getButtonToken().spacing(getButtonInfo())
|
||||
val shape = RoundedCornerShape(getButtonToken().cornerRadius(getButtonInfo()))
|
||||
val borders: List<BorderStroke> =
|
||||
getButtonToken().borderStroke(buttonInfo = getButtonInfo()).getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.height(getButtonToken().fixedHeight(getButtonInfo()))
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.semantics(true) {
|
||||
editableText = AnnotatedString(text ?: "")
|
||||
this.contentDescription = contentDescription ?: ""
|
||||
}
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(borderModifier),
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
iconSpacing,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (icon != null)
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
getButtonToken().iconSize(buttonInfo = getButtonInfo())
|
||||
),
|
||||
tint = getButtonToken().iconColor(buttonInfo = getButtonInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
)
|
||||
|
||||
if (text != null)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
color = getButtonToken().textColor(buttonInfo = getButtonInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
style = getButtonToken().typography(getButtonInfo()).merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
.clip(shape)
|
||||
.semantics(true) {
|
||||
editableText = AnnotatedString(text ?: "")
|
||||
this.contentDescription = contentDescription ?: ""
|
||||
}
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(borderModifier),
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
iconSpacing,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (icon != null)
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
token.iconSize(buttonInfo = buttonInfo)
|
||||
),
|
||||
tint = token.iconColor(buttonInfo = buttonInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
)
|
||||
|
||||
if (text != null)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
color = token.textColor(buttonInfo = buttonInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
style = token.typography(buttonInfo).merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getButtonToken(): ButtonTokens {
|
||||
return LocalButtonTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getButtonInfo(): ButtonInfo {
|
||||
return LocalButtonInfo.current
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -32,9 +30,6 @@ import com.microsoft.fluentui.theme.token.ControlTokens.ControlType
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.CheckBoxInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.CheckBoxTokens
|
||||
|
||||
val LocalCheckBoxTokens = compositionLocalOf { CheckBoxTokens() }
|
||||
val LocalCheckBoxInfo = compositionLocalOf { CheckBoxInfo() }
|
||||
|
||||
/**
|
||||
* API to create a checkbox. A checkbox is a type of button that lets the user choose between two opposite states,
|
||||
* actions, or values. A selected checkbox is considered on when it contains a checkmark and off when
|
||||
|
@ -59,86 +54,70 @@ fun CheckBox(
|
|||
|
||||
val token = checkBoxToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.CheckBox] as CheckBoxTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalCheckBoxTokens provides token,
|
||||
LocalCheckBoxInfo provides CheckBoxInfo(checked)
|
||||
) {
|
||||
val toggleModifier =
|
||||
modifier.triStateToggleable(
|
||||
state = ToggleableState(checked),
|
||||
enabled = enabled,
|
||||
onClick = { onCheckedChanged(!checked) },
|
||||
role = Role.Checkbox,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 24.dp
|
||||
)
|
||||
val checkBoxInfo = CheckBoxInfo(checked)
|
||||
val toggleModifier =
|
||||
modifier.triStateToggleable(
|
||||
state = ToggleableState(checked),
|
||||
enabled = enabled,
|
||||
onClick = { onCheckedChanged(!checked) },
|
||||
role = Role.Checkbox,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 24.dp
|
||||
)
|
||||
)
|
||||
|
||||
val backgroundColor: Color =
|
||||
getCheckBoxToken().backgroundColor(checkBoxInfo = getCheckBoxInfo()).getColorByState(
|
||||
val backgroundColor: Color =
|
||||
token.backgroundColor(checkBoxInfo = checkBoxInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = checked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val iconColor: Color =
|
||||
token.iconColor(checkBoxInfo = checkBoxInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = checked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val shape: Shape = RoundedCornerShape(token.fixedBorderRadius)
|
||||
|
||||
val borders: List<BorderStroke> =
|
||||
token.borderStroke(checkBoxInfo = checkBoxInfo)
|
||||
.getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = checked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val iconColor: Color =
|
||||
getCheckBoxToken().iconColor(checkBoxInfo = getCheckBoxInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = checked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val shape: Shape = RoundedCornerShape(getCheckBoxToken().fixedBorderRadius)
|
||||
|
||||
val borders: List<BorderStroke> =
|
||||
getCheckBoxToken().borderStroke(checkBoxInfo = getCheckBoxInfo())
|
||||
.getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = checked,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.indication(interactionSource, null),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(getCheckBoxToken().fixedSize)
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.then(borderModifier)
|
||||
.then(toggleModifier)
|
||||
)
|
||||
AnimatedVisibility(checked, enter = fadeIn(), exit = fadeOut()) {
|
||||
Icon(
|
||||
Icons.Filled.Done,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(getCheckBoxToken().fixedIconSize)
|
||||
.focusable(false)
|
||||
.clearAndSetSemantics {},
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getCheckBoxToken(): CheckBoxTokens {
|
||||
return LocalCheckBoxTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getCheckBoxInfo(): CheckBoxInfo {
|
||||
return LocalCheckBoxInfo.current
|
||||
Box(
|
||||
modifier = Modifier.indication(interactionSource, null),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(token.fixedSize)
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.then(borderModifier)
|
||||
.then(toggleModifier)
|
||||
)
|
||||
AnimatedVisibility(checked, enter = fadeIn(), exit = fadeOut()) {
|
||||
Icon(
|
||||
Icons.Filled.Done,
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(token.fixedIconSize)
|
||||
.focusable(false)
|
||||
.clearAndSetSemantics {},
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -29,9 +27,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.FABSize
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.FABState
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.FABTokens
|
||||
|
||||
val LocalFABTokens = compositionLocalOf { FABTokens() }
|
||||
val LocalFABInfo = compositionLocalOf { FABInfo() }
|
||||
|
||||
/**
|
||||
* API to create a Floating Action Button. This button has elevation and can be expanded and collapsed.
|
||||
* In expanded state, Icon + Text are displayed. In collapsed state, only icon is displayed.
|
||||
|
@ -64,118 +59,103 @@ fun FloatingActionButton(
|
|||
val token = fabTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.FloatingActionButton] as FABTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalFABTokens provides token,
|
||||
LocalFABInfo provides FABInfo(state, size)
|
||||
) {
|
||||
val clickAndSemanticsModifier = Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = LocalIndication.current,
|
||||
enabled = enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
)
|
||||
val isFabExpanded: Boolean =
|
||||
(text != null && text != "" && getFABInfo().state == FABState.Expanded)
|
||||
val backgroundColor = getFABToken().backgroundColor(fabInfo = getFABInfo()).getColorByState(
|
||||
val fabInfo = FABInfo(state, size)
|
||||
val clickAndSemanticsModifier = Modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = LocalIndication.current,
|
||||
enabled = enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = onClick
|
||||
)
|
||||
val isFabExpanded: Boolean =
|
||||
(text != null && text != "" && fabInfo.state == FABState.Expanded)
|
||||
val backgroundColor = token.backgroundColor(fabInfo = fabInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val contentPadding = if (isFabExpanded) token.textPadding(fabInfo)
|
||||
else token.iconPadding(fabInfo)
|
||||
val iconSpacing = if (isFabExpanded) token.spacing(fabInfo) else 0.dp
|
||||
val shape = CircleShape
|
||||
val borders: List<BorderStroke> =
|
||||
token.borderStroke(fabInfo = fabInfo).getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val contentPadding = if (isFabExpanded) getFABToken().textPadding(getFABInfo())
|
||||
else getFABToken().iconPadding(getFABInfo())
|
||||
val iconSpacing = if (isFabExpanded) getFABToken().spacing(getFABInfo()) else 0.dp
|
||||
val shape = CircleShape
|
||||
val borders: List<BorderStroke> =
|
||||
getFABToken().borderStroke(fabInfo = getFABInfo()).getBorderStrokeByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.height(token.fixedHeight(fabInfo))
|
||||
.defaultMinSize(minWidth = token.minWidth(fabInfo))
|
||||
.shadow(
|
||||
elevation = token
|
||||
.elevation(fabInfo = fabInfo)
|
||||
.getElevationByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
|
||||
var borderModifier: Modifier = Modifier
|
||||
var borderWidth = 0.dp
|
||||
for (border in borders) {
|
||||
borderWidth += border.width
|
||||
borderModifier = borderModifier.border(borderWidth, border.brush, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.height(getFABToken().fixedHeight(getFABInfo()))
|
||||
.defaultMinSize(minWidth = getFABToken().minWidth(getFABInfo()))
|
||||
.shadow(
|
||||
elevation = getFABToken()
|
||||
.elevation(fabInfo = getFABInfo())
|
||||
.getElevationByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
shape = CircleShape
|
||||
)
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.semantics(mergeDescendants = true) { contentDescription = text ?: "" }
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(borderModifier),
|
||||
propagateMinConstraints = true
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.semantics(mergeDescendants = true) { contentDescription = text ?: "" }
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(borderModifier),
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
iconSpacing,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
iconSpacing,
|
||||
Alignment.CenterHorizontally
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (icon != null)
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
getFABToken().iconSize(getFABInfo())
|
||||
)
|
||||
.clearAndSetSemantics { },
|
||||
tint = getFABToken().iconColor(fabInfo = getFABInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
if (icon != null)
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
token.iconSize(fabInfo)
|
||||
)
|
||||
.clearAndSetSemantics { },
|
||||
tint = token.iconColor(fabInfo = fabInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
)
|
||||
|
||||
AnimatedVisibility(isFabExpanded) {
|
||||
Text(
|
||||
text = text!!,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
style = getFABToken().typography(getFABInfo()),
|
||||
color = getFABToken().textColor(fabInfo = getFABInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(isFabExpanded) {
|
||||
Text(
|
||||
text = text!!,
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
style = token.typography(fabInfo),
|
||||
color = token.textColor(fabInfo = fabInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = false,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun getFABToken(): FABTokens {
|
||||
return LocalFABTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getFABInfo(): FABInfo {
|
||||
return LocalFABInfo.current
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.wrapContentSize
|
|||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -27,9 +25,6 @@ import com.microsoft.fluentui.theme.token.ControlTokens
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.RadioButtonInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.RadioButtonTokens
|
||||
|
||||
val LocalRadioButtonTokens = compositionLocalOf { RadioButtonTokens() }
|
||||
val LocalRadioButtonInfo = compositionLocalOf { RadioButtonInfo() }
|
||||
|
||||
/**
|
||||
* API to create a Radio Button. A Radio selection lets user choose one option out of all values.
|
||||
* This API provides a single instance of radio button and not a group.
|
||||
|
@ -52,73 +47,57 @@ fun RadioButton(
|
|||
) {
|
||||
val token = radioButtonToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.RadioButton] as RadioButtonTokens
|
||||
val radioButtonInfo = RadioButtonInfo(selected)
|
||||
val dotRadius = animateDpAsState(
|
||||
targetValue = if (selected) token.innerCircleRadius else 0.dp,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
)
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalRadioButtonTokens provides token,
|
||||
LocalRadioButtonInfo provides RadioButtonInfo(selected)
|
||||
) {
|
||||
val dotRadius = animateDpAsState(
|
||||
targetValue = if (selected) getRadioButtonTokens().innerCircleRadius else 0.dp,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
val selectableModifier = modifier.selectable(
|
||||
selected = selected,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.RadioButton,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 24.dp
|
||||
)
|
||||
)
|
||||
|
||||
val selectableModifier = modifier.selectable(
|
||||
selected = selected,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.RadioButton,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(
|
||||
bounded = false,
|
||||
radius = 24.dp
|
||||
)
|
||||
)
|
||||
|
||||
val outerStrokeColor =
|
||||
getRadioButtonTokens().backgroundColor(radioButtonInfo = getRadioButtonInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val innerColor = getRadioButtonTokens().iconColor(radioButtonInfo = getRadioButtonInfo())
|
||||
val outerStrokeColor =
|
||||
token.backgroundColor(radioButtonInfo = radioButtonInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val innerColor = token.iconColor(radioButtonInfo = radioButtonInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
val outerRadius = getRadioButtonTokens().outerCircleRadius
|
||||
val strokeWidth = getRadioButtonTokens().strokeWidthInwards
|
||||
val outerRadius = token.outerCircleRadius
|
||||
val strokeWidth = token.strokeWidthInwards
|
||||
|
||||
val contentDesc = LocalContext.current.resources.getString(R.string.fluentui_radio_button)
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.then(selectableModifier)
|
||||
.size(24.dp)
|
||||
.wrapContentSize(Alignment.Center)
|
||||
.semantics { contentDescription = contentDesc }
|
||||
) {
|
||||
drawCircle(
|
||||
outerStrokeColor,
|
||||
(outerRadius - (strokeWidth / 2)).toPx(),
|
||||
style = Stroke(1.5.dp.toPx())
|
||||
)
|
||||
val contentDesc = LocalContext.current.resources.getString(R.string.fluentui_radio_button)
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.then(selectableModifier)
|
||||
.size(24.dp)
|
||||
.wrapContentSize(Alignment.Center)
|
||||
.semantics { contentDescription = contentDesc }
|
||||
) {
|
||||
drawCircle(
|
||||
outerStrokeColor,
|
||||
(outerRadius - (strokeWidth / 2)).toPx(),
|
||||
style = Stroke(1.5.dp.toPx())
|
||||
)
|
||||
|
||||
if (dotRadius.value > 0.dp) {
|
||||
drawCircle(innerColor, (dotRadius.value).toPx(), style = Fill)
|
||||
}
|
||||
if (dotRadius.value > 0.dp) {
|
||||
drawCircle(innerColor, (dotRadius.value).toPx(), style = Fill)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun getRadioButtonTokens(): RadioButtonTokens {
|
||||
return LocalRadioButtonTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getRadioButtonInfo(): RadioButtonInfo {
|
||||
return LocalRadioButtonInfo.current
|
||||
}
|
||||
|
|
|
@ -35,9 +35,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.ToggleSwitchInfo
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.ToggleSwitchTokens
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
val LocalToggleSwitchTokens = compositionLocalOf { ToggleSwitchTokens() }
|
||||
val LocalToggleSwitchInfo = compositionLocalOf { ToggleSwitchInfo() }
|
||||
|
||||
/**
|
||||
* API to create a Toggle Switch. This switch toggles state on tap and swipe gestures.
|
||||
* The two states of toggle are mutually exclusive.
|
||||
|
@ -62,128 +59,112 @@ fun ToggleSwitch(
|
|||
|
||||
val token = switchTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.ToggleSwitch] as ToggleSwitchTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalToggleSwitchTokens provides token,
|
||||
LocalToggleSwitchInfo provides ToggleSwitchInfo(checkedState)
|
||||
) {
|
||||
|
||||
val backgroundColor: Color = animateColorAsState(
|
||||
targetValue =
|
||||
getToggleSwitchToken().trackColor(switchInfo = getToggleSwitchInfo()).getColorByState(
|
||||
enabled = enabledSwitch,
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
animationSpec = tween(500)
|
||||
).value
|
||||
val foregroundColor: Color =
|
||||
getToggleSwitchToken().knobColor(switchInfo = getToggleSwitchInfo()).getColorByState(
|
||||
enabled = enabledSwitch,
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val elevation: Dp =
|
||||
getToggleSwitchToken().elevation(switchInfo = getToggleSwitchInfo())
|
||||
.getElevationByState(
|
||||
enabled = enabledSwitch,
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val padding: Dp = animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
getToggleSwitchToken().pressedPaddingTrack
|
||||
else
|
||||
getToggleSwitchToken().restPaddingTrack
|
||||
).value
|
||||
val knobMovementWidth: Dp = animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
22.dp
|
||||
else
|
||||
23.dp
|
||||
).value
|
||||
|
||||
// Swipe Logic
|
||||
val minBound = with(LocalDensity.current) { padding.toPx() }
|
||||
val maxBound = with(LocalDensity.current) { knobMovementWidth.toPx() }
|
||||
val animationSpec = TweenSpec<Float>(durationMillis = 300)
|
||||
val swipeState =
|
||||
rememberSwipeableState(checkedState, animationSpec, confirmStateChange = { true })
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
|
||||
val forceAnimationCheck = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(checkedState, forceAnimationCheck.value) {
|
||||
if (checkedState != swipeState.currentValue) {
|
||||
swipeState.animateTo(checkedState)
|
||||
}
|
||||
}
|
||||
DisposableEffect(swipeState.currentValue) {
|
||||
if (checkedState != swipeState.currentValue) {
|
||||
onValueChange.invoke(swipeState.currentValue)
|
||||
forceAnimationCheck.value = !forceAnimationCheck.value
|
||||
}
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
// Toggle Logic
|
||||
val toggleModifier = modifier.toggleable(
|
||||
value = getToggleSwitchInfo().checked,
|
||||
val toggleSwitchInfo = ToggleSwitchInfo(checkedState)
|
||||
val backgroundColor: Color = animateColorAsState(
|
||||
targetValue =
|
||||
token.trackColor(switchInfo = toggleSwitchInfo).getColorByState(
|
||||
enabled = enabledSwitch,
|
||||
role = Role.Switch,
|
||||
onValueChange = onValueChange,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
animationSpec = tween(500)
|
||||
).value
|
||||
val foregroundColor: Color =
|
||||
token.knobColor(switchInfo = toggleSwitchInfo).getColorByState(
|
||||
enabled = enabledSwitch,
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val elevation: Dp =
|
||||
token.elevation(switchInfo = toggleSwitchInfo)
|
||||
.getElevationByState(
|
||||
enabled = enabledSwitch,
|
||||
selected = checkedState,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val padding: Dp = animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
token.pressedPaddingTrack
|
||||
else
|
||||
token.restPaddingTrack
|
||||
).value
|
||||
val knobMovementWidth: Dp = animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
22.dp
|
||||
else
|
||||
23.dp
|
||||
).value
|
||||
|
||||
// UI Implementation
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(toggleModifier)
|
||||
.swipeable(
|
||||
state = swipeState,
|
||||
anchors = mapOf(minBound to false, maxBound to true),
|
||||
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = enabledSwitch,
|
||||
reverseDirection = isRtl,
|
||||
interactionSource = interactionSource,
|
||||
resistance = null
|
||||
), contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(getToggleSwitchToken().fixedTrackWidth)
|
||||
.height(getToggleSwitchToken().fixedTrackHeight)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
)
|
||||
Spacer(modifier = Modifier
|
||||
.offset { IntOffset(swipeState.offset.value.roundToInt(), 0) }
|
||||
.shadow(elevation, CircleShape)
|
||||
.size(
|
||||
animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
getToggleSwitchToken().pressedKnobDiameter
|
||||
else
|
||||
getToggleSwitchToken().restKnobDiameter
|
||||
).value
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(foregroundColor)
|
||||
)
|
||||
// Swipe Logic
|
||||
val minBound = with(LocalDensity.current) { padding.toPx() }
|
||||
val maxBound = with(LocalDensity.current) { knobMovementWidth.toPx() }
|
||||
val animationSpec = TweenSpec<Float>(durationMillis = 300)
|
||||
val swipeState =
|
||||
rememberSwipeableState(checkedState, animationSpec, confirmStateChange = { true })
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
|
||||
val forceAnimationCheck = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(checkedState, forceAnimationCheck.value) {
|
||||
if (checkedState != swipeState.currentValue) {
|
||||
swipeState.animateTo(checkedState)
|
||||
}
|
||||
}
|
||||
}
|
||||
DisposableEffect(swipeState.currentValue) {
|
||||
if (checkedState != swipeState.currentValue) {
|
||||
onValueChange.invoke(swipeState.currentValue)
|
||||
forceAnimationCheck.value = !forceAnimationCheck.value
|
||||
}
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getToggleSwitchToken(): ToggleSwitchTokens {
|
||||
return LocalToggleSwitchTokens.current
|
||||
}
|
||||
// Toggle Logic
|
||||
val toggleModifier = modifier.toggleable(
|
||||
value = toggleSwitchInfo.checked,
|
||||
enabled = enabledSwitch,
|
||||
role = Role.Switch,
|
||||
onValueChange = onValueChange,
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun getToggleSwitchInfo(): ToggleSwitchInfo {
|
||||
return LocalToggleSwitchInfo.current
|
||||
// UI Implementation
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(toggleModifier)
|
||||
.swipeable(
|
||||
state = swipeState,
|
||||
anchors = mapOf(minBound to false, maxBound to true),
|
||||
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = enabledSwitch,
|
||||
reverseDirection = isRtl,
|
||||
interactionSource = interactionSource,
|
||||
resistance = null
|
||||
), contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(token.fixedTrackWidth)
|
||||
.height(token.fixedTrackHeight)
|
||||
.clip(CircleShape)
|
||||
.background(backgroundColor)
|
||||
)
|
||||
Spacer(modifier = Modifier
|
||||
.offset { IntOffset(swipeState.offset.value.roundToInt(), 0) }
|
||||
.shadow(elevation, CircleShape)
|
||||
.size(
|
||||
animateDpAsState(
|
||||
targetValue =
|
||||
if (interactionSource.collectIsPressedAsState().value)
|
||||
token.pressedKnobDiameter
|
||||
else
|
||||
token.restKnobDiameter
|
||||
).value
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(foregroundColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.microsoft.fluentui.theme.FluentTheme
|
||||
import com.microsoft.fluentui.theme.token.AliasTokens
|
||||
import com.microsoft.fluentui.theme.token.ControlInfo
|
||||
import com.microsoft.fluentui.theme.token.ControlToken
|
||||
import com.microsoft.fluentui.theme.token.GlobalTokens
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
@ -14,23 +15,25 @@ enum class ShimmerShape {
|
|||
Box,
|
||||
Circle
|
||||
}
|
||||
|
||||
data class ShimmerInfo(
|
||||
val shape: ShimmerShape = ShimmerShape.Box
|
||||
):ControlInfo
|
||||
@Parcelize
|
||||
open class ShimmerTokens : ControlToken, Parcelable {
|
||||
@Composable
|
||||
open fun cornerRadius(): Dp {
|
||||
open fun cornerRadius(shimmerInfo: ShimmerInfo): Dp {
|
||||
return GlobalTokens.cornerRadius(GlobalTokens.CornerRadiusTokens.CornerRadius40)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun knockoutEffectColor(): Color {
|
||||
open fun knockoutEffectColor(shimmerInfo: ShimmerInfo): Color {
|
||||
return FluentTheme.aliasTokens.neutralBackgroundColor[AliasTokens.NeutralBackgroundColorTokens.Stencil2].value(
|
||||
themeMode = FluentTheme.themeMode
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun color(): Color {
|
||||
open fun color(shimmerInfo: ShimmerInfo): Color {
|
||||
return FluentTheme.aliasTokens.neutralBackgroundColor[AliasTokens.NeutralBackgroundColorTokens.Stencil1].value(
|
||||
themeMode = FluentTheme.themeMode
|
||||
)
|
||||
|
|
|
@ -182,19 +182,6 @@ private const val BOTTOMSHEET_SCRIM_TAG = "BottomSheet Scrim"
|
|||
|
||||
private const val BottomSheetOpenFraction = 0.5f
|
||||
|
||||
internal val LocalBottomSheetTokens = compositionLocalOf { BottomSheetTokens() }
|
||||
internal val LocalBottomSheetInfo = compositionLocalOf { BottomSheetInfo() }
|
||||
|
||||
@Composable
|
||||
private fun getDrawerTokens(): BottomSheetTokens {
|
||||
return LocalBottomSheetTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBottomSheetInfo(): BottomSheetInfo {
|
||||
return LocalBottomSheetInfo.current
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Bottom sheets present a set of choices while blocking interaction with the rest of the
|
||||
|
@ -238,197 +225,192 @@ fun BottomSheet(
|
|||
val tokens = bottomSheetTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.BottomSheet] as BottomSheetTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalBottomSheetTokens provides tokens,
|
||||
LocalBottomSheetInfo provides BottomSheetInfo()
|
||||
) {
|
||||
val bottomSheetInfo = BottomSheetInfo()
|
||||
val sheetShape: Shape = RoundedCornerShape(
|
||||
topStart = tokens.cornerRadius(bottomSheetInfo),
|
||||
topEnd = tokens.cornerRadius(bottomSheetInfo)
|
||||
)
|
||||
val sheetElevation: Dp = tokens.elevation(bottomSheetInfo)
|
||||
val sheetBackgroundColor: Color = tokens.backgroundColor(bottomSheetInfo)
|
||||
val sheetContentColor: Color = Color.Transparent
|
||||
val sheetHandleColor: Color = tokens.handleColor(bottomSheetInfo)
|
||||
val scrimOpacity: Float = tokens.scrimOpacity(bottomSheetInfo)
|
||||
val scrimColor: Color =
|
||||
tokens.scrimColor(bottomSheetInfo).copy(alpha = scrimOpacity)
|
||||
|
||||
val sheetShape: Shape = RoundedCornerShape(
|
||||
topStart = getDrawerTokens().cornerRadius(getBottomSheetInfo()),
|
||||
topEnd = getDrawerTokens().cornerRadius(getBottomSheetInfo())
|
||||
)
|
||||
val sheetElevation: Dp = getDrawerTokens().elevation(getBottomSheetInfo())
|
||||
val sheetBackgroundColor: Color = getDrawerTokens().backgroundColor(getBottomSheetInfo())
|
||||
val sheetContentColor: Color = Color.Transparent
|
||||
val sheetHandleColor: Color = getDrawerTokens().handleColor(getBottomSheetInfo())
|
||||
val scrimOpacity: Float = getDrawerTokens().scrimOpacity(getBottomSheetInfo())
|
||||
val scrimColor: Color =
|
||||
getDrawerTokens().scrimColor(getBottomSheetInfo()).copy(alpha = scrimOpacity)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
BoxWithConstraints(modifier) {
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
val sheetHeightState =
|
||||
remember(sheetContent.hashCode()) { mutableStateOf<Float?>(null) }
|
||||
|
||||
BoxWithConstraints(modifier) {
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
val sheetHeightState =
|
||||
remember(sheetContent.hashCode()) { mutableStateOf<Float?>(null) }
|
||||
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
content()
|
||||
if (slideOver) {
|
||||
Scrim(
|
||||
color = if (scrimVisible) scrimColor else Color.Transparent,
|
||||
onDismiss = {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
|
||||
scope.launch { sheetState.show() }
|
||||
}
|
||||
},
|
||||
fraction = {
|
||||
if (sheetState.anchors.isEmpty()
|
||||
|| !sheetState.anchors.containsValue(BottomSheetValue.Expanded)
|
||||
) {
|
||||
0.toFloat()
|
||||
} else {
|
||||
calculateFraction(
|
||||
sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Shown }?.key!!,
|
||||
sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded }?.key!!,
|
||||
sheetState.offset.value
|
||||
)
|
||||
}
|
||||
},
|
||||
visible = (sheetState.targetValue == BottomSheetValue.Expanded
|
||||
|| (sheetState.targetValue == BottomSheetValue.Shown
|
||||
&& sheetState.currentValue == BottomSheetValue.Expanded)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.nestedScroll(if (slideOver) sheetState.PreUpPostDownNestedScrollConnection else sheetState.PostDownNestedScrollConnection)
|
||||
.offset {
|
||||
val y = if (sheetState.anchors.isEmpty()) {
|
||||
// if we don't know our anchors yet, render the sheet as hidden
|
||||
fullHeight.roundToInt()
|
||||
} else {
|
||||
// if we do know our anchors, respect them
|
||||
sheetState.offset.value.roundToInt()
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.bottomSheetSwipeable(
|
||||
sheetState,
|
||||
expandable,
|
||||
peekHeight,
|
||||
fullHeight,
|
||||
sheetHeightState.value,
|
||||
slideOver
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
if (slideOver) {
|
||||
val originalSize = it.size.height.toFloat()
|
||||
sheetHeightState.value = if (expandable) {
|
||||
originalSize
|
||||
} else {
|
||||
min(
|
||||
originalSize,
|
||||
min(dpToPx(peekHeight), fullHeight * BottomSheetOpenFraction)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheetHeight(
|
||||
expandable,
|
||||
slideOver,
|
||||
fullHeight,
|
||||
peekHeight,
|
||||
sheetState
|
||||
)
|
||||
.semantics(mergeDescendants = true) {
|
||||
if (sheetState.isVisible) {
|
||||
dismiss {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
|
||||
scope.launch { sheetState.hide() }
|
||||
}
|
||||
true
|
||||
}
|
||||
if (sheetState.currentValue == BottomSheetValue.Shown) {
|
||||
expand {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
|
||||
scope.launch { sheetState.expand() }
|
||||
}
|
||||
true
|
||||
}
|
||||
} else if (sheetState.hasExpandedState) {
|
||||
collapse {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Shown)) {
|
||||
scope.launch { sheetState.show() }
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
content()
|
||||
if (slideOver) {
|
||||
Scrim(
|
||||
color = if (scrimVisible) scrimColor else Color.Transparent,
|
||||
onDismiss = {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
|
||||
scope.launch { sheetState.show() }
|
||||
}
|
||||
},
|
||||
shape = sheetShape,
|
||||
elevation = sheetElevation,
|
||||
color = sheetBackgroundColor,
|
||||
contentColor = sheetContentColor
|
||||
) {
|
||||
Column {
|
||||
if (showHandle) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
//TODO : Revisit SwipeableState usage across module to abstract out common modifier.
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
sheetState.performDrag(delta)
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
launch {
|
||||
sheetState.performFling(velocity)
|
||||
if (!sheetState.isVisible) {
|
||||
scope.launch { sheetState.hide() }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(BOTTOMSHEET_HANDLE_TAG)
|
||||
fraction = {
|
||||
if (sheetState.anchors.isEmpty()
|
||||
|| !sheetState.anchors.containsValue(BottomSheetValue.Expanded)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(id = com.microsoft.fluentui.drawer.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() }
|
||||
}
|
||||
} else if (sheetState.hasExpandedState) {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
|
||||
scope.launch { sheetState.expand() }
|
||||
}
|
||||
}
|
||||
}
|
||||
0.toFloat()
|
||||
} else {
|
||||
calculateFraction(
|
||||
sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Shown }?.key!!,
|
||||
sheetState.anchors.entries.firstOrNull { it.value == BottomSheetValue.Expanded }?.key!!,
|
||||
sheetState.offset.value
|
||||
)
|
||||
}
|
||||
},
|
||||
visible = (sheetState.targetValue == BottomSheetValue.Expanded
|
||||
|| (sheetState.targetValue == BottomSheetValue.Shown
|
||||
&& sheetState.currentValue == BottomSheetValue.Expanded)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.nestedScroll(if (slideOver) sheetState.PreUpPostDownNestedScrollConnection else sheetState.PostDownNestedScrollConnection)
|
||||
.offset {
|
||||
val y = if (sheetState.anchors.isEmpty()) {
|
||||
// if we don't know our anchors yet, render the sheet as hidden
|
||||
fullHeight.roundToInt()
|
||||
} else {
|
||||
// if we do know our anchors, respect them
|
||||
sheetState.offset.value.roundToInt()
|
||||
}
|
||||
IntOffset(0, y)
|
||||
}
|
||||
.bottomSheetSwipeable(
|
||||
sheetState,
|
||||
expandable,
|
||||
peekHeight,
|
||||
fullHeight,
|
||||
sheetHeightState.value,
|
||||
slideOver
|
||||
)
|
||||
.onGloballyPositioned {
|
||||
if (slideOver) {
|
||||
val originalSize = it.size.height.toFloat()
|
||||
sheetHeightState.value = if (expandable) {
|
||||
originalSize
|
||||
} else {
|
||||
min(
|
||||
originalSize,
|
||||
min(dpToPx(peekHeight), fullHeight * BottomSheetOpenFraction)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier
|
||||
.testTag(BOTTOMSHEET_CONTENT_TAG)
|
||||
.then(if (slideOver) Modifier else Modifier.fillMaxSize()),
|
||||
content = { sheetContent() })
|
||||
}
|
||||
.sheetHeight(
|
||||
expandable,
|
||||
slideOver,
|
||||
fullHeight,
|
||||
peekHeight,
|
||||
sheetState
|
||||
)
|
||||
.semantics(mergeDescendants = true) {
|
||||
if (sheetState.isVisible) {
|
||||
dismiss {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Hidden)) {
|
||||
scope.launch { sheetState.hide() }
|
||||
}
|
||||
true
|
||||
}
|
||||
if (sheetState.currentValue == BottomSheetValue.Shown) {
|
||||
expand {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
|
||||
scope.launch { sheetState.expand() }
|
||||
}
|
||||
true
|
||||
}
|
||||
} else if (sheetState.hasExpandedState) {
|
||||
collapse {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Shown)) {
|
||||
scope.launch { sheetState.show() }
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = sheetShape,
|
||||
elevation = sheetElevation,
|
||||
color = sheetBackgroundColor,
|
||||
contentColor = sheetContentColor
|
||||
) {
|
||||
Column {
|
||||
if (showHandle) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
//TODO : Revisit SwipeableState usage across module to abstract out common modifier.
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
sheetState.performDrag(delta)
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
launch {
|
||||
sheetState.performFling(velocity)
|
||||
if (!sheetState.isVisible) {
|
||||
scope.launch { sheetState.hide() }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(BOTTOMSHEET_HANDLE_TAG)
|
||||
) {
|
||||
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() }
|
||||
}
|
||||
} else if (sheetState.hasExpandedState) {
|
||||
if (sheetState.confirmStateChange(BottomSheetValue.Expanded)) {
|
||||
scope.launch { sheetState.expand() }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier
|
||||
.testTag(BOTTOMSHEET_CONTENT_TAG)
|
||||
.then(if (slideOver) Modifier else Modifier.fillMaxSize()),
|
||||
content = { sheetContent() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -783,20 +783,6 @@ private fun BottomDrawer(
|
|||
}
|
||||
}
|
||||
|
||||
internal val LocalDrawerTokens = compositionLocalOf { DrawerTokens() }
|
||||
internal val LocalDrawerInfo = compositionLocalOf { DrawerInfo() }
|
||||
|
||||
@Composable
|
||||
private fun getDrawerTokens(): DrawerTokens {
|
||||
return LocalDrawerTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDrawerInfo(): DrawerInfo {
|
||||
return LocalDrawerInfo.current
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Drawer block interaction with the rest of an app’s content with a scrim.
|
||||
|
@ -835,82 +821,77 @@ fun Drawer(
|
|||
scope.launch { drawerState.close() }
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalDrawerTokens provides tokens,
|
||||
LocalDrawerInfo provides DrawerInfo(type = behaviorType)
|
||||
) {
|
||||
Popup(
|
||||
onDismissRequest = close,
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
properties = PopupProperties(focusable = true)
|
||||
)
|
||||
{
|
||||
val drawerShape: Shape =
|
||||
when (behaviorType) {
|
||||
BehaviorType.BOTTOM, BehaviorType.BOTTOM_SLIDE_OVER -> RoundedCornerShape(
|
||||
topStart = getDrawerTokens().borderRadius(getDrawerInfo()),
|
||||
topEnd = getDrawerTokens().borderRadius(getDrawerInfo())
|
||||
)
|
||||
BehaviorType.TOP -> RoundedCornerShape(
|
||||
bottomStart = getDrawerTokens().borderRadius(getDrawerInfo()),
|
||||
bottomEnd = getDrawerTokens().borderRadius(getDrawerInfo())
|
||||
)
|
||||
else -> RoundedCornerShape(getDrawerTokens().borderRadius(getDrawerInfo()))
|
||||
}
|
||||
val drawerElevation: Dp = getDrawerTokens().elevation(getDrawerInfo())
|
||||
val drawerBackgroundColor: Color =
|
||||
getDrawerTokens().backgroundColor(getDrawerInfo())
|
||||
val drawerContentColor: Color = Color.Transparent
|
||||
val drawerHandleColor: Color = getDrawerTokens().handleColor(getDrawerInfo())
|
||||
val scrimOpacity: Float = getDrawerTokens().scrimOpacity(getDrawerInfo())
|
||||
val scrimColor: Color =
|
||||
getDrawerTokens().scrimColor(getDrawerInfo()).copy(alpha = scrimOpacity)
|
||||
|
||||
val drawerInfo = DrawerInfo(type = behaviorType)
|
||||
Popup(
|
||||
onDismissRequest = close,
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
properties = PopupProperties(focusable = true)
|
||||
)
|
||||
{
|
||||
val drawerShape: Shape =
|
||||
when (behaviorType) {
|
||||
BehaviorType.BOTTOM, BehaviorType.BOTTOM_SLIDE_OVER -> BottomDrawer(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
expandable = expandable,
|
||||
slideOver = behaviorType == BehaviorType.BOTTOM_SLIDE_OVER,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
BehaviorType.BOTTOM, BehaviorType.BOTTOM_SLIDE_OVER -> RoundedCornerShape(
|
||||
topStart = tokens.borderRadius(drawerInfo),
|
||||
topEnd = tokens.borderRadius(drawerInfo)
|
||||
)
|
||||
BehaviorType.TOP -> TopDrawer(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
)
|
||||
|
||||
BehaviorType.LEFT_SLIDE_OVER, BehaviorType.RIGHT_SLIDE_OVER -> HorizontalDrawer(
|
||||
behaviorType = behaviorType,
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
BehaviorType.TOP -> RoundedCornerShape(
|
||||
bottomStart = tokens.borderRadius(drawerInfo),
|
||||
bottomEnd = tokens.borderRadius(drawerInfo)
|
||||
)
|
||||
else -> RoundedCornerShape(tokens.borderRadius(drawerInfo))
|
||||
}
|
||||
val drawerElevation: Dp = tokens.elevation(drawerInfo)
|
||||
val drawerBackgroundColor: Color =
|
||||
tokens.backgroundColor(drawerInfo)
|
||||
val drawerContentColor: Color = Color.Transparent
|
||||
val drawerHandleColor: Color = tokens.handleColor(drawerInfo)
|
||||
val scrimOpacity: Float = tokens.scrimOpacity(drawerInfo)
|
||||
val scrimColor: Color =
|
||||
tokens.scrimColor(drawerInfo).copy(alpha = scrimOpacity)
|
||||
|
||||
when (behaviorType) {
|
||||
BehaviorType.BOTTOM, BehaviorType.BOTTOM_SLIDE_OVER -> BottomDrawer(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
expandable = expandable,
|
||||
slideOver = behaviorType == BehaviorType.BOTTOM_SLIDE_OVER,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
)
|
||||
BehaviorType.TOP -> TopDrawer(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
)
|
||||
|
||||
BehaviorType.LEFT_SLIDE_OVER, BehaviorType.RIGHT_SLIDE_OVER -> HorizontalDrawer(
|
||||
behaviorType = behaviorType,
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackgroundColor = drawerBackgroundColor,
|
||||
drawerContentColor = drawerContentColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ import com.microsoft.fluentui.theme.token.FluentStyle
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.*
|
||||
import com.microsoft.fluentui.tokenized.divider.Divider
|
||||
import com.microsoft.fluentui.tokenized.listitem.ListItem
|
||||
import com.microsoft.fluentui.tokenized.listitem.getListItemInfo
|
||||
import com.microsoft.fluentui.tokenized.tabItem.TabItem
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
@ -307,7 +306,7 @@ class ListContentBuilder {
|
|||
subText = item.subTitle,
|
||||
leadingAccessoryView = {
|
||||
if (item.icon != null) {
|
||||
Icon(item.icon!!, null, tint = token.iconColor(getListItemInfo()).rest)
|
||||
Icon(item.icon!!, null, tint = token.iconColor(ListItemInfo()).rest)
|
||||
}
|
||||
},
|
||||
trailingAccessoryView = item.accessory,
|
||||
|
|
|
@ -4,8 +4,6 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -14,19 +12,6 @@ import com.microsoft.fluentui.theme.token.ControlTokens
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.DividerInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.DividerTokens
|
||||
|
||||
val LocalDividerTokens = compositionLocalOf { DividerTokens() }
|
||||
val LocalDividerInfo = compositionLocalOf { DividerInfo() }
|
||||
|
||||
@Composable
|
||||
fun getDividerTokens(): DividerTokens {
|
||||
return LocalDividerTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDividerInfo(): DividerInfo {
|
||||
return LocalDividerInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Divider(
|
||||
height: Dp = 1.dp,
|
||||
|
@ -35,23 +20,19 @@ fun Divider(
|
|||
val token =
|
||||
dividerToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Divider] as DividerTokens
|
||||
CompositionLocalProvider(
|
||||
LocalDividerTokens provides token,
|
||||
LocalDividerInfo provides DividerInfo()
|
||||
val dividerInfo = DividerInfo()
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(token.background(dividerInfo = dividerInfo))
|
||||
.focusable(false)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.background(getDividerTokens().background(dividerInfo = getDividerInfo()))
|
||||
.focusable(false)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(height)
|
||||
.background(getDividerTokens().dividerColor(dividerInfo = getDividerInfo()))
|
||||
)
|
||||
}
|
||||
.requiredHeight(height)
|
||||
.background(token.dividerColor(dividerInfo = dividerInfo))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package com.microsoft.fluentui.tokenized.listitem
|
||||
|
||||
import android.R
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.Canvas
|
||||
|
@ -44,19 +43,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.TextPlacement.Bottom
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.TextPlacement.Top
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
|
||||
val LocalListItemTokens = compositionLocalOf { ListItemTokens() }
|
||||
val LocalListItemInfo = compositionLocalOf { ListItemInfo() }
|
||||
|
||||
@Composable
|
||||
internal fun getListItemTokens(): ListItemTokens {
|
||||
return LocalListItemTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun getListItemInfo(): ListItemInfo {
|
||||
return LocalListItemInfo.current
|
||||
}
|
||||
|
||||
object ListItem {
|
||||
|
||||
private fun clearSemantics(properties: (SemanticsPropertyReceiver.() -> Unit)?): Modifier {
|
||||
|
@ -260,174 +246,169 @@ object ListItem {
|
|||
}
|
||||
val token = listItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.ListItem] as ListItemTokens
|
||||
CompositionLocalProvider(
|
||||
LocalListItemTokens provides token, LocalListItemInfo provides ListItemInfo(
|
||||
listItemType = listItemType,
|
||||
borderInset = borderInset,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
unreadDot = unreadDot
|
||||
val listItemInfo = ListItemInfo(
|
||||
listItemType = listItemType,
|
||||
borderInset = borderInset,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
unreadDot = unreadDot
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(listItemInfo).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = token.cellHeight(listItemInfo)
|
||||
val primaryTextTypography = token.primaryTextTypography(listItemInfo)
|
||||
val subTextTypography = token.subTextTypography(listItemInfo)
|
||||
val secondarySubTextTypography =
|
||||
token.secondarySubTextTypography(listItemInfo)
|
||||
val primaryTextColor = token.primaryTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val subTextColor = token.subTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val secondarySubTextColor = token.secondarySubTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = token.rippleColor(listItemInfo)
|
||||
val unreadDotColor = token.unreadDotColor(listItemInfo)
|
||||
val padding = token.padding(listItemInfo)
|
||||
val borderSize = token.borderSize(listItemInfo).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
token.borderInset(listItemInfo).toPx()
|
||||
}
|
||||
val borderColor = token.borderColor(listItemInfo).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource, onClick = onClick ?: {}, enabled, rippleColor
|
||||
), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
val backgroundColor =
|
||||
getListItemTokens().backgroundColor(getListItemInfo()).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = getListItemTokens().cellHeight(getListItemInfo())
|
||||
val primaryTextTypography = getListItemTokens().primaryTextTypography(getListItemInfo())
|
||||
val subTextTypography = getListItemTokens().subTextTypography(getListItemInfo())
|
||||
val secondarySubTextTypography =
|
||||
getListItemTokens().secondarySubTextTypography(getListItemInfo())
|
||||
val primaryTextColor = getListItemTokens().primaryTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val subTextColor = getListItemTokens().subTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val secondarySubTextColor = getListItemTokens().secondarySubTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = getListItemTokens().rippleColor(getListItemInfo())
|
||||
val unreadDotColor = getListItemTokens().unreadDotColor(getListItemInfo())
|
||||
val padding = getListItemTokens().padding(getListItemInfo())
|
||||
val borderSize = getListItemTokens().borderSize(getListItemInfo()).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
getListItemTokens().borderInset(getListItemInfo()).toPx()
|
||||
}
|
||||
val borderColor = getListItemTokens().borderColor(getListItemInfo()).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource, onClick = onClick ?: {}, enabled, rippleColor
|
||||
)
|
||||
, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (unreadDot) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.sizeIn(minWidth = 8.dp, minHeight = 8.dp)
|
||||
) {
|
||||
drawCircle(
|
||||
color = unreadDotColor, style = Fill, radius = dpToPx(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (leadingAccessoryView != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (unreadDot) 4.dp else padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
), contentAlignment = Alignment.Center
|
||||
) {
|
||||
leadingAccessoryView()
|
||||
}
|
||||
}
|
||||
val contentAlignment =
|
||||
if (textAlignment == ListItemTextAlignment.Regular) Alignment.CenterStart else Alignment.Center
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = padding.calculateStartPadding(LocalLayoutDirection.current))
|
||||
.weight(1f)
|
||||
.then(clearSemantics(textAccessibilityProperties)),contentAlignment = contentAlignment
|
||||
if (unreadDot) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp)
|
||||
.sizeIn(minWidth = 8.dp, minHeight = 8.dp)
|
||||
) {
|
||||
Column(Modifier.padding(vertical = padding.calculateTopPadding())) {
|
||||
drawCircle(
|
||||
color = unreadDotColor, style = Fill, radius = dpToPx(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (leadingAccessoryView != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Box(
|
||||
Modifier.padding(
|
||||
start = if (unreadDot) 4.dp else padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
), contentAlignment = Alignment.Center
|
||||
) {
|
||||
leadingAccessoryView()
|
||||
}
|
||||
}
|
||||
val contentAlignment =
|
||||
if (textAlignment == ListItemTextAlignment.Regular) Alignment.CenterStart else Alignment.Center
|
||||
Box(
|
||||
Modifier
|
||||
.padding(horizontal = padding.calculateStartPadding(LocalLayoutDirection.current))
|
||||
.weight(1f)
|
||||
.then(clearSemantics(textAccessibilityProperties)),
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Column(Modifier.padding(vertical = padding.calculateTopPadding())) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (primaryTextLeadingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
primaryTextLeadingIcons.icon1()
|
||||
primaryTextLeadingIcons.icon2?.let { it() }
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = textMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (primaryTextTrailingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
primaryTextTrailingIcons.icon1()
|
||||
primaryTextTrailingIcons.icon2?.let { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (subText != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Text(
|
||||
text = subText,
|
||||
style = subTextTypography,
|
||||
color = subTextColor,
|
||||
maxLines = subTextMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
if (textAlignment == ListItemTextAlignment.Regular) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (primaryTextLeadingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
primaryTextLeadingIcons.icon1()
|
||||
primaryTextLeadingIcons.icon2?.let { it() }
|
||||
if (bottomView != null) {
|
||||
Row(modifier.padding(top = 7.dp, bottom = 7.dp)) {
|
||||
bottomView()
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = textMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (primaryTextTrailingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
primaryTextTrailingIcons.icon1()
|
||||
primaryTextTrailingIcons.icon2?.let { it() }
|
||||
} else {
|
||||
if (secondarySubTextLeadingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
secondarySubTextLeadingIcons.icon1()
|
||||
secondarySubTextLeadingIcons.icon2?.let { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (subText != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Text(
|
||||
text = subText,
|
||||
style = subTextTypography,
|
||||
color = subTextColor,
|
||||
maxLines = subTextMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
if (textAlignment == ListItemTextAlignment.Regular) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (bottomView != null) {
|
||||
Row(modifier.padding(top = 7.dp, bottom = 7.dp)) {
|
||||
bottomView()
|
||||
}
|
||||
} else {
|
||||
if (secondarySubTextLeadingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
secondarySubTextLeadingIcons.icon1()
|
||||
secondarySubTextLeadingIcons.icon2?.let { it() }
|
||||
}
|
||||
}
|
||||
if (secondarySubText != null) {
|
||||
Text(
|
||||
text = secondarySubText,
|
||||
style = secondarySubTextTypography,
|
||||
color = secondarySubTextColor,
|
||||
maxLines = secondarySubTextMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (secondarySubTextTailingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
secondarySubTextTailingIcons.icon1()
|
||||
secondarySubTextTailingIcons.icon2?.let { it() }
|
||||
}
|
||||
if (secondarySubText != null) {
|
||||
Text(
|
||||
text = secondarySubText,
|
||||
style = secondarySubTextTypography,
|
||||
color = secondarySubTextColor,
|
||||
maxLines = secondarySubTextMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (secondarySubTextTailingIcons != null) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
secondarySubTextTailingIcons.icon1()
|
||||
secondarySubTextTailingIcons.icon2?.let { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bottomView == null && trailingAccessoryView != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Box(
|
||||
Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
if (bottomView == null && trailingAccessoryView != null && textAlignment == ListItemTextAlignment.Regular) {
|
||||
Box(
|
||||
Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -478,136 +459,132 @@ object ListItem {
|
|||
|
||||
val token = listItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.ListItem] as ListItemTokens
|
||||
CompositionLocalProvider(
|
||||
LocalListItemTokens provides token, LocalListItemInfo provides ListItemInfo(
|
||||
listItemType = SectionHeader,
|
||||
borderInset = borderInset,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size120,
|
||||
style = style
|
||||
val listItemInfo = ListItemInfo(
|
||||
listItemType = SectionHeader,
|
||||
borderInset = borderInset,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size120,
|
||||
style = style
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(listItemInfo).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
) {
|
||||
|
||||
val backgroundColor =
|
||||
getListItemTokens().backgroundColor(getListItemInfo()).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
val cellHeight = token.cellHeight(listItemInfo)
|
||||
val primaryTextTypography =
|
||||
token.sectionHeaderPrimaryTextTypography(listItemInfo)
|
||||
val actionTextTypography =
|
||||
token.sectionHeaderActionTextTypography(listItemInfo)
|
||||
val primaryTextColor = token.primaryTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = token.actionTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = token.rippleColor(listItemInfo)
|
||||
val padding = token.padding(listItemInfo)
|
||||
val borderSize = token.borderSize(listItemInfo).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
token.borderInset(listItemInfo).toPx()
|
||||
}
|
||||
val borderColor = token.borderColor(listItemInfo).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val chevronTint = token.chevronTint(listItemInfo)
|
||||
var expandedState by rememberSaveable { mutableStateOf(false) }
|
||||
val rotationState by animateFloatAsState(
|
||||
targetValue = if (!enableContentOpenCloseTransition || expandedState) chevronOrientation.enterTransition else chevronOrientation.exitTransition
|
||||
)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource,
|
||||
onClick = { expandedState = !expandedState },
|
||||
enabled,
|
||||
rippleColor
|
||||
)
|
||||
val cellHeight = getListItemTokens().cellHeight(getListItemInfo())
|
||||
val primaryTextTypography =
|
||||
getListItemTokens().sectionHeaderPrimaryTextTypography(getListItemInfo())
|
||||
val actionTextTypography =
|
||||
getListItemTokens().sectionHeaderActionTextTypography(getListItemInfo())
|
||||
val primaryTextColor = getListItemTokens().primaryTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = getListItemTokens().actionTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = getListItemTokens().rippleColor(getListItemInfo())
|
||||
val padding = getListItemTokens().padding(getListItemInfo())
|
||||
val borderSize = getListItemTokens().borderSize(getListItemInfo()).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
getListItemTokens().borderInset(getListItemInfo()).toPx()
|
||||
}
|
||||
val borderColor = getListItemTokens().borderColor(getListItemInfo()).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val chevronTint = getListItemTokens().chevronTint(getListItemInfo())
|
||||
var expandedState by rememberSaveable { mutableStateOf(false) }
|
||||
val rotationState by animateFloatAsState(
|
||||
targetValue = if (!enableContentOpenCloseTransition || expandedState) chevronOrientation.enterTransition else chevronOrientation.exitTransition
|
||||
)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource,
|
||||
onClick = { expandedState = !expandedState },
|
||||
enabled,
|
||||
rippleColor
|
||||
)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.padding(bottom = padding.calculateBottomPadding()),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.background(backgroundColor)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.padding(bottom = padding.calculateBottomPadding()),
|
||||
verticalAlignment = Alignment.Bottom
|
||||
.padding(
|
||||
horizontal = padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
)
|
||||
.weight(1f), contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (enableChevron) {
|
||||
Icon(painter = rememberVectorPainter(image = ListItemIcons.Chevron),
|
||||
contentDescription = "Chevron",
|
||||
Modifier
|
||||
.clickable { expandedState = !expandedState }
|
||||
.rotate(rotationState),
|
||||
tint = chevronTint)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = titleMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Row(Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current))) {
|
||||
if (accessoryTextTitle != null) {
|
||||
Text(
|
||||
text = accessoryTextTitle,
|
||||
Modifier.clickable(role = Role.Button,
|
||||
onClick = accessoryTextOnClick ?: {}),
|
||||
color = actionTextColor,
|
||||
style = actionTextTypography
|
||||
)
|
||||
}
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
Modifier.padding(
|
||||
end = padding.calculateEndPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
.weight(1f), contentAlignment = Alignment.BottomStart
|
||||
), contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (enableChevron) {
|
||||
Icon(painter = rememberVectorPainter(image = ListItemIcons.Chevron),
|
||||
contentDescription = "Chevron",
|
||||
Modifier
|
||||
.clickable { expandedState = !expandedState }
|
||||
.rotate(rotationState),
|
||||
tint = chevronTint)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = titleMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Row(Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current))) {
|
||||
if (accessoryTextTitle != null) {
|
||||
Text(
|
||||
text = accessoryTextTitle,
|
||||
Modifier.clickable(role = Role.Button,
|
||||
onClick = accessoryTextOnClick ?: {}),
|
||||
color = actionTextColor,
|
||||
style = actionTextTypography
|
||||
)
|
||||
}
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier.padding(
|
||||
end = padding.calculateEndPadding(
|
||||
LocalLayoutDirection.current
|
||||
)
|
||||
), contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
Row {
|
||||
if (content != null) {
|
||||
AnimatedVisibility(
|
||||
visible = !enableContentOpenCloseTransition || expandedState,
|
||||
enter = enter,
|
||||
exit = exit
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
Row {
|
||||
if (content != null) {
|
||||
AnimatedVisibility(
|
||||
visible = !enableContentOpenCloseTransition || expandedState,
|
||||
enter = enter,
|
||||
exit = exit
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -648,100 +625,96 @@ object ListItem {
|
|||
) {
|
||||
val token = listItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.ListItem] as ListItemTokens
|
||||
CompositionLocalProvider(
|
||||
LocalListItemTokens provides token, LocalListItemInfo provides ListItemInfo(
|
||||
listItemType = SectionDescription,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size80,
|
||||
borderInset = borderInset,
|
||||
placement = descriptionPlacement
|
||||
val listItemInfo = ListItemInfo(
|
||||
listItemType = SectionDescription,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size80,
|
||||
borderInset = borderInset,
|
||||
placement = descriptionPlacement
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(listItemInfo).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = token.cellHeight(listItemInfo)
|
||||
val descriptionTextTypography =
|
||||
token.descriptionTextTypography(listItemInfo)
|
||||
val actionTextTypography = token.actionTextTypography(listItemInfo)
|
||||
val descriptionTextColor = token.descriptionTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = token.actionTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = token.rippleColor(listItemInfo)
|
||||
val borderSize = token.borderSize(listItemInfo).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
token.borderInset(listItemInfo).toPx()
|
||||
}
|
||||
val borderColor = token.borderColor(listItemInfo).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val descriptionAlignment = token.descriptionPlacement(listItemInfo)
|
||||
val padding = token.padding(listItemInfo)
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource, onClick = onClick ?: {}, enabled, rippleColor
|
||||
), verticalAlignment = descriptionAlignment
|
||||
) {
|
||||
|
||||
val backgroundColor =
|
||||
getListItemTokens().backgroundColor(getListItemInfo()).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = getListItemTokens().cellHeight(getListItemInfo())
|
||||
val descriptionTextTypography =
|
||||
getListItemTokens().descriptionTextTypography(getListItemInfo())
|
||||
val actionTextTypography = getListItemTokens().actionTextTypography(getListItemInfo())
|
||||
val descriptionTextColor = getListItemTokens().descriptionTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = getListItemTokens().actionTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val rippleColor = getListItemTokens().rippleColor(getListItemInfo())
|
||||
val borderSize = getListItemTokens().borderSize(getListItemInfo()).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
getListItemTokens().borderInset(getListItemInfo()).toPx()
|
||||
}
|
||||
val borderColor = getListItemTokens().borderColor(getListItemInfo()).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val descriptionAlignment = getListItemTokens().descriptionPlacement(getListItemInfo())
|
||||
val padding = getListItemTokens().padding(getListItemInfo())
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.clickAndSemanticsModifier(
|
||||
interactionSource, onClick = onClick ?: {}, enabled, rippleColor
|
||||
), verticalAlignment = descriptionAlignment
|
||||
) {
|
||||
if (leadingAccessoryView != null && descriptionPlacement == Top) {
|
||||
Box(
|
||||
Modifier.padding(padding.calculateStartPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
leadingAccessoryView()
|
||||
}
|
||||
}
|
||||
if (leadingAccessoryView != null && descriptionPlacement == Top) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (leadingAccessoryView == null) padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
) else 0.dp,
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
top = if (descriptionPlacement == Top) padding.calculateTopPadding() else 0.dp,
|
||||
bottom = if (descriptionPlacement == Bottom) padding.calculateTopPadding() else 0.dp
|
||||
)
|
||||
.weight(1f)
|
||||
Modifier.padding(padding.calculateStartPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!actionText.isNullOrBlank()) {
|
||||
InlineText(
|
||||
description = description,
|
||||
actionText = actionText,
|
||||
onClick = onActionClick ?: {},
|
||||
actionTextTypography = actionTextTypography,
|
||||
actionTextColor = actionTextColor,
|
||||
descriptionTextColor = descriptionTextColor,
|
||||
descriptionTextTypography = descriptionTextTypography,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = description,
|
||||
color = descriptionTextColor,
|
||||
style = descriptionTextTypography
|
||||
)
|
||||
}
|
||||
leadingAccessoryView()
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
Box(
|
||||
Modifier
|
||||
.padding(
|
||||
start = if (leadingAccessoryView == null) padding.calculateStartPadding(
|
||||
LocalLayoutDirection.current
|
||||
) else 0.dp,
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
top = if (descriptionPlacement == Top) padding.calculateTopPadding() else 0.dp,
|
||||
bottom = if (descriptionPlacement == Bottom) padding.calculateTopPadding() else 0.dp
|
||||
)
|
||||
.weight(1f)
|
||||
) {
|
||||
if (!actionText.isNullOrBlank()) {
|
||||
InlineText(
|
||||
description = description,
|
||||
actionText = actionText,
|
||||
onClick = onActionClick ?: {},
|
||||
actionTextTypography = actionTextTypography,
|
||||
actionTextColor = actionTextColor,
|
||||
descriptionTextColor = descriptionTextColor,
|
||||
descriptionTextTypography = descriptionTextTypography,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = description,
|
||||
color = descriptionTextColor,
|
||||
style = descriptionTextTypography
|
||||
)
|
||||
}
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier.padding(end = padding.calculateEndPadding(LocalLayoutDirection.current)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -781,98 +754,94 @@ object ListItem {
|
|||
) {
|
||||
val token = listItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlType.ListItem] as ListItemTokens
|
||||
CompositionLocalProvider(
|
||||
LocalListItemTokens provides token, LocalListItemInfo provides ListItemInfo(
|
||||
listItemType = OneLine,
|
||||
style = style,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size80,
|
||||
borderInset = borderInset
|
||||
val listItemInfo = ListItemInfo(
|
||||
listItemType = OneLine,
|
||||
style = style,
|
||||
horizontalSpacing = GlobalTokens.SizeTokens.Size160,
|
||||
verticalSpacing = GlobalTokens.SizeTokens.Size80,
|
||||
borderInset = borderInset
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(listItemInfo).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = token.cellHeight(listItemInfo)
|
||||
val primaryTextTypography =
|
||||
token.sectionHeaderPrimaryTextTypography(listItemInfo)
|
||||
val actionTextTypography =
|
||||
token.sectionHeaderActionTextTypography(listItemInfo)
|
||||
val primaryTextColor = token.primaryTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = token.actionTextColor(
|
||||
listItemInfo
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val padding = token.padding(listItemInfo)
|
||||
val borderSize = token.borderSize(listItemInfo).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
token.borderInset(listItemInfo).toPx()
|
||||
}
|
||||
val borderColor = token.borderColor(listItemInfo).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.focusable(false)
|
||||
) {
|
||||
|
||||
val backgroundColor =
|
||||
getListItemTokens().backgroundColor(getListItemInfo()).getColorByState(
|
||||
enabled = true, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val cellHeight = getListItemTokens().cellHeight(getListItemInfo())
|
||||
val primaryTextTypography =
|
||||
getListItemTokens().sectionHeaderPrimaryTextTypography(getListItemInfo())
|
||||
val actionTextTypography =
|
||||
getListItemTokens().sectionHeaderActionTextTypography(getListItemInfo())
|
||||
val primaryTextColor = getListItemTokens().primaryTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val actionTextColor = getListItemTokens().actionTextColor(
|
||||
getListItemInfo()
|
||||
).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
val padding = getListItemTokens().padding(getListItemInfo())
|
||||
val borderSize = getListItemTokens().borderSize(getListItemInfo()).value
|
||||
val borderInsetToPx = with(LocalDensity.current) {
|
||||
getListItemTokens().borderInset(getListItemInfo()).toPx()
|
||||
}
|
||||
val borderColor = getListItemTokens().borderColor(getListItemInfo()).getColorByState(
|
||||
enabled = enabled, selected = false, interactionSource = interactionSource
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = modifier
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.borderModifier(border, borderColor, borderSize, borderInsetToPx)
|
||||
.focusable(false)
|
||||
.focusable(true), verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = cellHeight)
|
||||
.background(backgroundColor)
|
||||
.focusable(true), verticalAlignment = Alignment.Bottom
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
)
|
||||
.weight(1f),
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = titleMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (accessoryTextTitle != null) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
text = accessoryTextTitle,
|
||||
Modifier
|
||||
.padding(
|
||||
start = padding.calculateStartPadding(LocalLayoutDirection.current),
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
)
|
||||
.weight(1f),
|
||||
style = primaryTextTypography,
|
||||
color = primaryTextColor,
|
||||
maxLines = titleMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = accessoryTextOnClick ?: {}),
|
||||
color = actionTextColor,
|
||||
style = actionTextTypography
|
||||
)
|
||||
|
||||
if (accessoryTextTitle != null) {
|
||||
Text(
|
||||
text = accessoryTextTitle,
|
||||
Modifier
|
||||
.padding(
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = accessoryTextOnClick ?: {}),
|
||||
color = actionTextColor,
|
||||
style = actionTextTypography
|
||||
)
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier.padding(
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
), contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
if (trailingAccessoryView != null) {
|
||||
Box(
|
||||
Modifier.padding(
|
||||
end = padding.calculateEndPadding(LocalLayoutDirection.current),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
), contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
trailingAccessoryView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ import androidx.compose.material.Icon
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -30,20 +28,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.TabItemInfo
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.TabItemTokens
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.TabTextAlignment
|
||||
|
||||
val LocalTabItemTokens = compositionLocalOf { TabItemTokens() }
|
||||
val LocalTabItemInfo =
|
||||
compositionLocalOf { TabItemInfo(TabTextAlignment.VERTICAL, FluentStyle.Neutral) }
|
||||
|
||||
@Composable
|
||||
fun getTabItemTokens(): TabItemTokens {
|
||||
return LocalTabItemTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getTabItemInfo(): TabItemInfo {
|
||||
return LocalTabItemInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabItem(
|
||||
title: String,
|
||||
|
@ -63,169 +47,165 @@ fun TabItem(
|
|||
tabItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.TabItem] as TabItemTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTabItemTokens provides token,
|
||||
LocalTabItemInfo provides TabItemInfo(textAlignment, style)
|
||||
) {
|
||||
val textColor =
|
||||
getTabItemTokens().textColor(tabItemInfo = getTabItemInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val iconColor =
|
||||
getTabItemTokens().iconColor(tabItemInfo = getTabItemInfo()).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
val tabItemInfo = TabItemInfo(textAlignment, style)
|
||||
val textColor =
|
||||
token.textColor(tabItemInfo = tabItemInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val iconColor =
|
||||
token.iconColor(tabItemInfo = tabItemInfo).getColorByState(
|
||||
enabled = enabled,
|
||||
selected = selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
val backgroundColor = token.backgroundColor(tabItemInfo = tabItemInfo)
|
||||
val rippleColor = token.rippleColor(tabItemInfo = tabItemInfo)
|
||||
|
||||
val clickableModifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = rippleColor),
|
||||
onClickLabel = null,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.Tab
|
||||
)
|
||||
|
||||
val widthModifier = if (fixedWidth) {
|
||||
Modifier.width(token.width(tabItemInfo = tabItemInfo))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
val iconContent: @Composable () -> Unit = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
modifier = Modifier.size(if (textAlignment == TabTextAlignment.NO_TEXT) 28.dp else 24.dp),
|
||||
contentDescription = if (textAlignment == TabTextAlignment.NO_TEXT) title else null,
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
|
||||
if (textAlignment == TabTextAlignment.HORIZONTAL) {
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.then(clickableModifier)
|
||||
.background(backgroundColor)
|
||||
.padding(top = 8.dp, start = 4.dp, bottom = 4.dp, end = 8.dp)
|
||||
.then(widthModifier)
|
||||
)
|
||||
{
|
||||
val (iconConstrain, textConstrain, badgeConstrain) = createRefs()
|
||||
|
||||
Box(modifier = Modifier.constrainAs(iconConstrain) {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(textConstrain.start)
|
||||
}
|
||||
) {
|
||||
iconContent()
|
||||
}
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.constrainAs(textConstrain) {
|
||||
start.linkTo(iconConstrain.end)
|
||||
end.linkTo(badgeConstrain.start)
|
||||
width = Dimension.preferredWrapContent
|
||||
}
|
||||
.padding(start = 8.dp),
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
val backgroundColor = getTabItemTokens().backgroundColor(tabItemInfo = getTabItemInfo())
|
||||
val rippleColor = getTabItemTokens().rippleColor(tabItemInfo = getTabItemInfo())
|
||||
|
||||
val clickableModifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = rippleColor),
|
||||
onClickLabel = null,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.Tab
|
||||
)
|
||||
|
||||
val widthModifier = if (fixedWidth) {
|
||||
Modifier.width(getTabItemTokens().width(tabItemInfo = getTabItemInfo()))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
val iconContent: @Composable () -> Unit = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
modifier = Modifier.size(if (textAlignment == TabTextAlignment.NO_TEXT) 28.dp else 24.dp),
|
||||
contentDescription = if (textAlignment == TabTextAlignment.NO_TEXT) title else null,
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
|
||||
if (textAlignment == TabTextAlignment.HORIZONTAL) {
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.then(clickableModifier)
|
||||
.background(backgroundColor)
|
||||
.padding(top = 8.dp, start = 4.dp, bottom = 4.dp, end = 8.dp)
|
||||
.then(widthModifier)
|
||||
)
|
||||
{
|
||||
val (iconConstrain, textConstrain, badgeConstrain) = createRefs()
|
||||
|
||||
Box(modifier = Modifier.constrainAs(iconConstrain) {
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(textConstrain.start)
|
||||
}
|
||||
if (accessory != null) {
|
||||
Box(modifier = Modifier
|
||||
.constrainAs(badgeConstrain) {
|
||||
start.linkTo(textConstrain.end)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
) {
|
||||
iconContent()
|
||||
accessory()
|
||||
}
|
||||
}
|
||||
createHorizontalChain(
|
||||
iconConstrain,
|
||||
textConstrain,
|
||||
badgeConstrain,
|
||||
chainStyle = ChainStyle.Packed
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val badgeWithIcon: @Composable () -> Unit = {
|
||||
Layout(
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier.layoutId("anchor"),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
iconContent()
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.layoutId("badge")) {
|
||||
if (accessory != null)
|
||||
accessory()
|
||||
}
|
||||
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
val badgePlaceable = measurables.first { it.layoutId == "badge" }.measure(
|
||||
// Measure with loose constraints for height as we don't want the text to take up more
|
||||
// space than it needs.
|
||||
constraints.copy(minHeight = 0)
|
||||
)
|
||||
|
||||
val anchorPlaceable =
|
||||
measurables.first { it.layoutId == "anchor" }.measure(constraints)
|
||||
|
||||
// Use the width of the badge to infer whether it has any content (based on radius used
|
||||
// in [Badge]) and determine its horizontal offset.
|
||||
val hasContent = badgePlaceable.width > 16.dp.roundToPx()
|
||||
val contentOffset = if (hasContent) -2.dp.roundToPx() else 0
|
||||
val badgeHorizontalOffset = -anchorPlaceable.width / 2 + contentOffset
|
||||
val badgeVerticalOffset = (-4).dp
|
||||
|
||||
val totalWidth = anchorPlaceable.width
|
||||
val totalHeight = anchorPlaceable.height
|
||||
|
||||
layout(
|
||||
totalWidth,
|
||||
totalHeight
|
||||
) {
|
||||
|
||||
anchorPlaceable.placeRelative(0, 0)
|
||||
val badgeX = anchorPlaceable.width + badgeHorizontalOffset
|
||||
val badgeY = badgeVerticalOffset.roundToPx()
|
||||
badgePlaceable.placeRelative(badgeX, badgeY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier
|
||||
.then(clickableModifier)
|
||||
.background(backgroundColor)
|
||||
.padding(top = 8.dp, start = 8.dp, bottom = 4.dp, end = 8.dp)
|
||||
.then(widthModifier)
|
||||
) {
|
||||
badgeWithIcon()
|
||||
if (textAlignment == TabTextAlignment.VERTICAL) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.constrainAs(textConstrain) {
|
||||
start.linkTo(iconConstrain.end)
|
||||
end.linkTo(badgeConstrain.start)
|
||||
width = Dimension.preferredWrapContent
|
||||
}
|
||||
.padding(start = 8.dp),
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (accessory != null) {
|
||||
Box(modifier = Modifier
|
||||
.constrainAs(badgeConstrain) {
|
||||
start.linkTo(textConstrain.end)
|
||||
end.linkTo(parent.end)
|
||||
}
|
||||
) {
|
||||
accessory()
|
||||
}
|
||||
}
|
||||
createHorizontalChain(
|
||||
iconConstrain,
|
||||
textConstrain,
|
||||
badgeConstrain,
|
||||
chainStyle = ChainStyle.Packed
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val badgeWithIcon: @Composable () -> Unit = {
|
||||
Layout(
|
||||
{
|
||||
Box(
|
||||
modifier = Modifier.layoutId("anchor"),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
iconContent()
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.layoutId("badge")) {
|
||||
if (accessory != null)
|
||||
accessory()
|
||||
}
|
||||
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
val badgePlaceable = measurables.first { it.layoutId == "badge" }.measure(
|
||||
// Measure with loose constraints for height as we don't want the text to take up more
|
||||
// space than it needs.
|
||||
constraints.copy(minHeight = 0)
|
||||
)
|
||||
|
||||
val anchorPlaceable =
|
||||
measurables.first { it.layoutId == "anchor" }.measure(constraints)
|
||||
|
||||
// Use the width of the badge to infer whether it has any content (based on radius used
|
||||
// in [Badge]) and determine its horizontal offset.
|
||||
val hasContent = badgePlaceable.width > 16.dp.roundToPx()
|
||||
val contentOffset = if (hasContent) -2.dp.roundToPx() else 0
|
||||
val badgeHorizontalOffset = -anchorPlaceable.width / 2 + contentOffset
|
||||
val badgeVerticalOffset = (-4).dp
|
||||
|
||||
val totalWidth = anchorPlaceable.width
|
||||
val totalHeight = anchorPlaceable.height
|
||||
|
||||
layout(
|
||||
totalWidth,
|
||||
totalHeight
|
||||
) {
|
||||
|
||||
anchorPlaceable.placeRelative(0, 0)
|
||||
val badgeX = anchorPlaceable.width + badgeHorizontalOffset
|
||||
val badgeY = badgeVerticalOffset.roundToPx()
|
||||
badgePlaceable.placeRelative(badgeX, badgeY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier
|
||||
.then(clickableModifier)
|
||||
.background(backgroundColor)
|
||||
.padding(top = 8.dp, start = 8.dp, bottom = 4.dp, end = 8.dp)
|
||||
.then(widthModifier)
|
||||
) {
|
||||
badgeWithIcon()
|
||||
if (textAlignment == TabTextAlignment.VERTICAL) {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
text = title,
|
||||
color = textColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,8 @@ import androidx.compose.foundation.border
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -26,19 +23,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.BadgeTokens
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.BadgeType
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
|
||||
private val LocalBadgeInfo = compositionLocalOf { BadgeInfo(BadgeType.Character) }
|
||||
private val LocalBadgeToken = compositionLocalOf { BadgeTokens() }
|
||||
|
||||
@Composable
|
||||
private fun getBadgeInfo(): BadgeInfo {
|
||||
return LocalBadgeInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBadgeTokens(): BadgeTokens {
|
||||
return LocalBadgeToken.current
|
||||
}
|
||||
|
||||
/**
|
||||
* Badge represents dynamic information such as a number of pending requests on tab.
|
||||
* @param text Text to display, if not provided then it appear as dot.
|
||||
|
@ -51,69 +35,65 @@ private fun getBadgeTokens(): BadgeTokens {
|
|||
@OptIn(ExperimentalTextApi::class)
|
||||
@Composable
|
||||
fun Badge(
|
||||
text: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
text: String? = null,
|
||||
badgeType: BadgeType = BadgeType.List,
|
||||
badgeTokens: BadgeTokens? = null
|
||||
) {
|
||||
val token = badgeTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Badge] as BadgeTokens
|
||||
CompositionLocalProvider(
|
||||
LocalBadgeToken provides token,
|
||||
LocalBadgeInfo provides BadgeInfo(badgeType)
|
||||
) {
|
||||
val background = getBadgeTokens().backgroundColor(badgeInfo = getBadgeInfo())
|
||||
val borderStroke = getBadgeTokens().borderStroke(badgeInfo = getBadgeInfo())
|
||||
if (text.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(16.dp, 16.dp)
|
||||
val badgeInfo = BadgeInfo(badgeType)
|
||||
val background = token.backgroundColor(badgeInfo = badgeInfo)
|
||||
val borderStroke = token.borderStroke(badgeInfo = badgeInfo)
|
||||
if (text.isNullOrEmpty()) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(16.dp, 16.dp)
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, end = 3.dp, top = 3.dp, bottom = 5.dp)
|
||||
.sizeIn(minWidth = 8.dp, minHeight = 8.dp)
|
||||
) {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, end = 3.dp, top = 3.dp, bottom = 5.dp)
|
||||
.sizeIn(minWidth = 8.dp, minHeight = 8.dp)
|
||||
) {
|
||||
drawCircle(
|
||||
brush = borderStroke.brush,
|
||||
radius = dpToPx(borderStroke.width + 4.dp)
|
||||
)
|
||||
drawCircle(
|
||||
color = background,
|
||||
style = Fill,
|
||||
radius = dpToPx(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val textColor = getBadgeTokens().textColor(badgeInfo = getBadgeInfo())
|
||||
val typography = getBadgeTokens().typography(badgeInfo = getBadgeInfo())
|
||||
val paddingValues = getBadgeTokens().padding(badgeInfo = getBadgeInfo())
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.requiredHeight(if (badgeType == BadgeType.Character) 20.dp else 27.dp)
|
||||
.border(borderStroke.width, borderStroke.brush, CircleShape)
|
||||
.padding(0.5.dp) //TODO to check fix for https://issuetracker.google.com/issues/228985905
|
||||
.background(background, CircleShape)
|
||||
.clip(RoundedCornerShape(100.dp)),
|
||||
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
color = textColor,
|
||||
style = typography.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(
|
||||
includeFontPadding = false
|
||||
)
|
||||
)
|
||||
)
|
||||
drawCircle(
|
||||
brush = borderStroke.brush,
|
||||
radius = dpToPx(borderStroke.width + 4.dp)
|
||||
)
|
||||
drawCircle(
|
||||
color = background,
|
||||
style = Fill,
|
||||
radius = dpToPx(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val textColor = token.textColor(badgeInfo = badgeInfo)
|
||||
val typography = token.typography(badgeInfo = badgeInfo)
|
||||
val paddingValues = token.padding(badgeInfo = badgeInfo)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.requiredHeight(if (badgeType == BadgeType.Character) 20.dp else 27.dp)
|
||||
.border(borderStroke.width, borderStroke.brush, CircleShape)
|
||||
.padding(0.5.dp) //TODO to check fix for https://issuetracker.google.com/issues/228985905
|
||||
.background(background, CircleShape)
|
||||
.clip(RoundedCornerShape(100.dp)),
|
||||
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
color = textColor,
|
||||
style = typography.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(
|
||||
includeFontPadding = false
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,17 +38,14 @@ import com.microsoft.fluentui.tokenized.segmentedcontrols.PillMetaData
|
|||
import kotlin.math.roundToInt
|
||||
import androidx.compose.material.Icon as MaterialIcon
|
||||
|
||||
private val LocalCardNudgeTokens = compositionLocalOf { CardNudgeTokens() }
|
||||
private val LocalCardNudgeInfo = compositionLocalOf { CardNudgeInfo() }
|
||||
|
||||
// TAGS FOR TESTING
|
||||
private val CARDNUDGE = "CardNudge"
|
||||
private val ICON = "ICON"
|
||||
private val ACCENT_ICON = "Accent Icon"
|
||||
private val ACCENT_TEXT = "Accent Text"
|
||||
private val SUBTITLE = "Subtitle"
|
||||
private val ACTION_BUTTON = "Action Button"
|
||||
private val DISMISS_BUTTON = "Dismiss Button"
|
||||
private const val CARD_NUDGE = "CardNudge"
|
||||
private const val ICON = "ICON"
|
||||
private const val ACCENT_ICON = "Accent Icon"
|
||||
private const val ACCENT_TEXT = "Accent Text"
|
||||
private const val SUBTITLE = "Subtitle"
|
||||
private const val ACTION_BUTTON = "Action Button"
|
||||
private const val DISMISS_BUTTON = "Dismiss Button"
|
||||
|
||||
class CardNudgeMetaData(
|
||||
val message: String,
|
||||
|
@ -89,206 +86,192 @@ fun CardNudge(
|
|||
val token = cardNudgeTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.CardNudge] as CardNudgeTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalCardNudgeTokens provides token,
|
||||
LocalCardNudgeInfo provides CardNudgeInfo()
|
||||
) {
|
||||
val animationSpec = TweenSpec<Float>(durationMillis = 300)
|
||||
val state = rememberSwipeableState(initialValue = SwipeGesture.NONE, animationSpec) { true }
|
||||
val cardNudgeInfo = CardNudgeInfo()
|
||||
val animationSpec = TweenSpec<Float>(durationMillis = 300)
|
||||
val state = rememberSwipeableState(initialValue = SwipeGesture.NONE, animationSpec) { true }
|
||||
|
||||
BoxWithConstraints {
|
||||
val maxWidth = constraints.maxWidth.toFloat()
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
BoxWithConstraints {
|
||||
val maxWidth = constraints.maxWidth.toFloat()
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.offset { IntOffset(state.offset.value.roundToInt(), 0) }
|
||||
.background(
|
||||
getCardNudgeTokens().backgroundColor(getCardNudgeInfo()),
|
||||
shape
|
||||
)
|
||||
.then(
|
||||
if (outlineMode)
|
||||
Modifier.border(
|
||||
getCardNudgeTokens().borderSize(getCardNudgeInfo()),
|
||||
getCardNudgeTokens().borderStrokeColor(getCardNudgeInfo()),
|
||||
shape
|
||||
)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.swipeable(
|
||||
state,
|
||||
anchors = mapOf(
|
||||
-maxWidth to SwipeGesture.LEFT,
|
||||
0F to SwipeGesture.NONE,
|
||||
maxWidth to SwipeGesture.RIGHT
|
||||
),
|
||||
thresholds = { _, _ -> FractionalThreshold(0.3F) },
|
||||
orientation = Orientation.Horizontal,
|
||||
)
|
||||
.testTag(CARDNUDGE),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LaunchedEffect(state.currentValue) {
|
||||
if (state.currentValue == SwipeGesture.RIGHT) {
|
||||
metadata.rightSwipeGesture?.invoke()
|
||||
state.animateTo(SwipeGesture.NONE)
|
||||
} else if (state.currentValue == SwipeGesture.LEFT) {
|
||||
metadata.leftSwipeGesture?.invoke()
|
||||
state.animateTo(SwipeGesture.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.icon != null && metadata.icon.isIconAvailable()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(getCardNudgeTokens().leftIconBackgroundSize(getCardNudgeInfo()))
|
||||
.background(
|
||||
getCardNudgeTokens().iconBackgroundColor(getCardNudgeInfo()),
|
||||
CircleShape
|
||||
)
|
||||
.then(
|
||||
if (metadata.icon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClick = metadata.icon.onClick!!
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.testTag(ICON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
metadata.icon,
|
||||
modifier = Modifier
|
||||
.size(getCardNudgeTokens().leftIconSize(getCardNudgeInfo())),
|
||||
tint = getCardNudgeTokens().iconColor(getCardNudgeInfo())
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.offset { IntOffset(state.offset.value.roundToInt(), 0) }
|
||||
.background(
|
||||
token.backgroundColor(cardNudgeInfo),
|
||||
shape
|
||||
)
|
||||
.then(
|
||||
if (outlineMode)
|
||||
Modifier.border(
|
||||
token.borderSize(cardNudgeInfo),
|
||||
token.borderStrokeColor(cardNudgeInfo),
|
||||
shape
|
||||
)
|
||||
}
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.swipeable(
|
||||
state,
|
||||
anchors = mapOf(
|
||||
-maxWidth to SwipeGesture.LEFT,
|
||||
0F to SwipeGesture.NONE,
|
||||
maxWidth to SwipeGesture.RIGHT
|
||||
),
|
||||
thresholds = { _, _ -> FractionalThreshold(0.3F) },
|
||||
orientation = Orientation.Horizontal,
|
||||
)
|
||||
.testTag(CARD_NUDGE),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LaunchedEffect(state.currentValue) {
|
||||
if (state.currentValue == SwipeGesture.RIGHT) {
|
||||
metadata.rightSwipeGesture?.invoke()
|
||||
state.animateTo(SwipeGesture.NONE)
|
||||
} else if (state.currentValue == SwipeGesture.LEFT) {
|
||||
metadata.leftSwipeGesture?.invoke()
|
||||
state.animateTo(SwipeGesture.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
if (metadata.icon != null && metadata.icon.isIconAvailable()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
metadata.message,
|
||||
style = getCardNudgeTokens().titleTypography(getCardNudgeInfo()),
|
||||
)
|
||||
if (!metadata.subTitle.isNullOrBlank() || !metadata.accentText.isNullOrBlank() || metadata.accentIcon != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (metadata.accentIcon != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.testTag(ACCENT_ICON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
metadata.accentIcon,
|
||||
tint = getCardNudgeTokens().accentColor(getCardNudgeInfo())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.accentText.isNullOrBlank()) {
|
||||
Text(
|
||||
metadata.accentText,
|
||||
style = getCardNudgeTokens().accentTypography(getCardNudgeInfo()),
|
||||
modifier = Modifier.testTag(ACCENT_TEXT)
|
||||
)
|
||||
}
|
||||
|
||||
if (!metadata.subTitle.isNullOrBlank()) {
|
||||
Text(
|
||||
metadata.subTitle,
|
||||
style = getCardNudgeTokens().subtitleTypography(getCardNudgeInfo()),
|
||||
modifier = Modifier.testTag(SUBTITLE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
if (metadata.actionMetaData != null) {
|
||||
PillButton(
|
||||
metadata.actionMetaData,
|
||||
modifier = Modifier.testTag(ACTION_BUTTON),
|
||||
pillButtonTokens = object : PillButtonTokens() {
|
||||
@Composable
|
||||
override fun backgroundColor(pillButtonInfo: PillButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(),
|
||||
pressed = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(),
|
||||
focused = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun textColor(pillButtonInfo: PillButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value(),
|
||||
pressed = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value(),
|
||||
focused = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun typography(pillButtonInfo: PillButtonInfo): TextStyle {
|
||||
return aliasTokens.typography[AliasTokens.TypographyTokens.Body2Strong]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
|
||||
if (metadata.dismissOnClick != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClickLabel = "Dismiss",
|
||||
onClick = metadata.dismissOnClick
|
||||
)
|
||||
.testTag(DISMISS_BUTTON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MaterialIcon(
|
||||
Icons.Filled.Close,
|
||||
LocalContext.current.resources.getString(R.string.fluentui_dismiss_button),
|
||||
modifier = Modifier
|
||||
.size(getCardNudgeTokens().dismissIconSize(getCardNudgeInfo())),
|
||||
tint = getCardNudgeTokens().iconColor(getCardNudgeInfo())
|
||||
.size(token.leftIconBackgroundSize(cardNudgeInfo))
|
||||
.background(
|
||||
token.iconBackgroundColor(cardNudgeInfo),
|
||||
CircleShape
|
||||
)
|
||||
.then(
|
||||
if (metadata.icon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClick = metadata.icon.onClick!!
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.testTag(ICON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
metadata.icon,
|
||||
modifier = Modifier
|
||||
.size(token.leftIconSize(cardNudgeInfo)),
|
||||
tint = token.iconColor(cardNudgeInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
metadata.message,
|
||||
style = token.titleTypography(cardNudgeInfo),
|
||||
)
|
||||
if (!metadata.subTitle.isNullOrBlank() || !metadata.accentText.isNullOrBlank() || metadata.accentIcon != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (metadata.accentIcon != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.testTag(ACCENT_ICON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
metadata.accentIcon,
|
||||
tint = token.accentColor(cardNudgeInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!metadata.accentText.isNullOrBlank()) {
|
||||
Text(
|
||||
metadata.accentText,
|
||||
style = token.accentTypography(cardNudgeInfo),
|
||||
modifier = Modifier.testTag(ACCENT_TEXT)
|
||||
)
|
||||
}
|
||||
|
||||
if (!metadata.subTitle.isNullOrBlank()) {
|
||||
Text(
|
||||
metadata.subTitle,
|
||||
style = token.subtitleTypography(cardNudgeInfo),
|
||||
modifier = Modifier.testTag(SUBTITLE)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
}
|
||||
|
||||
if (metadata.actionMetaData != null) {
|
||||
PillButton(
|
||||
metadata.actionMetaData,
|
||||
modifier = Modifier.testTag(ACTION_BUTTON),
|
||||
pillButtonTokens = object : PillButtonTokens() {
|
||||
@Composable
|
||||
override fun backgroundColor(pillButtonInfo: PillButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(),
|
||||
pressed = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value(),
|
||||
focused = aliasTokens.brandBackgroundColor[AliasTokens.BrandBackgroundColorTokens.BrandBackgroundTint].value()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun textColor(pillButtonInfo: PillButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value(),
|
||||
pressed = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value(),
|
||||
focused = aliasTokens.brandForegroundColor[AliasTokens.BrandForegroundColorTokens.BrandForegroundTint].value()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun typography(pillButtonInfo: PillButtonInfo): TextStyle {
|
||||
return aliasTokens.typography[AliasTokens.TypographyTokens.Body2Strong]
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
}
|
||||
|
||||
if (metadata.dismissOnClick != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(20.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClickLabel = "Dismiss",
|
||||
onClick = metadata.dismissOnClick
|
||||
)
|
||||
.testTag(DISMISS_BUTTON),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
MaterialIcon(
|
||||
Icons.Filled.Close,
|
||||
LocalContext.current.resources.getString(R.string.fluentui_dismiss_button),
|
||||
modifier = Modifier
|
||||
.size(token.dismissIconSize(cardNudgeInfo)),
|
||||
tint = token.iconColor(cardNudgeInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getCardNudgeTokens(): CardNudgeTokens {
|
||||
return LocalCardNudgeTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getCardNudgeInfo(): CardNudgeInfo {
|
||||
return LocalCardNudgeInfo.current
|
||||
}
|
|
@ -32,15 +32,12 @@ import kotlinx.coroutines.sync.withLock
|
|||
import kotlin.coroutines.resume
|
||||
import androidx.compose.material.Icon as MaterialIcon
|
||||
|
||||
private val LocalSnackBarTokens = compositionLocalOf { SnackBarTokens() }
|
||||
private val LocalSnackBarInfo = compositionLocalOf { SnackBarInfo(SnackbarStyle.Neutral) }
|
||||
|
||||
// TAGS FOR TESTING
|
||||
private val SNACKBAR = "Snackbar"
|
||||
private val ICON = "ICON"
|
||||
private val SUBTITLE = "Subtitle"
|
||||
private val ACTION_BUTTON = "Action Button"
|
||||
private val DISMISS_BUTTON = "Dismiss Button"
|
||||
private const val SNACKBAR = "Snackbar"
|
||||
private const val ICON = "ICON"
|
||||
private const val SUBTITLE = "Subtitle"
|
||||
private const val ACTION_BUTTON = "Action Button"
|
||||
private const val DISMISS_BUTTON = "Dismiss Button"
|
||||
|
||||
class SnackbarMetadata(
|
||||
val message: String,
|
||||
|
@ -123,140 +120,126 @@ fun Snackbar(
|
|||
val token = snackbarTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Snackbar] as SnackBarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalSnackBarTokens provides token,
|
||||
LocalSnackBarInfo provides SnackBarInfo(metadata.style, !metadata.subTitle.isNullOrBlank())
|
||||
) {
|
||||
NotificationContainer(
|
||||
notificationMetadata = metadata,
|
||||
hasIcon = metadata.icon != null,
|
||||
hasAction = metadata.actionText != null,
|
||||
duration = metadata.duration
|
||||
) { alpha, scale ->
|
||||
Row(
|
||||
modifier
|
||||
.graphicsLayer(scaleX = scale.value, scaleY = scale.value, alpha = alpha.value)
|
||||
.padding(horizontal = 16.dp)
|
||||
.defaultMinSize(minHeight = 52.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(getSnackBarTokens().backgroundColor(getSnackBarInfo()))
|
||||
.testTag(SNACKBAR),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (metadata.icon != null && metadata.icon.isIconAvailable()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.testTag(ICON)
|
||||
.then(
|
||||
if (metadata.icon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClick = metadata.icon.onClick!!
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
metadata.icon,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
.size(getSnackBarTokens().leftIconSize(getSnackBarInfo())),
|
||||
tint = getSnackBarTokens().iconColor(getSnackBarInfo())
|
||||
val snackBarInfo = SnackBarInfo(metadata.style, !metadata.subTitle.isNullOrBlank())
|
||||
NotificationContainer(
|
||||
notificationMetadata = metadata,
|
||||
hasIcon = metadata.icon != null,
|
||||
hasAction = metadata.actionText != null,
|
||||
duration = metadata.duration
|
||||
) { alpha, scale ->
|
||||
Row(
|
||||
modifier
|
||||
.graphicsLayer(scaleX = scale.value, scaleY = scale.value, alpha = alpha.value)
|
||||
.padding(horizontal = 16.dp)
|
||||
.defaultMinSize(minHeight = 52.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(token.backgroundColor(snackBarInfo))
|
||||
.testTag(SNACKBAR),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (metadata.icon != null && metadata.icon.isIconAvailable()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.testTag(ICON)
|
||||
.then(
|
||||
if (metadata.icon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClick = metadata.icon.onClick!!
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
metadata.icon,
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
.size(token.leftIconSize(snackBarInfo)),
|
||||
tint = token.iconColor(snackBarInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.subTitle.isNullOrBlank()) {
|
||||
if (metadata.subTitle.isNullOrBlank()) {
|
||||
Text(
|
||||
text = metadata.message,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
|
||||
style = token.titleTypography(snackBarInfo)
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = metadata.message,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp),
|
||||
style = getSnackBarTokens().titleTypography(getSnackBarInfo())
|
||||
style = token.titleTypography(snackBarInfo)
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1F)
|
||||
.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = metadata.message,
|
||||
style = getSnackBarTokens().titleTypography(getSnackBarInfo())
|
||||
)
|
||||
Text(
|
||||
text = metadata.subTitle,
|
||||
style = getSnackBarTokens().subtitleTypography(getSnackBarInfo()),
|
||||
modifier = Modifier.testTag(SUBTITLE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.actionText != null) {
|
||||
Button(
|
||||
onClick = { metadata.clicked() },
|
||||
modifier = Modifier
|
||||
.testTag(ACTION_BUTTON)
|
||||
.then(
|
||||
if (!metadata.enableDismiss)
|
||||
Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
else
|
||||
Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
),
|
||||
text = metadata.actionText,
|
||||
style = ButtonStyle.TextButton,
|
||||
size = ButtonSize.Small,
|
||||
buttonTokens = object : ButtonTokens() {
|
||||
@Composable
|
||||
override fun textColor(buttonInfo: ButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = getSnackBarTokens().iconColor(getSnackBarInfo()),
|
||||
pressed = getSnackBarTokens().iconColor(getSnackBarInfo()),
|
||||
focused = getSnackBarTokens().iconColor(getSnackBarInfo()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = metadata.subTitle,
|
||||
style = token.subtitleTypography(snackBarInfo),
|
||||
modifier = Modifier.testTag(SUBTITLE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.enableDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClickLabel = "Dismiss",
|
||||
onClick = { metadata.dismiss() }
|
||||
if (metadata.actionText != null) {
|
||||
Button(
|
||||
onClick = { metadata.clicked() },
|
||||
modifier = Modifier
|
||||
.testTag(ACTION_BUTTON)
|
||||
.then(
|
||||
if (!metadata.enableDismiss)
|
||||
Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
else
|
||||
Modifier.padding(start = 16.dp, top = 12.dp, bottom = 12.dp)
|
||||
),
|
||||
text = metadata.actionText,
|
||||
style = ButtonStyle.TextButton,
|
||||
size = ButtonSize.Small,
|
||||
buttonTokens = object : ButtonTokens() {
|
||||
@Composable
|
||||
override fun textColor(buttonInfo: ButtonInfo): StateColor {
|
||||
return StateColor(
|
||||
rest = token.iconColor(snackBarInfo),
|
||||
pressed = token.iconColor(snackBarInfo),
|
||||
focused = token.iconColor(snackBarInfo),
|
||||
)
|
||||
.testTag(DISMISS_BUTTON)
|
||||
) {
|
||||
MaterialIcon(
|
||||
Icons.Filled.Close,
|
||||
"Dismiss",
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp)
|
||||
.size(getSnackBarTokens().dismissIconSize(getSnackBarInfo())),
|
||||
tint = getSnackBarTokens().iconColor(getSnackBarInfo())
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (metadata.enableDismiss) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = true,
|
||||
role = Role.Image,
|
||||
onClickLabel = "Dismiss",
|
||||
onClick = { metadata.dismiss() }
|
||||
)
|
||||
.testTag(DISMISS_BUTTON)
|
||||
) {
|
||||
MaterialIcon(
|
||||
Icons.Filled.Close,
|
||||
"Dismiss",
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, top = 12.dp, bottom = 12.dp, end = 16.dp)
|
||||
.size(token.dismissIconSize(snackBarInfo)),
|
||||
tint = token.iconColor(snackBarInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSnackBarTokens(): SnackBarTokens {
|
||||
return LocalSnackBarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSnackBarInfo(): SnackBarInfo {
|
||||
return LocalSnackBarInfo.current
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -30,9 +28,6 @@ import com.microsoft.fluentui.theme.token.ControlTokens
|
|||
import com.microsoft.fluentui.theme.token.FluentIcon
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.*
|
||||
|
||||
val LocalAvatarTokens = compositionLocalOf { AvatarTokens() }
|
||||
val LocalAvatarInfo = compositionLocalOf { AvatarInfo() }
|
||||
|
||||
const val IMAGE_TEST_TAG = "Image"
|
||||
const val ICON_TEST_TAG = "Icon"
|
||||
|
||||
|
@ -70,144 +65,139 @@ fun Avatar(
|
|||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Avatar] as AvatarTokens
|
||||
|
||||
val personInitials = person.getInitials()
|
||||
val avatarInfo = AvatarInfo(
|
||||
size, AvatarType.Person, person.isActive,
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAvatarTokens provides token,
|
||||
LocalAvatarInfo provides AvatarInfo(
|
||||
size, AvatarType.Person, person.isActive,
|
||||
person.status, person.isOOO, person.isImageAvailable(),
|
||||
personInitials.isNotEmpty(), person.getName(), cutoutStyle
|
||||
)
|
||||
val avatarSize = token.avatarSize(avatarInfo)
|
||||
val backgroundColor = token.backgroundColor(avatarInfo)
|
||||
val foregroundColor = token.foregroundColor(avatarInfo)
|
||||
val borders = token.borderStroke(avatarInfo)
|
||||
val fontTextStyle = token.fontTypography(avatarInfo)
|
||||
val cutoutCornerRadius = token.cutoutCornerRadius(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
|
||||
|
||||
person.status, person.isOOO, person.isImageAvailable(),
|
||||
personInitials.isNotEmpty(), person.getName(), cutoutStyle
|
||||
)
|
||||
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 ""
|
||||
}
|
||||
) {
|
||||
val avatarSize = getAvatarTokens().avatarSize(getAvatarInfo())
|
||||
val backgroundColor = getAvatarTokens().backgroundColor(getAvatarInfo())
|
||||
val foregroundColor = getAvatarTokens().foregroundColor(getAvatarInfo())
|
||||
val borders = getAvatarTokens().borderStroke(getAvatarInfo())
|
||||
val fontTextStyle = getAvatarTokens().fontTypography(getAvatarInfo())
|
||||
val cutoutCornerRadius = getAvatarTokens().cutoutCornerRadius(getAvatarInfo())
|
||||
val cutoutBackgroundColor =
|
||||
getAvatarTokens().cutoutBackgroundColor(avatarInfo = getAvatarInfo())
|
||||
val cutoutBorderColor = getAvatarTokens().cutoutBorderColor(avatarInfo = getAvatarInfo())
|
||||
val cutoutIconSize = getAvatarTokens().cutoutIconSize(avatarInfo = getAvatarInfo())
|
||||
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
|
||||
.then(modifier)
|
||||
.requiredSize(avatarSize)
|
||||
.background(backgroundColor, CircleShape), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.then(modifier)
|
||||
.requiredSize(avatarSize)
|
||||
.background(backgroundColor, CircleShape), contentAlignment = Alignment.Center
|
||||
) {
|
||||
when {
|
||||
person.image != null -> {
|
||||
Image(
|
||||
painter = painterResource(person.image), null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
}
|
||||
person.imageBitmap != null -> {
|
||||
Image(
|
||||
bitmap = person.imageBitmap, null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
}
|
||||
personInitials.isNotEmpty() -> {
|
||||
Text(personInitials,
|
||||
style = fontTextStyle,
|
||||
color = foregroundColor,
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics { })
|
||||
}
|
||||
else -> {
|
||||
isImageOrInitialsAvailable = false
|
||||
Icon(
|
||||
getAvatarTokens().icon(getAvatarInfo()),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.background(backgroundColor, CircleShape)
|
||||
.semantics {
|
||||
testTag = ICON_TEST_TAG
|
||||
},
|
||||
tint = foregroundColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (enableActivityRings)
|
||||
ActivityRing(radius = avatarSize / 2, borders)
|
||||
|
||||
if (isCutoutEnabled && isImageOrInitialsAvailable && cutoutIconSize > 0.dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(6.dp, 6.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.clip(shape = RoundedCornerShape(size = cutoutCornerRadius))
|
||||
) {
|
||||
if (cutoutIconDrawable != null) {
|
||||
Image(
|
||||
painter = painterResource(cutoutIconDrawable),
|
||||
modifier = Modifier
|
||||
.background(cutoutBackgroundColor)
|
||||
.border(
|
||||
2.dp,
|
||||
cutoutBorderColor,
|
||||
RoundedCornerShape(cutoutCornerRadius)
|
||||
)
|
||||
.padding(4.dp)
|
||||
.size(cutoutIconSize),
|
||||
contentDescription = cutoutContentDescription
|
||||
)
|
||||
} else if (cutoutIconImageVector != null) {
|
||||
Image(
|
||||
imageVector = cutoutIconImageVector,
|
||||
modifier = Modifier
|
||||
.background(cutoutBackgroundColor)
|
||||
.border(
|
||||
2.dp,
|
||||
cutoutBorderColor,
|
||||
RoundedCornerShape(cutoutCornerRadius)
|
||||
)
|
||||
.padding(4.dp)
|
||||
.size(cutoutIconSize),
|
||||
contentDescription = cutoutContentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCutoutEnabled && enablePresence) {
|
||||
val presenceOffset: DpOffset = getAvatarTokens().presenceOffset(getAvatarInfo())
|
||||
val image: FluentIcon = getAvatarTokens().presenceIcon(getAvatarInfo())
|
||||
when {
|
||||
person.image != null -> {
|
||||
Image(
|
||||
image.value(themeMode),
|
||||
null,
|
||||
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)
|
||||
painter = painterResource(person.image), null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
}
|
||||
person.imageBitmap != null -> {
|
||||
Image(
|
||||
bitmap = person.imageBitmap, null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(CircleShape)
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
}
|
||||
personInitials.isNotEmpty() -> {
|
||||
Text(personInitials,
|
||||
style = fontTextStyle,
|
||||
color = foregroundColor,
|
||||
modifier = Modifier
|
||||
.clearAndSetSemantics { })
|
||||
}
|
||||
else -> {
|
||||
isImageOrInitialsAvailable = false
|
||||
Icon(
|
||||
token.icon(avatarInfo),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.background(backgroundColor, CircleShape)
|
||||
.semantics {
|
||||
testTag = ICON_TEST_TAG
|
||||
},
|
||||
tint = foregroundColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (enableActivityRings)
|
||||
ActivityRing(radius = avatarSize / 2, borders)
|
||||
|
||||
if (isCutoutEnabled && isImageOrInitialsAvailable && cutoutIconSize > 0.dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(6.dp, 6.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
.clip(shape = RoundedCornerShape(size = cutoutCornerRadius))
|
||||
) {
|
||||
if (cutoutIconDrawable != null) {
|
||||
Image(
|
||||
painter = painterResource(cutoutIconDrawable),
|
||||
modifier = Modifier
|
||||
.background(cutoutBackgroundColor)
|
||||
.border(
|
||||
2.dp,
|
||||
cutoutBorderColor,
|
||||
RoundedCornerShape(cutoutCornerRadius)
|
||||
)
|
||||
.padding(4.dp)
|
||||
.size(cutoutIconSize),
|
||||
contentDescription = cutoutContentDescription
|
||||
)
|
||||
} else if (cutoutIconImageVector != null) {
|
||||
Image(
|
||||
imageVector = cutoutIconImageVector,
|
||||
modifier = Modifier
|
||||
.background(cutoutBackgroundColor)
|
||||
.border(
|
||||
2.dp,
|
||||
cutoutBorderColor,
|
||||
RoundedCornerShape(cutoutCornerRadius)
|
||||
)
|
||||
.padding(4.dp)
|
||||
.size(cutoutIconSize),
|
||||
contentDescription = cutoutContentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCutoutEnabled && enablePresence) {
|
||||
val presenceOffset: DpOffset = token.presenceOffset(avatarInfo)
|
||||
val image: FluentIcon = token.presenceIcon(avatarInfo)
|
||||
Image(
|
||||
image.value(themeMode),
|
||||
null,
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -234,77 +224,73 @@ fun Avatar(
|
|||
val token = avatarToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Avatar] as AvatarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAvatarTokens provides token,
|
||||
LocalAvatarInfo provides AvatarInfo(
|
||||
size, AvatarType.Group,
|
||||
isImageAvailable = group.isImageAvailable(),
|
||||
hasValidInitials = group.getInitials().isNotEmpty(),
|
||||
calculatedColorKey = group.groupName
|
||||
)
|
||||
val avatarInfo = AvatarInfo(
|
||||
size, AvatarType.Group,
|
||||
isImageAvailable = group.isImageAvailable(),
|
||||
hasValidInitials = group.getInitials().isNotEmpty(),
|
||||
calculatedColorKey = group.groupName
|
||||
)
|
||||
val avatarSize = token.avatarSize(avatarInfo)
|
||||
val cornerRadius = token.cornerRadius(avatarInfo)
|
||||
val fontTextStyle = token.fontTypography(avatarInfo)
|
||||
val backgroundColor = token.backgroundColor(avatarInfo)
|
||||
val foregroundColor = token.foregroundColor(avatarInfo)
|
||||
|
||||
var membersList = ""
|
||||
for (person in group.members)
|
||||
membersList += (person.firstName + person.lastName + "\n")
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.requiredSize(avatarSize)
|
||||
.semantics(mergeDescendants = false) {
|
||||
contentDescription =
|
||||
"Group Name ${group.getName()} ${group.members.size} members. $membersList"
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
val avatarSize = getAvatarTokens().avatarSize(getAvatarInfo())
|
||||
val cornerRadius = getAvatarTokens().cornerRadius(getAvatarInfo())
|
||||
val fontTextStyle = getAvatarTokens().fontTypography(getAvatarInfo())
|
||||
val backgroundColor = getAvatarTokens().backgroundColor(getAvatarInfo())
|
||||
val foregroundColor = getAvatarTokens().foregroundColor(getAvatarInfo())
|
||||
|
||||
var membersList = ""
|
||||
for (person in group.members)
|
||||
membersList += (person.firstName + person.lastName + "\n")
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.requiredSize(avatarSize)
|
||||
.semantics(mergeDescendants = false) {
|
||||
contentDescription =
|
||||
"Group Name ${group.getName()} ${group.members.size} members. $membersList"
|
||||
}, contentAlignment = Alignment.Center
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (group.image != null) {
|
||||
Image(
|
||||
painter = painterResource(group.image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
} else if (group.imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = group.imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
} else if (group.groupName.isNotEmpty()) {
|
||||
Text(group.getInitials(),
|
||||
style = fontTextStyle,
|
||||
color = foregroundColor,
|
||||
modifier = Modifier.clearAndSetSemantics { })
|
||||
} else {
|
||||
Icon(
|
||||
getAvatarTokens().icon(getAvatarInfo()),
|
||||
null,
|
||||
modifier = Modifier.semantics {
|
||||
testTag = ICON_TEST_TAG
|
||||
},
|
||||
tint = foregroundColor
|
||||
)
|
||||
}
|
||||
if (group.image != null) {
|
||||
Image(
|
||||
painter = painterResource(group.image),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
} else if (group.imageBitmap != null) {
|
||||
Image(
|
||||
bitmap = group.imageBitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(avatarSize)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.semantics {
|
||||
testTag = IMAGE_TEST_TAG
|
||||
}
|
||||
)
|
||||
} else if (group.groupName.isNotEmpty()) {
|
||||
Text(group.getInitials(),
|
||||
style = fontTextStyle,
|
||||
color = foregroundColor,
|
||||
modifier = Modifier.clearAndSetSemantics { })
|
||||
} else {
|
||||
Icon(
|
||||
token.icon(avatarInfo),
|
||||
null,
|
||||
modifier = Modifier.semantics {
|
||||
testTag = ICON_TEST_TAG
|
||||
},
|
||||
tint = foregroundColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -330,50 +316,36 @@ fun Avatar(
|
|||
val token = avatarToken
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.Avatar] as AvatarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAvatarTokens provides token,
|
||||
LocalAvatarInfo provides AvatarInfo(size, AvatarType.Overflow)
|
||||
val avatarInfo = AvatarInfo(size, AvatarType.Overflow)
|
||||
val avatarSize = token.avatarSize(avatarInfo)
|
||||
val borders = token.borderStroke(avatarInfo)
|
||||
val fontTextStyle = token.fontTypography(avatarInfo)
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.requiredSize(avatarSize)
|
||||
.semantics(mergeDescendants = false) {
|
||||
contentDescription = "+ $overflowCount Avatar More"
|
||||
}, contentAlignment = Alignment.Center
|
||||
) {
|
||||
val avatarSize = getAvatarTokens().avatarSize(getAvatarInfo())
|
||||
val borders = getAvatarTokens().borderStroke(getAvatarInfo())
|
||||
val fontTextStyle = getAvatarTokens().fontTypography(getAvatarInfo())
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.requiredSize(avatarSize)
|
||||
.semantics(mergeDescendants = false) {
|
||||
contentDescription = "+ $overflowCount Avatar More"
|
||||
}, contentAlignment = Alignment.Center
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.background(token.backgroundColor(avatarInfo))
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(CircleShape)
|
||||
.background(getAvatarTokens().backgroundColor(getAvatarInfo()))
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("+${overflowCount}",
|
||||
style = fontTextStyle,
|
||||
color = getAvatarTokens().foregroundColor(getAvatarInfo()),
|
||||
modifier = Modifier.clearAndSetSemantics { })
|
||||
}
|
||||
|
||||
if (enableActivityRings)
|
||||
ActivityRing(radius = avatarSize / 2, borders)
|
||||
Text("+${overflowCount}",
|
||||
style = fontTextStyle,
|
||||
color = token.foregroundColor(avatarInfo),
|
||||
modifier = Modifier.clearAndSetSemantics { })
|
||||
}
|
||||
|
||||
if (enableActivityRings)
|
||||
ActivityRing(radius = avatarSize / 2, borders)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAvatarTokens(): AvatarTokens {
|
||||
return LocalAvatarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAvatarInfo(): AvatarInfo {
|
||||
return LocalAvatarInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActivityRing(radius: Dp, borders: List<BorderStroke>) {
|
||||
val firstBorderMid = with(LocalDensity.current) { borders[0].width.toPx() / 2 }
|
||||
|
|
|
@ -13,7 +13,9 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.alpha
|
||||
|
@ -35,19 +37,6 @@ import com.microsoft.fluentui.util.getStringResource
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
val LocalAvatarCarouselTokens = compositionLocalOf { AvatarCarouselTokens() }
|
||||
val LocalAvatarCarouselInfo = compositionLocalOf { AvatarCarouselInfo() }
|
||||
|
||||
@Composable
|
||||
fun getAvatarCarouselTokens(): AvatarCarouselTokens {
|
||||
return LocalAvatarCarouselTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAvatarCarouselInfo(): AvatarCarouselInfo {
|
||||
return LocalAvatarCarouselInfo.current
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an AvatarCarousel. This is a horizontally scrollable bar which is made up of [AvatarCarouselItem].
|
||||
* Avatar Carousel internally is a group of [AvatarCarouselItem] which can be used to create onClick based Avatar buttons.
|
||||
|
@ -70,29 +59,27 @@ fun AvatarCarousel(
|
|||
) {
|
||||
val token = avatarCarouselTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AvatarCarousel] as AvatarCarouselTokens
|
||||
CompositionLocalProvider(
|
||||
LocalAvatarCarouselTokens provides token,
|
||||
LocalAvatarCarouselInfo provides AvatarCarouselInfo(size)
|
||||
) {
|
||||
val statusString = getStringResource(R.string.Status)
|
||||
val outOfOfficeString = getStringResource(R.string.Out_Of_Office)
|
||||
val activeString = getStringResource(R.string.Active)
|
||||
val inActiveString = getStringResource(R.string.Inactive)
|
||||
val scope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val avatarSize = getAvatarCarouselTokens().avatarSize(getAvatarCarouselInfo())
|
||||
val textStyle =
|
||||
getAvatarCarouselTokens().textTypography(getAvatarCarouselInfo())
|
||||
val subTextStyle =
|
||||
getAvatarCarouselTokens().subTextTypography(getAvatarCarouselInfo())
|
||||
val avatarTextPadding = getAvatarCarouselTokens().padding(getAvatarCarouselInfo())
|
||||
val bottomPadding = if (size == AvatarCarouselSize.Small) 8.dp else 0.dp
|
||||
val textColor = getAvatarCarouselTokens().textColor(getAvatarCarouselInfo())
|
||||
val subTextColor =
|
||||
getAvatarCarouselTokens().subTextColor(getAvatarCarouselInfo())
|
||||
val avatarCarouselInfo = AvatarCarouselInfo(size)
|
||||
val statusString = getStringResource(R.string.Status)
|
||||
val outOfOfficeString = getStringResource(R.string.Out_Of_Office)
|
||||
val activeString = getStringResource(R.string.Active)
|
||||
val inActiveString = getStringResource(R.string.Inactive)
|
||||
val scope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val avatarSize = token.avatarSize(avatarCarouselInfo)
|
||||
val textStyle =
|
||||
token.textTypography(avatarCarouselInfo)
|
||||
val subTextStyle =
|
||||
token.subTextTypography(avatarCarouselInfo)
|
||||
val avatarTextPadding = token.padding(avatarCarouselInfo)
|
||||
val bottomPadding = if (size == AvatarCarouselSize.Small) 8.dp else 0.dp
|
||||
val textColor = token.textColor(avatarCarouselInfo)
|
||||
val subTextColor =
|
||||
token.subTextColor(avatarCarouselInfo)
|
||||
|
||||
|
||||
LazyRow(state = lazyListState,
|
||||
LazyRow(
|
||||
state = lazyListState,
|
||||
modifier = modifier.draggable(
|
||||
orientation = Orientation.Horizontal,
|
||||
state = rememberDraggableState { delta ->
|
||||
|
@ -100,100 +87,101 @@ fun AvatarCarousel(
|
|||
lazyListState.scrollBy(-delta)
|
||||
}
|
||||
},
|
||||
)) {
|
||||
itemsIndexed(avatarList) { index, item ->
|
||||
val backgroundColor =
|
||||
getAvatarCarouselTokens().backgroundColor(getAvatarCarouselInfo())
|
||||
.getColorByState(
|
||||
enabled = item.enabled,
|
||||
selected = false,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
val nameString =
|
||||
if (size == AvatarCarouselSize.Large) "${item.person.getName()}. " else "${item.person.firstName}. "
|
||||
Column(
|
||||
Modifier
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(
|
||||
0,
|
||||
index - 2
|
||||
)
|
||||
)
|
||||
) {
|
||||
itemsIndexed(avatarList) { index, item ->
|
||||
val backgroundColor =
|
||||
token.backgroundColor(avatarCarouselInfo)
|
||||
.getColorByState(
|
||||
enabled = item.enabled,
|
||||
selected = false,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
)
|
||||
val nameString =
|
||||
if (size == AvatarCarouselSize.Large) "${item.person.getName()}. " else "${item.person.firstName}. "
|
||||
Column(
|
||||
Modifier
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(
|
||||
0,
|
||||
index - 2
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.requiredWidth(88.dp)
|
||||
.alpha(if (item.enabled) 1f else 0.7f)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
onClickLabel = null,
|
||||
enabled = item.enabled,
|
||||
onClick = item.onItemClick ?: {},
|
||||
role = Role.Button
|
||||
).testTag("item $index")
|
||||
.clearAndSetSemantics {
|
||||
contentDescription =
|
||||
nameString + "${if (enablePresence) "${statusString}, ${item.person.status}," else ""} " +
|
||||
"${if (enablePresence && item.person.isOOO) "${outOfOfficeString}," else ""} " +
|
||||
if (item.enableActivityRing) {
|
||||
if (item.person.isActive) activeString else inActiveString
|
||||
} else ""
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp),
|
||||
person = item.person,
|
||||
size = avatarSize,
|
||||
avatarToken = avatarTokens,
|
||||
enablePresence = enablePresence,
|
||||
enableActivityRings = item.enableActivityRing
|
||||
}
|
||||
.background(backgroundColor)
|
||||
.requiredWidth(88.dp)
|
||||
.alpha(if (item.enabled) 1f else 0.7f)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
onClickLabel = null,
|
||||
enabled = item.enabled,
|
||||
onClick = item.onItemClick ?: {},
|
||||
role = Role.Button
|
||||
)
|
||||
.testTag("item $index")
|
||||
.clearAndSetSemantics {
|
||||
contentDescription =
|
||||
nameString + "${if (enablePresence) "${statusString}, ${item.person.status}," else ""} " +
|
||||
"${if (enablePresence && item.person.isOOO) "${outOfOfficeString}," else ""} " +
|
||||
if (item.enableActivityRing) {
|
||||
if (item.person.isActive) activeString else inActiveString
|
||||
} else ""
|
||||
},
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Avatar(
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp),
|
||||
person = item.person,
|
||||
size = avatarSize,
|
||||
avatarToken = avatarTokens,
|
||||
enablePresence = enablePresence,
|
||||
enableActivityRings = item.enableActivityRing
|
||||
)
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
start = 2.dp,
|
||||
end = 2.dp,
|
||||
top = avatarTextPadding,
|
||||
bottom = bottomPadding
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
text = item.person.firstName,
|
||||
color = if (item.enabled) textColor.rest else textColor.disabled,
|
||||
style = textStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (size == AvatarCarouselSize.Large) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
start = 2.dp,
|
||||
end = 2.dp,
|
||||
top = avatarTextPadding,
|
||||
bottom = bottomPadding
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth()
|
||||
.padding(start = 2.dp, end = 2.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
text = item.person.firstName,
|
||||
color = if (item.enabled) textColor.rest else textColor.disabled,
|
||||
style = textStyle,
|
||||
text = item.person.lastName,
|
||||
color = if (item.enabled) subTextColor.rest else subTextColor.disabled,
|
||||
style = subTextStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (size == AvatarCarouselSize.Large) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 2.dp, end = 2.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.clearAndSetSemantics { },
|
||||
text = item.person.lastName,
|
||||
color = if (item.enabled) subTextColor.rest else subTextColor.disabled,
|
||||
style = subTextStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ package com.microsoft.fluentui.tokenized.persona
|
|||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
@ -15,9 +13,6 @@ import com.microsoft.fluentui.theme.token.ControlTokens
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.*
|
||||
import java.lang.Math.max
|
||||
|
||||
val LocalAvatarGroupTokens = compositionLocalOf { AvatarGroupTokens() }
|
||||
val LocalAvatarGroupInfo = compositionLocalOf { AvatarGroupInfo() }
|
||||
|
||||
const val DEFAULT_MAX_AVATAR = 5
|
||||
|
||||
/**
|
||||
|
@ -59,89 +54,75 @@ fun AvatarGroup(
|
|||
if (style == AvatarGroupStyle.Stack)
|
||||
enablePresence = false
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAvatarGroupTokens provides token,
|
||||
LocalAvatarGroupInfo provides AvatarGroupInfo(size, style)
|
||||
) {
|
||||
val spacing: MutableList<Int> = mutableListOf()
|
||||
for (i in 0 until visibleAvatar) {
|
||||
val person = group.members[i]
|
||||
if (i != 0) {
|
||||
spacing.add(with(LocalDensity.current) {
|
||||
getAvatarGroupTokens().spacing(getAvatarGroupInfo(), person.isActive)
|
||||
.roundToPx()
|
||||
})
|
||||
}
|
||||
}
|
||||
if (group.members.size > visibleAvatar || group.members.isEmpty()) {
|
||||
val avatarGroupInfo = AvatarGroupInfo(size, style)
|
||||
val spacing: MutableList<Int> = mutableListOf()
|
||||
for (i in 0 until visibleAvatar) {
|
||||
val person = group.members[i]
|
||||
if (i != 0) {
|
||||
spacing.add(with(LocalDensity.current) {
|
||||
getAvatarGroupTokens().spacing(getAvatarGroupInfo(), false).roundToPx()
|
||||
token.spacing(avatarGroupInfo, person.isActive)
|
||||
.roundToPx()
|
||||
})
|
||||
}
|
||||
}
|
||||
if (group.members.size > visibleAvatar || group.members.isEmpty()) {
|
||||
spacing.add(with(LocalDensity.current) {
|
||||
token.spacing(avatarGroupInfo, false).roundToPx()
|
||||
})
|
||||
}
|
||||
|
||||
val semanticModifier: Modifier = Modifier.semantics(true) {
|
||||
contentDescription =
|
||||
"Group Name: ${group.groupName}. Total ${group.members.size} members. "
|
||||
val semanticModifier: Modifier = Modifier.semantics(true) {
|
||||
contentDescription =
|
||||
"Group Name: ${group.groupName}. Total ${group.members.size} members. "
|
||||
}
|
||||
|
||||
Layout(modifier = modifier
|
||||
.padding(8.dp)
|
||||
.then(semanticModifier), content = {
|
||||
for (i in 0 until visibleAvatar) {
|
||||
val person = group.members[i]
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
if (group.members.size > visibleAvatar || group.members.isEmpty()) {
|
||||
Avatar(
|
||||
group.members.size - visibleAvatar, size = size,
|
||||
enableActivityRings = true, avatarToken = avatarToken
|
||||
)
|
||||
}
|
||||
}) { measurables, constraints ->
|
||||
val placeables = measurables.map { measurable ->
|
||||
measurable.measure(constraints)
|
||||
}
|
||||
|
||||
Layout(modifier = modifier
|
||||
.padding(8.dp)
|
||||
.then(semanticModifier), content = {
|
||||
for (i in 0 until visibleAvatar) {
|
||||
val person = group.members[i]
|
||||
var layoutHeight = 0
|
||||
var layoutWidth = 0
|
||||
placeables.forEach {
|
||||
layoutHeight = max(layoutHeight, it.height)
|
||||
layoutWidth += it.width
|
||||
}
|
||||
layoutWidth += spacing.sum()
|
||||
|
||||
var paddingModifier: Modifier = Modifier
|
||||
if (style == AvatarGroupStyle.Pile && person.isActive) {
|
||||
val padding = getAvatarGroupTokens().pilePadding(getAvatarGroupInfo())
|
||||
paddingModifier = paddingModifier.padding(start = padding, end = padding)
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}) { measurables, constraints ->
|
||||
val placeables = measurables.map { measurable ->
|
||||
measurable.measure(constraints)
|
||||
}
|
||||
|
||||
var layoutHeight = 0
|
||||
var layoutWidth = 0
|
||||
placeables.forEach {
|
||||
layoutHeight = max(layoutHeight, it.height)
|
||||
layoutWidth += it.width
|
||||
}
|
||||
layoutWidth += spacing.sum()
|
||||
|
||||
layout(layoutWidth, layoutHeight) {
|
||||
var xPosition = 0
|
||||
placeables.forEach { placeable ->
|
||||
placeable.placeRelative(y = 0, x = xPosition)
|
||||
if (placeable != placeables.last())
|
||||
xPosition += placeable.width + spacing[placeables.indexOf(placeable)]
|
||||
}
|
||||
layout(layoutWidth, layoutHeight) {
|
||||
var xPosition = 0
|
||||
placeables.forEach { placeable ->
|
||||
placeable.placeRelative(y = 0, x = xPosition)
|
||||
if (placeable != placeables.last())
|
||||
xPosition += placeable.width + spacing[placeables.indexOf(placeable)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAvatarGroupTokens(): AvatarGroupTokens {
|
||||
return LocalAvatarGroupTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getAvatarGroupInfo(): AvatarGroupInfo {
|
||||
return LocalAvatarGroupInfo.current
|
||||
}
|
|
@ -11,8 +11,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -29,9 +27,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipSize.Medium
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipStyle
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipTokens
|
||||
|
||||
private val LocalPersonaChipTokens = compositionLocalOf { PersonaChipTokens() }
|
||||
private val LocalPersonaChipInfo = compositionLocalOf { PersonaChipInfo() }
|
||||
|
||||
/**
|
||||
* [PersonaChip] is a compact representations of entities(most commonly, people)that can be types in, deleted or dragged easily
|
||||
*
|
||||
|
@ -61,89 +56,75 @@ fun PersonaChip(
|
|||
) {
|
||||
val token = personaChipTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PersonaChip] as PersonaChipTokens
|
||||
CompositionLocalProvider(
|
||||
LocalPersonaChipTokens provides token,
|
||||
LocalPersonaChipInfo provides PersonaChipInfo(
|
||||
style,
|
||||
enabled,
|
||||
size
|
||||
)
|
||||
) {
|
||||
val backgroundColor =
|
||||
getPersonaChipTokens().backgroundColor(personaChipInfo = getPersonaChipInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val textColor = getPersonaChipTokens().textColor(personaChipInfo = getPersonaChipInfo())
|
||||
val personaChipInfo = PersonaChipInfo(
|
||||
style,
|
||||
enabled,
|
||||
size
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(personaChipInfo = personaChipInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val typography = getPersonaChipTokens().typography(personaChipInfo = getPersonaChipInfo())
|
||||
val avatarSize = getPersonaChipTokens().avatarSize(personaChipInfo = getPersonaChipInfo())
|
||||
val verticalPadding =
|
||||
getPersonaChipTokens().verticalPadding(personaChipInfo = getPersonaChipInfo())
|
||||
val horizontalPadding =
|
||||
getPersonaChipTokens().horizontalPadding(personaChipInfo = getPersonaChipInfo())
|
||||
val avatarToTextSpacing =
|
||||
getPersonaChipTokens().avatarToTextSpacing(personaChipInfo = getPersonaChipInfo())
|
||||
val cornerRadius =
|
||||
getPersonaChipTokens().cornerRadius(personaChipInfo = getPersonaChipInfo())
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick ?: {},
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
val textColor = token.textColor(personaChipInfo = personaChipInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
{
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = horizontalPadding,
|
||||
vertical = verticalPadding
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(avatarToTextSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (size == Medium) {
|
||||
if (onCloseClick != null && selected) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onCloseClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_close),
|
||||
tint = textColor
|
||||
)
|
||||
} else {
|
||||
Avatar(person = person, size = avatarSize)
|
||||
}
|
||||
val typography = token.typography(personaChipInfo = personaChipInfo)
|
||||
val avatarSize = token.avatarSize(personaChipInfo = personaChipInfo)
|
||||
val verticalPadding =
|
||||
token.verticalPadding(personaChipInfo = personaChipInfo)
|
||||
val horizontalPadding =
|
||||
token.horizontalPadding(personaChipInfo = personaChipInfo)
|
||||
val avatarToTextSpacing =
|
||||
token.avatarToTextSpacing(personaChipInfo = personaChipInfo)
|
||||
val cornerRadius =
|
||||
token.cornerRadius(personaChipInfo = personaChipInfo)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick ?: {},
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
)
|
||||
{
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = horizontalPadding,
|
||||
vertical = verticalPadding
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(avatarToTextSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (size == Medium) {
|
||||
if (onCloseClick != null && selected) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onCloseClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_close),
|
||||
tint = textColor
|
||||
)
|
||||
} else {
|
||||
Avatar(person = person, size = avatarSize)
|
||||
}
|
||||
Text(
|
||||
text = person.getLabel(),
|
||||
color = textColor,
|
||||
style = typography
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = person.getLabel(),
|
||||
color = textColor,
|
||||
style = typography
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getPersonaChipTokens(): PersonaChipTokens {
|
||||
return LocalPersonaChipTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getPersonaChipInfo(): PersonaChipInfo {
|
||||
return LocalPersonaChipInfo.current
|
||||
}
|
|
@ -11,8 +11,6 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -28,9 +26,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.PersonaChipSize
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.SearchBarPersonaChipInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.SearchBarPersonaChipTokens
|
||||
|
||||
private val LocalSearchBarPersonaChipTokens = compositionLocalOf { SearchBarPersonaChipTokens() }
|
||||
private val LocalSearchBarPersonaChipInfo = compositionLocalOf { SearchBarPersonaChipInfo() }
|
||||
|
||||
/**
|
||||
* [SearchBarPersonaChip] is a compact representations of entities(most commonly, people)that can be types in, deleted or dragged easily
|
||||
*
|
||||
|
@ -60,93 +55,79 @@ fun SearchBarPersonaChip(
|
|||
) {
|
||||
val token = searchbarPersonaChipTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.SearchBarPersonaChip] as SearchBarPersonaChipTokens
|
||||
CompositionLocalProvider(
|
||||
LocalSearchBarPersonaChipTokens provides token,
|
||||
LocalSearchBarPersonaChipInfo provides SearchBarPersonaChipInfo(
|
||||
style,
|
||||
enabled,
|
||||
size = size
|
||||
)
|
||||
) {
|
||||
val backgroundColor =
|
||||
getSearchBarPersonaChipTokens().backgroundColor(searchBarPersonaChipInfo = getSearchBarPersonaChipInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val textColor =
|
||||
getSearchBarPersonaChipTokens().textColor(searchBarPersonaChipInfo = getSearchBarPersonaChipInfo())
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val fontStyle =
|
||||
getSearchBarPersonaChipTokens().typography(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val avatarSize =
|
||||
getSearchBarPersonaChipTokens().avatarSize(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val verticalPadding =
|
||||
getSearchBarPersonaChipTokens().verticalPadding(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val horizontalPadding =
|
||||
getSearchBarPersonaChipTokens().horizontalPadding(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val avatarToTextSpacing =
|
||||
getSearchBarPersonaChipTokens().avatarToTextSpacing(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val cornerRadius =
|
||||
getSearchBarPersonaChipTokens().cornerRadius(personaChipInfo = getSearchBarPersonaChipInfo())
|
||||
val searchBarPersonaChipInfo = SearchBarPersonaChipInfo(
|
||||
style,
|
||||
enabled,
|
||||
size = size
|
||||
)
|
||||
val backgroundColor =
|
||||
token.backgroundColor(searchBarPersonaChipInfo = searchBarPersonaChipInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val textColor =
|
||||
token.textColor(searchBarPersonaChipInfo = searchBarPersonaChipInfo)
|
||||
.getColorByState(
|
||||
enabled = enabled, selected = selected, interactionSource = interactionSource
|
||||
)
|
||||
val fontStyle =
|
||||
token.typography(personaChipInfo = searchBarPersonaChipInfo)
|
||||
val avatarSize =
|
||||
token.avatarSize(personaChipInfo = searchBarPersonaChipInfo)
|
||||
val verticalPadding =
|
||||
token.verticalPadding(personaChipInfo = searchBarPersonaChipInfo)
|
||||
val horizontalPadding =
|
||||
token.horizontalPadding(personaChipInfo = searchBarPersonaChipInfo)
|
||||
val avatarToTextSpacing =
|
||||
token.avatarToTextSpacing(personaChipInfo = searchBarPersonaChipInfo)
|
||||
val cornerRadius =
|
||||
token.cornerRadius(personaChipInfo = searchBarPersonaChipInfo)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick ?: {},
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
)
|
||||
{
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = horizontalPadding,
|
||||
vertical = verticalPadding
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(avatarToTextSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (size == PersonaChipSize.Medium) {
|
||||
if (onCloseClick != null && selected) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onCloseClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_close),
|
||||
tint = textColor
|
||||
)
|
||||
} else {
|
||||
Avatar(person = person, size = avatarSize)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
onClick = onClick ?: {},
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple()
|
||||
)
|
||||
)
|
||||
{
|
||||
Row(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = horizontalPadding,
|
||||
vertical = verticalPadding
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(avatarToTextSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (size == PersonaChipSize.Medium) {
|
||||
if (onCloseClick != null && selected) {
|
||||
Icon(
|
||||
Icons.Filled.Close,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clickable(
|
||||
enabled = true,
|
||||
onClick = onCloseClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_close),
|
||||
tint = textColor
|
||||
)
|
||||
} else {
|
||||
Avatar(person = person, size = avatarSize)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 2.dp),//Vertically center align text
|
||||
text = person.getLabel(),
|
||||
color = textColor,
|
||||
style = fontStyle
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(bottom = 2.dp),//Vertically center align text
|
||||
text = person.getLabel(),
|
||||
color = textColor,
|
||||
style = fontStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSearchBarPersonaChipTokens(): SearchBarPersonaChipTokens {
|
||||
return LocalSearchBarPersonaChipTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSearchBarPersonaChipInfo(): SearchBarPersonaChipInfo {
|
||||
return LocalSearchBarPersonaChipInfo.current
|
||||
}
|
|
@ -2,15 +2,8 @@ package com.microsoft.fluentui.tokenized.persona
|
|||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import com.microsoft.fluentui.theme.token.StateColor
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.AvatarSize
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.AvatarStatus
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
|
@ -5,8 +5,6 @@ import androidx.compose.foundation.Canvas
|
|||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
|
@ -24,19 +22,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.CircularProgressIndicato
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.CircularProgressIndicatorTokens
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
|
||||
val LocalCircularProgressIndicatorTokens = compositionLocalOf { CircularProgressIndicatorTokens() }
|
||||
val LocalCircularProgressIndicatorInfo = compositionLocalOf { CircularProgressIndicatorInfo() }
|
||||
|
||||
@Composable
|
||||
fun getCircularProgressIndicatorTokens(): CircularProgressIndicatorTokens {
|
||||
return LocalCircularProgressIndicatorTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getCircularProgressIndicatorInfo(): CircularProgressIndicatorInfo {
|
||||
return LocalCircularProgressIndicatorInfo.current
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Determinate Circular Progress Indicator
|
||||
*
|
||||
|
@ -57,51 +42,47 @@ fun CircularProgressIndicator(
|
|||
) {
|
||||
val tokens = circularProgressIndicatorTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.CircularProgressIndicator] as CircularProgressIndicatorTokens
|
||||
CompositionLocalProvider(
|
||||
LocalCircularProgressIndicatorTokens provides tokens,
|
||||
LocalCircularProgressIndicatorInfo provides CircularProgressIndicatorInfo(
|
||||
circularProgressIndicatorSize = size,
|
||||
style = style
|
||||
val circularProgressIndicatorInfo = CircularProgressIndicatorInfo(
|
||||
circularProgressIndicatorSize = size,
|
||||
style = style
|
||||
)
|
||||
val currentProgress = animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f..1f),
|
||||
animationSpec = tween(
|
||||
delayMillis = 0,
|
||||
durationMillis = 750,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
val circularProgressIndicatorColor =
|
||||
tokens.color(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val circularProgressIndicatorSize =
|
||||
tokens.size(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val circularProgressIndicatorStrokeWidth =
|
||||
tokens.strokeWidth(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val indicatorSizeInPx = dpToPx(circularProgressIndicatorSize)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.requiredSize(circularProgressIndicatorSize)
|
||||
.progressSemantics(progress)
|
||||
) {
|
||||
val currentProgress = animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f..1f),
|
||||
animationSpec = tween(
|
||||
delayMillis = 0,
|
||||
durationMillis = 750,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
drawArc(
|
||||
circularProgressIndicatorColor,
|
||||
-90f,
|
||||
currentProgress.value * 360,
|
||||
false,
|
||||
size = Size(
|
||||
indicatorSizeInPx,
|
||||
indicatorSizeInPx
|
||||
),
|
||||
style = Stroke(dpToPx(circularProgressIndicatorStrokeWidth), cap = StrokeCap.Round)
|
||||
)
|
||||
val circularProgressIndicatorColor =
|
||||
getCircularProgressIndicatorTokens().color(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val circularProgressIndicatorSize =
|
||||
getCircularProgressIndicatorTokens().size(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val circularProgressIndicatorStrokeWidth =
|
||||
getCircularProgressIndicatorTokens().strokeWidth(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val indicatorSizeInPx = dpToPx(circularProgressIndicatorSize)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.requiredSize(circularProgressIndicatorSize)
|
||||
.progressSemantics(progress)
|
||||
) {
|
||||
drawArc(
|
||||
circularProgressIndicatorColor,
|
||||
-90f,
|
||||
currentProgress.value * 360,
|
||||
false,
|
||||
size = Size(
|
||||
indicatorSizeInPx,
|
||||
indicatorSizeInPx
|
||||
),
|
||||
style = Stroke(dpToPx(circularProgressIndicatorStrokeWidth), cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,61 +104,57 @@ fun CircularProgressIndicator(
|
|||
) {
|
||||
val tokens = circularProgressIndicatorTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.CircularProgressIndicator] as CircularProgressIndicatorTokens
|
||||
CompositionLocalProvider(
|
||||
LocalCircularProgressIndicatorTokens provides tokens,
|
||||
LocalCircularProgressIndicatorInfo provides CircularProgressIndicatorInfo(
|
||||
circularProgressIndicatorSize = size,
|
||||
style = style
|
||||
val circularProgressIndicatorInfo = CircularProgressIndicatorInfo(
|
||||
circularProgressIndicatorSize = size,
|
||||
style = style
|
||||
)
|
||||
val circularProgressIndicatorColor =
|
||||
tokens.color(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val circularProgressIndicatorSize =
|
||||
tokens.size(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val circularProgressIndicatorStrokeWidth =
|
||||
tokens.strokeWidth(
|
||||
circularProgressIndicatorInfo
|
||||
)
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val startAngle by infiniteTransition.animateFloat(
|
||||
0f,
|
||||
360f,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
)
|
||||
val indicatorSizeInPx = dpToPx(circularProgressIndicatorSize)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.requiredSize(circularProgressIndicatorSize)
|
||||
.progressSemantics()
|
||||
.rotate(startAngle)
|
||||
) {
|
||||
val circularProgressIndicatorColor =
|
||||
getCircularProgressIndicatorTokens().color(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val circularProgressIndicatorSize =
|
||||
getCircularProgressIndicatorTokens().size(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val circularProgressIndicatorStrokeWidth =
|
||||
getCircularProgressIndicatorTokens().strokeWidth(
|
||||
getCircularProgressIndicatorInfo()
|
||||
)
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val startAngle by infiniteTransition.animateFloat(
|
||||
drawArc(
|
||||
Brush.sweepGradient(
|
||||
0f to Color.Transparent,
|
||||
0.6f to circularProgressIndicatorColor
|
||||
),
|
||||
0f,
|
||||
360f,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
270f,
|
||||
false,
|
||||
size = Size(
|
||||
indicatorSizeInPx, indicatorSizeInPx
|
||||
),
|
||||
style = Stroke(dpToPx(circularProgressIndicatorStrokeWidth))
|
||||
)
|
||||
drawCircle(
|
||||
color = circularProgressIndicatorColor,
|
||||
radius = dpToPx(circularProgressIndicatorStrokeWidth) / 2,
|
||||
center = Offset(indicatorSizeInPx / 2, 0f)
|
||||
)
|
||||
val indicatorSizeInPx = dpToPx(circularProgressIndicatorSize)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.requiredSize(circularProgressIndicatorSize)
|
||||
.progressSemantics()
|
||||
.rotate(startAngle)
|
||||
) {
|
||||
drawArc(
|
||||
Brush.sweepGradient(
|
||||
0f to Color.Transparent,
|
||||
0.6f to circularProgressIndicatorColor
|
||||
),
|
||||
0f,
|
||||
270f,
|
||||
false,
|
||||
size = Size(
|
||||
indicatorSizeInPx, indicatorSizeInPx
|
||||
),
|
||||
style = Stroke(dpToPx(circularProgressIndicatorStrokeWidth))
|
||||
)
|
||||
drawCircle(
|
||||
color = circularProgressIndicatorColor,
|
||||
radius = dpToPx(circularProgressIndicatorStrokeWidth) / 2,
|
||||
center = Offset(indicatorSizeInPx / 2, 0f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,8 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.progressSemantics
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
|
@ -20,19 +18,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.LinearProgressIndicatorI
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.LinearProgressIndicatorTokens
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
|
||||
val LocalLinearProgressIndicatorTokens = compositionLocalOf { LinearProgressIndicatorTokens() }
|
||||
val LocalLinearProgressIndicatorInfo = compositionLocalOf { LinearProgressIndicatorInfo() }
|
||||
|
||||
@Composable
|
||||
fun getLinearProgressIndicatorTokens(): LinearProgressIndicatorTokens {
|
||||
return LocalLinearProgressIndicatorTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getLinearProgressIndicatorInfo(): LinearProgressIndicatorInfo {
|
||||
return LocalLinearProgressIndicatorInfo.current
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Determinate Linear Progress Indicator
|
||||
*
|
||||
|
@ -45,65 +30,56 @@ fun getLinearProgressIndicatorInfo(): LinearProgressIndicatorInfo {
|
|||
@Composable
|
||||
fun LinearProgressIndicator(
|
||||
progress: Float,
|
||||
linearProgressIndicatorHeight: LinearProgressIndicatorHeight = LinearProgressIndicatorHeight.XXXSmall,
|
||||
modifier: Modifier = Modifier,
|
||||
linearProgressIndicatorHeight: LinearProgressIndicatorHeight = LinearProgressIndicatorHeight.XXXSmall,
|
||||
linearProgressIndicatorTokens: LinearProgressIndicatorTokens? = null
|
||||
) {
|
||||
val tokens = linearProgressIndicatorTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.LinearProgressIndicator] as LinearProgressIndicatorTokens
|
||||
CompositionLocalProvider(
|
||||
LocalLinearProgressIndicatorTokens provides tokens,
|
||||
LocalLinearProgressIndicatorInfo provides LinearProgressIndicatorInfo(
|
||||
linearProgressIndicatorHeight = linearProgressIndicatorHeight
|
||||
val linearProgressIndicatorInfo = LinearProgressIndicatorInfo(
|
||||
linearProgressIndicatorHeight = linearProgressIndicatorHeight
|
||||
)
|
||||
val currentProgress = animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f..1f), animationSpec = tween(
|
||||
delayMillis = 0, durationMillis = 1000, easing = LinearOutSlowInEasing
|
||||
)
|
||||
)
|
||||
val linearProgressIndicatorStrokeWidth = tokens.strokeWidth(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
val linearProgressIndicatorBackgroundColor =
|
||||
tokens.backgroundColor(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
val linearProgressIndicatorColor = tokens.color(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(linearProgressIndicatorStrokeWidth)
|
||||
.progressSemantics(progress)
|
||||
) {
|
||||
val currentProgress = animateFloatAsState(
|
||||
targetValue = progress.coerceIn(0f..1f),
|
||||
animationSpec = tween(
|
||||
delayMillis = 0,
|
||||
durationMillis = 1000,
|
||||
easing = LinearOutSlowInEasing
|
||||
)
|
||||
val strokeWidth = dpToPx(linearProgressIndicatorStrokeWidth)
|
||||
val yOffset = strokeWidth / 2
|
||||
val isLtr = layoutDirection == LayoutDirection.Ltr
|
||||
val barStart = (if (isLtr) 0f else size.width)
|
||||
val barEnd = (if (isLtr) size.width else 0f)
|
||||
drawLine(
|
||||
linearProgressIndicatorBackgroundColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(barEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
val progressIndicatorWidth = currentProgress.value * size.width
|
||||
val indicatorLineEnd =
|
||||
if (isLtr) progressIndicatorWidth else size.width - progressIndicatorWidth
|
||||
drawLine(
|
||||
linearProgressIndicatorColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(indicatorLineEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
val linearProgressIndicatorHeight =
|
||||
getLinearProgressIndicatorTokens().strokeWidth(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
val linearProgressIndicatorBackgroundColor =
|
||||
getLinearProgressIndicatorTokens().backgroundColor(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
val linearProgressIndicatorColor =
|
||||
getLinearProgressIndicatorTokens().color(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(linearProgressIndicatorHeight)
|
||||
.progressSemantics(progress)
|
||||
) {
|
||||
val strokeWidth = dpToPx(linearProgressIndicatorHeight)
|
||||
val yOffset = strokeWidth / 2
|
||||
val isLtr = layoutDirection == LayoutDirection.Ltr
|
||||
val barStart = (if (isLtr) 0f else size.width)
|
||||
val barEnd = (if (isLtr) size.width else 0f)
|
||||
drawLine(
|
||||
linearProgressIndicatorBackgroundColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(barEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
val progressIndicatorWidth = currentProgress.value * size.width
|
||||
val indicatorLineEnd =
|
||||
if (isLtr) progressIndicatorWidth else size.width - progressIndicatorWidth
|
||||
drawLine(
|
||||
linearProgressIndicatorColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(indicatorLineEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,89 +93,74 @@ fun LinearProgressIndicator(
|
|||
*/
|
||||
@Composable
|
||||
fun LinearProgressIndicator(
|
||||
linearProgressIndicatorHeight: LinearProgressIndicatorHeight = LinearProgressIndicatorHeight.XXXSmall,
|
||||
modifier: Modifier = Modifier,
|
||||
linearProgressIndicatorHeight: LinearProgressIndicatorHeight = LinearProgressIndicatorHeight.XXXSmall,
|
||||
linearProgressIndicatorTokens: LinearProgressIndicatorTokens? = null
|
||||
) {
|
||||
val tokens = linearProgressIndicatorTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.LinearProgressIndicator] as LinearProgressIndicatorTokens
|
||||
CompositionLocalProvider(
|
||||
LocalLinearProgressIndicatorTokens provides tokens,
|
||||
LocalLinearProgressIndicatorInfo provides LinearProgressIndicatorInfo(
|
||||
linearProgressIndicatorHeight = linearProgressIndicatorHeight
|
||||
)
|
||||
val linearProgressIndicatorInfo = LinearProgressIndicatorInfo(
|
||||
linearProgressIndicatorHeight = linearProgressIndicatorHeight
|
||||
)
|
||||
val linearProgressIndicatorStrokeWidth = tokens.strokeWidth(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
val linearProgressIndicatorBackgroundColor = tokens.backgroundColor(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
val linearProgressIndicatorColor = tokens.color(
|
||||
linearProgressIndicatorInfo
|
||||
)
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val animationDuration = (1750 * 0.5f).toInt()
|
||||
val headAnimationDelay = 0
|
||||
val tailAnimationDelay = (animationDuration * 0.5f).toInt()
|
||||
val indicatorHead by infiniteTransition.animateFloat(
|
||||
0f, 1f, infiniteRepeatable(animation = keyframes {
|
||||
durationMillis = animationDuration + 500
|
||||
0f at headAnimationDelay with FastOutSlowInEasing
|
||||
1f at animationDuration + headAnimationDelay
|
||||
})
|
||||
)
|
||||
val indicatorTail by infiniteTransition.animateFloat(
|
||||
0f, 1f, infiniteRepeatable(animation = keyframes {
|
||||
durationMillis = animationDuration + 500
|
||||
0f at tailAnimationDelay with FastOutSlowInEasing
|
||||
1f at animationDuration + tailAnimationDelay
|
||||
})
|
||||
)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.progressSemantics()
|
||||
.requiredHeight(linearProgressIndicatorStrokeWidth)
|
||||
) {
|
||||
val linearProgressIndicatorHeight =
|
||||
getLinearProgressIndicatorTokens().strokeWidth(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
val linearProgressIndicatorBackgroundColor =
|
||||
getLinearProgressIndicatorTokens().backgroundColor(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
val linearProgressIndicatorColor =
|
||||
getLinearProgressIndicatorTokens().color(
|
||||
getLinearProgressIndicatorInfo()
|
||||
)
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val animationDuration = (1750 * 0.5f).toInt()
|
||||
val headAnimationDelay = 0
|
||||
val tailAnimationDelay = (animationDuration * 0.5f).toInt()
|
||||
val indicatorHead by infiniteTransition.animateFloat(
|
||||
0f,
|
||||
1f,
|
||||
infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = animationDuration + 500
|
||||
0f at headAnimationDelay with FastOutSlowInEasing
|
||||
1f at animationDuration + headAnimationDelay
|
||||
}
|
||||
)
|
||||
val strokeWidth = dpToPx(linearProgressIndicatorStrokeWidth)
|
||||
val yOffset = strokeWidth / 2
|
||||
val isLtr = layoutDirection == LayoutDirection.Ltr
|
||||
val barStart = (if (isLtr) 0f else size.width)
|
||||
val barEnd = (if (isLtr) size.width else 0f)
|
||||
drawLine(
|
||||
linearProgressIndicatorBackgroundColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(barEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
val indicatorTail by infiniteTransition.animateFloat(
|
||||
0f,
|
||||
1f,
|
||||
infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
durationMillis = animationDuration + 500
|
||||
0f at tailAnimationDelay with FastOutSlowInEasing
|
||||
1f at animationDuration + tailAnimationDelay
|
||||
}
|
||||
)
|
||||
val progressIndicatorStart =
|
||||
(if (isLtr) indicatorHead * size.width else (size.width) - indicatorHead * size.width)
|
||||
val progressIndicatorEnd =
|
||||
(if (isLtr) indicatorTail * (size.width) else (size.width) - indicatorTail * (size.width))
|
||||
drawLine(
|
||||
Brush.linearGradient(
|
||||
0f to linearProgressIndicatorBackgroundColor,
|
||||
0.5f to linearProgressIndicatorColor,
|
||||
1.0f to linearProgressIndicatorBackgroundColor,
|
||||
start = Offset(progressIndicatorStart, yOffset),
|
||||
end = Offset(progressIndicatorEnd, yOffset)
|
||||
),
|
||||
Offset(progressIndicatorStart, yOffset),
|
||||
Offset(progressIndicatorEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
Canvas(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.progressSemantics()
|
||||
.requiredHeight(linearProgressIndicatorHeight)
|
||||
) {
|
||||
val strokeWidth = dpToPx(linearProgressIndicatorHeight)
|
||||
val yOffset = strokeWidth / 2
|
||||
val isLtr = layoutDirection == LayoutDirection.Ltr
|
||||
val barStart = (if (isLtr) 0f else size.width)
|
||||
val barEnd = (if (isLtr) size.width else 0f)
|
||||
drawLine(
|
||||
linearProgressIndicatorBackgroundColor,
|
||||
Offset(barStart, yOffset),
|
||||
Offset(barEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
val progressIndicatorStart =
|
||||
(if (isLtr) indicatorHead * size.width else (size.width) - indicatorHead * size.width)
|
||||
val progressIndicatorEnd =
|
||||
(if (isLtr) indicatorTail * (size.width) else (size.width) - indicatorTail * (size.width))
|
||||
drawLine(
|
||||
Brush.linearGradient(
|
||||
0f to linearProgressIndicatorBackgroundColor,
|
||||
0.5f to linearProgressIndicatorColor,
|
||||
1.0f to linearProgressIndicatorBackgroundColor,
|
||||
start = Offset(progressIndicatorStart, yOffset),
|
||||
end = Offset(progressIndicatorEnd, yOffset)
|
||||
),
|
||||
Offset(progressIndicatorStart, yOffset),
|
||||
Offset(progressIndicatorEnd, yOffset),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.microsoft.fluentui.tokenized.progress
|
||||
package com.microsoft.fluentui.tokenized.shimmer
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.width
|
|||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -22,17 +20,12 @@ import androidx.compose.ui.unit.LayoutDirection
|
|||
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.ShimmerInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.ShimmerShape
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.ShimmerTokens
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
val LocalShimmerTokens = compositionLocalOf { ShimmerTokens() }
|
||||
|
||||
@Composable
|
||||
fun getShimmerTokens(): ShimmerTokens {
|
||||
return LocalShimmerTokens.current
|
||||
}
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* Create a Shimmer effect
|
||||
|
@ -44,8 +37,8 @@ fun getShimmerTokens(): ShimmerTokens {
|
|||
*/
|
||||
@Composable
|
||||
fun Shimmer(
|
||||
shape: ShimmerShape = ShimmerShape.Box,
|
||||
modifier: Modifier = Modifier,
|
||||
shape: ShimmerShape = ShimmerShape.Box,
|
||||
shimmerTokens: ShimmerTokens? = null
|
||||
) {
|
||||
val tokens = shimmerTokens
|
||||
|
@ -54,51 +47,48 @@ fun Shimmer(
|
|||
val screenHeight = dpToPx(configuration.screenHeightDp.dp)
|
||||
val screenWidth = dpToPx(configuration.screenWidthDp.dp)
|
||||
val diagonal =
|
||||
Math.sqrt((screenHeight * screenHeight + screenWidth * screenWidth).toDouble()).toFloat()
|
||||
CompositionLocalProvider(
|
||||
LocalShimmerTokens provides tokens
|
||||
) {
|
||||
val shimmerBackgroundColor =
|
||||
getShimmerTokens().color()
|
||||
val shimmerKnockoutEffectColor = getShimmerTokens().knockoutEffectColor()
|
||||
val cornerRadius =
|
||||
dpToPx(getShimmerTokens().cornerRadius())
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val initialValue = if(isLtr) 0f else screenWidth
|
||||
val targetValue = if(isLtr) diagonal else 0f
|
||||
val shimmerEffect by infiniteTransition.animateFloat(
|
||||
initialValue,
|
||||
targetValue,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
easing = LinearEasing
|
||||
)
|
||||
sqrt((screenHeight * screenHeight + screenWidth * screenWidth).toDouble()).toFloat()
|
||||
val shimmerInfo = ShimmerInfo(shape = shape)
|
||||
val shimmerBackgroundColor =
|
||||
tokens.color(shimmerInfo)
|
||||
val shimmerKnockoutEffectColor = tokens.knockoutEffectColor(shimmerInfo)
|
||||
val cornerRadius =
|
||||
dpToPx(tokens.cornerRadius(shimmerInfo))
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||
val initialValue = if (isLtr) 0f else screenWidth
|
||||
val targetValue = if (isLtr) diagonal else 0f
|
||||
val shimmerEffect by infiniteTransition.animateFloat(
|
||||
initialValue,
|
||||
targetValue,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 1000,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
val gradientColor = Brush.linearGradient(
|
||||
0f to shimmerBackgroundColor,
|
||||
0.5f to shimmerKnockoutEffectColor,
|
||||
1.0f to shimmerBackgroundColor,
|
||||
start = Offset.Zero,
|
||||
end = Offset(shimmerEffect.absoluteValue, shimmerEffect.absoluteValue)
|
||||
)
|
||||
val gradientColor = Brush.linearGradient(
|
||||
0f to shimmerBackgroundColor,
|
||||
0.5f to shimmerKnockoutEffectColor,
|
||||
1.0f to shimmerBackgroundColor,
|
||||
start = Offset.Zero,
|
||||
end = Offset(shimmerEffect.absoluteValue, shimmerEffect.absoluteValue)
|
||||
)
|
||||
if (shape == ShimmerShape.Box) {
|
||||
Spacer(
|
||||
modifier = modifier
|
||||
.width(240.dp)
|
||||
.height(12.dp)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(gradientColor)
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = modifier
|
||||
.size(60.dp)
|
||||
.clip(CircleShape)
|
||||
.background(gradientColor)
|
||||
)
|
||||
if (shape == ShimmerShape.Box) {
|
||||
Spacer(
|
||||
modifier = modifier
|
||||
.width(240.dp)
|
||||
.height(12.dp)
|
||||
.clip(RoundedCornerShape(cornerRadius))
|
||||
.background(gradientColor)
|
||||
)
|
||||
} else {
|
||||
Spacer(
|
||||
modifier = modifier
|
||||
.size(60.dp)
|
||||
.clip(CircleShape)
|
||||
.background(gradientColor)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,8 +3,6 @@ package com.microsoft.fluentui.tokenized.navigation
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -24,19 +22,6 @@ data class TabData(
|
|||
var badge: @Composable (() -> Unit)? = null
|
||||
)
|
||||
|
||||
private val LocalTabBarInfo = compositionLocalOf { TabBarInfo() }
|
||||
private val LocalTabBarToken = compositionLocalOf { TabBarTokens() }
|
||||
|
||||
@Composable
|
||||
private fun getTabBarInfo(): TabBarInfo {
|
||||
return LocalTabBarInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getTabBarToken(): TabBarTokens {
|
||||
return LocalTabBarToken.current
|
||||
}
|
||||
|
||||
/**
|
||||
* TabBar displays tabs that are arranged horizontally.
|
||||
*
|
||||
|
@ -44,7 +29,7 @@ private fun getTabBarToken(): TabBarTokens {
|
|||
* @param modifier the [Modifier] to be applied to this item
|
||||
* @param selectedIndex Index of selected tax. This should be updated onClick of TabData & provide back to TabBar.
|
||||
* @param tabTextAlignment Placement of text in Tab
|
||||
* @param tabItemTokens [TabBarTabItemsTokens] to apply on tabs.
|
||||
* @param tabItemTokens [TabItemTokens] to apply on tabs.
|
||||
* @param tabBarTokens provide appearance values. If not provided then tokens will be picked from AppThemeController
|
||||
*
|
||||
*/
|
||||
|
@ -60,38 +45,34 @@ fun TabBar(
|
|||
val token = tabBarTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.TabBar] as TabBarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalTabBarToken provides token
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(getTabBarToken().topBorderWidth(tabBarInfo = getTabBarInfo()))
|
||||
.background(color = getTabBarToken().topBorderColor(tabBarInfo = getTabBarInfo()))
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
tabDataList.forEachIndexed { index, tabData ->
|
||||
tabData.selected = index == selectedIndex
|
||||
TabItem(
|
||||
title = tabData.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F),
|
||||
icon = if (tabData.selected) tabData.selectedIcon else tabData.icon,
|
||||
textAlignment = tabTextAlignment,
|
||||
selected = tabData.selected,
|
||||
onClick = tabData.onClick,
|
||||
accessory = tabData.badge,
|
||||
tabItemTokens = tabItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.TabItem] as TabItemTokens,
|
||||
style = if (tabData.selected) FluentStyle.Brand else FluentStyle.Neutral
|
||||
)
|
||||
}
|
||||
Column {
|
||||
Box(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(token.topBorderWidth(tabBarInfo = TabBarInfo()))
|
||||
.background(color = token.topBorderColor(tabBarInfo = TabBarInfo()))
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
tabDataList.forEachIndexed { index, tabData ->
|
||||
tabData.selected = index == selectedIndex
|
||||
TabItem(
|
||||
title = tabData.title,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F),
|
||||
icon = if (tabData.selected) tabData.selectedIcon else tabData.icon,
|
||||
textAlignment = tabTextAlignment,
|
||||
selected = tabData.selected,
|
||||
onClick = tabData.onClick,
|
||||
accessory = tabData.badge,
|
||||
tabItemTokens = tabItemTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.TabItem] as TabItemTokens,
|
||||
style = if (tabData.selected) FluentStyle.Brand else FluentStyle.Neutral
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,11 +54,6 @@ data class PillMetaData(
|
|||
var notificationDot: Boolean = false,
|
||||
)
|
||||
|
||||
val LocalPillButtonTokens = compositionLocalOf { PillButtonTokens() }
|
||||
val LocalPillButtonInfo = compositionLocalOf { PillButtonInfo() }
|
||||
val LocalPillBarTokens = compositionLocalOf { PillBarTokens() }
|
||||
val LocalPillBarInfo = compositionLocalOf { PillBarInfo() }
|
||||
|
||||
/**
|
||||
* API to create Pill shaped Button which will further be used in tabs and bars.
|
||||
*
|
||||
|
@ -78,154 +73,148 @@ fun PillButton(
|
|||
) {
|
||||
val token = pillButtonTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PillButton] as PillButtonTokens
|
||||
val pillButtonInfo = PillButtonInfo(
|
||||
style,
|
||||
pillMetaData.enabled,
|
||||
pillMetaData.selected
|
||||
)
|
||||
val shape = RoundedCornerShape(50)
|
||||
val scaleBox = remember { Animatable(1.0F) }
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalPillButtonTokens provides token,
|
||||
LocalPillButtonInfo provides PillButtonInfo(
|
||||
style,
|
||||
pillMetaData.enabled,
|
||||
pillMetaData.selected
|
||||
)
|
||||
|
||||
) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
val scaleBox = remember { Animatable(1.0F) }
|
||||
|
||||
LaunchedEffect(key1 = pillMetaData.selected) {
|
||||
if (pillMetaData.selected) {
|
||||
launch {
|
||||
scaleBox.animateTo(
|
||||
targetValue = 0.95F,
|
||||
animationSpec = tween(
|
||||
durationMillis = 50
|
||||
)
|
||||
LaunchedEffect(key1 = pillMetaData.selected) {
|
||||
if (pillMetaData.selected) {
|
||||
launch {
|
||||
scaleBox.animateTo(
|
||||
targetValue = 0.95F,
|
||||
animationSpec = tween(
|
||||
durationMillis = 50
|
||||
)
|
||||
scaleBox.animateTo(
|
||||
targetValue = 1.0F,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
)
|
||||
scaleBox.animateTo(
|
||||
targetValue = 1.0F,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioLowBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = getPillButtonTokens().backgroundColor(pillButtonInfo = getPillButtonInfo())
|
||||
.getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
),
|
||||
animationSpec = tween(200)
|
||||
)
|
||||
val iconColor =
|
||||
getPillButtonTokens().iconColor(pillButtonInfo = getPillButtonInfo()).getColorByState(
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = token.backgroundColor(pillButtonInfo = pillButtonInfo)
|
||||
.getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val textColor =
|
||||
getPillButtonTokens().textColor(pillButtonInfo = getPillButtonInfo()).getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
val fontStyle = getPillButtonTokens().typography(getPillButtonInfo())
|
||||
|
||||
val focusStroke = getPillButtonTokens().focusStroke(getPillButtonInfo())
|
||||
var focusedBorderModifier: Modifier = Modifier
|
||||
for (borderStroke in focusStroke) {
|
||||
focusedBorderModifier =
|
||||
focusedBorderModifier.border(borderStroke, shape)
|
||||
}
|
||||
|
||||
val clickAndSemanticsModifier = modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(),
|
||||
),
|
||||
animationSpec = tween(200)
|
||||
)
|
||||
val iconColor =
|
||||
token.iconColor(pillButtonInfo = pillButtonInfo).getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = pillMetaData.onClick
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
val textColor =
|
||||
token.textColor(pillButtonInfo = pillButtonInfo).getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
|
||||
val selectedString = if (pillMetaData.selected)
|
||||
LocalContext.current.resources.getString(R.string.fluentui_selected)
|
||||
else
|
||||
LocalContext.current.resources.getString(R.string.fluentui_not_selected)
|
||||
val fontStyle = token.typography(pillButtonInfo)
|
||||
|
||||
val enabledString = if (pillMetaData.enabled)
|
||||
LocalContext.current.resources.getString(R.string.fluentui_enabled)
|
||||
else
|
||||
LocalContext.current.resources.getString(R.string.fluentui_disabled)
|
||||
val focusStroke = token.focusStroke(pillButtonInfo)
|
||||
var focusedBorderModifier: Modifier = Modifier
|
||||
for (borderStroke in focusStroke) {
|
||||
focusedBorderModifier =
|
||||
focusedBorderModifier.border(borderStroke, shape)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.scale(scaleBox.value)
|
||||
.defaultMinSize(minHeight = getPillButtonTokens().minHeight(getPillButtonInfo()))
|
||||
.clip(shape)
|
||||
.background(backgroundColor, shape)
|
||||
.padding(vertical = getPillButtonTokens().verticalPadding(getPillButtonInfo()))
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(if (interactionSource.collectIsFocusedAsState().value || interactionSource.collectIsHoveredAsState().value) focusedBorderModifier else Modifier)
|
||||
.semantics(true) {
|
||||
contentDescription =
|
||||
"${pillMetaData.text} $selectedString $enabledString"
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(Modifier.width(IntrinsicSize.Max)) {
|
||||
if (pillMetaData.icon != null) {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size180)))
|
||||
Icon(
|
||||
pillMetaData.icon!!,
|
||||
pillMetaData.text,
|
||||
modifier = Modifier
|
||||
.size(getPillButtonTokens().iconSize(getPillButtonInfo()))
|
||||
.clearAndSetSemantics { },
|
||||
tint = iconColor
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size160)))
|
||||
Text(
|
||||
pillMetaData.text,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.clearAndSetSemantics { },
|
||||
color = textColor,
|
||||
style = fontStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
val clickAndSemanticsModifier = modifier.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = rememberRipple(),
|
||||
enabled = pillMetaData.enabled,
|
||||
onClickLabel = null,
|
||||
role = Role.Button,
|
||||
onClick = pillMetaData.onClick
|
||||
)
|
||||
|
||||
if (pillMetaData.notificationDot) {
|
||||
val notificationDotColor: Color =
|
||||
getPillButtonTokens().notificationDotColor(getPillButtonInfo())
|
||||
.getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size20)))
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp, bottom = 12.dp)
|
||||
.sizeIn(minWidth = 6.dp, minHeight = 6.dp)
|
||||
) {
|
||||
drawCircle(
|
||||
color = notificationDotColor, style = Fill, radius = dpToPx(3.dp)
|
||||
val selectedString = if (pillMetaData.selected)
|
||||
LocalContext.current.resources.getString(R.string.fluentui_selected)
|
||||
else
|
||||
LocalContext.current.resources.getString(R.string.fluentui_not_selected)
|
||||
|
||||
val enabledString = if (pillMetaData.enabled)
|
||||
LocalContext.current.resources.getString(R.string.fluentui_enabled)
|
||||
else
|
||||
LocalContext.current.resources.getString(R.string.fluentui_disabled)
|
||||
|
||||
Box(
|
||||
modifier
|
||||
.scale(scaleBox.value)
|
||||
.defaultMinSize(minHeight = token.minHeight(pillButtonInfo))
|
||||
.clip(shape)
|
||||
.background(backgroundColor, shape)
|
||||
.padding(vertical = token.verticalPadding(pillButtonInfo))
|
||||
.then(clickAndSemanticsModifier)
|
||||
.then(if (interactionSource.collectIsFocusedAsState().value || interactionSource.collectIsHoveredAsState().value) focusedBorderModifier else Modifier)
|
||||
.semantics(true) {
|
||||
contentDescription =
|
||||
"${pillMetaData.text} $selectedString $enabledString"
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Row(Modifier.width(IntrinsicSize.Max)) {
|
||||
if (pillMetaData.icon != null) {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size180)))
|
||||
Icon(
|
||||
pillMetaData.icon!!,
|
||||
pillMetaData.text,
|
||||
modifier = Modifier
|
||||
.size(token.iconSize(pillButtonInfo))
|
||||
.clearAndSetSemantics { },
|
||||
tint = iconColor
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size160)))
|
||||
Text(
|
||||
pillMetaData.text,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.clearAndSetSemantics { },
|
||||
color = textColor,
|
||||
style = fontStyle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
if (pillMetaData.notificationDot) {
|
||||
val notificationDotColor: Color =
|
||||
token.notificationDotColor(pillButtonInfo)
|
||||
.getColorByState(
|
||||
enabled = pillMetaData.enabled,
|
||||
selected = pillMetaData.selected,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
if (pillMetaData.icon != null)
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size100)))
|
||||
else
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size80)))
|
||||
} else {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size160)))
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size20)))
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp, bottom = 12.dp)
|
||||
.sizeIn(minWidth = 6.dp, minHeight = 6.dp)
|
||||
) {
|
||||
drawCircle(
|
||||
color = notificationDotColor, style = Fill, radius = dpToPx(3.dp)
|
||||
)
|
||||
}
|
||||
if (pillMetaData.icon != null)
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size100)))
|
||||
else
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size80)))
|
||||
} else {
|
||||
Spacer(Modifier.requiredWidth(GlobalTokens.size(GlobalTokens.SizeTokens.Size160)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -257,58 +246,34 @@ fun PillBar(
|
|||
val token = pillBarTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PillBar] as PillBarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalPillBarTokens provides token,
|
||||
LocalPillBarInfo provides PillBarInfo(style)
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val pillBarInfo = PillBarInfo(style)
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (showBackground) getPillBarTokens().background(getPillBarInfo()) else Color.Unspecified)
|
||||
.focusable(enabled = false),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
state = lazyListState
|
||||
) {
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
item(index.toString()) {
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(0, index - 2)
|
||||
)
|
||||
}
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(if (showBackground) token.background(pillBarInfo) else Color.Unspecified)
|
||||
.focusable(enabled = false),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
state = lazyListState
|
||||
) {
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
item(index.toString()) {
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(0, index - 2)
|
||||
)
|
||||
}
|
||||
}, style = style, pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
}
|
||||
}, style = style, pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillButtonTokens(): PillButtonTokens {
|
||||
return LocalPillButtonTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillButtonInfo(): PillButtonInfo {
|
||||
return LocalPillButtonInfo.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillBarTokens(): PillBarTokens {
|
||||
return LocalPillBarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillBarInfo(): PillBarInfo {
|
||||
return LocalPillBarInfo.current
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ import androidx.compose.foundation.lazy.LazyRow
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
@ -26,9 +24,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.PillSwitchTokens
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
val LocalPillSwitchTokens = compositionLocalOf { PillSwitchTokens() }
|
||||
val LocalPillSwitchInfo = compositionLocalOf { PillSwitchInfo() }
|
||||
|
||||
/**
|
||||
* API to create PillSwitches. The PillSwitch control is a linear set of two or more PillButton, each of which functions as a mutually exclusive button.
|
||||
* PillSwitches are used to toggling between two views.
|
||||
|
@ -55,63 +50,49 @@ fun PillSwitch(
|
|||
val token = pillSwitchTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PillSwitch] as PillSwitchTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalPillSwitchTokens provides token,
|
||||
LocalPillSwitchInfo provides PillSwitchInfo(style)
|
||||
val pillSwitchInfo = PillSwitchInfo(style)
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(Color.Transparent)
|
||||
.focusable(false)
|
||||
) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(Color.Transparent)
|
||||
.focusable(false)
|
||||
.focusable(enabled = false)
|
||||
.clip(shape)
|
||||
.background(token.background(pillSwitchInfo), shape),
|
||||
state = lazyListState
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = modifier
|
||||
.wrapContentWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.focusable(enabled = false)
|
||||
.clip(shape)
|
||||
.background(getPillSwitchTokens().background(getPillSwitchInfo()), shape),
|
||||
state = lazyListState
|
||||
) {
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
item(index.toString()) {
|
||||
pillMetadata.selected = (selectedIndex == index)
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(0, index - 2)
|
||||
)
|
||||
}
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
item(index.toString()) {
|
||||
pillMetadata.selected = (selectedIndex == index)
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.onFocusEvent { focusState ->
|
||||
if (focusState.isFocused) {
|
||||
scope.launch {
|
||||
lazyListState.animateScrollToItem(
|
||||
max(0, index - 2)
|
||||
)
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillSwitchTokens(): PillSwitchTokens {
|
||||
return LocalPillSwitchTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillSwitchInfo(): PillSwitchInfo {
|
||||
return LocalPillSwitchInfo.current
|
||||
}
|
||||
|
|
|
@ -2,12 +2,9 @@ package com.microsoft.fluentui.tokenized.segmentedcontrols
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -18,9 +15,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.PillButtonTokens
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.PillTabsInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.PillTabsTokens
|
||||
|
||||
val LocalPillTabsTokens = compositionLocalOf { PillTabsTokens() }
|
||||
val LocalPillTabsInfo = compositionLocalOf { PillTabsInfo() }
|
||||
|
||||
/**
|
||||
* API to create PillTabs. The PillTabs control is a linear set of two or more PillButton, each of which functions as a mutually exclusive button.
|
||||
* Within the control, all PillButton are equal in width.
|
||||
|
@ -53,52 +47,38 @@ fun PillTabs(
|
|||
tabsTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.PillTabs] as PillTabsTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalPillTabsTokens provides token,
|
||||
LocalPillTabsInfo provides PillTabsInfo(style)
|
||||
) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
val pillTabsInfo = PillTabsInfo(style)
|
||||
val shape = RoundedCornerShape(50)
|
||||
|
||||
if (scrollable && metadataList.size > 4) {
|
||||
metadataList.forEachIndexed { index, pillMetaData ->
|
||||
pillMetaData.selected = index == selectedIndex
|
||||
}
|
||||
PillBar(
|
||||
metadataList,
|
||||
modifier = modifier,
|
||||
style = style,
|
||||
showBackground = false,
|
||||
pillButtonTokens = pillButtonTokens,
|
||||
pillBarTokens = tabsTokens
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(getPillTabsTokens().trackBackground(getPillTabsInfo()), shape)
|
||||
) {
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
pillMetadata.selected = (selectedIndex == index)
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier
|
||||
.weight(1F),
|
||||
style = style,
|
||||
pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
if (scrollable && metadataList.size > 4) {
|
||||
metadataList.forEachIndexed { index, pillMetaData ->
|
||||
pillMetaData.selected = index == selectedIndex
|
||||
}
|
||||
PillBar(
|
||||
metadataList,
|
||||
modifier = modifier,
|
||||
style = style,
|
||||
showBackground = false,
|
||||
pillButtonTokens = pillButtonTokens,
|
||||
pillBarTokens = tabsTokens
|
||||
)
|
||||
} else {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.padding(horizontal = 16.dp)
|
||||
.background(token.trackBackground(pillTabsInfo), shape)
|
||||
) {
|
||||
metadataList.forEachIndexed { index, pillMetadata ->
|
||||
pillMetadata.selected = (selectedIndex == index)
|
||||
PillButton(
|
||||
pillMetadata,
|
||||
modifier = Modifier
|
||||
.weight(1F),
|
||||
style = style,
|
||||
pillButtonTokens = pillButtonTokens
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillTabsTokens(): PillTabsTokens {
|
||||
return LocalPillTabsTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getPillTabsInfo(): PillTabsInfo {
|
||||
return LocalPillTabsInfo.current
|
||||
}
|
||||
|
|
|
@ -7,8 +7,6 @@ import androidx.compose.foundation.layout.*
|
|||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
@ -31,9 +29,6 @@ import com.microsoft.fluentui.theme.token.controlTokens.AppBarInfo
|
|||
import com.microsoft.fluentui.theme.token.controlTokens.AppBarSize
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.AppBarTokens
|
||||
|
||||
private val LocalAppBarTokens = compositionLocalOf { AppBarTokens() }
|
||||
private val LocalAppBarInfo = compositionLocalOf { AppBarInfo(FluentStyle.Neutral) }
|
||||
|
||||
/**
|
||||
* An app bar appears at the top of an app screen, below the status bar,
|
||||
* and enables navigation through a series of hierarchical screens.
|
||||
|
@ -90,207 +85,193 @@ fun AppBar(
|
|||
val token = appBarTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.AppBar] as AppBarTokens
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalAppBarTokens provides token,
|
||||
LocalAppBarInfo provides AppBarInfo(style, appBarSize)
|
||||
val appBarInfo = AppBarInfo(style, appBarSize)
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(token.backgroundColor(appBarInfo))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
Row(
|
||||
Modifier
|
||||
.requiredHeight(56.dp * appTitleDelta)
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.background(getAppBarTokens().backgroundColor(getAppBarInfo()))
|
||||
.scale(scaleX = 1.0F, scaleY = appTitleDelta)
|
||||
.alpha(if (appTitleDelta != 1.0F) appTitleDelta / 3 else 1.0F),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.requiredHeight(56.dp * appTitleDelta)
|
||||
.animateContentSize()
|
||||
.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()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (navigationIcon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = navigationIcon.onClick!!
|
||||
)
|
||||
} else
|
||||
Modifier
|
||||
), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
navigationIcon,
|
||||
modifier = Modifier
|
||||
.padding(getAppBarTokens().navigationIconPadding(getAppBarInfo()))
|
||||
.size(getAppBarTokens().leftIconSize(getAppBarInfo())),
|
||||
tint = getAppBarTokens().navigationIconColor(getAppBarInfo())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (appBarSize != AppBarSize.Medium) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (appBarSize == AppBarSize.Large)
|
||||
Modifier.padding(start = 16.dp)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
) {
|
||||
logo?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
val titleTextStyle = getAppBarTokens().titleTypography(getAppBarInfo())
|
||||
val subtitleTextStyle = getAppBarTokens().subtitleTypography(getAppBarInfo())
|
||||
|
||||
if (appBarSize != AppBarSize.Large && !subTitle.isNullOrBlank()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(getAppBarTokens().textPadding(getAppBarInfo()))
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (postTitleIcon.onClick != null && appBarSize == AppBarSize.Small)
|
||||
Modifier.clickable(onClick = postTitleIcon.onClick!!)
|
||||
else
|
||||
Modifier
|
||||
), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = getAppBarTokens().titleTextColor(getAppBarInfo()),
|
||||
style = titleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = true)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (postTitleIcon.isIconAvailable() && appBarSize == AppBarSize.Small)
|
||||
Icon(
|
||||
postTitleIcon,
|
||||
modifier = Modifier
|
||||
.size(getAppBarTokens().titleIconSize(getAppBarInfo())),
|
||||
tint = getAppBarTokens().titleIconColor(getAppBarInfo())
|
||||
if (appBarSize != AppBarSize.Large && navigationIcon.isIconAvailable()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (navigationIcon.onClick != null) {
|
||||
Modifier.clickable(
|
||||
role = Role.Button,
|
||||
onClick = navigationIcon.onClick!!
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (postSubtitleIcon.onClick != null)
|
||||
Modifier.clickable(onClick = postSubtitleIcon.onClick!!)
|
||||
else
|
||||
Modifier
|
||||
), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (preSubtitleIcon.isIconAvailable())
|
||||
Icon(
|
||||
preSubtitleIcon,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
getAppBarTokens().subtitleIconSize(
|
||||
getAppBarInfo()
|
||||
)
|
||||
),
|
||||
tint = getAppBarTokens().subtitleIconColor(getAppBarInfo())
|
||||
)
|
||||
Text(
|
||||
subTitle,
|
||||
color = getAppBarTokens().subtitleTextColor(
|
||||
getAppBarInfo()
|
||||
),
|
||||
style = subtitleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = true)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (postSubtitleIcon.isIconAvailable())
|
||||
Icon(
|
||||
postSubtitleIcon,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
getAppBarTokens().subtitleIconSize(
|
||||
getAppBarInfo()
|
||||
)
|
||||
),
|
||||
tint = getAppBarTokens().subtitleIconColor(getAppBarInfo())
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = title,
|
||||
} else
|
||||
Modifier
|
||||
), contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
navigationIcon,
|
||||
modifier = Modifier
|
||||
.padding(getAppBarTokens().textPadding(getAppBarInfo()))
|
||||
.weight(1F),
|
||||
color = getAppBarTokens().titleTextColor(getAppBarInfo()),
|
||||
style = titleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(
|
||||
includeFontPadding = true
|
||||
)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
.padding(token.navigationIconPadding(appBarInfo))
|
||||
.size(token.leftIconSize(appBarInfo)),
|
||||
tint = token.navigationIconColor(appBarInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (rightAccessoryView != null) {
|
||||
rightAccessoryView()
|
||||
if (appBarSize != AppBarSize.Medium) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (appBarSize == AppBarSize.Large)
|
||||
Modifier.padding(start = 16.dp)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
) {
|
||||
logo?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
if (searchBar != null) {
|
||||
Row(
|
||||
modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.then(if (!searchMode) Modifier.height(56.dp * accessoryDelta) else Modifier)
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
val titleTextStyle = token.titleTypography(appBarInfo)
|
||||
val subtitleTextStyle = token.subtitleTypography(appBarInfo)
|
||||
|
||||
if (appBarSize != AppBarSize.Large && !subTitle.isNullOrBlank()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.padding(token.textPadding(appBarInfo))
|
||||
) {
|
||||
searchBar()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (postTitleIcon.onClick != null && appBarSize == AppBarSize.Small)
|
||||
Modifier.clickable(onClick = postTitleIcon.onClick!!)
|
||||
else
|
||||
Modifier
|
||||
), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = token.titleTextColor(appBarInfo),
|
||||
style = titleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = true)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (postTitleIcon.isIconAvailable() && appBarSize == AppBarSize.Small)
|
||||
Icon(
|
||||
postTitleIcon,
|
||||
modifier = Modifier
|
||||
.size(token.titleIconSize(appBarInfo)),
|
||||
tint = token.titleIconColor(appBarInfo)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (postSubtitleIcon.onClick != null)
|
||||
Modifier.clickable(onClick = postSubtitleIcon.onClick!!)
|
||||
else
|
||||
Modifier
|
||||
), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (preSubtitleIcon.isIconAvailable())
|
||||
Icon(
|
||||
preSubtitleIcon,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
token.subtitleIconSize(
|
||||
appBarInfo
|
||||
)
|
||||
),
|
||||
tint = token.subtitleIconColor(appBarInfo)
|
||||
)
|
||||
Text(
|
||||
subTitle,
|
||||
color = token.subtitleTextColor(
|
||||
appBarInfo
|
||||
),
|
||||
style = subtitleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = true)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (postSubtitleIcon.isIconAvailable())
|
||||
Icon(
|
||||
postSubtitleIcon,
|
||||
modifier = Modifier
|
||||
.size(
|
||||
token.subtitleIconSize(
|
||||
appBarInfo
|
||||
)
|
||||
),
|
||||
tint = token.subtitleIconColor(appBarInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier
|
||||
.padding(token.textPadding(appBarInfo))
|
||||
.weight(1F),
|
||||
color = token.titleTextColor(appBarInfo),
|
||||
style = titleTextStyle.merge(
|
||||
TextStyle(
|
||||
platformStyle = PlatformTextStyle(
|
||||
includeFontPadding = true
|
||||
)
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (bottomBar != null && (searchMode || searchBar == null)) {
|
||||
Row(
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.then(if (!searchMode) Modifier.height(48.dp * accessoryDelta) else Modifier)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
bottomBar()
|
||||
}
|
||||
|
||||
if (rightAccessoryView != null) {
|
||||
rightAccessoryView()
|
||||
}
|
||||
}
|
||||
|
||||
if (searchBar != null) {
|
||||
Row(
|
||||
modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.then(if (!searchMode) Modifier.height(56.dp * accessoryDelta) else Modifier)
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
searchBar()
|
||||
}
|
||||
}
|
||||
if (bottomBar != null && (searchMode || searchBar == null)) {
|
||||
Row(
|
||||
Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.then(if (!searchMode) Modifier.height(48.dp * accessoryDelta) else Modifier)
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
bottomBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAppBarTokens(): AppBarTokens {
|
||||
return LocalAppBarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAppBarInfo(): AppBarInfo {
|
||||
return LocalAppBarInfo.current
|
||||
}
|
|
@ -52,9 +52,6 @@ import com.microsoft.fluentui.tokenized.persona.SearchBarPersonaChip
|
|||
import com.microsoft.fluentui.tokenized.progress.CircularProgressIndicator
|
||||
import com.microsoft.fluentui.topappbars.R
|
||||
|
||||
private val LocalSearchBarTokens = compositionLocalOf { SearchBarTokens() }
|
||||
private val LocalSearchBarInfo = compositionLocalOf { SearchBarInfo(FluentStyle.Neutral) }
|
||||
|
||||
/**
|
||||
* API to create a searchbar. This control takes input from user's keyboard and runs it against a lambda
|
||||
* function provided by user to generate results. It allows user to select a person and display in the form
|
||||
|
@ -99,326 +96,311 @@ fun SearchBar(
|
|||
|
||||
val token = searchBarTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.SearchBar] as SearchBarTokens
|
||||
val searchBarInfo = SearchBarInfo(style)
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalSearchBarTokens provides token,
|
||||
LocalSearchBarInfo provides SearchBarInfo(style)
|
||||
var queryText by rememberSaveable { mutableStateOf("") }
|
||||
var searchHasFocus by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var personaChipSelected by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedPerson: Person? = selectedPerson
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(token.backgroundColor(searchBarInfo))
|
||||
.padding(token.searchBarPadding(searchBarInfo))
|
||||
) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
var queryText by rememberSaveable { mutableStateOf("") }
|
||||
var searchHasFocus by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
var personaChipSelected by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedPerson: Person? = selectedPerson
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(getSearchBarTokens().backgroundColor(getSearchBarInfo()))
|
||||
.padding(getSearchBarTokens().searchBarPadding(getSearchBarInfo()))
|
||||
Modifier
|
||||
.requiredHeight(token.height(searchBarInfo))
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
token.inputBackgroundColor(searchBarInfo),
|
||||
RoundedCornerShape(8.dp)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.requiredHeight(getSearchBarTokens().height(getSearchBarInfo()))
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(
|
||||
getSearchBarTokens().inputBackgroundColor(getSearchBarInfo()),
|
||||
RoundedCornerShape(8.dp)
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
//Left Section
|
||||
AnimatedContent(searchHasFocus) {
|
||||
var onClick: (() -> Unit)? = null
|
||||
var icon: ImageVector? = null
|
||||
var contentDescription: String? = null
|
||||
//Left Section
|
||||
AnimatedContent(searchHasFocus) {
|
||||
var onClick: (() -> Unit)? = null
|
||||
var icon: ImageVector? = null
|
||||
var contentDescription: String? = null
|
||||
|
||||
var mirrorImage = false
|
||||
var mirrorImage = false
|
||||
|
||||
when (it) {
|
||||
true -> {
|
||||
onClick = {
|
||||
queryText = ""
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
|
||||
focusManager.clearFocus()
|
||||
searchHasFocus = false
|
||||
|
||||
navigationIconCallback?.invoke()
|
||||
}
|
||||
icon = SearchBarIcons.Arrowback
|
||||
contentDescription =
|
||||
LocalContext.current.resources.getString(R.string.fluentui_back)
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
mirrorImage = true
|
||||
}
|
||||
false -> {
|
||||
onClick = {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
icon = SearchBarIcons.Search
|
||||
contentDescription =
|
||||
LocalContext.current.resources.getString(R.string.fluentui_search)
|
||||
mirrorImage = false
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
FluentIcon(
|
||||
icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = getSearchBarTokens().leftIconColor(getSearchBarInfo()),
|
||||
flipOnRtl = mirrorImage
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(getSearchBarTokens().leftIconSize(getSearchBarInfo())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//Center Section
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.weight(1F)
|
||||
.onKeyEvent {
|
||||
if (it.key == Key.Backspace) {
|
||||
|
||||
if (personaChipSelected) {
|
||||
selectedPerson = null
|
||||
personaChipSelected = false
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
} else {
|
||||
personaChipSelected = true
|
||||
}
|
||||
|
||||
}
|
||||
true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LaunchedEffect(selectedPerson) {
|
||||
queryText = ""
|
||||
if (personaChipSelected)
|
||||
personaChipSelected = false
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
}
|
||||
|
||||
if (selectedPerson != null) {
|
||||
SearchBarPersonaChip(
|
||||
person = selectedPerson!!,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
style = style,
|
||||
enabled = enabled,
|
||||
selected = personaChipSelected,
|
||||
onClick = {
|
||||
personaChipSelected = !personaChipSelected
|
||||
personaChipOnClick?.invoke()
|
||||
},
|
||||
onCloseClick = {
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = queryText,
|
||||
onValueChange = {
|
||||
queryText = it
|
||||
personaChipSelected = false
|
||||
when (it) {
|
||||
true -> {
|
||||
onClick = {
|
||||
queryText = ""
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { focusState ->
|
||||
when {
|
||||
focusState.isFocused ->
|
||||
searchHasFocus = true
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics {contentDescription = searchHint},
|
||||
textStyle = getSearchBarTokens().typography(getSearchBarInfo()).merge(
|
||||
TextStyle(
|
||||
color = getSearchBarTokens().textColor(getSearchBarInfo()),
|
||||
textDirection = TextDirection.ContentOrLtr
|
||||
)
|
||||
),
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = if (LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
Alignment.CenterEnd
|
||||
else
|
||||
Alignment.CenterStart
|
||||
) {
|
||||
if (queryText.isEmpty()) {
|
||||
Text(
|
||||
searchHint,
|
||||
style = getSearchBarTokens().typography(getSearchBarInfo()),
|
||||
color = getSearchBarTokens().textColor(getSearchBarInfo())
|
||||
)
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
},
|
||||
cursorBrush = getSearchBarTokens().cursorColor(getSearchBarInfo())
|
||||
)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (focusByDefault)
|
||||
focusRequester.requestFocus()
|
||||
|
||||
focusManager.clearFocus()
|
||||
searchHasFocus = false
|
||||
|
||||
navigationIconCallback?.invoke()
|
||||
}
|
||||
icon = SearchBarIcons.Arrowback
|
||||
contentDescription =
|
||||
LocalContext.current.resources.getString(R.string.fluentui_back)
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
mirrorImage = true
|
||||
}
|
||||
false -> {
|
||||
onClick = {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
icon = SearchBarIcons.Search
|
||||
contentDescription =
|
||||
LocalContext.current.resources.getString(R.string.fluentui_search)
|
||||
mirrorImage = false
|
||||
}
|
||||
}
|
||||
|
||||
//Right Section
|
||||
AnimatedContent((queryText.isBlank() && selectedPerson == null)) {
|
||||
when (it) {
|
||||
true ->
|
||||
if (microphoneCallback != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = microphoneCallback,
|
||||
role = Role.Button
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
FluentIcon(
|
||||
SearchBarIcons.Microphone,
|
||||
contentDescription = LocalContext.current.resources.getString(
|
||||
R.string.fluentui_microphone
|
||||
),
|
||||
tint = getSearchBarTokens().rightIconColor(
|
||||
getSearchBarInfo()
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(
|
||||
getSearchBarTokens().rightIconSize(
|
||||
getSearchBarInfo()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
role = Role.Button
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
FluentIcon(
|
||||
icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = token.leftIconColor(searchBarInfo),
|
||||
flipOnRtl = mirrorImage
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(token.leftIconSize(searchBarInfo)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//Center Section
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(24.dp)
|
||||
.weight(1F)
|
||||
.onKeyEvent {
|
||||
if (it.key == Key.Backspace) {
|
||||
|
||||
if (personaChipSelected) {
|
||||
selectedPerson = null
|
||||
personaChipSelected = false
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
} else {
|
||||
personaChipSelected = true
|
||||
}
|
||||
false ->
|
||||
|
||||
}
|
||||
true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LaunchedEffect(selectedPerson) {
|
||||
queryText = ""
|
||||
if (personaChipSelected)
|
||||
personaChipSelected = false
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
}
|
||||
|
||||
if (selectedPerson != null) {
|
||||
SearchBarPersonaChip(
|
||||
person = selectedPerson!!,
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
style = style,
|
||||
enabled = enabled,
|
||||
selected = personaChipSelected,
|
||||
onClick = {
|
||||
personaChipSelected = !personaChipSelected
|
||||
personaChipOnClick?.invoke()
|
||||
},
|
||||
onCloseClick = {
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
BasicTextField(
|
||||
value = queryText,
|
||||
onValueChange = {
|
||||
queryText = it
|
||||
personaChipSelected = false
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
modifier = Modifier
|
||||
.weight(1F)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { focusState ->
|
||||
when {
|
||||
focusState.isFocused ->
|
||||
searchHasFocus = true
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp)
|
||||
.semantics {contentDescription = searchHint},
|
||||
textStyle = token.typography(searchBarInfo).merge(
|
||||
TextStyle(
|
||||
color = token.textColor(searchBarInfo),
|
||||
textDirection = TextDirection.ContentOrLtr
|
||||
)
|
||||
),
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = if (LocalLayoutDirection.current == LayoutDirection.Rtl)
|
||||
Alignment.CenterEnd
|
||||
else
|
||||
Alignment.CenterStart
|
||||
) {
|
||||
if (queryText.isEmpty()) {
|
||||
Text(
|
||||
searchHint,
|
||||
style = token.typography(searchBarInfo),
|
||||
color = token.textColor(searchBarInfo)
|
||||
)
|
||||
}
|
||||
}
|
||||
innerTextField()
|
||||
},
|
||||
cursorBrush = token.cursorColor(searchBarInfo)
|
||||
)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
if (focusByDefault)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
//Right Section
|
||||
AnimatedContent((queryText.isBlank() && selectedPerson == null)) {
|
||||
when (it) {
|
||||
true ->
|
||||
if (microphoneCallback != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.padding(
|
||||
getSearchBarTokens().progressIndicatorRightPadding(
|
||||
getSearchBarInfo()
|
||||
)
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
queryText = ""
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
},
|
||||
onClick = microphoneCallback,
|
||||
role = Role.Button
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
size = getSearchBarTokens().circularProgressIndicatorSize(
|
||||
getSearchBarInfo()
|
||||
)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
FluentIcon(
|
||||
SearchBarIcons.Dismisscircle,
|
||||
SearchBarIcons.Microphone,
|
||||
contentDescription = LocalContext.current.resources.getString(
|
||||
R.string.fluentui_clear_text
|
||||
R.string.fluentui_microphone
|
||||
),
|
||||
tint = getSearchBarTokens().rightIconColor(getSearchBarInfo())
|
||||
tint = token.rightIconColor(
|
||||
searchBarInfo
|
||||
)
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(getSearchBarTokens().rightIconSize(getSearchBarInfo()))
|
||||
.size(
|
||||
token.rightIconSize(
|
||||
searchBarInfo
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rightAccessoryIcon?.isIconAvailable() == true && rightAccessoryIcon.onClick != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = rightAccessoryIcon.onClick!!,
|
||||
role = Role.Button
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
rightAccessoryIcon,
|
||||
}
|
||||
false ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(getSearchBarTokens().rightIconSize(getSearchBarInfo())),
|
||||
tint = getSearchBarTokens().rightIconColor(getSearchBarInfo())
|
||||
)
|
||||
Icon(
|
||||
FluentIcon(
|
||||
ListItemIcons.Chevron,
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_chevron),
|
||||
tint = getSearchBarTokens().rightIconColor(getSearchBarInfo())
|
||||
),
|
||||
Modifier.rotate(90F)
|
||||
)
|
||||
}
|
||||
.size(44.dp, 40.dp)
|
||||
.padding(
|
||||
token.progressIndicatorRightPadding(
|
||||
searchBarInfo
|
||||
)
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
queryText = ""
|
||||
selectedPerson = null
|
||||
|
||||
onValueChange(queryText, selectedPerson)
|
||||
},
|
||||
role = Role.Button
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (loading) {
|
||||
CircularProgressIndicator(
|
||||
size = token.circularProgressIndicatorSize(
|
||||
searchBarInfo
|
||||
)
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
FluentIcon(
|
||||
SearchBarIcons.Dismisscircle,
|
||||
contentDescription = LocalContext.current.resources.getString(
|
||||
R.string.fluentui_clear_text
|
||||
),
|
||||
tint = token.rightIconColor(searchBarInfo)
|
||||
),
|
||||
modifier = Modifier
|
||||
.size(token.rightIconSize(searchBarInfo))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rightAccessoryIcon?.isIconAvailable() == true && rightAccessoryIcon.onClick != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(44.dp, 40.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(),
|
||||
enabled = enabled,
|
||||
onClick = rightAccessoryIcon.onClick!!,
|
||||
role = Role.Button
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
rightAccessoryIcon,
|
||||
modifier = Modifier
|
||||
.size(token.rightIconSize(searchBarInfo)),
|
||||
tint = token.rightIconColor(searchBarInfo)
|
||||
)
|
||||
Icon(
|
||||
FluentIcon(
|
||||
ListItemIcons.Chevron,
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.fluentui_chevron),
|
||||
tint = token.rightIconColor(searchBarInfo)
|
||||
),
|
||||
Modifier.rotate(90F)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSearchBarTokens(): SearchBarTokens {
|
||||
return LocalSearchBarTokens.current
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getSearchBarInfo(): SearchBarInfo {
|
||||
return LocalSearchBarInfo.current
|
||||
}
|
Загрузка…
Ссылка в новой задаче