From 262a2128f3c3266446972863f6be90a34e70c76e Mon Sep 17 00:00:00 2001 From: Mugurell Date: Mon, 22 Aug 2022 17:43:40 +0300 Subject: [PATCH] [fenix] For https://github.com/mozilla-mobile/fenix/issues/26584 - Add support to align the CFR composable inside a wider anchor Supported anchorings will now be: - INDICATOR_CENTERED_IN_ANCHOR - previous functionality - allows to have the indicator point to exactly the middle of a smaller anchor. - BODY_TO_ANCHOR_CENTER - new default - allows to align the popup inside a wider anchor - BODY_TO_ANCHOR_START - new anchoring - allows to align the popup flushed to it's anchor's start. --- .../org/mozilla/fenix/compose/cfr/CFRPopup.kt | 26 +++++ .../compose/cfr/CFRPopupFullscreenLayout.kt | 96 ++++++++++++------- 2 files changed, 87 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt index 64797d367c..49e96719ec 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopup.kt @@ -9,12 +9,14 @@ import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.mozilla.fenix.compose.cfr.CFRPopup.PopupAlignment import java.lang.ref.WeakReference /** * Properties used to customize the behavior of a [CFRPopup]. * * @property popupWidth Width of the popup. Defaults to [CFRPopup.DEFAULT_WIDTH]. + * @property popupAlignment Where in relation to it's anchor should the popup be placed. * @property indicatorDirection The direction the indicator arrow is pointing. * @property dismissOnBackPress Whether the popup can be dismissed by pressing the back button. * If true, pressing the back button will also call onDismiss(). @@ -29,6 +31,7 @@ import java.lang.ref.WeakReference */ data class CFRPopupProperties( val popupWidth: Dp = CFRPopup.DEFAULT_WIDTH.dp, + val popupAlignment: PopupAlignment = PopupAlignment.BODY_TO_ANCHOR_CENTER, val indicatorDirection: CFRPopup.IndicatorDirection = CFRPopup.IndicatorDirection.UP, val dismissOnBackPress: Boolean = true, val dismissOnClickOutside: Boolean = true, @@ -90,6 +93,29 @@ class CFRPopup( DOWN } + /** + * Possible alignments of the popup in relation to it's anchor. + */ + enum class PopupAlignment { + /** + * The popup body will be centered in the space occupied by the anchor. + * Recommended to be used when the anchor is wider than the popup. + */ + BODY_TO_ANCHOR_CENTER, + + /** + * The popup body will be shown aligned to exactly the anchor start. + */ + BODY_TO_ANCHOR_START, + + /** + * The popup will be aligned such that the indicator arrow will point to exactly the middle of the anchor. + * Recommended to be used when there are multiple widgets displayed horizontally so that this will allow + * to indicate exactly which widget the popup refers to. + */ + INDICATOR_CENTERED_IN_ANCHOR + } + companion object { /** * Default width for all CFRs. diff --git a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopupFullscreenLayout.kt b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopupFullscreenLayout.kt index 3cfd163ceb..ac5a362755 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopupFullscreenLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/cfr/CFRPopupFullscreenLayout.kt @@ -28,14 +28,17 @@ import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.marginStart import androidx.lifecycle.ViewTreeLifecycleOwner import androidx.savedstate.ViewTreeSavedStateRegistryOwner import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.compose.cfr.CFRPopup.IndicatorDirection.DOWN import org.mozilla.fenix.compose.cfr.CFRPopup.IndicatorDirection.UP +import org.mozilla.fenix.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_CENTER +import org.mozilla.fenix.compose.cfr.CFRPopup.PopupAlignment.BODY_TO_ANCHOR_START +import org.mozilla.fenix.compose.cfr.CFRPopup.PopupAlignment.INDICATOR_CENTERED_IN_ANCHOR import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.gecko.GeckoScreenOrientation -import kotlin.math.absoluteValue import kotlin.math.roundToInt /** @@ -123,17 +126,16 @@ internal class CFRPopupFullScreenLayout( anchor.getLocationOnScreen(this) } - val anchorXCoordMiddle = Pixels(anchorLocation.first() + anchor.width / 2) val indicatorArrowHeight = Pixels( CFRPopup.DEFAULT_INDICATOR_HEIGHT.dp.toPx() ) val popupBounds = computePopupHorizontalBounds( - anchorMiddleXCoord = anchorXCoordMiddle, arrowIndicatorWidth = Pixels(CFRPopupShape.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value)), ) + val indicatorOffset = computeIndicatorArrowStartCoord( - anchorMiddleXCoord = anchorXCoordMiddle, + anchorMiddleXCoord = Pixels(anchorLocation.first() + anchor.width / 2), popupStartCoord = popupBounds.startCoord, arrowIndicatorWidth = Pixels( CFRPopupShape.getIndicatorBaseWidthForHeight(indicatorArrowHeight.value) @@ -219,24 +221,35 @@ internal class CFRPopupFullScreenLayout( * This assumes anchoring is indicated with an arrow to the horizontal middle of the anchor with the popup's * body potentially extending to the `start` of the arrow indicator. * - * @param anchorMiddleXCoord x-coordinate for the middle of the anchor. * @param arrowIndicatorWidth x-distance the arrow indicator occupies. */ @Composable private fun computePopupHorizontalBounds( - anchorMiddleXCoord: Pixels, arrowIndicatorWidth: Pixels ): PopupHorizontalBounds { val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2 return if (LocalConfiguration.current.layoutDirection == View.LAYOUT_DIRECTION_LTR) { - // Push the popup as far to the start as needed including any needed paddings. - val startCoord = Pixels( - (anchorMiddleXCoord.value - arrowIndicatorHalfWidth) - .minus(properties.indicatorArrowStartOffset.toPx()) - .minus(CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) - .coerceAtLeast(getLeftInsets()) - ) + val startCoord = when (properties.popupAlignment) { + BODY_TO_ANCHOR_START -> { + val visibleAnchorStart = anchor.x.roundToInt() + anchor.paddingStart + anchor.marginStart + Pixels(visibleAnchorStart - CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) + } + BODY_TO_ANCHOR_CENTER -> { + val popupWidth = (properties.popupWidth + CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp * 2).toPx() + Pixels(((anchor.x.roundToInt() + anchor.width) - popupWidth) / 2) + } + INDICATOR_CENTERED_IN_ANCHOR -> { + val anchorMiddleXCoord = Pixels(anchor.x.roundToInt() + anchor.width / 2) + // Push the popup as far to the start as needed including any needed paddings. + Pixels( + (anchorMiddleXCoord.value - arrowIndicatorHalfWidth) + .minus(properties.indicatorArrowStartOffset.toPx()) + .minus(CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) + .coerceAtLeast(getLeftInsets()) + ) + } + } PopupHorizontalBounds( startCoord = startCoord, @@ -247,20 +260,30 @@ internal class CFRPopupFullScreenLayout( ) ) } else { - val startCoord = Pixels( - // Push the popup as far to the start (in RTL) as possible. - anchorMiddleXCoord.value - .plus(arrowIndicatorHalfWidth) - .plus(properties.indicatorArrowStartOffset.toPx()) - .plus(CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) - .coerceAtMost( - LocalDensity.current.run { - LocalConfiguration.current.screenWidthDp.dp.toPx() - } - .roundToInt() + val startCoord = when (properties.popupAlignment) { + BODY_TO_ANCHOR_START -> { + val visibleAnchorEnd = anchor.x.roundToInt() + anchor.width - anchor.paddingStart + Pixels(visibleAnchorEnd + CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) + } + BODY_TO_ANCHOR_CENTER -> { + val popupWidth = (properties.popupWidth + CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp * 2).toPx() + val screenWidth = LocalConfiguration.current.screenWidthDp.dp.toPx() + Pixels(screenWidth - ((anchor.x.roundToInt() + anchor.width) - popupWidth) / 2) + } + INDICATOR_CENTERED_IN_ANCHOR -> { + val anchorMiddleXCoord = Pixels(anchor.x.roundToInt() + anchor.width / 2) + Pixels( + // Push the popup as far to the start (in RTL) as possible. + anchorMiddleXCoord.value + .plus(arrowIndicatorHalfWidth) + .plus(properties.indicatorArrowStartOffset.toPx()) + .plus(CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx()) + .coerceAtMost(LocalConfiguration.current.screenWidthDp.dp.toPx()) .plus(getLeftInsets()) ) - ) + } + } + PopupHorizontalBounds( startCoord = startCoord, endCoord = Pixels( @@ -286,18 +309,21 @@ internal class CFRPopupFullScreenLayout( popupStartCoord: Pixels, arrowIndicatorWidth: Pixels ): Pixels { - val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2 + return when (properties.popupAlignment) { + BODY_TO_ANCHOR_START, + BODY_TO_ANCHOR_CENTER -> Pixels(properties.indicatorArrowStartOffset.toPx()) + INDICATOR_CENTERED_IN_ANCHOR -> { + val arrowIndicatorHalfWidth = arrowIndicatorWidth.value / 2 + if (LocalConfiguration.current.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + val visiblePopupStartCoord = popupStartCoord.value + CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx() - return if (LocalConfiguration.current.layoutDirection == View.LAYOUT_DIRECTION_LTR) { - val visiblePopupStartCoord = popupStartCoord.value + CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx() - val arrowIndicatorStartCoord = anchorMiddleXCoord.value - arrowIndicatorHalfWidth + Pixels(anchorMiddleXCoord.value - arrowIndicatorHalfWidth - visiblePopupStartCoord) + } else { + val visiblePopupEndCoord = popupStartCoord.value - CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx() - Pixels((visiblePopupStartCoord - arrowIndicatorStartCoord).absoluteValue) - } else { - val indicatorStartCoord = popupStartCoord.value - CFRPopup.DEFAULT_HORIZONTAL_PADDING.dp.toPx() - - anchorMiddleXCoord.value - arrowIndicatorHalfWidth - - Pixels(indicatorStartCoord.absoluteValue) + Pixels(visiblePopupEndCoord - anchorMiddleXCoord.value - arrowIndicatorHalfWidth) + } + } } }