initial changes for anchored draggable
This commit is contained in:
Родитель
3776ab4a13
Коммит
3b017e90ab
|
@ -29,8 +29,8 @@ import com.microsoft.fluentui.theme.FluentTheme
|
|||
import com.microsoft.fluentui.theme.token.FluentAliasTokens
|
||||
import com.microsoft.fluentui.tokenized.controls.RadioButton
|
||||
import com.microsoft.fluentui.tokenized.controls.ToggleSwitch
|
||||
import com.microsoft.fluentui.tokenized.drawer.BottomDrawer
|
||||
import com.microsoft.fluentui.tokenized.drawer.rememberBottomDrawerState
|
||||
import com.microsoft.fluentui.tokenized.drawer.BottomDrawerV2
|
||||
import com.microsoft.fluentui.tokenized.drawer.rememberBottomDrawerStateV2
|
||||
import com.microsoft.fluentui.tokenized.listitem.ListItem
|
||||
import com.microsoft.fluentuidemo.R
|
||||
import com.microsoft.fluentuidemo.V2DemoActivity
|
||||
|
@ -412,7 +412,7 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
|
|||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val drawerState = rememberBottomDrawerState(expandable = expandable, skipOpenState = skipOpenState)
|
||||
val drawerState = rememberBottomDrawerStateV2(expandable = expandable, skipOpenState = skipOpenState)
|
||||
|
||||
val open: () -> Unit = {
|
||||
scope.launch { drawerState.open() }
|
||||
|
@ -435,7 +435,7 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
|
|||
)
|
||||
}
|
||||
|
||||
BottomDrawer(
|
||||
BottomDrawerV2(
|
||||
drawerState = drawerState,
|
||||
drawerContent = { drawerContent(close) },
|
||||
scrimVisible = scrimVisible,
|
||||
|
|
|
@ -35,8 +35,8 @@ import com.microsoft.fluentui.tokenized.controls.Button
|
|||
import com.microsoft.fluentui.tokenized.controls.Label
|
||||
import com.microsoft.fluentui.tokenized.controls.RadioButton
|
||||
import com.microsoft.fluentui.tokenized.controls.ToggleSwitch
|
||||
import com.microsoft.fluentui.tokenized.drawer.Drawer
|
||||
import com.microsoft.fluentui.tokenized.drawer.rememberDrawerState
|
||||
import com.microsoft.fluentui.tokenized.drawer.DrawerV2
|
||||
import com.microsoft.fluentui.tokenized.drawer.rememberDrawerStateV2
|
||||
import com.microsoft.fluentui.tokenized.listitem.ListItem
|
||||
import com.microsoft.fluentuidemo.R
|
||||
import com.microsoft.fluentuidemo.V2DemoActivity
|
||||
|
@ -383,7 +383,7 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
|
|||
preventDismissalOnScrimClick: Boolean
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val drawerState = rememberDrawerState()
|
||||
val drawerState = rememberDrawerStateV2()
|
||||
val open: () -> Unit = {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
|
@ -405,7 +405,7 @@ private fun CreateDrawerWithButtonOnPrimarySurfaceToInvokeIt(
|
|||
)
|
||||
}
|
||||
|
||||
Drawer(
|
||||
DrawerV2(
|
||||
drawerState = drawerState,
|
||||
offset = offset,
|
||||
drawerContent = { drawerContent(close) },
|
||||
|
|
|
@ -67,6 +67,7 @@ allprojects {
|
|||
tokenautocompleteVersion = '2.0.8'
|
||||
threetenabpVersion = '1.1.0'
|
||||
universalPkgDir = "universal"
|
||||
anchoredDraggableVersion = '1.6.0-alpha01'
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
|
|
|
@ -69,6 +69,7 @@ dependencies {
|
|||
implementation "com.microsoft.device:dualscreen-layout:$duoVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
|
||||
implementation "androidx.compose.material:material"
|
||||
implementation "androidx.compose.foundation:foundation:$anchoredDraggableVersion"
|
||||
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation "org.robolectric:robolectric:$robolectric"
|
||||
|
|
|
@ -0,0 +1,322 @@
|
|||
package com.microsoft.fluentui.tokenized.drawer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.collapse
|
||||
import androidx.compose.ui.semantics.dismiss
|
||||
import androidx.compose.ui.semantics.expand
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.fluentui.drawer.R
|
||||
import com.microsoft.fluentui.theme.token.Icon
|
||||
import com.microsoft.fluentui.tokenized.calculateFraction
|
||||
import com.microsoft.fluentui.util.pxToDp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.drawerHeight(
|
||||
slideOver: Boolean,
|
||||
fixedHeight: Float,
|
||||
fullHeight: Float,
|
||||
drawerState: DrawerStateV2
|
||||
): Modifier {
|
||||
val modifier = if (slideOver) {
|
||||
if (drawerState.expandable) {
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.heightIn(
|
||||
0.dp,
|
||||
pxToDp(fixedHeight)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Modifier.height(pxToDp(fullHeight - drawerState.anchoredDraggableState.offset))
|
||||
}
|
||||
|
||||
return this.then(modifier)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BottomDrawerV2(
|
||||
modifier: Modifier,
|
||||
drawerState: DrawerStateV2,
|
||||
drawerShape: Shape,
|
||||
drawerElevation: Dp,
|
||||
drawerBackground: Brush,
|
||||
drawerHandleColor: Color,
|
||||
scrimColor: Color,
|
||||
scrimVisible: Boolean,
|
||||
slideOver: Boolean,
|
||||
enableSwipeDismiss: Boolean = true,
|
||||
showHandle: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
drawerContent: @Composable () -> Unit,
|
||||
maxLandscapeWidthFraction : Float = 1F,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {}
|
||||
) {
|
||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
val drawerHeight =
|
||||
remember(drawerContent.hashCode()) { mutableStateOf<Float?>(null) }
|
||||
val maxOpenHeight = fullHeight * DrawerOpenFraction
|
||||
|
||||
val drawerConstraints = with(LocalDensity.current) {
|
||||
Modifier
|
||||
.sizeIn(
|
||||
maxWidth = constraints.maxWidth.toDp(),
|
||||
maxHeight = constraints.maxHeight.toDp()
|
||||
)
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
val drawerStateAnchors = drawerState.anchoredDraggableState.anchors
|
||||
val drawerStateOffset = drawerState.anchoredDraggableState.offset
|
||||
|
||||
Scrim(
|
||||
open = !drawerState.isClosed || (drawerHeight != null && drawerHeight.value == 0f),
|
||||
onClose = onDismiss,
|
||||
fraction = {
|
||||
if (drawerStateAnchors.size == 0 || (drawerHeight != null && drawerHeight.value == 0f)) {
|
||||
0.toFloat()
|
||||
} else {
|
||||
val targetValue: DrawerValue = if (slideOver) {
|
||||
drawerStateAnchors.closestAnchor(drawerStateAnchors.maxAnchor())!!
|
||||
|
||||
} else if (drawerState.skipOpenState) {
|
||||
DrawerValue.Expanded
|
||||
} else {
|
||||
DrawerValue.Open
|
||||
}
|
||||
calculateFraction(
|
||||
drawerStateAnchors.positionOf(DrawerValue.Closed),
|
||||
drawerStateAnchors.positionOf(targetValue),
|
||||
drawerStateOffset
|
||||
)
|
||||
}
|
||||
},
|
||||
color = if (scrimVisible) scrimColor else Color.Transparent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
val configuration = LocalConfiguration.current
|
||||
Box(
|
||||
drawerConstraints
|
||||
.fillMaxWidth(
|
||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) maxLandscapeWidthFraction
|
||||
else 1F
|
||||
)
|
||||
// .nestedScroll(
|
||||
// if (!enableSwipeDismiss && drawerStateOffset >= maxOpenHeight) drawerState.NonDismissiblePreUpPostDownNestedScrollConnection else
|
||||
// if (slideOver) drawerState.nestedScrollConnection else drawerState.PostDownNestedScrollConnection
|
||||
// )
|
||||
.offset {
|
||||
val y = if (drawerStateAnchors.size == 0) {
|
||||
fullHeight.roundToInt()
|
||||
} else {
|
||||
drawerStateOffset.roundToInt()
|
||||
}
|
||||
IntOffset(x = 0, y = y)
|
||||
}
|
||||
.then(
|
||||
if (maxLandscapeWidthFraction != 1F
|
||||
&& configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
) Modifier.align(Alignment.TopCenter)
|
||||
else Modifier
|
||||
)
|
||||
.onGloballyPositioned { layoutCoordinates ->
|
||||
if (!drawerState.animationInProgress
|
||||
&& drawerState.anchoredDraggableState.currentValue == DrawerValue.Closed
|
||||
&& drawerState.anchoredDraggableState.targetValue == DrawerValue.Closed
|
||||
) {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
if (slideOver) {
|
||||
val originalSize = layoutCoordinates.size.height.toFloat()
|
||||
drawerHeight.value = if (drawerState.expandable) {
|
||||
originalSize
|
||||
} else {
|
||||
min(
|
||||
originalSize,
|
||||
maxOpenHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottomDrawerAnchoredDraggable(
|
||||
drawerState,
|
||||
slideOver,
|
||||
maxOpenHeight,
|
||||
fullHeight,
|
||||
drawerHeight.value
|
||||
)
|
||||
.drawerHeight(
|
||||
slideOver,
|
||||
maxOpenHeight,
|
||||
fullHeight,
|
||||
drawerState
|
||||
)
|
||||
.shadow(drawerElevation)
|
||||
.clip(drawerShape)
|
||||
.background(drawerBackground)
|
||||
.semantics {
|
||||
if (!drawerState.isClosed) {
|
||||
dismiss {
|
||||
onDismiss()
|
||||
true
|
||||
}
|
||||
if (drawerState.anchoredDraggableState.currentValue == DrawerValue.Open && drawerState.hasExpandedState) {
|
||||
expand {
|
||||
if (drawerState.confirmValueChange(DrawerValue.Expanded)) {
|
||||
scope.launch { drawerState.expand() }
|
||||
}
|
||||
true
|
||||
}
|
||||
} else if (drawerState.hasExpandedState && drawerState.hasOpenedState) {
|
||||
collapse {
|
||||
if (drawerState.confirmValueChange(DrawerValue.Open)) {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.focusable(false),
|
||||
) {
|
||||
Column {
|
||||
if (showHandle) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
if (!enableSwipeDismiss && drawerStateOffset >= maxOpenHeight) {
|
||||
if (delta < 0) {
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(delta)
|
||||
}
|
||||
} else {
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(delta)
|
||||
}
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
launch {
|
||||
drawerState.anchoredDraggableState.settle(
|
||||
velocity
|
||||
)
|
||||
if (drawerState.isClosed) {
|
||||
if (enableSwipeDismiss)
|
||||
onDismiss()
|
||||
else
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(DRAWER_HANDLE_TAG)
|
||||
) {
|
||||
val collapsed = LocalContext.current.resources.getString(R.string.collapsed)
|
||||
val expanded = LocalContext.current.resources.getString(R.string.expanded)
|
||||
val accessibilityManager = LocalContext.current.getSystemService(Context.ACCESSIBILITY_SERVICE) as? AccessibilityManager
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_drawer_handle),
|
||||
contentDescription = LocalContext.current.resources.getString(R.string.drag_handle),
|
||||
tint = drawerHandleColor,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
enabled = drawerState.hasExpandedState,
|
||||
role = Role.Button,
|
||||
onClickLabel =
|
||||
if (drawerState.anchoredDraggableState.currentValue == DrawerValue.Expanded) {
|
||||
LocalContext.current.resources.getString(R.string.collapse)
|
||||
} else {
|
||||
if (drawerState.hasExpandedState && !drawerState.isClosed) LocalContext.current.resources.getString(
|
||||
R.string.expand
|
||||
) else null
|
||||
}
|
||||
) {
|
||||
if (drawerState.anchoredDraggableState.currentValue == DrawerValue.Expanded) {
|
||||
if (drawerState.hasOpenedState && drawerState.confirmValueChange(
|
||||
DrawerValue.Open
|
||||
)
|
||||
) {
|
||||
scope.launch { drawerState.open() }
|
||||
accessibilityManager?.let { manager ->
|
||||
if(manager.isEnabled){
|
||||
val event = AccessibilityEvent.obtain(
|
||||
AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
|
||||
text.add(collapsed)
|
||||
}
|
||||
manager.sendAccessibilityEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (drawerState.hasExpandedState) {
|
||||
if (drawerState.confirmValueChange(DrawerValue.Expanded)) {
|
||||
scope.launch { drawerState.expand() }
|
||||
accessibilityManager?.let { manager ->
|
||||
if(manager.isEnabled){
|
||||
val event = AccessibilityEvent.obtain(
|
||||
AccessibilityEvent.TYPE_ANNOUNCEMENT).apply {
|
||||
text.add(expanded)
|
||||
}
|
||||
manager.sendAccessibilityEvent(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(modifier = Modifier
|
||||
.testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -348,67 +348,6 @@ fun rememberBottomDrawerState(
|
|||
}
|
||||
}
|
||||
|
||||
private class DrawerPositionProvider(val offset: IntOffset?) : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize
|
||||
): IntOffset {
|
||||
if (offset != null) {
|
||||
return IntOffset(anchorBounds.left + offset.x, anchorBounds.top + offset.y)
|
||||
}
|
||||
return IntOffset(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Scrim(
|
||||
open: Boolean,
|
||||
onClose: () -> Unit,
|
||||
fraction: () -> Float,
|
||||
color: Color,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {},
|
||||
) {
|
||||
val dismissDrawer = if (open) {
|
||||
Modifier.pointerInput(onClose) {
|
||||
detectTapGestures {
|
||||
if (!preventDismissalOnScrimClick) {
|
||||
onClose()
|
||||
}
|
||||
onScrimClick() //this function runs post onClose() so that the drawer is closed before the callback is invoked
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissDrawer)
|
||||
.testTag(DRAWER_SCRIM_TAG)
|
||||
) {
|
||||
drawRect(color, alpha = fraction())
|
||||
}
|
||||
}
|
||||
|
||||
private val EndDrawerPadding = 56.dp
|
||||
private val DrawerVelocityThreshold = 400.dp
|
||||
|
||||
private val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||
|
||||
private const val DrawerOpenFraction = 0.5f
|
||||
|
||||
//Tag use for testing
|
||||
const val DRAWER_HANDLE_TAG = "Fluent Drawer Handle"
|
||||
const val DRAWER_CONTENT_TAG = "Fluent Drawer Content"
|
||||
const val DRAWER_SCRIM_TAG = "Fluent Drawer Scrim"
|
||||
|
||||
//Drawer Handle height + padding
|
||||
private val DrawerHandleHeightOffset = 20.dp
|
||||
|
||||
private fun Modifier.drawerHeight(
|
||||
slideOver: Boolean,
|
||||
fixedHeight: Float,
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
package com.microsoft.fluentui.tokenized.drawer
|
||||
|
||||
import androidx.compose.animation.core.TweenSpec
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import com.microsoft.fluentui.util.pxToDp
|
||||
|
||||
object DraggableDefaults {
|
||||
val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||
|
||||
/**
|
||||
* The default velocity threshold for the drawer to snap to the nearest anchor.
|
||||
*/
|
||||
val VelocityThreshold = 0.5f
|
||||
|
||||
val PositionalThreshold = 0.5f
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun convertDpToFloat(dpValue: Dp): Float {
|
||||
return with(LocalDensity.current) { dpValue.toPx() }
|
||||
}
|
||||
|
||||
|
||||
val EndDrawerPadding = 56.dp
|
||||
val DrawerVelocityThreshold = 400.dp
|
||||
|
||||
val AnimationSpec = TweenSpec<Float>(durationMillis = 256)
|
||||
|
||||
const val DrawerOpenFraction = 0.5f
|
||||
|
||||
//Tag use for testing
|
||||
const val DRAWER_HANDLE_TAG = "Fluent Drawer Handle"
|
||||
const val DRAWER_CONTENT_TAG = "Fluent Drawer Content"
|
||||
const val DRAWER_SCRIM_TAG = "Fluent Drawer Scrim"
|
||||
|
||||
//Drawer Handle height + padding
|
||||
val DrawerHandleHeightOffset = 20.dp
|
||||
|
||||
class DrawerPositionProvider(val offset: IntOffset?) : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize
|
||||
): IntOffset {
|
||||
if (offset != null) {
|
||||
return IntOffset(anchorBounds.left + offset.x, anchorBounds.top + offset.y)
|
||||
}
|
||||
return IntOffset(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Scrim(
|
||||
open: Boolean,
|
||||
onClose: () -> Unit,
|
||||
fraction: () -> Float,
|
||||
color: Color,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {},
|
||||
) {
|
||||
val dismissDrawer = if (open) {
|
||||
Modifier.pointerInput(onClose) {
|
||||
detectTapGestures {
|
||||
if (!preventDismissalOnScrimClick) {
|
||||
onClose()
|
||||
}
|
||||
onScrimClick() //this function runs post onClose() so that the drawer is closed before the callback is invoked
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.then(dismissDrawer)
|
||||
.testTag(DRAWER_SCRIM_TAG)
|
||||
) {
|
||||
drawRect(color, alpha = fraction())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,483 @@
|
|||
package com.microsoft.fluentui.tokenized.drawer
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.animateTo
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.microsoft.fluentui.compose.ModalPopup
|
||||
import com.microsoft.fluentui.theme.FluentTheme
|
||||
import com.microsoft.fluentui.theme.token.ControlTokens
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.BehaviorType
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.DrawerInfo
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.DrawerTokens
|
||||
import com.microsoft.fluentui.tokenized.drawer.DraggableDefaults.AnimationSpec
|
||||
import com.microsoft.fluentui.tokenized.drawer.DraggableDefaults.PositionalThreshold
|
||||
import com.microsoft.fluentui.tokenized.drawer.DraggableDefaults.VelocityThreshold
|
||||
import com.microsoft.fluentui.tokenized.drawer.DrawerState.Companion.Saver
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
class DrawerStateV2(
|
||||
private val initialValue: DrawerValue = DrawerValue.Closed,
|
||||
internal val confirmValueChange: (DrawerValue) -> Boolean,
|
||||
internal val expandable: Boolean = true,
|
||||
internal val skipOpenState: Boolean = false
|
||||
) {
|
||||
internal val anchoredDraggableState: AnchoredDraggableState<DrawerValue> = AnchoredDraggableState(
|
||||
initialValue,
|
||||
anchors = DraggableAnchors {},
|
||||
animationSpec = AnimationSpec,
|
||||
confirmValueChange = confirmValueChange,
|
||||
positionalThreshold = { PositionalThreshold },
|
||||
velocityThreshold = { VelocityThreshold }
|
||||
)
|
||||
|
||||
init {
|
||||
if (skipOpenState) {
|
||||
require(initialValue != DrawerValue.Open) {
|
||||
"The initial value must not be set to Open if skipOpenState is set to" +
|
||||
" true."
|
||||
}
|
||||
require(expandable) {
|
||||
"Invalid state: expandable = false & skipOpenState = true"
|
||||
}
|
||||
}
|
||||
if (!expandable) {
|
||||
require(initialValue != DrawerValue.Expanded) {
|
||||
"The initial value must not be set to Expanded if expandable is set to" +
|
||||
" false."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var enable: Boolean by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* Whether drawer has Open state.
|
||||
* It is false in case of skipOpenState is true.
|
||||
*/
|
||||
internal val hasOpenedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(DrawerValue.Open)
|
||||
|
||||
/**
|
||||
* Whether the drawer is closed.
|
||||
*/
|
||||
val isClosed: Boolean
|
||||
get() = anchoredDraggableState.currentValue == DrawerValue.Closed
|
||||
|
||||
/**
|
||||
* Whether drawer has expanded state.
|
||||
*/
|
||||
internal val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(DrawerValue.Expanded)
|
||||
|
||||
|
||||
var animationInProgress: Boolean = false
|
||||
|
||||
suspend fun open() {
|
||||
enable = true
|
||||
animationInProgress = true
|
||||
/*
|
||||
* first try to open the drawer
|
||||
* if not possible then try to expand the drawer
|
||||
*/
|
||||
val targetValue = when {
|
||||
hasOpenedState -> DrawerValue.Open
|
||||
hasExpandedState -> DrawerValue.Expanded
|
||||
else -> DrawerValue.Closed
|
||||
}
|
||||
if (targetValue != anchoredDraggableState.currentValue) {
|
||||
anchoredDraggableState.animateTo(targetValue, VelocityThreshold)
|
||||
animationInProgress = false
|
||||
} else {
|
||||
animationInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun close() {
|
||||
animationInProgress = true
|
||||
try {
|
||||
anchoredDraggableState.animateTo(DrawerValue.Closed, VelocityThreshold)
|
||||
} catch (e: Exception) {
|
||||
anchoredDraggableState.animateTo(DrawerValue.Closed, VelocityThreshold)
|
||||
} finally {
|
||||
animationInProgress = false
|
||||
enable = false
|
||||
anchoredDraggableState.updateAnchors(DraggableAnchors { })
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun expand() {
|
||||
enable = true
|
||||
animationInProgress = true
|
||||
/*
|
||||
* first try to expand the drawer
|
||||
* if not possible then try to open the drawer
|
||||
*/
|
||||
val targetValue = when {
|
||||
hasExpandedState -> DrawerValue.Expanded
|
||||
hasOpenedState -> DrawerValue.Open
|
||||
else -> DrawerValue.Closed
|
||||
}
|
||||
if (targetValue != anchoredDraggableState.currentValue) {
|
||||
anchoredDraggableState.animateTo(targetValue = targetValue, VelocityThreshold)
|
||||
animationInProgress = false
|
||||
} else {
|
||||
animationInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default [Saver] implementation for [DrawerStateV2].
|
||||
*/
|
||||
fun Saver(
|
||||
expandable: Boolean,
|
||||
skipOpenState: Boolean,
|
||||
confirmValueChange: (DrawerValue) -> Boolean
|
||||
) =
|
||||
Saver<DrawerStateV2, DrawerValue>(
|
||||
save = { it.anchoredDraggableState.currentValue },
|
||||
restore = {
|
||||
DrawerStateV2(
|
||||
initialValue = it,
|
||||
expandable = expandable,
|
||||
skipOpenState = skipOpenState,
|
||||
confirmValueChange = confirmValueChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun rememberDrawerStateV2(confirmValueChange: (DrawerValue) -> Boolean = { true }): DrawerStateV2 {
|
||||
return rememberSaveable(
|
||||
saver = DrawerStateV2.Saver(
|
||||
expandable = true,
|
||||
skipOpenState = false,
|
||||
confirmValueChange = confirmValueChange
|
||||
)
|
||||
) {
|
||||
DrawerStateV2(
|
||||
initialValue = DrawerValue.Closed,
|
||||
expandable = true,
|
||||
skipOpenState = false,
|
||||
confirmValueChange = confirmValueChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberBottomDrawerStateV2(
|
||||
expandable: Boolean = true,
|
||||
skipOpenState: Boolean = false,
|
||||
confirmValueChange: (DrawerValue) -> Boolean = { true }
|
||||
): DrawerStateV2 {
|
||||
return rememberSaveable(
|
||||
confirmValueChange, expandable, skipOpenState,
|
||||
saver = DrawerStateV2.Saver(expandable, skipOpenState, confirmValueChange)
|
||||
) {
|
||||
DrawerStateV2(DrawerValue.Closed, confirmValueChange, expandable, skipOpenState)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun Modifier.bottomDrawerAnchoredDraggable(
|
||||
drawerState: DrawerStateV2,
|
||||
slideOver: Boolean,
|
||||
maxOpenHeight: Float,
|
||||
fullHeight: Float,
|
||||
drawerHeight: Float?
|
||||
): Modifier {
|
||||
val modifier = if (slideOver) {
|
||||
if (drawerHeight != null) {
|
||||
val minHeight = 0f
|
||||
val bottomOpenStateY = max(maxOpenHeight, fullHeight - drawerHeight)
|
||||
val bottomExpandedStateY = max(minHeight, fullHeight - drawerHeight)
|
||||
val drawerStateAnchors = drawerState.anchoredDraggableState.anchors
|
||||
val anchors: DraggableAnchors<DrawerValue> =
|
||||
if (drawerHeight <= maxOpenHeight) { // when contentHeight is less than maxOpenHeight
|
||||
if (drawerStateAnchors.hasAnchorFor(DrawerValue.Expanded)) {
|
||||
/*
|
||||
*For dynamic content when drawerHeight was previously greater than maxOpenHeight and now less than maxOpenHEight
|
||||
*The old anchors won't have Open state, so we need to continue with Expanded state.
|
||||
*/
|
||||
DraggableAnchors {
|
||||
DrawerValue.Expanded at bottomOpenStateY
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at bottomOpenStateY
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (drawerState.expandable) {
|
||||
if (drawerState.skipOpenState) {
|
||||
if (drawerStateAnchors.hasAnchorFor(DrawerValue.Open)) {
|
||||
/*
|
||||
*For dynamic content when drawerHeight was previously less than maxOpenHeight and now greater than maxOpenHEight
|
||||
*The old anchors won't have Expanded state, so we need to continue with Open state.
|
||||
*/
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at bottomExpandedStateY // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Expanded at bottomExpandedStateY // when drawerHeight is greater than maxOpenHeight but less than fullHeight, then Expanded state starts from fullHeight-drawerHeight
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at maxOpenHeight
|
||||
DrawerValue.Expanded at bottomExpandedStateY
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at maxOpenHeight
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
drawerState.anchoredDraggableState.updateAnchors(anchors)
|
||||
Modifier.anchoredDraggable(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = false
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
} else {
|
||||
val anchors: DraggableAnchors<DrawerValue> = if (drawerState.expandable) {
|
||||
if (drawerState.skipOpenState) {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Expanded at 0f
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at maxOpenHeight
|
||||
DrawerValue.Expanded at 0F
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DraggableAnchors {
|
||||
DrawerValue.Open at maxOpenHeight
|
||||
DrawerValue.Closed at fullHeight
|
||||
}
|
||||
}
|
||||
drawerState.anchoredDraggableState.updateAnchors(anchors)
|
||||
Modifier.anchoredDraggable(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = false
|
||||
)
|
||||
}
|
||||
return this.then(modifier)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun DrawerV2(
|
||||
modifier: Modifier = Modifier,
|
||||
behaviorType: BehaviorType = BehaviorType.BOTTOM,
|
||||
drawerState: DrawerStateV2 = rememberDrawerStateV2(),
|
||||
scrimVisible: Boolean = true,
|
||||
offset: IntOffset? = null,
|
||||
drawerTokens: DrawerTokens? = null,
|
||||
drawerContent: @Composable () -> Unit,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {}
|
||||
) {
|
||||
if (drawerState.enable) {
|
||||
val themeID =
|
||||
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
|
||||
val tokens = drawerTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.DrawerControlType] as DrawerTokens
|
||||
|
||||
val popupPositionProvider = DrawerPositionProvider(offset)
|
||||
val scope = rememberCoroutineScope()
|
||||
val close: () -> Unit = {
|
||||
if (drawerState.confirmValueChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
}
|
||||
val drawerInfo = DrawerInfo(type = behaviorType)
|
||||
Popup(
|
||||
onDismissRequest = close,
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
properties = PopupProperties(focusable = true, clippingEnabled = (offset == null))
|
||||
)
|
||||
{
|
||||
val drawerShape: Shape =
|
||||
when (behaviorType) {
|
||||
BehaviorType.BOTTOM, BehaviorType.BOTTOM_SLIDE_OVER -> RoundedCornerShape(
|
||||
topStart = tokens.borderRadius(drawerInfo),
|
||||
topEnd = tokens.borderRadius(drawerInfo)
|
||||
)
|
||||
|
||||
BehaviorType.TOP -> RoundedCornerShape(
|
||||
bottomStart = tokens.borderRadius(drawerInfo),
|
||||
bottomEnd = tokens.borderRadius(drawerInfo)
|
||||
)
|
||||
|
||||
else -> RoundedCornerShape(tokens.borderRadius(drawerInfo))
|
||||
}
|
||||
val drawerElevation: Dp = tokens.elevation(drawerInfo)
|
||||
val drawerBackgroundColor: Brush =
|
||||
tokens.backgroundBrush(drawerInfo)
|
||||
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 -> BottomDrawerV2(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackground = drawerBackgroundColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
slideOver = behaviorType == BehaviorType.BOTTOM_SLIDE_OVER,
|
||||
showHandle = true,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
|
||||
BehaviorType.TOP -> TopDrawerV2(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackground = drawerBackgroundColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
|
||||
BehaviorType.LEFT_SLIDE_OVER, BehaviorType.RIGHT_SLIDE_OVER -> HorizontalDrawerV2(
|
||||
behaviorType = behaviorType,
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackground = drawerBackgroundColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomDrawerV2(
|
||||
modifier: Modifier = Modifier,
|
||||
drawerState: DrawerStateV2 = rememberDrawerStateV2(),
|
||||
slideOver: Boolean = true,
|
||||
scrimVisible: Boolean = true,
|
||||
showHandle: Boolean = true,
|
||||
enableSwipeDismiss: Boolean = true,
|
||||
windowInsetsType: Int = WindowInsetsCompat.Type.systemBars(),
|
||||
drawerTokens: DrawerTokens? = null,
|
||||
drawerContent: @Composable () -> Unit,
|
||||
maxLandscapeWidthFraction: Float = 1F,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {},
|
||||
) {
|
||||
if (drawerState.enable) {
|
||||
val themeID =
|
||||
FluentTheme.themeID //Adding This only for recomposition in case of Token Updates. Unused otherwise.
|
||||
val tokens = drawerTokens
|
||||
?: FluentTheme.controlTokens.tokens[ControlTokens.ControlType.DrawerControlType] as DrawerTokens
|
||||
val scope = rememberCoroutineScope()
|
||||
val close: () -> Unit = {
|
||||
if (drawerState.confirmValueChange(DrawerValue.Closed)) {
|
||||
scope.launch { drawerState.close() }
|
||||
}
|
||||
}
|
||||
val behaviorType =
|
||||
if (slideOver) BehaviorType.BOTTOM_SLIDE_OVER else BehaviorType.BOTTOM
|
||||
val drawerInfo = DrawerInfo(type = behaviorType)
|
||||
ModalPopup(
|
||||
onDismissRequest = close,
|
||||
windowInsetsType = windowInsetsType
|
||||
)
|
||||
{
|
||||
val drawerShape: Shape =
|
||||
RoundedCornerShape(
|
||||
topStart = tokens.borderRadius(drawerInfo),
|
||||
topEnd = tokens.borderRadius(drawerInfo)
|
||||
)
|
||||
|
||||
val drawerElevation: Dp = tokens.elevation(drawerInfo)
|
||||
val drawerBackgroundColor: Brush =
|
||||
tokens.backgroundBrush(drawerInfo)
|
||||
val drawerHandleColor: Color = tokens.handleColor(drawerInfo)
|
||||
val scrimOpacity: Float = tokens.scrimOpacity(drawerInfo)
|
||||
val scrimColor: Color =
|
||||
tokens.scrimColor(drawerInfo).copy(alpha = scrimOpacity)
|
||||
BottomDrawerV2(
|
||||
modifier = modifier,
|
||||
drawerState = drawerState,
|
||||
drawerShape = drawerShape,
|
||||
drawerElevation = drawerElevation,
|
||||
drawerBackground = drawerBackgroundColor,
|
||||
drawerHandleColor = drawerHandleColor,
|
||||
scrimColor = scrimColor,
|
||||
scrimVisible = scrimVisible,
|
||||
slideOver = slideOver,
|
||||
showHandle = showHandle,
|
||||
enableSwipeDismiss = enableSwipeDismiss,
|
||||
onDismiss = close,
|
||||
drawerContent = drawerContent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
maxLandscapeWidthFraction = maxLandscapeWidthFraction,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package com.microsoft.fluentui.tokenized.drawer
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.dismiss
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.microsoft.fluentui.theme.token.controlTokens.BehaviorType
|
||||
import com.microsoft.fluentui.tokenized.calculateFraction
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
import com.microsoft.fluentui.util.pxToDp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HorizontalDrawerV2(
|
||||
modifier: Modifier,
|
||||
behaviorType: BehaviorType,
|
||||
drawerState: DrawerStateV2,
|
||||
drawerShape: Shape,
|
||||
drawerElevation: Dp,
|
||||
drawerBackground: Brush,
|
||||
scrimColor: Color,
|
||||
scrimVisible: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
drawerContent: @Composable () -> Unit,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {}
|
||||
) {
|
||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
val modalDrawerConstraints = constraints
|
||||
|
||||
// TODO : think about Infinite max bounds case
|
||||
if (!modalDrawerConstraints.hasBoundedWidth) {
|
||||
throw IllegalStateException("Drawer shouldn't have infinite width")
|
||||
}
|
||||
|
||||
val fullWidth = modalDrawerConstraints.maxWidth.toFloat()
|
||||
var drawerWidth by remember(fullWidth) { mutableStateOf(fullWidth) }
|
||||
//Hack to get exact drawerHeight wrt to content.
|
||||
val visible = remember { mutableStateOf(true) }
|
||||
if (visible.value) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
drawerWidth = placeable.width.toFloat()
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
} else {
|
||||
val paddingPx = pxToDp(max(dpToPx(EndDrawerPadding), (fullWidth - drawerWidth)))
|
||||
val leftSlide = behaviorType == BehaviorType.LEFT_SLIDE_OVER
|
||||
|
||||
val minValue =
|
||||
modalDrawerConstraints.maxWidth.toFloat() * (if (leftSlide) (-1F) else (1F))
|
||||
val maxValue = 0f
|
||||
|
||||
val anchors = DraggableAnchors {
|
||||
DrawerValue.Closed at minValue
|
||||
DrawerValue.Open at maxValue
|
||||
}
|
||||
drawerState.anchoredDraggableState.updateAnchors(anchors)
|
||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||
val offset = drawerState.anchoredDraggableState.offset
|
||||
Scrim(
|
||||
open = !drawerState.isClosed,
|
||||
onClose = onDismiss,
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, offset)
|
||||
},
|
||||
color = if (scrimVisible) scrimColor else Color.Transparent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = with(LocalDensity.current) {
|
||||
Modifier
|
||||
.sizeIn(
|
||||
minWidth = modalDrawerConstraints.minWidth.toDp(),
|
||||
minHeight = modalDrawerConstraints.minHeight.toDp(),
|
||||
maxWidth = modalDrawerConstraints.maxWidth.toDp(),
|
||||
maxHeight = modalDrawerConstraints.maxHeight.toDp()
|
||||
)
|
||||
}
|
||||
.offset { IntOffset(offset.roundToInt(), 0) }
|
||||
.padding(
|
||||
start = if (leftSlide) 0.dp else paddingPx,
|
||||
end = if (leftSlide) paddingPx else 0.dp
|
||||
)
|
||||
.semantics {
|
||||
if (!drawerState.isClosed) {
|
||||
dismiss {
|
||||
onDismiss()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(drawerElevation)
|
||||
.clip(drawerShape)
|
||||
.background(drawerBackground)
|
||||
.anchoredDraggable(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
// thresholds = { _, _ -> FixedThreshold(pxToDp(value = drawerWidth / 2)) },
|
||||
orientation = Orientation.Horizontal,
|
||||
enabled = false,
|
||||
reverseDirection = isRtl,
|
||||
// velocityThreshold = DrawerVelocityThreshold,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.draggable(
|
||||
orientation = Orientation.Horizontal,
|
||||
state = rememberDraggableState { delta ->
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(delta)
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
launch {
|
||||
drawerState.anchoredDraggableState.settle(
|
||||
velocity
|
||||
)
|
||||
if (drawerState.isClosed) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(DRAWER_CONTENT_TAG), content = { drawerContent() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
package com.microsoft.fluentui.tokenized.drawer
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.focus.focusTarget
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.semantics.dismiss
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.microsoft.fluentui.drawer.R
|
||||
import com.microsoft.fluentui.theme.token.Icon
|
||||
import com.microsoft.fluentui.tokenized.calculateFraction
|
||||
import com.microsoft.fluentui.util.dpToPx
|
||||
import com.microsoft.fluentui.util.pxToDp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TopDrawerV2(
|
||||
modifier: Modifier,
|
||||
drawerState: DrawerStateV2,
|
||||
drawerShape: Shape,
|
||||
drawerElevation: Dp,
|
||||
drawerBackground: Brush,
|
||||
drawerHandleColor: Color,
|
||||
scrimColor: Color,
|
||||
scrimVisible: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
drawerContent: @Composable () -> Unit,
|
||||
preventDismissalOnScrimClick: Boolean = false,
|
||||
onScrimClick: () -> Unit = {}
|
||||
) {
|
||||
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||
val fullHeight = constraints.maxHeight.toFloat()
|
||||
var drawerHeight by remember(fullHeight) { mutableStateOf(fullHeight) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alpha(0f)
|
||||
.layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
drawerHeight =
|
||||
placeable.height.toFloat() + dpToPx(DrawerHandleHeightOffset)
|
||||
}
|
||||
}
|
||||
) {
|
||||
drawerContent()
|
||||
}
|
||||
val maxOpenHeight = fullHeight * DrawerOpenFraction
|
||||
val minHeight = 0f
|
||||
val topCloseHeight = minHeight
|
||||
val topOpenHeight = min(maxOpenHeight, drawerHeight)
|
||||
|
||||
val minValue: Float = topCloseHeight
|
||||
val maxValue: Float = topOpenHeight
|
||||
|
||||
val anchors = DraggableAnchors<DrawerValue> {
|
||||
DrawerValue.Closed at topCloseHeight
|
||||
DrawerValue.Open at topOpenHeight
|
||||
}
|
||||
drawerState.anchoredDraggableState.updateAnchors(anchors)
|
||||
|
||||
val drawerConstraints = with(LocalDensity.current) {
|
||||
Modifier
|
||||
.sizeIn(
|
||||
maxWidth = constraints.maxWidth.toDp(),
|
||||
maxHeight = constraints.maxHeight.toDp()
|
||||
)
|
||||
}
|
||||
val drawerStateOffset = drawerState.anchoredDraggableState.offset
|
||||
|
||||
Scrim(
|
||||
open = !drawerState.isClosed,
|
||||
onClose = onDismiss,
|
||||
fraction = {
|
||||
calculateFraction(minValue, maxValue, drawerStateOffset)
|
||||
},
|
||||
color = if (scrimVisible) scrimColor else Color.Transparent,
|
||||
preventDismissalOnScrimClick = preventDismissalOnScrimClick,
|
||||
onScrimClick = onScrimClick
|
||||
)
|
||||
|
||||
Box(
|
||||
drawerConstraints
|
||||
.offset { IntOffset(0, 0) }
|
||||
.semantics {
|
||||
if (!drawerState.isClosed) {
|
||||
dismiss {
|
||||
onDismiss()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
.height(
|
||||
pxToDp(drawerStateOffset)
|
||||
)
|
||||
.shadow(drawerElevation)
|
||||
.clip(drawerShape)
|
||||
.background(drawerBackground)
|
||||
.anchoredDraggable(
|
||||
state = drawerState.anchoredDraggableState,
|
||||
orientation = Orientation.Vertical,
|
||||
enabled = false
|
||||
)
|
||||
.focusable(false),
|
||||
) {
|
||||
ConstraintLayout(modifier = Modifier.padding(bottom = 8.dp)) {
|
||||
val (drawerContentConstrain, drawerHandleConstrain) = createRefs()
|
||||
Column(modifier = Modifier
|
||||
.offset { IntOffset(0, 0) }
|
||||
.padding(bottom = 8.dp)
|
||||
.constrainAs(drawerContentConstrain) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(drawerHandleConstrain.top)
|
||||
}
|
||||
.focusTarget()
|
||||
.testTag(DRAWER_CONTENT_TAG), content = { drawerContent() }
|
||||
)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.constrainAs(drawerHandleConstrain) {
|
||||
top.linkTo(drawerContentConstrain.bottom)
|
||||
bottom.linkTo(parent.bottom)
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.draggable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberDraggableState { delta ->
|
||||
drawerState.anchoredDraggableState.dispatchRawDelta(delta)
|
||||
},
|
||||
onDragStopped = { velocity ->
|
||||
launch {
|
||||
drawerState.anchoredDraggableState.settle(
|
||||
velocity
|
||||
)
|
||||
if (drawerState.isClosed) {
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.testTag(DRAWER_HANDLE_TAG)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(id = R.drawable.ic_drawer_handle),
|
||||
contentDescription = null,
|
||||
tint = drawerHandleColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче