package com.swmansion.rnscreens.bottomsheet import android.view.View import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN import com.swmansion.rnscreens.BuildConfig import com.swmansion.rnscreens.Screen object SheetUtils { /** * Verifies whether BottomSheetBehavior.State is one of stable states. As unstable states * we consider `STATE_DRAGGING` and `STATE_SETTLING`. * * @param state bottom sheet state to verify */ fun isStateStable(state: Int): Boolean = when (state) { STATE_HIDDEN, STATE_EXPANDED, STATE_COLLAPSED, STATE_HALF_EXPANDED, -> true else -> false } /** * This method maps indices from legal detents array (prop) to appropriate values * recognized by BottomSheetBehaviour. In particular used when setting up the initial behaviour * of the form sheet. * * @param index index from array with detents fractions * @param detentCount length of array with detents fractions * * @throws IllegalArgumentException for invalid index / detentCount combinations */ fun sheetStateFromDetentIndex( index: Int, detentCount: Int, ): Int = when (detentCount) { 1 -> when (index) { -1 -> STATE_HIDDEN 0 -> STATE_EXPANDED else -> throw IllegalArgumentException("[RNScreens] Invalid detentCount/index combination $detentCount / $index") } 2 -> when (index) { -1 -> STATE_HIDDEN 0 -> STATE_COLLAPSED 1 -> STATE_EXPANDED else -> throw IllegalArgumentException("[RNScreens] Invalid detentCount/index combination $detentCount / $index") } 3 -> when (index) { -1 -> STATE_HIDDEN 0 -> STATE_COLLAPSED 1 -> STATE_HALF_EXPANDED 2 -> STATE_EXPANDED else -> throw IllegalArgumentException("[RNScreens] Invalid detentCount/index combination $detentCount / $index") } else -> throw IllegalArgumentException("[RNScreens] Invalid detentCount/index combination $detentCount / $index") } /** * This method maps BottomSheetBehavior.State values to appropriate indices of detents array. * * @param state state of the bottom sheet * @param detentCount length of array with detents fractions * * @throws IllegalArgumentException for invalid state / detentCount combinations */ fun detentIndexFromSheetState( @BottomSheetBehavior.State state: Int, detentCount: Int, ): Int = when (detentCount) { 1 -> when (state) { STATE_HIDDEN -> -1 STATE_EXPANDED -> 0 else -> throw IllegalArgumentException("[RNScreens] Invalid state $state for detentCount $detentCount") } 2 -> when (state) { STATE_HIDDEN -> -1 STATE_COLLAPSED -> 0 STATE_EXPANDED -> 1 else -> throw IllegalArgumentException("[RNScreens] Invalid state $state for detentCount $detentCount") } 3 -> when (state) { STATE_HIDDEN -> -1 STATE_COLLAPSED -> 0 STATE_HALF_EXPANDED -> 1 STATE_EXPANDED -> 2 else -> throw IllegalArgumentException("[RNScreens] Invalid state $state for detentCount $detentCount") } else -> throw IllegalArgumentException("[RNScreens] Invalid state $state for detentCount $detentCount") } fun isStateLessEqualThan( state: Int, otherState: Int, ): Boolean { if (state == otherState) { return true } if (state != STATE_HALF_EXPANDED && otherState != STATE_HALF_EXPANDED) { return state > otherState } if (state == STATE_HALF_EXPANDED) { return otherState == BottomSheetBehavior.STATE_EXPANDED } if (state == STATE_COLLAPSED) { return otherState != STATE_HIDDEN } return false } } fun Screen.isSheetFitToContents(): Boolean = stackPresentation === Screen.StackPresentation.FORM_SHEET && sheetDetents.count == 1 && sheetDetents.shortest() == SheetDetents.SHEET_FIT_TO_CONTENTS fun Screen.usesFormSheetPresentation(): Boolean = stackPresentation === Screen.StackPresentation.FORM_SHEET fun Screen.requiresEnterTransitionPostponing(): Boolean { // On old architecture the content wrapper might not have received its frame yet, // which is required to determine height of the sheet after animation. Therefore // we delay the transition and trigger it after views receive the layout. // This is used only for formSheet presentation, because we use value animators // there. Tween animations have some magic way to make this work (maybe they // postpone the transition internally, dunno). // // On Fabric, system insets are applied after the initial layout pass. However, // the BottomSheet height might be measured earlier due to internal BottomSheet logic // or layout callbacks, before those insets are applied. // To ensure the BottomSheet height respects the top inset we delay starting the enter // transition until both layout and insets are fully applied. if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED && !this.sheetShouldOverflowTopInset && this.usesFormSheetPresentation()) { return true } if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED || !this.usesFormSheetPresentation()) { return false } // Assumes that formSheet uses content wrapper return !this.isLaidOutOrHasCachedLayout() || this.contentWrapper?.isLaidOutOrHasCachedLayout() != true } /** * The view might not be laid out, but have cached dimensions e.g. when host fragment * is reattached to container. */ fun View.isLaidOutOrHasCachedLayout() = this.isLaidOut || height > 0 || width > 0