basic scrollview on the native side

This commit is contained in:
Hailey
2024-10-09 23:03:17 -07:00
parent ad896cb8dd
commit df86081d19
8 changed files with 113 additions and 119 deletions
@@ -20,7 +20,6 @@ public class BottomSheetModule: Module {
} }
AsyncFunction("updateLayout") { (view: SheetView) in AsyncFunction("updateLayout") { (view: SheetView) in
view.updateLayout()
} }
Prop("containerBackgroundColor") { (view: SheetView, prop: UIColor) in Prop("containerBackgroundColor") { (view: SheetView, prop: UIColor) in
@@ -6,30 +6,97 @@
// //
class BottomSheetScrollView: UIScrollView, UIScrollViewDelegate { class BottomSheetScrollView: UIScrollView, UIScrollViewDelegate {
private let sheetView: SheetView
private var previousHeight: CGFloat? private var previousHeight: CGFloat?
private var beganDraggingY: CGFloat?
init() { init(sheetView: SheetView) {
self.sheetView = sheetView
super.init(frame: .zero) super.init(frame: .zero)
if let window = Util.getWindow() { if let window = Util.getWindow() {
self.frame = window.bounds self.frame = window.bounds
self.contentInset = UIEdgeInsets(top: 0, let offset = window.safeAreaInsets.bottom * 3
left: 0, self.contentInset.bottom = offset
bottom: window.safeAreaInsets.bottom, self.verticalScrollIndicatorInsets.top = 20
right: 0) self.verticalScrollIndicatorInsets.bottom = offset
} }
self.delegate = self
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(notification:)),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide(notification:)),
name: UIResponder.keyboardWillHideNotification,
object: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
NotificationCenter.default.removeObserver(self)
}
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
if let height = self.subviews.first?.frame.size.height, if let height = self.subviews.first?.frame.size.height,
self.previousHeight != height, self.previousHeight != height,
let window = self.window { let window = self.window {
self.contentSize = CGSize(width: window.bounds.width, height: height + window.safeAreaInsets.bottom) let newDetent = self.sheetView.sheetVc.updateDetents(contentHeight: self.sheetView.clampHeight(height),
preventExpansion: self.sheetView.preventExpansion)
self.sheetView.selectedDetentIdentifier = newDetent
self.contentSize = CGSize(width: window.bounds.width, height: height)
self.previousHeight = height
}
}
@objc func keyboardWillShow(notification: Notification) {
if let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
let keyboardHeight = keyboardFrame.height
let newOffset = self.contentInset.bottom + keyboardHeight
// We purposefully add keyboard height to content inset and only update
// the scroll indicator inset
self.contentInset.bottom = newOffset
self.verticalScrollIndicatorInsets.bottom = keyboardHeight
if let firstResponder = self.findFirstResponder(),
let window = self.window {
let position = firstResponder.convert(firstResponder.bounds,
to: window)
if position.minY > window.bounds.height - keyboardHeight {
self.setContentOffset(CGPoint(x: 0, y: self.contentOffset.y + keyboardHeight),
animated: true)
}
}
}
}
@objc func keyboardWillHide(notification: Notification) {
if let window = Util.getWindow() {
let offset = window.safeAreaInsets.bottom * 3
self.contentInset.bottom = offset
self.verticalScrollIndicatorInsets.bottom = offset
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.beganDraggingY = self.contentOffset.y
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if let beganDraggingY = self.beganDraggingY,
self.contentOffset.y < beganDraggingY {
if let firstResponder = self.findFirstResponder() {
firstResponder.resignFirstResponder()
}
self.beganDraggingY = nil
} }
} }
} }
@@ -0,0 +1,20 @@
//
// UIView+findFirstResponder.swift
// Pods
//
// Created by Hailey on 10/9/24.
//
extension UIView {
func findFirstResponder() -> UIView? {
if self.isFirstResponder {
return self
}
for subview in subviews {
if let responder = subview.findFirstResponder() {
return responder
}
}
return nil
}
}
+7 -22
View File
@@ -3,13 +3,8 @@ import UIKit
class SheetView: ExpoView, UISheetPresentationControllerDelegate { class SheetView: ExpoView, UISheetPresentationControllerDelegate {
// Views // Views
private var sheetVc: SheetViewController! var sheetVc: SheetViewController!
private var innerView: UIView? private var innerView: UIView?
private var contentHeight: CGFloat? {
get {
self.innerView?.frame.height
}
}
// Scroll view // Scroll view
private var scrollView: BottomSheetScrollView! private var scrollView: BottomSheetScrollView!
@@ -59,6 +54,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
} }
} }
} }
private var isClosing = false { private var isClosing = false {
didSet { didSet {
if isClosing { if isClosing {
@@ -68,7 +64,8 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
} }
} }
} }
private var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
var selectedDetentIdentifier: UISheetPresentationController.Detent.Identifier? {
didSet { didSet {
if selectedDetentIdentifier == .large { if selectedDetentIdentifier == .large {
onSnapPointChange([ onSnapPointChange([
@@ -89,12 +86,12 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
required init (appContext: AppContext? = nil) { required init (appContext: AppContext? = nil) {
super.init(appContext: appContext) super.init(appContext: appContext)
self.scrollView = BottomSheetScrollView()
self.sheetVc = SheetViewController() self.sheetVc = SheetViewController()
if let sheet = self.sheetVc.sheetPresentationController { if let sheet = self.sheetVc.sheetPresentationController {
sheet.delegate = self sheet.delegate = self
} }
self.scrollView = BottomSheetScrollView(sheetView: self)
self.sheetVc.view.addSubview(self.scrollView) self.sheetVc.view.addSubview(self.scrollView)
self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge) self.touchHandler = RCTTouchHandler(bridge: appContext?.reactBridge)
@@ -132,12 +129,10 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
guard !self.isOpen, guard !self.isOpen,
!self.isOpening, !self.isOpening,
!self.isClosing, !self.isClosing,
let contentHeight = self.contentHeight,
let rvc = self.reactViewController() else { let rvc = self.reactViewController() else {
return return
} }
self.sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
if let sheet = sheetVc.sheetPresentationController { if let sheet = sheetVc.sheetPresentationController {
sheet.preferredCornerRadius = self.cornerRadius sheet.preferredCornerRadius = self.cornerRadius
self.selectedDetentIdentifier = sheet.selectedDetentIdentifier self.selectedDetentIdentifier = sheet.selectedDetentIdentifier
@@ -151,16 +146,6 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
} }
} }
func updateLayout() {
if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier,
let contentHeight = self.contentHeight {
self.sheetVc.updateDetents(contentHeight: self.clampHeight(contentHeight),
preventExpansion: self.preventExpansion)
self.selectedDetentIdentifier = self.sheetVc.getCurrentDetentIdentifier()
}
self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier
}
func dismiss() { func dismiss() {
self.isClosing = true self.isClosing = true
self.sheetVc.dismiss(animated: true) { [weak self] in self.sheetVc.dismiss(animated: true) { [weak self] in
@@ -170,7 +155,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
// MARK: - Utils // MARK: - Utils
private func clampHeight(_ height: CGFloat) -> CGFloat { func clampHeight(_ height: CGFloat) -> CGFloat {
if height < self.minHeight { if height < self.minHeight {
return self.minHeight return self.minHeight
} else if height > self.maxHeight { } else if height > self.maxHeight {
@@ -20,9 +20,9 @@ class SheetViewController: UIViewController {
} }
} }
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) { func setDetents(contentHeight: CGFloat, preventExpansion: Bool) -> UISheetPresentationController.Detent.Identifier? {
guard let sheet = self.sheetPresentationController else { guard let sheet = self.sheetPresentationController else {
return return nil
} }
if contentHeight > Util.getScreenHeight() - 100 { if contentHeight > Util.getScreenHeight() - 100 {
@@ -48,17 +48,20 @@ class SheetViewController: UIViewController {
} }
sheet.selectedDetentIdentifier = .medium sheet.selectedDetentIdentifier = .medium
} }
return sheet.selectedDetentIdentifier
} }
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) { func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) -> UISheetPresentationController.Detent.Identifier? {
var newDetent: UISheetPresentationController.Detent.Identifier?
if let sheet = self.sheetPresentationController { if let sheet = self.sheetPresentationController {
sheet.animateChanges { sheet.animateChanges {
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion) newDetent = self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
sheet.invalidateDetents() sheet.invalidateDetents()
} }
} }
} }
return newDetent
} }
func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? { func getCurrentDetentIdentifier() -> UISheetPresentationController.Detent.Identifier? {
@@ -2,10 +2,7 @@ import * as React from 'react'
import { import {
Dimensions, Dimensions,
NativeSyntheticEvent, NativeSyntheticEvent,
Platform,
ScrollView,
StyleProp, StyleProp,
View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core' import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
@@ -66,7 +63,6 @@ export class BottomSheetNativeComponent extends React.Component<
render() { render() {
const {children, ...rest} = this.props const {children, ...rest} = this.props
const cornerRadius = rest.cornerRadius ?? 0
if (!this.state.open) { if (!this.state.open) {
return null return null
@@ -82,19 +78,7 @@ export class BottomSheetNativeComponent extends React.Component<
height: screenHeight, height: screenHeight,
width: '100%', width: '100%',
}}> }}>
{/*<View*/}
{/* style={[*/}
{/* {*/}
{/* flex: 1,*/}
{/* backgroundColor,*/}
{/* },*/}
{/* Platform.OS === 'android' && {*/}
{/* borderTopLeftRadius: cornerRadius,*/}
{/* borderTopRightRadius: cornerRadius,*/}
{/* },*/}
{/* ]}>*/}
<BottomSheetPortalProvider>{children}</BottomSheetPortalProvider> <BottomSheetPortalProvider>{children}</BottomSheetPortalProvider>
{/*</View>*/}
</NativeView> </NativeView>
) )
} }
+5 -66
View File
@@ -1,26 +1,17 @@
import React, {useImperativeHandle} from 'react' import React, {useImperativeHandle} from 'react'
import { import {
NativeScrollEvent,
NativeSyntheticEvent,
Pressable, Pressable,
processColor, processColor,
ScrollView,
StyleProp, StyleProp,
TextInput, TextInput,
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import {
KeyboardAwareScrollView,
useKeyboardHandler,
} from 'react-native-keyboard-controller'
import {runOnJS} from 'react-native-reanimated'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isAndroid, isIOS} from '#/platform/detection'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
import {useDialogStateControlContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import {List, ListMethods, ListProps} from '#/view/com/util/List' import {List, ListMethods, ListProps} from '#/view/com/util/List'
@@ -161,6 +152,7 @@ export function Outer({
onSnapPointChange={onSnapPointChange} onSnapPointChange={onSnapPointChange}
onStateChange={onStateChange} onStateChange={onStateChange}
disableDrag={disableDrag} disableDrag={disableDrag}
// @ts-expect-error @TODO
containerBackgroundColor={processColor(t.atoms.bg.backgroundColor)}> containerBackgroundColor={processColor(t.atoms.bg.backgroundColor)}>
<Context.Provider value={context}> <Context.Provider value={context}>
<View testID={testID}>{children}</View> <View testID={testID}>{children}</View>
@@ -169,69 +161,16 @@ export function Outer({
) )
} }
export function Inner({children, style}: DialogInnerProps) { export function Inner({children, style, ...props}: DialogInnerProps) {
const insets = useSafeAreaInsets()
return ( return (
<View <View style={[a.pt_2xl, a.px_xl, style]} {...props}>
style={[
a.pt_2xl,
a.px_xl,
{
paddingBottom: insets.bottom + insets.top,
},
style,
]}>
{children} {children}
</View> </View>
) )
} }
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>( // @TODO remove this later
function ScrollableInner({children, style, ...props}, ref) { export const ScrollableInner = Inner
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const insets = useSafeAreaInsets()
const [keyboardHeight, setKeyboardHeight] = React.useState(0)
useKeyboardHandler({
onEnd: e => {
'worklet'
runOnJS(setKeyboardHeight)(e.height)
},
})
const basePading =
(isIOS ? 30 : 50) + (isIOS ? keyboardHeight / 4 : keyboardHeight)
const fullPaddingBase = insets.bottom + insets.top + basePading
const fullPadding = isIOS ? fullPaddingBase : fullPaddingBase + 50
const paddingBottom =
nativeSnapPoint === BottomSheetSnapPoint.Full ? fullPadding : basePading
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
const {contentOffset} = e.nativeEvent
if (contentOffset.y > 0 && !disableDrag) {
setDisableDrag(true)
} else if (contentOffset.y <= 1 && disableDrag) {
setDisableDrag(false)
}
}
return children
return (
<KeyboardAwareScrollView
style={[style]}
contentContainerStyle={[a.pt_2xl, a.px_xl, {paddingBottom}]}
ref={ref}
{...props}
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
bottomOffset={30}
scrollEventThrottle={50}
onScroll={isAndroid ? onScroll : undefined}>
{children}
</KeyboardAwareScrollView>
)
},
)
export const InnerFlatList = React.forwardRef< export const InnerFlatList = React.forwardRef<
ListMethods, ListMethods,
+1 -4
View File
@@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -38,10 +37,8 @@ function ReportDialogInner(props: ReportDialogProps) {
} = useMyLabelersQuery() } = useMyLabelersQuery()
const isLoading = useDelayedLoading(500, isLabelerLoading) const isLoading = useDelayedLoading(500, isLabelerLoading)
const ref = React.useRef<ScrollView>(null)
return ( return (
<Dialog.ScrollableInner label={_(msg`Report dialog`)} ref={ref}> <Dialog.ScrollableInner label={_(msg`Report dialog`)}>
{isLoading ? ( {isLoading ? (
<View style={[a.align_center, {height: 100}]}> <View style={[a.align_center, {height: 100}]}>
<Loader size="xl" /> <Loader size="xl" />