Expand menus from the generating element direction
Bug: 158804726
Test: MenuTest
Change-Id: Iad5aa511b69d8d6d5a15c092560f36bd100a47cd
diff --git a/ui/ui-material/src/androidTest/java/androidx/ui/material/MenuTest.kt b/ui/ui-material/src/androidTest/java/androidx/ui/material/MenuTest.kt
index a5512a9..ed16c4a 100644
--- a/ui/ui-material/src/androidTest/java/androidx/ui/material/MenuTest.kt
+++ b/ui/ui-material/src/androidTest/java/androidx/ui/material/MenuTest.kt
@@ -46,7 +46,10 @@
import androidx.ui.unit.IntOffset
import androidx.ui.unit.IntSize
import androidx.ui.unit.Position
+import androidx.ui.unit.PxBounds
import androidx.ui.unit.dp
+import androidx.ui.unit.toOffset
+import androidx.ui.unit.toSize
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
@@ -234,6 +237,51 @@
}
@Test
+ fun menu_positioning_callback() {
+ val screenWidth = 500
+ val screenHeight = 1000
+ val density = Density(1f)
+ val displayMetrics = DisplayMetrics().apply {
+ widthPixels = screenWidth
+ heightPixels = screenHeight
+ }
+ val anchorPosition = IntOffset(100, 200)
+ val anchorSize = IntSize(10, 20)
+ val inset = with(density) { MenuElevation.toIntPx() }
+ val offsetX = 20
+ val offsetY = 40
+ val popupSize = IntSize(50, 80)
+
+ var obtainedParentBounds = PxBounds(0f, 0f, 0f, 0f)
+ var obtainedMenuBounds = PxBounds(0f, 0f, 0f, 0f)
+ DropdownMenuPositionProvider(
+ Position(offsetX.dp, offsetY.dp),
+ density,
+ displayMetrics
+ ) { parentBounds, menuBounds ->
+ obtainedParentBounds = parentBounds
+ obtainedMenuBounds = menuBounds
+ }.calculatePosition(
+ anchorPosition,
+ anchorSize,
+ LayoutDirection.Ltr,
+ popupSize
+ )
+
+ assertThat(obtainedParentBounds).isEqualTo(
+ PxBounds(anchorPosition.toOffset(), anchorSize.toSize())
+ )
+ assertThat(obtainedMenuBounds).isEqualTo(
+ PxBounds(
+ anchorPosition.x.toFloat() + anchorSize.width - inset + offsetX,
+ anchorPosition.y.toFloat() + anchorSize.height - inset + offsetY,
+ anchorPosition.x.toFloat() + anchorSize.width - inset + offsetX + popupSize.width,
+ anchorPosition.y.toFloat() + anchorSize.height - inset + offsetY + popupSize.height
+ )
+ )
+ }
+
+ @Test
fun dropdownMenuItem_emphasis() {
var >
var enabledContentColor = Color.Unset
diff --git a/ui/ui-material/src/main/java/androidx/ui/material/Menu.kt b/ui/ui-material/src/main/java/androidx/ui/material/Menu.kt
index 7290df1..e420d35 100644
--- a/ui/ui-material/src/main/java/androidx/ui/material/Menu.kt
+++ b/ui/ui-material/src/main/java/androidx/ui/material/Menu.kt
@@ -23,17 +23,19 @@
import androidx.compose.Composable
import androidx.compose.Immutable
import androidx.compose.getValue
+import androidx.compose.remember
import androidx.compose.setValue
import androidx.compose.state
import androidx.ui.animation.Transition
import androidx.ui.core.ContextAmbient
import androidx.ui.core.DensityAmbient
+import androidx.ui.core.DrawLayerModifier
import androidx.ui.core.LayoutDirection
import androidx.ui.core.Modifier
import androidx.ui.core.Popup
import androidx.ui.core.PopupPositionProvider
+import androidx.ui.core.TransformOrigin
import androidx.ui.unit.Position
-import androidx.ui.core.drawLayer
import androidx.ui.foundation.Box
import androidx.ui.foundation.ContentGravity
import androidx.ui.foundation.ProvideTextStyle
@@ -50,7 +52,15 @@
import androidx.ui.unit.Density
import androidx.ui.unit.IntOffset
import androidx.ui.unit.IntSize
+import androidx.ui.unit.PxBounds
import androidx.ui.unit.dp
+import androidx.ui.unit.height
+import androidx.ui.unit.toOffset
+import androidx.ui.unit.toSize
+import androidx.ui.unit.width
+import kotlin.math.max
+import kotlin.math.min
+
/**
* A Material Design [dropdown menu](https://material.io/components/menus#dropdown-menu).
*
@@ -94,11 +104,15 @@
toggle()
if (visibleMenu) {
+ var transformOrigin by state { TransformOrigin.Center }
+ val density = DensityAmbient.current
val popupPositionProvider = DropdownMenuPositionProvider(
dropdownOffset,
- DensityAmbient.current,
+ density,
ContextAmbient.current.resources.displayMetrics
- )
+ ) { parentBounds, menuBounds ->
+ transformOrigin = calculateTransformOrigin(parentBounds, menuBounds, density)
+ }
Popup(
isFocusable = true,
@@ -113,18 +127,22 @@
visibleMenu = it
}
) { state ->
- val scale = state[Scale]
- val alpha = state[Alpha]
+ val drawLayer = remember {
+ MenuDrawLayerModifier(
+ { state[Scale] },
+ { state[Alpha] },
+ { transformOrigin }
+ )
+ }
Card(
- modifier = Modifier
- .drawLayer(scaleX = scale, scaleY = scale, alpha = alpha, clip = true)
+ modifier = drawLayer
// Padding to account for the elevation, otherwise it is clipped.
.padding(MenuElevation),
elevation = MenuElevation
) {
@OptIn(ExperimentalLayout::class)
Column(
- dropdownModifier
+ modifier = dropdownModifier
.padding(vertical = DropdownMenuVerticalPadding)
.preferredWidth(IntrinsicSize.Max),
children = dropdownContent
@@ -227,6 +245,53 @@
}
}
+private class MenuDrawLayerModifier(
+ val scaleProvider: () -> Float,
+ val alphaProvider: () -> Float,
+ val transformOriginProvider: () -> TransformOrigin
+) : DrawLayerModifier {
+ override val scaleX: Float get() = scaleProvider()
+ override val scaleY: Float get() = scaleProvider()
+ override val alpha: Float get() = alphaProvider()
+ override val transformOrigin: TransformOrigin get() = transformOriginProvider()
+ override val clip: Boolean = true
+}
+
+private fun calculateTransformOrigin(
+ parentBounds: PxBounds,
+ menuBounds: PxBounds,
+ density: Density
+): TransformOrigin {
+ val inset = with(density) { MenuElevation.toPx() }
+ val realMenuBounds = PxBounds(
+ menuBounds.left + inset,
+ menuBounds.top + inset,
+ menuBounds.right - inset,
+ menuBounds.bottom - inset
+ )
+ val pivotX = when {
+ realMenuBounds.left >= parentBounds.right -> 0f
+ realMenuBounds.right <= parentBounds.left -> 1f
+ else -> {
+ val intersectionCenter =
+ (max(parentBounds.left, realMenuBounds.left) +
+ min(parentBounds.right, realMenuBounds.right)) / 2
+ (intersectionCenter + inset - menuBounds.left) / menuBounds.width
+ }
+ }
+ val pivotY = when {
+ realMenuBounds.top >= parentBounds.bottom -> 0f
+ realMenuBounds.bottom <= parentBounds.top -> 1f
+ else -> {
+ val intersectionCenter =
+ (max(parentBounds.top, realMenuBounds.top) +
+ min(parentBounds.bottom, realMenuBounds.bottom)) / 2
+ (intersectionCenter + inset - menuBounds.top) / menuBounds.height
+ }
+ }
+ return TransformOrigin(pivotX, pivotY)
+}
+
// Menu positioning.
/**
@@ -237,7 +302,8 @@
internal data class DropdownMenuPositionProvider(
val contentOffset: Position,
val density: Density,
- val displayMetrics: DisplayMetrics
+ val displayMetrics: DisplayMetrics,
+ val onPositionCalculated: (PxBounds, PxBounds) -> Unit = { _, _ -> }
) : PopupPositionProvider {
override fun calculatePosition(
parentLayoutPosition: IntOffset,
@@ -276,6 +342,16 @@
it >= 0 && it + realPopupHeight <= displayMetrics.heightPixels
} ?: toTop
+ // TODO(popam, b/159596546): we should probably have androidx.ui.unit.IntBounds instead
+ onPositionCalculated(
+ PxBounds(parentLayoutPosition.toOffset(), parentLayoutSize.toSize()),
+ PxBounds(
+ x.toFloat() - inset,
+ y.toFloat() - inset,
+ x.toFloat() + inset + realPopupWidth,
+ y.toFloat() + inset + realPopupHeight
+ )
+ )
return IntOffset(x - inset, y - inset)
}
}