Android fixes (#515)

* Fix profile screen performance on android and remove dead code

* Correctly handle android hardware back btn

* Fix EditProfile modal for android

* Fix lint
This commit is contained in:
Paul Frazee 2023-04-22 17:14:20 -05:00 committed by GitHub
parent eb6b36be61
commit d35f7c1f1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 273 additions and 594 deletions

View file

@ -128,7 +128,7 @@ const styles = StyleSheet.create({
width: 24,
height: 24,
bottom: 8,
right: 8,
right: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',

View file

@ -1,12 +1,13 @@
import React, {useEffect, useState} from 'react'
import {View} from 'react-native'
import {Selector} from './Selector'
import {HorzSwipe} from './gestures/HorzSwipe'
import {Pressable, StyleSheet, View} from 'react-native'
import {FlatList} from './Views'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {Text} from './text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {clamp} from 'lib/numbers'
import {s} from 'lib/styles'
import {s, colors} from 'lib/styles'
import {isAndroid} from 'platform/detection'
const HEADER_ITEM = {_reactKey: '__header__'}
const SELECTOR_ITEM = {_reactKey: '__selector__'}
@ -16,7 +17,6 @@ export function ViewSelector({
sections,
items,
refreshing,
swipeEnabled,
renderHeader,
renderItem,
ListFooterComponent,
@ -42,19 +42,12 @@ export function ViewSelector({
onEndReached?: (info: {distanceFromEnd: number}) => void
}) {
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const panX = useAnimatedValue(0)
// events
// =
const onSwipeEnd = React.useCallback(
(dx: number) => {
if (dx !== 0) {
setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length))
}
},
[setSelectedIndex, selectedIndex, sections],
)
const keyExtractor = React.useCallback(item => item._reactKey, [])
const onPressSelection = React.useCallback(
(index: number) => setSelectedIndex(clamp(index, 0, sections.length)),
[setSelectedIndex, sections],
@ -77,7 +70,6 @@ export function ViewSelector({
return (
<Selector
items={sections}
panX={panX}
selectedIndex={selectedIndex}
onSelect={onPressSelection}
/>
@ -86,7 +78,7 @@ export function ViewSelector({
return renderItem(item)
}
},
[sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem],
[sections, selectedIndex, onPressSelection, renderHeader, renderItem],
)
const data = React.useMemo(
@ -94,28 +86,98 @@ export function ViewSelector({
[items],
)
return (
<HorzSwipe
hasPriority
panX={panX}
swipeEnabled={swipeEnabled || false}
canSwipeLeft={selectedIndex > 0}
canSwipeRight={selectedIndex < sections.length - 1}
onSwipeEnd={onSwipeEnd}>
<FlatList
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
stickyHeaderIndices={STICKY_HEADER_INDICES}
refreshing={refreshing}
onScroll={onScroll}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
/>
</HorzSwipe>
<FlatList
data={data}
keyExtractor={keyExtractor}
renderItem={renderItemInternal}
ListFooterComponent={ListFooterComponent}
// NOTE sticky header disabled on android due to major performance issues -prf
stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES}
refreshing={refreshing}
onScroll={onScroll}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
contentContainerStyle={s.contentContainer}
removeClippedSubviews={true}
scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464
/>
)
}
export function Selector({
selectedIndex,
items,
onSelect,
}: {
selectedIndex: number
items: string[]
onSelect?: (index: number) => void
}) {
const pal = usePalette('default')
const borderColor = useColorSchemeStyle(
{borderColor: colors.black},
{borderColor: colors.white},
)
const onPressItem = (index: number) => {
onSelect?.(index)
}
return (
<View style={[pal.view, styles.outer]}>
{items.map((item, i) => {
const selected = i === selectedIndex
return (
<Pressable
testID={`selector-${i}`}
key={item}
onPress={() => onPressItem(i)}>
<View
style={[
styles.item,
selected && styles.itemSelected,
borderColor,
]}>
<Text
style={
selected
? [styles.labelSelected, pal.text]
: [styles.label, pal.textLight]
}>
{item}
</Text>
</View>
</Pressable>
)
})}
</View>
)
}
const styles = StyleSheet.create({
outer: {
flexDirection: 'row',
paddingHorizontal: 14,
},
item: {
marginRight: 14,
paddingHorizontal: 10,
paddingTop: 8,
paddingBottom: 12,
},
itemSelected: {
borderBottomWidth: 3,
},
label: {
fontWeight: '600',
},
labelSelected: {
fontWeight: '600',
},
underline: {
position: 'absolute',
height: 4,
bottom: 0,
},
})

View file

@ -1,157 +0,0 @@
import React, {useState} from 'react'
import {
Animated,
GestureResponderEvent,
I18nManager,
PanResponder,
PanResponderGestureState,
useWindowDimensions,
View,
} from 'react-native'
import {clamp} from 'lodash'
import {s} from 'lib/styles'
interface Props {
panX: Animated.Value
canSwipeLeft?: boolean
canSwipeRight?: boolean
swipeEnabled?: boolean
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
distThresholdDivisor?: number
useNativeDriver?: boolean
onSwipeStart?: () => void
onSwipeStartDirection?: (dx: number) => void
onSwipeEnd?: (dx: number) => void
children: React.ReactNode
}
export function HorzSwipe({
panX,
canSwipeLeft = false,
canSwipeRight = false,
swipeEnabled = true,
hasPriority = false,
distThresholdDivisor = 1.75,
useNativeDriver = false,
onSwipeStart,
onSwipeStartDirection,
onSwipeEnd,
children,
}: Props) {
const winDim = useWindowDimensions()
const [dir, setDir] = useState<number>(0)
const swipeVelocityThreshold = 35
const swipeDistanceThreshold = winDim.width / distThresholdDivisor
const isMovingHorizontally = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
return (
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
)
}
const canMoveScreen = (
event: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
if (swipeEnabled === false) {
return false
}
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
const willHandle =
isMovingHorizontally(event, gestureState) &&
((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight))
return willHandle
}
const startGesture = () => {
setDir(0)
onSwipeStart?.()
panX.stopAnimation()
// @ts-expect-error: _value is private, but docs use it as well
panX.setOffset(panX._value)
}
const respondToGesture = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
if (
// swiping left
(diffX > 0 && !canSwipeLeft) ||
// swiping right
(diffX < 0 && !canSwipeRight)
) {
panX.setValue(0)
return
}
panX.setValue(clamp(diffX / swipeDistanceThreshold, -1, 1) * -1)
const newDir = diffX > 0 ? -1 : diffX < 0 ? 1 : 0
if (newDir !== dir) {
setDir(newDir)
onSwipeStartDirection?.(newDir)
}
}
const finishGesture = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
if (
Math.abs(gestureState.dx) > Math.abs(gestureState.dy) &&
Math.abs(gestureState.vx) > Math.abs(gestureState.vy) &&
(Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 ||
Math.abs(gestureState.vx) > swipeVelocityThreshold)
) {
const final = Math.floor(
(gestureState.dx / Math.abs(gestureState.dx)) * -1,
)
Animated.timing(panX, {
toValue: final,
duration: 100,
useNativeDriver,
isInteraction: false,
}).start(() => {
onSwipeEnd?.(final)
panX.flattenOffset()
panX.setValue(0)
})
} else {
onSwipeEnd?.(0)
Animated.timing(panX, {
toValue: 0,
duration: 100,
useNativeDriver,
isInteraction: false,
}).start(() => {
panX.flattenOffset()
panX.setValue(0)
})
}
}
const panResponder = PanResponder.create({
onMoveShouldSetPanResponder: canMoveScreen,
onPanResponderGrant: startGesture,
onPanResponderMove: respondToGesture,
onPanResponderTerminate: finishGesture,
onPanResponderRelease: finishGesture,
onPanResponderTerminationRequest: () => !hasPriority,
})
return (
<View {...panResponder.panHandlers} style={s.h100pct}>
{children}
</View>
)
}

View file

@ -1,302 +0,0 @@
import React, {useState} from 'react'
import {
Animated,
GestureResponderEvent,
I18nManager,
PanResponder,
PanResponderGestureState,
useWindowDimensions,
View,
} from 'react-native'
import {clamp} from 'lodash'
import {s} from 'lib/styles'
export enum Dir {
None,
Up,
Down,
Left,
Right,
Zoom,
}
interface Props {
panX: Animated.Value
panY: Animated.Value
zoom: Animated.Value
canSwipeLeft?: boolean
canSwipeRight?: boolean
canSwipeUp?: boolean
canSwipeDown?: boolean
swipeEnabled?: boolean
zoomEnabled?: boolean
hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture
horzDistThresholdDivisor?: number
vertDistThresholdDivisor?: number
useNativeDriver?: boolean
onSwipeStart?: () => void
onSwipeStartDirection?: (dir: Dir) => void
onSwipeEnd?: (dir: Dir) => void
children: React.ReactNode
}
export function SwipeAndZoom({
panX,
panY,
zoom,
canSwipeLeft = false,
canSwipeRight = false,
canSwipeUp = false,
canSwipeDown = false,
swipeEnabled = false,
zoomEnabled = false,
hasPriority = false,
horzDistThresholdDivisor = 1.75,
vertDistThresholdDivisor = 1.75,
useNativeDriver = false,
onSwipeStart,
onSwipeStartDirection,
onSwipeEnd,
children,
}: Props) {
const winDim = useWindowDimensions()
const [dir, setDir] = useState<Dir>(Dir.None)
const [initialDistance, setInitialDistance] = useState<number | undefined>(
undefined,
)
const swipeVelocityThreshold = 35
const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor
const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor
const isMovingHorizontally = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
return (
Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) &&
Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25)
)
}
const isMovingVertically = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
return (
Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) &&
Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25)
)
}
const canDir = (d: Dir) => {
if (d === Dir.Left) {
return canSwipeLeft
}
if (d === Dir.Right) {
return canSwipeRight
}
if (d === Dir.Up) {
return canSwipeUp
}
if (d === Dir.Down) {
return canSwipeDown
}
if (d === Dir.Zoom) {
return zoomEnabled
}
return false
}
const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right
const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down
const canMoveScreen = (
event: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
if (zoomEnabled && gestureState.numberActiveTouches === 2) {
return true
} else if (swipeEnabled && gestureState.numberActiveTouches === 1) {
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
const dy = gestureState.dy
const willHandle =
(isMovingHorizontally(event, gestureState) &&
((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) ||
(isMovingVertically(event, gestureState) &&
((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown)))
return willHandle
}
return false
}
const startGesture = () => {
setDir(Dir.None)
onSwipeStart?.()
// reset all state
panX.stopAnimation()
// @ts-expect-error: _value is private, but docs use it as well
panX.setOffset(panX._value)
panY.stopAnimation()
// @ts-expect-error: _value is private, but docs use it as well
panY.setOffset(panY._value)
zoom.stopAnimation()
// @ts-expect-error: _value is private, but docs use it as well
zoom.setOffset(zoom._value)
setInitialDistance(undefined)
}
const respondToGesture = (
e: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx
const dy = gestureState.dy
let newDir = Dir.None
if (dir === Dir.None) {
// establish if the user is swiping horz or vert, or zooming
if (gestureState.numberActiveTouches === 2) {
newDir = Dir.Zoom
} else if (Math.abs(dx) > Math.abs(dy)) {
newDir = dx > 0 ? Dir.Left : Dir.Right
} else {
newDir = dy > 0 ? Dir.Up : Dir.Down
}
} else if (isHorz(dir)) {
// direction update
newDir = dx > 0 ? Dir.Left : Dir.Right
} else if (isVert(dir)) {
// direction update
newDir = dy > 0 ? Dir.Up : Dir.Down
} else {
newDir = dir
}
if (newDir === Dir.Zoom) {
if (zoomEnabled) {
if (gestureState.numberActiveTouches === 2) {
// zoom in/out
const x0 = e.nativeEvent.touches[0].pageX
const x1 = e.nativeEvent.touches[1].pageX
const y0 = e.nativeEvent.touches[0].pageY
const y1 = e.nativeEvent.touches[1].pageY
const zoomDx = Math.abs(x0 - x1)
const zoomDy = Math.abs(y0 - y1)
const dist = Math.sqrt(zoomDx * zoomDx + zoomDy * zoomDy) / 100
if (
typeof initialDistance === 'undefined' ||
dist - initialDistance < 0
) {
setInitialDistance(dist)
} else {
zoom.setValue(dist - initialDistance)
}
} else {
// pan around after zooming
panX.setValue(clamp(dx / winDim.width, -1, 1) * -1)
panY.setValue(clamp(dy / winDim.height, -1, 1) * -1)
}
}
} else if (isHorz(newDir)) {
// swipe left/right
panX.setValue(
clamp(
dx / swipeHorzDistanceThreshold,
canSwipeRight ? -1 : 0,
canSwipeLeft ? 1 : 0,
) * -1,
)
panY.setValue(0)
} else if (isVert(newDir)) {
// swipe up/down
panY.setValue(
clamp(
dy / swipeVertDistanceThreshold,
canSwipeDown ? -1 : 0,
canSwipeUp ? 1 : 0,
) * -1,
)
panX.setValue(0)
}
if (!canDir(newDir)) {
newDir = Dir.None
}
if (newDir !== dir) {
setDir(newDir)
onSwipeStartDirection?.(newDir)
}
}
const finishGesture = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
const finish = (finalDir: Dir) => () => {
if (finalDir !== Dir.None) {
onSwipeEnd?.(finalDir)
}
setDir(Dir.None)
panX.flattenOffset()
panX.setValue(0)
panY.flattenOffset()
panY.setValue(0)
}
if (
isHorz(dir) &&
(Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 ||
Math.abs(gestureState.vx) > swipeVelocityThreshold)
) {
// horizontal swipe reset
Animated.timing(panX, {
toValue: dir === Dir.Left ? -1 : 1,
duration: 100,
useNativeDriver,
}).start(finish(dir))
} else if (
isVert(dir) &&
(Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 ||
Math.abs(gestureState.vy) > swipeVelocityThreshold)
) {
// vertical swipe reset
Animated.timing(panY, {
toValue: dir === Dir.Up ? -1 : 1,
duration: 100,
useNativeDriver,
}).start(finish(dir))
} else {
// zoom (or no direction) reset
onSwipeEnd?.(Dir.None)
Animated.timing(panX, {
toValue: 0,
duration: 100,
useNativeDriver,
}).start()
Animated.timing(panY, {
toValue: 0,
duration: 100,
useNativeDriver,
}).start()
Animated.timing(zoom, {
toValue: 0,
duration: 100,
useNativeDriver,
}).start()
}
}
const panResponder = PanResponder.create({
onMoveShouldSetPanResponder: canMoveScreen,
onPanResponderGrant: startGesture,
onPanResponderMove: respondToGesture,
onPanResponderTerminate: finishGesture,
onPanResponderRelease: finishGesture,
onPanResponderTerminationRequest: () => !hasPriority,
})
return (
<View {...panResponder.panHandlers} style={s.h100pct}>
{children}
</View>
)
}