From 1c105aba91a69f2aea02a7a52cf58a8983d07945 Mon Sep 17 00:00:00 2001 From: lodev09 Date: Thu, 18 Apr 2024 14:17:42 +0800 Subject: [PATCH 1/4] refactor: remove `TrueSheetBehavior` --- .../lodev09/truesheet/TrueSheetBehavior.kt | 230 ------------------ .../com/lodev09/truesheet/TrueSheetDialog.kt | 217 +++++++++++++++-- .../com/lodev09/truesheet/TrueSheetView.kt | 77 +++--- .../lodev09/truesheet/TrueSheetViewManager.kt | 6 + .../{RootViewGroup.kt => RootSheetView.kt} | 2 +- example/src/sheets/FlatListSheet.tsx | 1 + example/src/sheets/ScrollViewSheet.tsx | 7 +- 7 files changed, 236 insertions(+), 304 deletions(-) delete mode 100644 android/src/main/java/com/lodev09/truesheet/TrueSheetBehavior.kt rename android/src/main/java/com/lodev09/truesheet/core/{RootViewGroup.kt => RootSheetView.kt} (99%) diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetBehavior.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetBehavior.kt deleted file mode 100644 index ff652e6..0000000 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetBehavior.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.lodev09.truesheet - -import android.view.MotionEvent -import android.view.ViewGroup -import android.widget.ScrollView -import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.facebook.react.bridge.ReactContext -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.lodev09.truesheet.core.Utils - -data class SizeInfo(val index: Int, val value: Float) - -class TrueSheetBehavior(private val reactContext: ReactContext) : BottomSheetBehavior() { - var maxScreenHeight: Int = 0 - var maxSheetHeight: Int? = null - - var contentView: ViewGroup? = null - var footerView: ViewGroup? = null - - var sizes: Array = arrayOf("medium", "large") - - override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: ViewGroup, event: MotionEvent): Boolean { - contentView?.let { - val isDownEvent = (event.actionMasked == MotionEvent.ACTION_DOWN) - val expanded = state == STATE_EXPANDED - - if (isDownEvent && expanded) { - for (i in 0 until it.childCount) { - val contentChild = it.getChildAt(i) - val scrolled = (contentChild is ScrollView && contentChild.scrollY > 0) - - if (!scrolled) continue - if (isInsideSheet(contentChild as ScrollView, event)) { - return false - } - } - } - } - - return super.onInterceptTouchEvent(parent, child, event) - } - - private fun isInsideSheet(scrollView: ScrollView, event: MotionEvent): Boolean { - val x = event.x - val y = event.y - - val position = IntArray(2) - scrollView.getLocationOnScreen(position) - - val nestedX = position[0] - val nestedY = position[1] - - val boundRight = nestedX + scrollView.width - val boundBottom = nestedY + scrollView.height - - return (x > nestedX && x < boundRight && y > nestedY && y < boundBottom) || - event.action == MotionEvent.ACTION_CANCEL - } - - /** - * Get the height value based on the size config value. - */ - private fun getSizeHeight(size: Any, contentHeight: Int): Int { - val height = - when (size) { - is Double -> Utils.toPixel(size) - - is Int -> Utils.toPixel(size.toDouble()) - - is String -> { - when (size) { - "auto" -> contentHeight - - "large" -> maxScreenHeight - - "medium" -> (maxScreenHeight * 0.50).toInt() - - "small" -> (maxScreenHeight * 0.25).toInt() - - else -> { - if (size.endsWith('%')) { - val percent = size.trim('%').toDoubleOrNull() - if (percent == null) { - 0 - } else { - ((percent / 100) * maxScreenHeight).toInt() - } - } else { - val fixedHeight = size.toDoubleOrNull() - if (fixedHeight == null) { - 0 - } else { - Utils.toPixel(fixedHeight) - } - } - } - } - } - - else -> (maxScreenHeight * 0.5).toInt() - } - - return minOf(height, maxSheetHeight ?: maxScreenHeight) - } - - /** - * Determines the state based on the given size index. - */ - fun getStateForSizeIndex(index: Int) = - when (sizes.size) { - 1 -> STATE_EXPANDED - - 2 -> { - when (index) { - 0 -> STATE_COLLAPSED - 1 -> STATE_EXPANDED - else -> STATE_HIDDEN - } - } - - 3 -> { - when (index) { - 0 -> STATE_COLLAPSED - 1 -> STATE_HALF_EXPANDED - 2 -> STATE_EXPANDED - else -> STATE_HIDDEN - } - } - - else -> STATE_HIDDEN - } - - /** - * Configure the sheet based on size preferences. - */ - fun configure() { - // Update the usable sheet height - maxScreenHeight = Utils.screenHeight(reactContext) - - var contentHeight = 0 - - contentView?.let { contentHeight = it.height } - footerView?.let { contentHeight += it.height } - - // Configure sheet sizes - apply { - skipCollapsed = false - isFitToContents = true - - // m3 max width 640dp - maxWidth = Utils.toPixel(640.0) - - when (sizes.size) { - 1 -> { - maxHeight = getSizeHeight(sizes[0], contentHeight) - peekHeight = maxHeight - skipCollapsed = true - } - - 2 -> { - peekHeight = getSizeHeight(sizes[0], contentHeight) - maxHeight = getSizeHeight(sizes[1], contentHeight) - } - - 3 -> { - // Enables half expanded - isFitToContents = false - - peekHeight = getSizeHeight(sizes[0], contentHeight) - halfExpandedRatio = getSizeHeight(sizes[1], contentHeight).toFloat() / maxScreenHeight.toFloat() - maxHeight = getSizeHeight(sizes[2], contentHeight) - } - } - } - } - - /** - * Get the SizeInfo data by state. - */ - fun getSizeInfoForState(state: Int): SizeInfo? = - when (sizes.size) { - 1 -> { - when (state) { - STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(maxHeight)) - else -> null - } - } - - 2 -> { - when (state) { - STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(peekHeight)) - STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(maxHeight)) - else -> null - } - } - - 3 -> { - when (state) { - STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(peekHeight)) - - STATE_HALF_EXPANDED -> { - val height = halfExpandedRatio * maxScreenHeight - SizeInfo(1, Utils.toDIP(height.toInt())) - } - - STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(maxHeight)) - - else -> null - } - } - - else -> null - } - - /** - * Get SizeInfo data for given size index. - */ - fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f) - - /** - * Set the state based on the given size index. - */ - fun setStateForSizeIndex(index: Int) { - state = getStateForSizeIndex(index) - } - - companion object { - const val TAG = "TrueSheetView" - } -} diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt index 74e2c59..0a05531 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetDialog.kt @@ -3,38 +3,34 @@ package com.lodev09.truesheet import android.graphics.Color import android.view.ViewGroup import android.view.WindowManager -import android.widget.LinearLayout -import androidx.coordinatorlayout.widget.CoordinatorLayout import com.facebook.react.uimanager.ThemedReactContext +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.lodev09.truesheet.core.KeyboardManager -import com.lodev09.truesheet.core.RootViewGroup +import com.lodev09.truesheet.core.RootSheetView import com.lodev09.truesheet.core.Utils -class TrueSheetDialog( - private val reactContext: ThemedReactContext, - private val behavior: TrueSheetBehavior, - private val rootViewGroup: RootViewGroup -) : BottomSheetDialog(reactContext) { +data class SizeInfo(val index: Int, val value: Float) + +class TrueSheetDialog(private val reactContext: ThemedReactContext, private val rootSheetView: RootSheetView) : + BottomSheetDialog(reactContext) { private var keyboardManager = KeyboardManager(reactContext) - var sheetView: ViewGroup + var maxScreenHeight: Int = 0 + var maxSheetHeight: Int? = null - init { - LinearLayout(reactContext).apply { - addView(rootViewGroup) - setContentView(this) + var contentView: ViewGroup? = null + var footerView: ViewGroup? = null - sheetView = parent as ViewGroup + var sizes: Array = arrayOf("medium", "large") - // Set to transparent background to support corner radius - sheetView.setBackgroundColor(Color.TRANSPARENT) + var sheetView: ViewGroup - // Assign our main BottomSheetBehavior - val sheetViewParams = sheetView.layoutParams as CoordinatorLayout.LayoutParams - sheetViewParams.behavior = behavior - } + init { + setContentView(rootSheetView) + sheetView = rootSheetView.parent as ViewGroup + sheetView.setBackgroundColor(Color.TRANSPARENT) // Setup window params to adjust layout based on Keyboard state. window?.apply { @@ -47,10 +43,10 @@ class TrueSheetDialog( fun show(sizeIndex: Int) { if (isShowing) { - behavior.setStateForSizeIndex(sizeIndex) + setStateForSizeIndex(sizeIndex) } else { - behavior.configure() - behavior.setStateForSizeIndex(sizeIndex) + configure() + setStateForSizeIndex(sizeIndex) this.show() } @@ -64,12 +60,12 @@ class TrueSheetDialog( keyboardManager.registerKeyboardListener(object : KeyboardManager.OnKeyboardListener { override fun onKeyboardStateChange(isVisible: Boolean, visibleHeight: Int?) { when (isVisible) { - true -> behavior.maxScreenHeight = visibleHeight ?: 0 - else -> behavior.maxScreenHeight = Utils.screenHeight(reactContext) + true -> maxScreenHeight = visibleHeight ?: 0 + else -> maxScreenHeight = Utils.screenHeight(reactContext) } - behavior.footerView?.apply { - y = (behavior.maxScreenHeight - (sheetView.top ?: 0) - height).toFloat() + footerView?.apply { + y = (maxScreenHeight - (sheetView.top ?: 0) - height).toFloat() } } }) @@ -82,6 +78,173 @@ class TrueSheetDialog( keyboardManager.unregisterKeyboardListener() } + /** + * Get the height value based on the size config value. + */ + private fun getSizeHeight(size: Any, contentHeight: Int): Int { + val height = + when (size) { + is Double -> Utils.toPixel(size) + + is Int -> Utils.toPixel(size.toDouble()) + + is String -> { + when (size) { + "auto" -> contentHeight + + "large" -> maxScreenHeight + + "medium" -> (maxScreenHeight * 0.50).toInt() + + "small" -> (maxScreenHeight * 0.25).toInt() + + else -> { + if (size.endsWith('%')) { + val percent = size.trim('%').toDoubleOrNull() + if (percent == null) { + 0 + } else { + ((percent / 100) * maxScreenHeight).toInt() + } + } else { + val fixedHeight = size.toDoubleOrNull() + if (fixedHeight == null) { + 0 + } else { + Utils.toPixel(fixedHeight) + } + } + } + } + } + + else -> (maxScreenHeight * 0.5).toInt() + } + + return minOf(height, maxSheetHeight ?: maxScreenHeight) + } + + /** + * Determines the state based on the given size index. + */ + private fun getStateForSizeIndex(index: Int) = + when (sizes.size) { + 1 -> BottomSheetBehavior.STATE_EXPANDED + + 2 -> { + when (index) { + 0 -> BottomSheetBehavior.STATE_COLLAPSED + 1 -> BottomSheetBehavior.STATE_EXPANDED + else -> BottomSheetBehavior.STATE_HIDDEN + } + } + + 3 -> { + when (index) { + 0 -> BottomSheetBehavior.STATE_COLLAPSED + 1 -> BottomSheetBehavior.STATE_HALF_EXPANDED + 2 -> BottomSheetBehavior.STATE_EXPANDED + else -> BottomSheetBehavior.STATE_HIDDEN + } + } + + else -> BottomSheetBehavior.STATE_HIDDEN + } + + /** + * Configure the sheet based on size preferences. + */ + fun configure() { + // Update the usable sheet height + maxScreenHeight = Utils.screenHeight(reactContext) + + var contentHeight = 0 + + contentView?.let { contentHeight = it.height } + footerView?.let { contentHeight += it.height } + + // Configure sheet sizes + behavior.apply { + skipCollapsed = false + isFitToContents = true + + // m3 max width 640dp + maxWidth = Utils.toPixel(640.0) + + when (sizes.size) { + 1 -> { + maxHeight = getSizeHeight(sizes[0], contentHeight) + peekHeight = maxHeight + skipCollapsed = true + } + + 2 -> { + peekHeight = getSizeHeight(sizes[0], contentHeight) + maxHeight = getSizeHeight(sizes[1], contentHeight) + } + + 3 -> { + // Enables half expanded + isFitToContents = false + + peekHeight = getSizeHeight(sizes[0], contentHeight) + halfExpandedRatio = getSizeHeight(sizes[1], contentHeight).toFloat() / maxScreenHeight.toFloat() + maxHeight = getSizeHeight(sizes[2], contentHeight) + } + } + } + } + + /** + * Get the SizeInfo data by state. + */ + fun getSizeInfoForState(state: Int): SizeInfo? = + when (sizes.size) { + 1 -> { + when (state) { + BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(0, Utils.toDIP(behavior.maxHeight)) + else -> null + } + } + + 2 -> { + when (state) { + BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight)) + BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(1, Utils.toDIP(behavior.maxHeight)) + else -> null + } + } + + 3 -> { + when (state) { + BottomSheetBehavior.STATE_COLLAPSED -> SizeInfo(0, Utils.toDIP(behavior.peekHeight)) + + BottomSheetBehavior.STATE_HALF_EXPANDED -> { + val height = behavior.halfExpandedRatio * maxScreenHeight + SizeInfo(1, Utils.toDIP(height.toInt())) + } + + BottomSheetBehavior.STATE_EXPANDED -> SizeInfo(2, Utils.toDIP(behavior.maxHeight)) + + else -> null + } + } + + else -> null + } + + /** + * Get SizeInfo data for given size index. + */ + fun getSizeInfoForIndex(index: Int) = getSizeInfoForState(getStateForSizeIndex(index)) ?: SizeInfo(0, 0f) + + /** + * Set the state based on the given size index. + */ + fun setStateForSizeIndex(index: Int) { + behavior.state = getStateForSizeIndex(index) + } + companion object { const val TAG = "TrueSheetView" } diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt index ed3a270..2298eae 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt @@ -13,7 +13,7 @@ import com.facebook.react.uimanager.events.EventDispatcher import com.google.android.material.bottomsheet.BottomSheetBehavior import com.lodev09.truesheet.core.DismissEvent import com.lodev09.truesheet.core.PresentEvent -import com.lodev09.truesheet.core.RootViewGroup +import com.lodev09.truesheet.core.RootSheetView import com.lodev09.truesheet.core.SizeChangeEvent class TrueSheetView(context: Context) : @@ -47,15 +47,10 @@ class TrueSheetView(context: Context) : */ private val sheetDialog: TrueSheetDialog - /** - * The custom BottomSheetDialogBehavior instance. - */ - private val sheetBehavior: TrueSheetBehavior - /** * React root view placeholder. */ - private val sheetRootView: RootViewGroup + val rootSheetView: RootSheetView /** * 1st child of the container view. @@ -71,11 +66,9 @@ class TrueSheetView(context: Context) : reactContext.addLifecycleEventListener(this) eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id) - sheetRootView = RootViewGroup(context) - sheetRootView.eventDispatcher = eventDispatcher + rootSheetView = RootSheetView(context) - sheetBehavior = TrueSheetBehavior(reactContext) - sheetDialog = TrueSheetDialog(reactContext, sheetBehavior, sheetRootView) + sheetDialog = TrueSheetDialog(reactContext, rootSheetView) // Configure Sheet Dialog sheetDialog.apply { @@ -86,7 +79,7 @@ class TrueSheetView(context: Context) : // Initialize footer y footerView?.apply { UiThreadUtil.runOnUiThread { - y = (sheetBehavior.maxScreenHeight - sheetView.top - height).toFloat() + y = (sheetDialog.maxScreenHeight - sheetView.top - height).toFloat() } } @@ -96,7 +89,7 @@ class TrueSheetView(context: Context) : } // dispatch onPresent event - eventDispatcher?.dispatchEvent(PresentEvent(surfaceId, id, sheetBehavior.getSizeInfoForIndex(activeIndex))) + eventDispatcher?.dispatchEvent(PresentEvent(surfaceId, id, sheetDialog.getSizeInfoForIndex(activeIndex))) } // Setup listener when the dialog has been dismissed. @@ -113,8 +106,8 @@ class TrueSheetView(context: Context) : } // Configure sheet behavior events - sheetBehavior.apply { - addBottomSheetCallback( + sheetDialog.apply { + behavior.addBottomSheetCallback( object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(sheetView: View, slideOffset: Float) { footerView?.let { @@ -128,24 +121,18 @@ class TrueSheetView(context: Context) : } override fun onStateChanged(view: View, newState: Int) { - when (newState) { - BottomSheetBehavior.STATE_HIDDEN -> sheetDialog.dismiss() - - else -> { - val sizeInfo = getSizeInfoForState(newState) - if (sizeInfo != null && sizeInfo.index != activeIndex) { - // Invoke promise when sheet resized programmatically - presentPromise?.let { promise -> - promise() - presentPromise = null - } - - activeIndex = sizeInfo.index - - // dispatch onSizeChange event - eventDispatcher?.dispatchEvent(SizeChangeEvent(surfaceId, id, sizeInfo)) - } + val sizeInfo = getSizeInfoForState(newState) + if (sizeInfo != null && sizeInfo.index != activeIndex) { + // Invoke promise when sheet resized programmatically + presentPromise?.let { promise -> + promise() + presentPromise = null } + + activeIndex = sizeInfo.index + + // dispatch onSizeChange event + eventDispatcher?.dispatchEvent(SizeChangeEvent(surfaceId, id, sizeInfo)) } } } @@ -154,7 +141,7 @@ class TrueSheetView(context: Context) : } override fun dispatchProvideStructure(structure: ViewStructure) { - sheetRootView.dispatchProvideStructure(structure) + rootSheetView.dispatchProvideStructure(structure) } override fun onLayout( @@ -181,29 +168,29 @@ class TrueSheetView(context: Context) : contentView = it.getChildAt(0) as ViewGroup footerView = it.getChildAt(1) as ViewGroup - sheetBehavior.contentView = contentView - sheetBehavior.footerView = footerView + sheetDialog.contentView = contentView + sheetDialog.footerView = footerView // rootView's first child is the Container View - sheetRootView.addView(it, index) + rootSheetView.addView(it, index) } } override fun getChildCount(): Int { // This method may be called by the parent constructor // before rootView is initialized. - return sheetRootView.childCount + return rootSheetView.childCount } - override fun getChildAt(index: Int): View = sheetRootView.getChildAt(index) + override fun getChildAt(index: Int): View = rootSheetView.getChildAt(index) override fun removeView(child: View) { - sheetRootView.removeView(child) + rootSheetView.removeView(child) } override fun removeViewAt(index: Int) { val child = getChildAt(index) - sheetRootView.removeView(child) + rootSheetView.removeView(child) } override fun addChildrenForAccessibility(outChildren: ArrayList) { @@ -232,18 +219,18 @@ class TrueSheetView(context: Context) : } fun setMaxHeight(height: Int) { - sheetBehavior.maxSheetHeight = height - sheetBehavior.configure() + sheetDialog.maxSheetHeight = height + sheetDialog.configure() } fun setDismissible(dismissible: Boolean) { - sheetBehavior.isHideable = dismissible + sheetDialog.behavior.isHideable = dismissible sheetDialog.setCancelable(dismissible) } fun setSizes(newSizes: Array) { - sheetBehavior.sizes = newSizes - sheetBehavior.configure() + sheetDialog.sizes = newSizes + sheetDialog.configure() } /** diff --git a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt index b26c439..3d93902 100644 --- a/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +++ b/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt @@ -5,6 +5,7 @@ import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableType import com.facebook.react.common.MapBuilder import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.ViewGroupManager import com.facebook.react.uimanager.annotations.ReactProp import com.lodev09.truesheet.core.DismissEvent @@ -53,6 +54,11 @@ class TrueSheetViewManager : ViewGroupManager() { view.setSizes(result.toArray()) } + override fun addEventEmitters(reactContext: ThemedReactContext, view: TrueSheetView) { + super.addEventEmitters(reactContext, view) + view.rootSheetView.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.getId()) + } + companion object { const val TAG = "TrueSheetView" } diff --git a/android/src/main/java/com/lodev09/truesheet/core/RootViewGroup.kt b/android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt similarity index 99% rename from android/src/main/java/com/lodev09/truesheet/core/RootViewGroup.kt rename to android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt index d9e21db..cd38359 100644 --- a/android/src/main/java/com/lodev09/truesheet/core/RootViewGroup.kt +++ b/android/src/main/java/com/lodev09/truesheet/core/RootSheetView.kt @@ -26,7 +26,7 @@ import com.facebook.react.views.view.ReactViewGroup * styleHeight on the LayoutShadowNode to be the window size. This is done through the * UIManagerModule, and will then cause the children to layout as if they can fill the window. */ -class RootViewGroup(context: Context?) : +class RootSheetView(context: Context?) : ReactViewGroup(context), RootView { private var hasAdjustedSize = false diff --git a/example/src/sheets/FlatListSheet.tsx b/example/src/sheets/FlatListSheet.tsx index 498c395..4f91bcd 100644 --- a/example/src/sheets/FlatListSheet.tsx +++ b/example/src/sheets/FlatListSheet.tsx @@ -24,6 +24,7 @@ export const FlatListSheet = forwardRef((props: FlatListSheetProps, ref: Ref ref={flatListRef} + nestedScrollEnabled data={times(50, (i) => i)} contentContainerStyle={$content} indicatorStyle="black" diff --git a/example/src/sheets/ScrollViewSheet.tsx b/example/src/sheets/ScrollViewSheet.tsx index c85aff3..bcdb1b9 100644 --- a/example/src/sheets/ScrollViewSheet.tsx +++ b/example/src/sheets/ScrollViewSheet.tsx @@ -19,7 +19,12 @@ export const ScrollViewSheet = forwardRef((props: ScrollViewSheetProps, ref: Ref FooterComponent={