Skip to content

Commit a8812b1

Browse files
authored
Merge pull request #1930 from didi/feat-sticky-rn
feat: RN&web环境支持sticky
2 parents 44e03b5 + 5368640 commit a8812b1

11 files changed

Lines changed: 559 additions & 69 deletions

File tree

packages/webpack-plugin/lib/platform/template/wx/component-config/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const wxs = require('./wxs')
4242
const component = require('./component')
4343
const fixComponentName = require('./fix-component-name')
4444
const rootPortal = require('./root-portal')
45+
const stickyHeader = require('./sticky-header')
46+
const stickySection = require('./sticky-section')
4547

4648
module.exports = function getComponentConfigs ({ warn, error }) {
4749
/**
@@ -125,6 +127,8 @@ module.exports = function getComponentConfigs ({ warn, error }) {
125127
hyphenTagName({ print }),
126128
label({ print }),
127129
component(),
128-
rootPortal({ print })
130+
rootPortal({ print }),
131+
stickyHeader({ print }),
132+
stickySection({ print })
129133
]
130134
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const TAG_NAME = 'sticky-header'
2+
3+
module.exports = function ({ print }) {
4+
return {
5+
test: TAG_NAME,
6+
android (tag, { el }) {
7+
el.isBuiltIn = true
8+
return 'mpx-sticky-header'
9+
},
10+
ios (tag, { el }) {
11+
el.isBuiltIn = true
12+
return 'mpx-sticky-header'
13+
},
14+
harmony (tag, { el }) {
15+
el.isBuiltIn = true
16+
return 'mpx-sticky-header'
17+
},
18+
web (tag, { el }) {
19+
el.isBuiltIn = true
20+
return 'mpx-sticky-header'
21+
}
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const TAG_NAME = 'sticky-section'
2+
3+
module.exports = function ({ print }) {
4+
return {
5+
test: TAG_NAME,
6+
android (tag, { el }) {
7+
el.isBuiltIn = true
8+
return 'mpx-sticky-section'
9+
},
10+
ios (tag, { el }) {
11+
el.isBuiltIn = true
12+
return 'mpx-sticky-section'
13+
},
14+
harmony (tag, { el }) {
15+
el.isBuiltIn = true
16+
return 'mpx-sticky-section'
17+
},
18+
web (tag, { el }) {
19+
el.isBuiltIn = true
20+
return 'mpx-sticky-section'
21+
}
22+
}
23+
}

packages/webpack-plugin/lib/runtime/components/react/context.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createContext, Dispatch, MutableRefObject, SetStateAction } from 'react'
2-
import { NativeSyntheticEvent } from 'react-native'
2+
import { NativeSyntheticEvent, Animated } from 'react-native'
3+
import { noop } from '@mpxjs/utils'
34

45
export type LabelContextValue = MutableRefObject<{
56
triggerChange: (evt: NativeSyntheticEvent<TouchEvent>) => void
@@ -42,14 +43,20 @@ export interface PortalContextValue {
4243
}
4344

4445
export interface ScrollViewContextValue {
45-
gestureRef: React.RefObject<any> | null
46+
gestureRef: React.RefObject<any> | null,
47+
scrollOffset: Animated.Value
4648
}
4749

4850
export interface RouteContextValue {
4951
pageId: number
5052
navigation: Record<string, any>
5153
}
5254

55+
export interface StickyContextValue {
56+
registerStickyHeader: Function,
57+
unregisterStickyHeader: Function
58+
}
59+
5360
export const MovableAreaContext = createContext({ width: 0, height: 0 })
5461

5562
export const FormContext = createContext<FormContextValue | null>(null)
@@ -72,6 +79,8 @@ export const SwiperContext = createContext({})
7279

7380
export const KeyboardAvoidContext = createContext<KeyboardAvoidContextValue | null>(null)
7481

75-
export const ScrollViewContext = createContext<ScrollViewContextValue>({ gestureRef: null })
82+
export const ScrollViewContext = createContext<ScrollViewContextValue>({ gestureRef: null, scrollOffset: new Animated.Value(0) })
7683

7784
export const PortalContext = createContext<PortalContextValue>(null as any)
85+
86+
export const StickyContext = createContext<StickyContextValue>({ registerStickyHeader: noop, unregisterStickyHeader: noop })

packages/webpack-plugin/lib/runtime/components/react/mpx-button.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ import {
4242
TextStyle,
4343
Animated,
4444
Easing,
45-
NativeSyntheticEvent
45+
NativeSyntheticEvent,
46+
useAnimatedValue
4647
} from 'react-native'
4748
import { warn } from '@mpxjs/utils'
4849
import { GestureDetector, PanGesture } from 'react-native-gesture-handler'
@@ -157,7 +158,7 @@ const timer = (data: any, time = 3000) => new Promise((resolve) => {
157158
})
158159

159160
const Loading = ({ alone = false }: { alone: boolean }): JSX.Element => {
160-
const image = useRef(new Animated.Value(0)).current
161+
const image = useAnimatedValue(0)
161162

162163
const rotate = image.interpolate({
163164
inputRange: [0, 1],

packages/webpack-plugin/lib/runtime/components/react/mpx-scroll-view.tsx

Lines changed: 84 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* ✔ bindscroll
3333
*/
3434
import { ScrollView, RefreshControl, Gesture, GestureDetector } from 'react-native-gesture-handler'
35-
import { View, NativeSyntheticEvent, NativeScrollEvent, LayoutChangeEvent, ViewStyle } from 'react-native'
35+
import { View, NativeSyntheticEvent, NativeScrollEvent, LayoutChangeEvent, ViewStyle, Animated as RNAnimated } from 'react-native'
3636
import { isValidElement, Children, JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext, useMemo, createElement } from 'react'
3737
import Animated, { useAnimatedRef, useSharedValue, withTiming, useAnimatedStyle, runOnJS } from 'react-native-reanimated'
3838
import { warn, hasOwn } from '@mpxjs/utils'
@@ -43,48 +43,49 @@ import { IntersectionObserverContext, ScrollViewContext } from './context'
4343
import Portal from './mpx-portal'
4444

4545
interface ScrollViewProps {
46-
children?: ReactNode
47-
enhanced?: boolean
48-
bounces?: boolean
49-
style?: ViewStyle
50-
scrollEventThrottle?: number
51-
'scroll-x'?: boolean
52-
'scroll-y'?: boolean
53-
'enable-back-to-top'?: boolean
54-
'show-scrollbar'?: boolean
55-
'paging-enabled'?: boolean
56-
'upper-threshold'?: number
57-
'lower-threshold'?: number
58-
'scroll-with-animation'?: boolean
59-
'refresher-triggered'?: boolean
60-
'refresher-enabled'?: boolean
61-
'refresher-default-style'?: 'black' | 'white' | 'none'
62-
'refresher-background'?: string
63-
'refresher-threshold'?: number
64-
'scroll-top'?: number
65-
'scroll-left'?: number
66-
'enable-offset'?: boolean
67-
'scroll-into-view'?: string
68-
'enable-trigger-intersection-observer'?: boolean
69-
'enable-var'?: boolean
70-
'external-var-context'?: Record<string, any>
71-
'parent-font-size'?: number
72-
'parent-width'?: number
73-
'parent-height'?: number
74-
'wait-for'?: Array<GestureHandler>
75-
'simultaneous-handlers'?: Array<GestureHandler>
76-
'scroll-event-throttle'?: number
77-
bindscrolltoupper?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
78-
bindscrolltolower?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
79-
bindscroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
80-
bindrefresherrefresh?: (event: NativeSyntheticEvent<unknown>) => void
81-
binddragstart?: (event: NativeSyntheticEvent<DragEvent>) => void
82-
binddragging?: (event: NativeSyntheticEvent<DragEvent>) => void
83-
binddragend?: (event: NativeSyntheticEvent<DragEvent>) => void
84-
bindtouchstart?: (event: NativeSyntheticEvent<TouchEvent>) => void
85-
bindtouchmove?: (event: NativeSyntheticEvent<TouchEvent>) => void
86-
bindtouchend?: (event: NativeSyntheticEvent<TouchEvent>) => void
87-
bindscrollend?: (event: NativeSyntheticEvent<TouchEvent>) => void
46+
children?: ReactNode;
47+
enhanced?: boolean;
48+
bounces?: boolean;
49+
style?: ViewStyle;
50+
'scroll-x'?: boolean;
51+
'scroll-y'?: boolean;
52+
'enable-back-to-top'?: boolean;
53+
'show-scrollbar'?: boolean;
54+
'paging-enabled'?: boolean;
55+
'upper-threshold'?: number;
56+
'lower-threshold'?: number;
57+
'scroll-with-animation'?: boolean;
58+
'refresher-triggered'?: boolean;
59+
'refresher-enabled'?: boolean;
60+
'refresher-default-style'?: 'black' | 'white' | 'none';
61+
'refresher-background'?: string;
62+
'refresher-threshold'?: number;
63+
'scroll-top'?: number;
64+
'scroll-left'?: number;
65+
'enable-offset'?: boolean;
66+
'scroll-into-view'?: string;
67+
'enable-trigger-intersection-observer'?: boolean;
68+
'enable-var'?: boolean;
69+
'external-var-context'?: Record<string, any>;
70+
'parent-font-size'?: number;
71+
'parent-width'?: number;
72+
'parent-height'?: number;
73+
'enable-sticky'?: boolean;
74+
'wait-for'?: Array<GestureHandler>;
75+
'simultaneous-handlers'?: Array<GestureHandler>;
76+
'scroll-event-throttle'?:number;
77+
'scroll-into-view-offset'?: number;
78+
bindscrolltoupper?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
79+
bindscrolltolower?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
80+
bindscroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
81+
bindrefresherrefresh?: (event: NativeSyntheticEvent<unknown>) => void;
82+
binddragstart?: (event: NativeSyntheticEvent<DragEvent>) => void;
83+
binddragging?: (event: NativeSyntheticEvent<DragEvent>) => void;
84+
binddragend?: (event: NativeSyntheticEvent<DragEvent>) => void;
85+
bindtouchstart?: (event: NativeSyntheticEvent<TouchEvent>) => void;
86+
bindtouchmove?: (event: NativeSyntheticEvent<TouchEvent>) => void;
87+
bindtouchend?: (event: NativeSyntheticEvent<TouchEvent>) => void;
88+
bindscrollend?: (event: NativeSyntheticEvent<TouchEvent>) => void;
8889
__selectRef?: (selector: string, nodeType: 'node' | 'component', all?: boolean) => HandlerRef<any, any>
8990
}
9091
type ScrollAdditionalProps = {
@@ -109,6 +110,8 @@ type ScrollAdditionalProps = {
109110
onMomentumScrollEnd?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void
110111
}
111112

113+
const AnimatedScrollView = RNAnimated.createAnimatedComponent(ScrollView) as React.ComponentType<any>
114+
112115
const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, ScrollViewProps>((scrollViewProps: ScrollViewProps = {}, ref): JSX.Element => {
113116
const { textProps, innerProps: props = {} } = splitProps(scrollViewProps)
114117
const {
@@ -145,10 +148,14 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
145148
'parent-height': parentHeight,
146149
'simultaneous-handlers': originSimultaneousHandlers,
147150
'wait-for': waitFor,
151+
'enable-sticky': enableSticky,
148152
'scroll-event-throttle': scrollEventThrottle = 0,
153+
'scroll-into-view-offset': scrollIntoViewOffset = 0,
149154
__selectRef
150155
} = props
151156

157+
const scrollOffset = useRef(new RNAnimated.Value(0)).current
158+
152159
const simultaneousHandlers = flatGesture(originSimultaneousHandlers)
153160
const waitForHandlers = flatGesture(waitFor)
154161

@@ -180,7 +187,7 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
180187
const initialTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)
181188
const intersectionObservers = useContext(IntersectionObserverContext)
182189

183-
const firstScrollIntoViewChange = useRef<boolean>(false)
190+
const firstScrollIntoViewChange = useRef<boolean>(true)
184191

185192
const refreshColor = {
186193
black: ['#000'],
@@ -213,19 +220,21 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
213220
pagingEnabled,
214221
fastDeceleration: false,
215222
decelerationDisabled: false,
216-
scrollTo
223+
scrollTo,
224+
scrollIntoView: handleScrollIntoView
217225
},
218226
gestureRef: scrollViewRef
219227
})
220228

229+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout })
230+
221231
const contextValue = useMemo(() => {
222232
return {
223-
gestureRef: scrollViewRef
233+
gestureRef: scrollViewRef,
234+
scrollOffset
224235
}
225236
}, [])
226237

227-
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout })
228-
229238
const hasRefresherLayoutRef = useRef(false)
230239

231240
// layout 完成前先隐藏,避免安卓闪烁问题
@@ -251,13 +260,15 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
251260

252261
useEffect(() => {
253262
if (scrollIntoView && __selectRef) {
254-
if (!firstScrollIntoViewChange.current) {
255-
setTimeout(handleScrollIntoView)
263+
if (firstScrollIntoViewChange.current) {
264+
setTimeout(() => {
265+
handleScrollIntoView(scrollIntoView, { offset: scrollIntoViewOffset, animated: scrollWithAnimation })
266+
})
256267
} else {
257-
handleScrollIntoView()
268+
handleScrollIntoView(scrollIntoView, { offset: scrollIntoViewOffset, animated: scrollWithAnimation })
258269
}
259270
}
260-
firstScrollIntoViewChange.current = true
271+
firstScrollIntoViewChange.current = false
261272
}, [scrollIntoView])
262273

263274
useEffect(() => {
@@ -280,14 +291,16 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
280291
scrollToOffset(left, top, animated)
281292
}
282293

283-
function handleScrollIntoView () {
284-
const refs = __selectRef!(`#${scrollIntoView}`, 'node')
294+
function handleScrollIntoView (selector = '', { offset = 0, animated = true } = {}) {
295+
const refs = __selectRef!(`#${selector}`, 'node')
285296
if (!refs) return
286297
const { nodeRef } = refs.getNodeInstance()
287298
nodeRef.current?.measureLayout(
288299
scrollViewRef.current,
289300
(left: number, top: number) => {
290-
scrollToOffset(left, top)
301+
const adjustedLeft = scrollX ? left + offset : left
302+
const adjustedTop = scrollY ? top + offset : top
303+
scrollToOffset(adjustedLeft, adjustedTop, animated)
291304
}
292305
)
293306
}
@@ -487,6 +500,16 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
487500
updateIntersection()
488501
}
489502

503+
const scrollHandler = RNAnimated.event(
504+
[{ nativeEvent: { contentOffset: { y: scrollOffset } } }],
505+
{
506+
useNativeDriver: true,
507+
listener: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
508+
onScroll(event)
509+
}
510+
}
511+
)
512+
490513
function onScrollDragStart (e: NativeSyntheticEvent<NativeScrollEvent>) {
491514
hasCallScrollToLower.current = false
492515
hasCallScrollToUpper.current = false
@@ -661,7 +684,7 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
661684
scrollEnabled: !enableScroll ? false : !!(scrollX || scrollY),
662685
bounces: false,
663686
ref: scrollViewRef,
664-
onScroll: onScroll,
687+
onScroll: enableSticky ? scrollHandler : onScroll,
665688
onContentSizeChange: onContentSizeChange,
666689
bindtouchstart: ((enhanced && binddragstart) || bindtouchstart) && onScrollTouchStart,
667690
bindtouchmove: ((enhanced && binddragging) || bindtouchmove) && onScrollTouchMove,
@@ -716,11 +739,13 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
716739
'bindrefresherrefresh'
717740
], { layoutRef })
718741

742+
const ScrollViewComponent = enableSticky ? AnimatedScrollView : ScrollView
743+
719744
const withRefresherScrollView = createElement(
720745
GestureDetector,
721746
{ gesture: panGesture },
722747
createElement(
723-
ScrollView,
748+
ScrollViewComponent,
724749
innerProps,
725750
createElement(
726751
Animated.View,
@@ -748,8 +773,8 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
748773
)
749774

750775
const commonScrollView = createElement(
751-
ScrollView,
752-
extendObject(innerProps, {
776+
ScrollViewComponent,
777+
extendObject({}, innerProps, {
753778
refreshControl: refresherEnabled
754779
? createElement(RefreshControl, extendObject({
755780
progressBackgroundColor: refresherBackground,

0 commit comments

Comments
 (0)