@mpxjs/webpack-plugin 2.10.4-beta.4 → 2.10.4-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/platform/template/wx/component-config/index.js +5 -1
- package/lib/platform/template/wx/component-config/sticky-header.js +23 -0
- package/lib/platform/template/wx/component-config/sticky-section.js +23 -0
- package/lib/runtime/components/react/context.ts +12 -3
- package/lib/runtime/components/react/dist/context.js +4 -1
- package/lib/runtime/components/react/dist/mpx-scroll-view.jsx +17 -6
- package/lib/runtime/components/react/dist/mpx-sticky-header.jsx +112 -0
- package/lib/runtime/components/react/dist/mpx-sticky-section.jsx +45 -0
- package/lib/runtime/components/react/dist/utils.jsx +12 -1
- package/lib/runtime/components/react/mpx-scroll-view.tsx +27 -9
- package/lib/runtime/components/react/mpx-sticky-header.tsx +176 -0
- package/lib/runtime/components/react/mpx-sticky-section.tsx +96 -0
- package/lib/runtime/components/react/utils.tsx +12 -1
- package/lib/runtime/components/web/mpx-scroll-view.vue +21 -4
- package/lib/runtime/components/web/mpx-sticky-header.vue +91 -0
- package/lib/runtime/components/web/mpx-sticky-section.vue +15 -0
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ const wxs = require('./wxs')
|
|
|
42
42
|
const component = require('./component')
|
|
43
43
|
const fixComponentName = require('./fix-component-name')
|
|
44
44
|
const rootPortal = require('./root-portal')
|
|
45
|
+
const stickyHeader = require('./sticky-header')
|
|
46
|
+
const stickySection = require('./sticky-section')
|
|
45
47
|
|
|
46
48
|
module.exports = function getComponentConfigs ({ warn, error }) {
|
|
47
49
|
/**
|
|
@@ -125,6 +127,8 @@ module.exports = function getComponentConfigs ({ warn, error }) {
|
|
|
125
127
|
hyphenTagName({ print }),
|
|
126
128
|
label({ print }),
|
|
127
129
|
component(),
|
|
128
|
-
rootPortal({ print })
|
|
130
|
+
rootPortal({ print }),
|
|
131
|
+
stickyHeader({ print }),
|
|
132
|
+
stickySection({ print })
|
|
129
133
|
]
|
|
130
134
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
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'
|
|
3
4
|
|
|
4
5
|
export type LabelContextValue = MutableRefObject<{
|
|
5
6
|
triggerChange: (evt: NativeSyntheticEvent<TouchEvent>) => void
|
|
@@ -42,7 +43,8 @@ export interface PortalContextValue {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export interface ScrollViewContextValue {
|
|
45
|
-
|
|
46
|
+
gestureRef: React.RefObject<any> | null,
|
|
47
|
+
scrollOffset: Animated.Value
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export interface RouteContextValue {
|
|
@@ -50,6 +52,11 @@ export interface RouteContextValue {
|
|
|
50
52
|
navigation: Record<string, any>
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
export interface StickyContextValue {
|
|
56
|
+
registerStickyHeader: Function,
|
|
57
|
+
unregisterStickyHeader: Function
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
export const MovableAreaContext = createContext({ width: 0, height: 0 })
|
|
54
61
|
|
|
55
62
|
export const FormContext = createContext<FormContextValue | null>(null)
|
|
@@ -72,6 +79,8 @@ export const SwiperContext = createContext({})
|
|
|
72
79
|
|
|
73
80
|
export const KeyboardAvoidContext = createContext<KeyboardAvoidContextValue | null>(null)
|
|
74
81
|
|
|
75
|
-
export const ScrollViewContext = createContext<ScrollViewContextValue>({ gestureRef: null })
|
|
82
|
+
export const ScrollViewContext = createContext<ScrollViewContextValue>({ gestureRef: null, scrollOffset: new Animated.Value(0) })
|
|
76
83
|
|
|
77
84
|
export const PortalContext = createContext<PortalContextValue>(null as any)
|
|
85
|
+
|
|
86
|
+
export const StickyContext = createContext<StickyContextValue>({ registerStickyHeader: noop, unregisterStickyHeader: noop })
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createContext } from 'react';
|
|
2
|
+
import { Animated } from 'react-native';
|
|
3
|
+
import { noop } from '@mpxjs/utils';
|
|
2
4
|
export const MovableAreaContext = createContext({ width: 0, height: 0 });
|
|
3
5
|
export const FormContext = createContext(null);
|
|
4
6
|
export const CheckboxGroupContext = createContext(null);
|
|
@@ -10,5 +12,6 @@ export const IntersectionObserverContext = createContext(null);
|
|
|
10
12
|
export const RouteContext = createContext(null);
|
|
11
13
|
export const SwiperContext = createContext({});
|
|
12
14
|
export const KeyboardAvoidContext = createContext(null);
|
|
13
|
-
export const ScrollViewContext = createContext({ gestureRef: null });
|
|
15
|
+
export const ScrollViewContext = createContext({ gestureRef: null, scrollOffset: new Animated.Value(0) });
|
|
14
16
|
export const PortalContext = createContext(null);
|
|
17
|
+
export const StickyContext = createContext({ registerStickyHeader: noop, unregisterStickyHeader: noop });
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
* ✔ bindscroll
|
|
33
33
|
*/
|
|
34
34
|
import { ScrollView, RefreshControl, Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
35
|
+
import { Animated as RNAnimated } from 'react-native';
|
|
35
36
|
import { isValidElement, Children, useRef, useState, useEffect, forwardRef, useContext, useMemo, createElement } from 'react';
|
|
36
37
|
import Animated, { useAnimatedRef, useSharedValue, withTiming, useAnimatedStyle, runOnJS } from 'react-native-reanimated';
|
|
37
38
|
import { warn } from '@mpxjs/utils';
|
|
@@ -39,9 +40,11 @@ import useInnerProps, { getCustomEvent } from './getInnerListeners';
|
|
|
39
40
|
import useNodesRef from './useNodesRef';
|
|
40
41
|
import { splitProps, splitStyle, useTransformStyle, useLayout, wrapChildren, extendObject, flatGesture, HIDDEN_STYLE } from './utils';
|
|
41
42
|
import { IntersectionObserverContext, ScrollViewContext } from './context';
|
|
43
|
+
const AnimatedScrollView = RNAnimated.createAnimatedComponent(ScrollView);
|
|
42
44
|
const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
43
45
|
const { textProps, innerProps: props = {} } = splitProps(scrollViewProps);
|
|
44
|
-
const { enhanced = false, bounces = true, style = {}, binddragstart, binddragging, binddragend, bindtouchstart, bindtouchmove, bindtouchend, 'scroll-x': scrollX = false, 'scroll-y': scrollY = false, 'enable-back-to-top': enableBackToTop = false, 'enable-trigger-intersection-observer': enableTriggerIntersectionObserver = false, 'paging-enabled': pagingEnabled = false, 'upper-threshold': upperThreshold = 50, 'lower-threshold': lowerThreshold = 50, 'scroll-with-animation': scrollWithAnimation = false, 'refresher-enabled': refresherEnabled, 'refresher-default-style': refresherDefaultStyle, 'refresher-background': refresherBackground, 'refresher-threshold': refresherThreshold = 45, 'show-scrollbar': showScrollbar = true, 'scroll-into-view': scrollIntoView = '', 'scroll-top': scrollTop = 0, 'scroll-left': scrollLeft = 0, 'refresher-triggered': refresherTriggered, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, 'simultaneous-handlers': originSimultaneousHandlers, 'wait-for': waitFor, 'scroll-event-throttle': scrollEventThrottle = 0, __selectRef } = props;
|
|
46
|
+
const { enhanced = false, bounces = true, style = {}, binddragstart, binddragging, binddragend, bindtouchstart, bindtouchmove, bindtouchend, 'scroll-x': scrollX = false, 'scroll-y': scrollY = false, 'enable-back-to-top': enableBackToTop = false, 'enable-trigger-intersection-observer': enableTriggerIntersectionObserver = false, 'paging-enabled': pagingEnabled = false, 'upper-threshold': upperThreshold = 50, 'lower-threshold': lowerThreshold = 50, 'scroll-with-animation': scrollWithAnimation = false, 'refresher-enabled': refresherEnabled, 'refresher-default-style': refresherDefaultStyle, 'refresher-background': refresherBackground, 'refresher-threshold': refresherThreshold = 45, 'show-scrollbar': showScrollbar = true, 'scroll-into-view': scrollIntoView = '', 'scroll-top': scrollTop = 0, 'scroll-left': scrollLeft = 0, 'refresher-triggered': refresherTriggered, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight, 'simultaneous-handlers': originSimultaneousHandlers, 'wait-for': waitFor, 'enable-sticky': enableSticky, 'scroll-event-throttle': scrollEventThrottle = 0, __selectRef } = props;
|
|
47
|
+
const scrollOffset = useRef(new RNAnimated.Value(0)).current;
|
|
45
48
|
const simultaneousHandlers = flatGesture(originSimultaneousHandlers);
|
|
46
49
|
const waitForHandlers = flatGesture(waitFor);
|
|
47
50
|
const snapScrollTop = useRef(0);
|
|
@@ -89,12 +92,13 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
89
92
|
},
|
|
90
93
|
gestureRef: scrollViewRef
|
|
91
94
|
});
|
|
95
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout });
|
|
92
96
|
const contextValue = useMemo(() => {
|
|
93
97
|
return {
|
|
94
|
-
gestureRef: scrollViewRef
|
|
98
|
+
gestureRef: scrollViewRef,
|
|
99
|
+
scrollOffset
|
|
95
100
|
};
|
|
96
101
|
}, []);
|
|
97
|
-
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout });
|
|
98
102
|
const hasRefresherLayoutRef = useRef(false);
|
|
99
103
|
// layout 完成前先隐藏,避免安卓闪烁问题
|
|
100
104
|
const refresherLayoutStyle = useMemo(() => { return !hasRefresherLayoutRef.current ? HIDDEN_STYLE : {}; }, [hasRefresherLayoutRef.current]);
|
|
@@ -320,6 +324,12 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
320
324
|
updateScrollOptions(e, { scrollLeft, scrollTop });
|
|
321
325
|
updateIntersection();
|
|
322
326
|
}
|
|
327
|
+
const scrollHandler = RNAnimated.event([{ nativeEvent: { contentOffset: { y: scrollOffset } } }], {
|
|
328
|
+
useNativeDriver: true,
|
|
329
|
+
listener: (event) => {
|
|
330
|
+
onScroll(event);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
323
333
|
function onScrollDragStart(e) {
|
|
324
334
|
hasCallScrollToLower.current = false;
|
|
325
335
|
hasCallScrollToUpper.current = false;
|
|
@@ -476,7 +486,7 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
476
486
|
scrollEnabled: !enableScroll ? false : !!(scrollX || scrollY),
|
|
477
487
|
bounces: false,
|
|
478
488
|
ref: scrollViewRef,
|
|
479
|
-
onScroll: onScroll,
|
|
489
|
+
onScroll: enableSticky ? scrollHandler : onScroll,
|
|
480
490
|
onContentSizeChange: onContentSizeChange,
|
|
481
491
|
bindtouchstart: ((enhanced && binddragstart) || bindtouchstart) && onScrollTouchStart,
|
|
482
492
|
bindtouchmove: ((enhanced && binddragging) || bindtouchmove) && onScrollTouchMove,
|
|
@@ -518,13 +528,14 @@ const _ScrollView = forwardRef((scrollViewProps = {}, ref) => {
|
|
|
518
528
|
'bindscrolltolower',
|
|
519
529
|
'bindrefresherrefresh'
|
|
520
530
|
], { layoutRef });
|
|
521
|
-
const
|
|
531
|
+
const ScrollViewComponent = enableSticky ? AnimatedScrollView : ScrollView;
|
|
532
|
+
const withRefresherScrollView = createElement(GestureDetector, { gesture: panGesture }, createElement(ScrollViewComponent, innerProps, createElement(Animated.View, { style: [refresherAnimatedStyle, refresherLayoutStyle], onLayout: onRefresherLayout }, refresherContent), createElement(Animated.View, { style: contentAnimatedStyle }, createElement(ScrollViewContext.Provider, { value: contextValue }, wrapChildren(extendObject({}, props, { children: otherContent }), {
|
|
522
533
|
hasVarDec,
|
|
523
534
|
varContext: varContextRef.current,
|
|
524
535
|
textStyle,
|
|
525
536
|
textProps
|
|
526
537
|
})))));
|
|
527
|
-
const commonScrollView = createElement(
|
|
538
|
+
const commonScrollView = createElement(ScrollViewComponent, extendObject({}, innerProps, {
|
|
528
539
|
refreshControl: refresherEnabled
|
|
529
540
|
? createElement(RefreshControl, extendObject({
|
|
530
541
|
progressBackgroundColor: refresherBackground,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect, useRef, useContext, forwardRef, useMemo, createElement, useId } from 'react';
|
|
2
|
+
import { Animated, StyleSheet } from 'react-native';
|
|
3
|
+
import { ScrollViewContext, StickyContext } from './context';
|
|
4
|
+
import useNodesRef from './useNodesRef';
|
|
5
|
+
import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils';
|
|
6
|
+
import { error } from '@mpxjs/utils';
|
|
7
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners';
|
|
8
|
+
const _StickyHeader = forwardRef((stickyHeaderProps = {}, ref) => {
|
|
9
|
+
const { textProps, innerProps: props = {} } = splitProps(stickyHeaderProps);
|
|
10
|
+
const { style, bindstickontopchange, padding = [0, 0, 0, 0], 'offset-top': offsetTop = 0, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
|
|
11
|
+
const scrollViewContext = useContext(ScrollViewContext);
|
|
12
|
+
const stickyContext = useContext(StickyContext);
|
|
13
|
+
const { scrollOffset } = scrollViewContext;
|
|
14
|
+
const { registerStickyHeader, unregisterStickyHeader } = stickyContext;
|
|
15
|
+
const headerRef = useRef(null);
|
|
16
|
+
const isStickOnTopRef = useRef(false);
|
|
17
|
+
const id = useId();
|
|
18
|
+
const { normalStyle, hasVarDec, varContextRef, hasSelfPercent, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
|
|
19
|
+
const { layoutRef, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: headerRef, onLayout });
|
|
20
|
+
const { textStyle, innerStyle = {} } = splitStyle(normalStyle);
|
|
21
|
+
const headerTopAnimated = useRef(new Animated.Value(0)).current;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
registerStickyHeader({ key: id, updatePosition });
|
|
24
|
+
return () => {
|
|
25
|
+
unregisterStickyHeader(id);
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
function updatePosition() {
|
|
29
|
+
if (headerRef.current) {
|
|
30
|
+
const scrollViewRef = scrollViewContext.gestureRef;
|
|
31
|
+
if (scrollViewRef && scrollViewRef.current) {
|
|
32
|
+
headerRef.current.measureLayout(scrollViewRef.current, (left, top) => {
|
|
33
|
+
Animated.timing(headerTopAnimated, {
|
|
34
|
+
toValue: top,
|
|
35
|
+
duration: 0,
|
|
36
|
+
useNativeDriver: true
|
|
37
|
+
}).start();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
error('StickyHeader measureLayout error: scrollViewRef is not a valid native component reference');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function onLayout(e) {
|
|
46
|
+
updatePosition();
|
|
47
|
+
}
|
|
48
|
+
useNodesRef(props, ref, headerRef, {
|
|
49
|
+
style: normalStyle
|
|
50
|
+
});
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!bindstickontopchange)
|
|
53
|
+
return;
|
|
54
|
+
const listener = scrollOffset.addListener((state) => {
|
|
55
|
+
const currentScrollValue = state.value;
|
|
56
|
+
const newIsStickOnTop = currentScrollValue > headerTopAnimated._value;
|
|
57
|
+
if (newIsStickOnTop !== isStickOnTopRef.current) {
|
|
58
|
+
isStickOnTopRef.current = newIsStickOnTop;
|
|
59
|
+
bindstickontopchange(getCustomEvent('stickontopchange', {}, {
|
|
60
|
+
detail: {
|
|
61
|
+
isStickOnTop: newIsStickOnTop
|
|
62
|
+
},
|
|
63
|
+
layoutRef
|
|
64
|
+
}, props));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
scrollOffset.removeListener(listener);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
const animatedStyle = useMemo(() => {
|
|
72
|
+
const translateY = Animated.subtract(scrollOffset, headerTopAnimated).interpolate({
|
|
73
|
+
inputRange: [0, 1],
|
|
74
|
+
outputRange: [0, 1],
|
|
75
|
+
extrapolateLeft: 'clamp',
|
|
76
|
+
extrapolateRight: 'extend'
|
|
77
|
+
});
|
|
78
|
+
const finalTranslateY = offsetTop === 0
|
|
79
|
+
? translateY
|
|
80
|
+
: Animated.add(translateY, Animated.subtract(scrollOffset, headerTopAnimated).interpolate({
|
|
81
|
+
inputRange: [0, 1],
|
|
82
|
+
outputRange: [0, offsetTop],
|
|
83
|
+
extrapolate: 'clamp'
|
|
84
|
+
}));
|
|
85
|
+
return {
|
|
86
|
+
transform: [{ translateY: finalTranslateY }]
|
|
87
|
+
};
|
|
88
|
+
}, [scrollOffset, headerTopAnimated, offsetTop]);
|
|
89
|
+
const innerProps = useInnerProps(props, extendObject({}, {
|
|
90
|
+
ref: headerRef,
|
|
91
|
+
style: extendObject({}, styles.content, innerStyle, animatedStyle, {
|
|
92
|
+
paddingTop: padding[0] || 0,
|
|
93
|
+
paddingRight: padding[1] || 0,
|
|
94
|
+
paddingBottom: padding[2] || 0,
|
|
95
|
+
paddingLeft: padding[3] || 0
|
|
96
|
+
})
|
|
97
|
+
}, layoutProps), [], { layoutRef });
|
|
98
|
+
return (createElement(Animated.View, innerProps, wrapChildren(props, {
|
|
99
|
+
hasVarDec,
|
|
100
|
+
varContext: varContextRef.current,
|
|
101
|
+
textStyle,
|
|
102
|
+
textProps
|
|
103
|
+
})));
|
|
104
|
+
});
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
content: {
|
|
107
|
+
width: '100%',
|
|
108
|
+
zIndex: 10
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
_StickyHeader.displayName = 'MpxStickyHeader';
|
|
112
|
+
export default _StickyHeader;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useRef, forwardRef, createElement, useCallback, useMemo } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import useNodesRef from './useNodesRef';
|
|
4
|
+
import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils';
|
|
5
|
+
import { StickyContext } from './context';
|
|
6
|
+
import useInnerProps from './getInnerListeners';
|
|
7
|
+
const _StickySection = forwardRef((stickySectionProps = {}, ref) => {
|
|
8
|
+
const { textProps, innerProps: props = {} } = splitProps(stickySectionProps);
|
|
9
|
+
const { style, 'enable-var': enableVar, 'external-var-context': externalVarContext, 'parent-font-size': parentFontSize, 'parent-width': parentWidth, 'parent-height': parentHeight } = props;
|
|
10
|
+
const sectionRef = useRef(null);
|
|
11
|
+
const { normalStyle, hasVarDec, varContextRef, hasSelfPercent, setWidth, setHeight } = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight });
|
|
12
|
+
const { layoutRef, layoutProps, layoutStyle } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: sectionRef, onLayout });
|
|
13
|
+
const { textStyle, innerStyle = {} } = splitStyle(normalStyle);
|
|
14
|
+
const stickyHeaders = useRef(new Map());
|
|
15
|
+
const registerStickyHeader = useCallback((item) => {
|
|
16
|
+
stickyHeaders.current.set(item.id, item);
|
|
17
|
+
}, []);
|
|
18
|
+
const unregisterStickyHeader = useCallback((id) => {
|
|
19
|
+
stickyHeaders.current.delete(id);
|
|
20
|
+
}, []);
|
|
21
|
+
const contextValue = useMemo(() => ({
|
|
22
|
+
registerStickyHeader,
|
|
23
|
+
unregisterStickyHeader
|
|
24
|
+
}), []);
|
|
25
|
+
useNodesRef(props, ref, sectionRef, {
|
|
26
|
+
style: normalStyle
|
|
27
|
+
});
|
|
28
|
+
function onLayout() {
|
|
29
|
+
stickyHeaders.current.forEach(item => {
|
|
30
|
+
item.updatePosition();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const innerProps = useInnerProps(props, extendObject({
|
|
34
|
+
style: extendObject(innerStyle, layoutStyle),
|
|
35
|
+
ref: sectionRef
|
|
36
|
+
}, layoutProps), [], { layoutRef });
|
|
37
|
+
return (createElement(View, innerProps, createElement(StickyContext.Provider, { value: contextValue }, wrapChildren(props, {
|
|
38
|
+
hasVarDec,
|
|
39
|
+
varContext: varContextRef.current,
|
|
40
|
+
textStyle,
|
|
41
|
+
textProps
|
|
42
|
+
}))));
|
|
43
|
+
});
|
|
44
|
+
_StickySection.displayName = 'MpxStickySection';
|
|
45
|
+
export default _StickySection;
|
|
@@ -451,7 +451,18 @@ export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout
|
|
|
451
451
|
if (enableOffset) {
|
|
452
452
|
nodeRef.current?.measure((x, y, width, height, offsetLeft, offsetTop) => {
|
|
453
453
|
const { y: navigationY = 0 } = navigation?.layout || {};
|
|
454
|
-
layoutRef.current = {
|
|
454
|
+
layoutRef.current = {
|
|
455
|
+
x,
|
|
456
|
+
y: y - navigationY,
|
|
457
|
+
width,
|
|
458
|
+
height,
|
|
459
|
+
offsetLeft,
|
|
460
|
+
offsetTop: offsetTop - navigationY,
|
|
461
|
+
_x: x,
|
|
462
|
+
_y: y,
|
|
463
|
+
_offsetLeft: offsetLeft,
|
|
464
|
+
_offsetTop: offsetTop
|
|
465
|
+
};
|
|
455
466
|
});
|
|
456
467
|
}
|
|
457
468
|
onLayout && onLayout(e);
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* ✔ bindscroll
|
|
33
33
|
*/
|
|
34
34
|
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'
|
|
36
36
|
import { isValidElement, Children, JSX, ReactNode, RefObject, useRef, useState, useEffect, forwardRef, useContext, useMemo, createElement } from 'react'
|
|
37
37
|
import Animated, { useAnimatedRef, useSharedValue, withTiming, useAnimatedStyle, runOnJS } from 'react-native-reanimated'
|
|
38
38
|
import { warn } from '@mpxjs/utils'
|
|
@@ -46,7 +46,6 @@ interface ScrollViewProps {
|
|
|
46
46
|
enhanced?: boolean;
|
|
47
47
|
bounces?: boolean;
|
|
48
48
|
style?: ViewStyle;
|
|
49
|
-
scrollEventThrottle?: number;
|
|
50
49
|
'scroll-x'?: boolean;
|
|
51
50
|
'scroll-y'?: boolean;
|
|
52
51
|
'enable-back-to-top'?: boolean;
|
|
@@ -70,6 +69,7 @@ interface ScrollViewProps {
|
|
|
70
69
|
'parent-font-size'?: number;
|
|
71
70
|
'parent-width'?: number;
|
|
72
71
|
'parent-height'?: number;
|
|
72
|
+
'enable-sticky'?: boolean;
|
|
73
73
|
'wait-for'?: Array<GestureHandler>;
|
|
74
74
|
'simultaneous-handlers'?: Array<GestureHandler>;
|
|
75
75
|
'scroll-event-throttle'?:number;
|
|
@@ -108,6 +108,8 @@ type ScrollAdditionalProps = {
|
|
|
108
108
|
onMomentumScrollEnd?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
const AnimatedScrollView = RNAnimated.createAnimatedComponent(ScrollView) as React.ComponentType<any>
|
|
112
|
+
|
|
111
113
|
const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, ScrollViewProps>((scrollViewProps: ScrollViewProps = {}, ref): JSX.Element => {
|
|
112
114
|
const { textProps, innerProps: props = {} } = splitProps(scrollViewProps)
|
|
113
115
|
const {
|
|
@@ -144,10 +146,13 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
144
146
|
'parent-height': parentHeight,
|
|
145
147
|
'simultaneous-handlers': originSimultaneousHandlers,
|
|
146
148
|
'wait-for': waitFor,
|
|
149
|
+
'enable-sticky': enableSticky,
|
|
147
150
|
'scroll-event-throttle': scrollEventThrottle = 0,
|
|
148
151
|
__selectRef
|
|
149
152
|
} = props
|
|
150
153
|
|
|
154
|
+
const scrollOffset = useRef(new RNAnimated.Value(0)).current
|
|
155
|
+
|
|
151
156
|
const simultaneousHandlers = flatGesture(originSimultaneousHandlers)
|
|
152
157
|
const waitForHandlers = flatGesture(waitFor)
|
|
153
158
|
|
|
@@ -216,14 +221,15 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
216
221
|
gestureRef: scrollViewRef
|
|
217
222
|
})
|
|
218
223
|
|
|
224
|
+
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout })
|
|
225
|
+
|
|
219
226
|
const contextValue = useMemo(() => {
|
|
220
227
|
return {
|
|
221
|
-
gestureRef: scrollViewRef
|
|
228
|
+
gestureRef: scrollViewRef,
|
|
229
|
+
scrollOffset
|
|
222
230
|
}
|
|
223
231
|
}, [])
|
|
224
232
|
|
|
225
|
-
const { layoutRef, layoutStyle, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: scrollViewRef, onLayout })
|
|
226
|
-
|
|
227
233
|
const hasRefresherLayoutRef = useRef(false)
|
|
228
234
|
|
|
229
235
|
// layout 完成前先隐藏,避免安卓闪烁问题
|
|
@@ -485,6 +491,16 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
485
491
|
updateIntersection()
|
|
486
492
|
}
|
|
487
493
|
|
|
494
|
+
const scrollHandler = RNAnimated.event(
|
|
495
|
+
[{ nativeEvent: { contentOffset: { y: scrollOffset } } }],
|
|
496
|
+
{
|
|
497
|
+
useNativeDriver: true,
|
|
498
|
+
listener: (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
499
|
+
onScroll(event)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
)
|
|
503
|
+
|
|
488
504
|
function onScrollDragStart (e: NativeSyntheticEvent<NativeScrollEvent>) {
|
|
489
505
|
hasCallScrollToLower.current = false
|
|
490
506
|
hasCallScrollToUpper.current = false
|
|
@@ -655,7 +671,7 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
655
671
|
scrollEnabled: !enableScroll ? false : !!(scrollX || scrollY),
|
|
656
672
|
bounces: false,
|
|
657
673
|
ref: scrollViewRef,
|
|
658
|
-
onScroll: onScroll,
|
|
674
|
+
onScroll: enableSticky ? scrollHandler : onScroll,
|
|
659
675
|
onContentSizeChange: onContentSizeChange,
|
|
660
676
|
bindtouchstart: ((enhanced && binddragstart) || bindtouchstart) && onScrollTouchStart,
|
|
661
677
|
bindtouchmove: ((enhanced && binddragging) || bindtouchmove) && onScrollTouchMove,
|
|
@@ -704,11 +720,13 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
704
720
|
'bindrefresherrefresh'
|
|
705
721
|
], { layoutRef })
|
|
706
722
|
|
|
723
|
+
const ScrollViewComponent = enableSticky ? AnimatedScrollView : ScrollView
|
|
724
|
+
|
|
707
725
|
const withRefresherScrollView = createElement(
|
|
708
726
|
GestureDetector,
|
|
709
727
|
{ gesture: panGesture },
|
|
710
728
|
createElement(
|
|
711
|
-
|
|
729
|
+
ScrollViewComponent,
|
|
712
730
|
innerProps,
|
|
713
731
|
createElement(
|
|
714
732
|
Animated.View,
|
|
@@ -736,8 +754,8 @@ const _ScrollView = forwardRef<HandlerRef<ScrollView & View, ScrollViewProps>, S
|
|
|
736
754
|
)
|
|
737
755
|
|
|
738
756
|
const commonScrollView = createElement(
|
|
739
|
-
|
|
740
|
-
extendObject(innerProps, {
|
|
757
|
+
ScrollViewComponent,
|
|
758
|
+
extendObject({}, innerProps, {
|
|
741
759
|
refreshControl: refresherEnabled
|
|
742
760
|
? createElement(RefreshControl, extendObject({
|
|
743
761
|
progressBackgroundColor: refresherBackground,
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useContext, forwardRef, useMemo, createElement, ReactNode, useId } from 'react'
|
|
2
|
+
import { Animated, StyleSheet, View, NativeSyntheticEvent, ViewStyle, LayoutChangeEvent } from 'react-native'
|
|
3
|
+
import { ScrollViewContext, StickyContext } from './context'
|
|
4
|
+
import useNodesRef, { HandlerRef } from './useNodesRef'
|
|
5
|
+
import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils'
|
|
6
|
+
import { error } from '@mpxjs/utils'
|
|
7
|
+
import useInnerProps, { getCustomEvent } from './getInnerListeners'
|
|
8
|
+
|
|
9
|
+
interface StickyHeaderProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
style?: ViewStyle;
|
|
12
|
+
padding?: [number, number, number, number];
|
|
13
|
+
'offset-top'?: number;
|
|
14
|
+
'enable-var'?: boolean;
|
|
15
|
+
'external-var-context'?: Record<string, any>;
|
|
16
|
+
'parent-font-size'?: number;
|
|
17
|
+
'parent-width'?: number;
|
|
18
|
+
'parent-height'?: number;
|
|
19
|
+
bindstickontopchange?: (e: NativeSyntheticEvent<unknown>) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const _StickyHeader = forwardRef<HandlerRef<View, StickyHeaderProps>, StickyHeaderProps>((stickyHeaderProps: StickyHeaderProps = {}, ref): JSX.Element => {
|
|
23
|
+
const { textProps, innerProps: props = {} } = splitProps(stickyHeaderProps)
|
|
24
|
+
const {
|
|
25
|
+
style,
|
|
26
|
+
bindstickontopchange,
|
|
27
|
+
padding = [0, 0, 0, 0],
|
|
28
|
+
'offset-top': offsetTop = 0,
|
|
29
|
+
'enable-var': enableVar,
|
|
30
|
+
'external-var-context': externalVarContext,
|
|
31
|
+
'parent-font-size': parentFontSize,
|
|
32
|
+
'parent-width': parentWidth,
|
|
33
|
+
'parent-height': parentHeight
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
const scrollViewContext = useContext(ScrollViewContext)
|
|
37
|
+
const stickyContext = useContext(StickyContext)
|
|
38
|
+
const { scrollOffset } = scrollViewContext
|
|
39
|
+
const { registerStickyHeader, unregisterStickyHeader } = stickyContext
|
|
40
|
+
const headerRef = useRef<View>(null)
|
|
41
|
+
const isStickOnTopRef = useRef(false)
|
|
42
|
+
const id = useId()
|
|
43
|
+
|
|
44
|
+
const {
|
|
45
|
+
normalStyle,
|
|
46
|
+
hasVarDec,
|
|
47
|
+
varContextRef,
|
|
48
|
+
hasSelfPercent,
|
|
49
|
+
setWidth,
|
|
50
|
+
setHeight
|
|
51
|
+
} = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight })
|
|
52
|
+
|
|
53
|
+
const { layoutRef, layoutProps } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: headerRef, onLayout })
|
|
54
|
+
|
|
55
|
+
const { textStyle, innerStyle = {} } = splitStyle(normalStyle)
|
|
56
|
+
|
|
57
|
+
const headerTopAnimated = useRef(new Animated.Value(0)).current
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
registerStickyHeader({ key: id, updatePosition })
|
|
61
|
+
return () => {
|
|
62
|
+
unregisterStickyHeader(id)
|
|
63
|
+
}
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
function updatePosition () {
|
|
67
|
+
if (headerRef.current) {
|
|
68
|
+
const scrollViewRef = scrollViewContext.gestureRef
|
|
69
|
+
if (scrollViewRef && scrollViewRef.current) {
|
|
70
|
+
headerRef.current.measureLayout(
|
|
71
|
+
scrollViewRef.current,
|
|
72
|
+
(left: number, top: number) => {
|
|
73
|
+
Animated.timing(headerTopAnimated, {
|
|
74
|
+
toValue: top,
|
|
75
|
+
duration: 0,
|
|
76
|
+
useNativeDriver: true
|
|
77
|
+
}).start()
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
} else {
|
|
81
|
+
error('StickyHeader measureLayout error: scrollViewRef is not a valid native component reference')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onLayout (e: LayoutChangeEvent) {
|
|
87
|
+
updatePosition()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
useNodesRef(props, ref, headerRef, {
|
|
91
|
+
style: normalStyle
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!bindstickontopchange) return
|
|
96
|
+
|
|
97
|
+
const listener = scrollOffset.addListener((state: { value: number }) => {
|
|
98
|
+
const currentScrollValue = state.value
|
|
99
|
+
const newIsStickOnTop = currentScrollValue > (headerTopAnimated as any)._value
|
|
100
|
+
if (newIsStickOnTop !== isStickOnTopRef.current) {
|
|
101
|
+
isStickOnTopRef.current = newIsStickOnTop
|
|
102
|
+
bindstickontopchange(
|
|
103
|
+
getCustomEvent('stickontopchange', {}, {
|
|
104
|
+
detail: {
|
|
105
|
+
isStickOnTop: newIsStickOnTop
|
|
106
|
+
},
|
|
107
|
+
layoutRef
|
|
108
|
+
}, props))
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
scrollOffset.removeListener(listener)
|
|
114
|
+
}
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
const animatedStyle = useMemo(() => {
|
|
118
|
+
const translateY = Animated.subtract(scrollOffset, headerTopAnimated).interpolate({
|
|
119
|
+
inputRange: [0, 1],
|
|
120
|
+
outputRange: [0, 1],
|
|
121
|
+
extrapolateLeft: 'clamp',
|
|
122
|
+
extrapolateRight: 'extend'
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const finalTranslateY = offsetTop === 0
|
|
126
|
+
? translateY
|
|
127
|
+
: Animated.add(
|
|
128
|
+
translateY,
|
|
129
|
+
Animated.subtract(scrollOffset, headerTopAnimated).interpolate({
|
|
130
|
+
inputRange: [0, 1],
|
|
131
|
+
outputRange: [0, offsetTop],
|
|
132
|
+
extrapolate: 'clamp'
|
|
133
|
+
})
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
transform: [{ translateY: finalTranslateY }]
|
|
138
|
+
}
|
|
139
|
+
}, [scrollOffset, headerTopAnimated, offsetTop])
|
|
140
|
+
|
|
141
|
+
const innerProps = useInnerProps(props, extendObject({}, {
|
|
142
|
+
ref: headerRef,
|
|
143
|
+
style: extendObject({}, styles.content, innerStyle, animatedStyle, {
|
|
144
|
+
paddingTop: padding[0] || 0,
|
|
145
|
+
paddingRight: padding[1] || 0,
|
|
146
|
+
paddingBottom: padding[2] || 0,
|
|
147
|
+
paddingLeft: padding[3] || 0
|
|
148
|
+
})
|
|
149
|
+
}, layoutProps), [], { layoutRef })
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
createElement(
|
|
153
|
+
Animated.View,
|
|
154
|
+
innerProps,
|
|
155
|
+
wrapChildren(
|
|
156
|
+
props,
|
|
157
|
+
{
|
|
158
|
+
hasVarDec,
|
|
159
|
+
varContext: varContextRef.current,
|
|
160
|
+
textStyle,
|
|
161
|
+
textProps
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const styles = StyleSheet.create({
|
|
169
|
+
content: {
|
|
170
|
+
width: '100%',
|
|
171
|
+
zIndex: 10
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
_StickyHeader.displayName = 'MpxStickyHeader'
|
|
176
|
+
export default _StickyHeader
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
import { useRef, forwardRef, createElement, ReactNode, useCallback, useMemo } from 'react'
|
|
3
|
+
import { View, ViewStyle } from 'react-native'
|
|
4
|
+
import useNodesRef, { HandlerRef } from './useNodesRef'
|
|
5
|
+
import { splitProps, splitStyle, useTransformStyle, wrapChildren, useLayout, extendObject } from './utils'
|
|
6
|
+
import { StickyContext } from './context'
|
|
7
|
+
import useInnerProps from './getInnerListeners'
|
|
8
|
+
|
|
9
|
+
interface StickySectionProps {
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
style?: ViewStyle;
|
|
12
|
+
'offset-top'?: number;
|
|
13
|
+
'enable-var'?: boolean;
|
|
14
|
+
'external-var-context'?: Record<string, any>;
|
|
15
|
+
'parent-font-size'?: number;
|
|
16
|
+
'parent-width'?: number;
|
|
17
|
+
'parent-height'?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const _StickySection = forwardRef<HandlerRef<View, StickySectionProps>, StickySectionProps>((stickySectionProps: StickySectionProps = {}, ref): JSX.Element => {
|
|
21
|
+
const { textProps, innerProps: props = {} } = splitProps(stickySectionProps)
|
|
22
|
+
const {
|
|
23
|
+
style,
|
|
24
|
+
'enable-var': enableVar,
|
|
25
|
+
'external-var-context': externalVarContext,
|
|
26
|
+
'parent-font-size': parentFontSize,
|
|
27
|
+
'parent-width': parentWidth,
|
|
28
|
+
'parent-height': parentHeight
|
|
29
|
+
} = props
|
|
30
|
+
const sectionRef = useRef<View>(null)
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
normalStyle,
|
|
34
|
+
hasVarDec,
|
|
35
|
+
varContextRef,
|
|
36
|
+
hasSelfPercent,
|
|
37
|
+
setWidth,
|
|
38
|
+
setHeight
|
|
39
|
+
} = useTransformStyle(style, { enableVar, externalVarContext, parentFontSize, parentWidth, parentHeight })
|
|
40
|
+
|
|
41
|
+
const { layoutRef, layoutProps, layoutStyle } = useLayout({ props, hasSelfPercent, setWidth, setHeight, nodeRef: sectionRef, onLayout })
|
|
42
|
+
|
|
43
|
+
const { textStyle, innerStyle = {} } = splitStyle(normalStyle)
|
|
44
|
+
|
|
45
|
+
const stickyHeaders = useRef<Map<string, any>>(new Map())
|
|
46
|
+
|
|
47
|
+
const registerStickyHeader = useCallback((item: { id: string, updatePosition: Function }) => {
|
|
48
|
+
stickyHeaders.current.set(item.id, item)
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const unregisterStickyHeader = useCallback((id: string) => {
|
|
52
|
+
stickyHeaders.current.delete(id)
|
|
53
|
+
}, [])
|
|
54
|
+
|
|
55
|
+
const contextValue = useMemo(() => ({
|
|
56
|
+
registerStickyHeader,
|
|
57
|
+
unregisterStickyHeader
|
|
58
|
+
}), [])
|
|
59
|
+
|
|
60
|
+
useNodesRef(props, ref, sectionRef, {
|
|
61
|
+
style: normalStyle
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
function onLayout () {
|
|
65
|
+
stickyHeaders.current.forEach(item => {
|
|
66
|
+
item.updatePosition()
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const innerProps = useInnerProps(props, extendObject({
|
|
71
|
+
style: extendObject(innerStyle, layoutStyle),
|
|
72
|
+
ref: sectionRef
|
|
73
|
+
}, layoutProps), [], { layoutRef })
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
createElement(
|
|
77
|
+
View,
|
|
78
|
+
innerProps,
|
|
79
|
+
createElement(
|
|
80
|
+
StickyContext.Provider,
|
|
81
|
+
{ value: contextValue },
|
|
82
|
+
wrapChildren(
|
|
83
|
+
props,
|
|
84
|
+
{
|
|
85
|
+
hasVarDec,
|
|
86
|
+
varContext: varContextRef.current,
|
|
87
|
+
textStyle,
|
|
88
|
+
textProps
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
))
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
_StickySection.displayName = 'MpxStickySection'
|
|
96
|
+
export default _StickySection
|
|
@@ -533,7 +533,18 @@ export const useLayout = ({ props, hasSelfPercent, setWidth, setHeight, onLayout
|
|
|
533
533
|
if (enableOffset) {
|
|
534
534
|
nodeRef.current?.measure((x: number, y: number, width: number, height: number, offsetLeft: number, offsetTop: number) => {
|
|
535
535
|
const { y: navigationY = 0 } = navigation?.layout || {}
|
|
536
|
-
layoutRef.current = {
|
|
536
|
+
layoutRef.current = {
|
|
537
|
+
x,
|
|
538
|
+
y: y - navigationY,
|
|
539
|
+
width,
|
|
540
|
+
height,
|
|
541
|
+
offsetLeft,
|
|
542
|
+
offsetTop: offsetTop - navigationY,
|
|
543
|
+
_x: x,
|
|
544
|
+
_y: y,
|
|
545
|
+
_offsetLeft: offsetLeft,
|
|
546
|
+
_offsetTop: offsetTop
|
|
547
|
+
}
|
|
537
548
|
})
|
|
538
549
|
}
|
|
539
550
|
onLayout && onLayout(e)
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
enhanced: Boolean,
|
|
45
45
|
refresherEnabled: Boolean,
|
|
46
46
|
refresherTriggered: Boolean,
|
|
47
|
+
enableSticky: Boolean,
|
|
47
48
|
refresherThreshold: {
|
|
48
49
|
type: Number,
|
|
49
50
|
default: 45
|
|
@@ -57,6 +58,16 @@
|
|
|
57
58
|
default: ''
|
|
58
59
|
}
|
|
59
60
|
},
|
|
61
|
+
provide () {
|
|
62
|
+
return {
|
|
63
|
+
scrollOffset: {
|
|
64
|
+
get: () => this.lastY
|
|
65
|
+
},
|
|
66
|
+
refreshVersion: {
|
|
67
|
+
get: () => this.refreshVersion
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
60
71
|
data () {
|
|
61
72
|
return {
|
|
62
73
|
isLoading: false,
|
|
@@ -68,7 +79,8 @@
|
|
|
68
79
|
lastContentWidth: 0,
|
|
69
80
|
lastContentHeight: 0,
|
|
70
81
|
lastWrapperWidth: 0,
|
|
71
|
-
lastWrapperHeight: 0
|
|
82
|
+
lastWrapperHeight: 0,
|
|
83
|
+
refreshVersion: 0
|
|
72
84
|
}
|
|
73
85
|
},
|
|
74
86
|
computed: {
|
|
@@ -222,6 +234,9 @@
|
|
|
222
234
|
stop: 56
|
|
223
235
|
}
|
|
224
236
|
}
|
|
237
|
+
if(this.enableSticky) {
|
|
238
|
+
originBsOptions.useTransition = false
|
|
239
|
+
}
|
|
225
240
|
const bsOptions = Object.assign({}, originBsOptions, this.scrollOptions, { observeDOM: false })
|
|
226
241
|
this.bs = new BScroll(this.$refs.wrapper, bsOptions)
|
|
227
242
|
this.lastX = -this.currentX
|
|
@@ -251,7 +266,7 @@
|
|
|
251
266
|
}
|
|
252
267
|
this.lastX = x
|
|
253
268
|
this.lastY = y
|
|
254
|
-
}, 30, {
|
|
269
|
+
}, this.enableSticky ? 0 : 30, {
|
|
255
270
|
leading: true,
|
|
256
271
|
trailing: true
|
|
257
272
|
}))
|
|
@@ -327,6 +342,7 @@
|
|
|
327
342
|
const scrollWrapperHeight = wrapper?.clientHeight || 0
|
|
328
343
|
if (wrapper) {
|
|
329
344
|
const computedStyle = getComputedStyle(wrapper)
|
|
345
|
+
this.refreshVersion = this.refreshVersion + 1
|
|
330
346
|
// 考虑子元素样式可能会设置100%,如果直接继承 scrollContent 的样式可能会有问题
|
|
331
347
|
// 所以使用 wrapper 作为 innerWrapper 的宽高参考依据
|
|
332
348
|
this.$refs.innerWrapper.style.width = `${scrollWrapperWidth - parseInt(computedStyle.paddingLeft) - parseInt(computedStyle.paddingRight)}px`
|
|
@@ -458,7 +474,8 @@
|
|
|
458
474
|
}
|
|
459
475
|
|
|
460
476
|
const innerWrapper = createElement('div', {
|
|
461
|
-
ref: 'innerWrapper'
|
|
477
|
+
ref: 'innerWrapper',
|
|
478
|
+
class: 'mpx-inner-wrapper'
|
|
462
479
|
}, this.$slots.default)
|
|
463
480
|
|
|
464
481
|
const pullDownContent = this.refresherDefaultStyle !== 'none' ? createElement('div', {
|
|
@@ -568,4 +585,4 @@
|
|
|
568
585
|
background: rgba(255, 255, 255, .7)
|
|
569
586
|
100%
|
|
570
587
|
background: rgba(255, 255, 255, .3)
|
|
571
|
-
</style>
|
|
588
|
+
</style>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { warn } from '@mpxjs/utils'
|
|
3
|
+
import { getCustomEvent } from './getInnerListeners'
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'mpx-sticky-header',
|
|
7
|
+
inject: ['scrollOffset', 'refreshVersion'],
|
|
8
|
+
props: {
|
|
9
|
+
'offsetTop': {
|
|
10
|
+
type: Number,
|
|
11
|
+
default: 0
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
data() {
|
|
15
|
+
return {
|
|
16
|
+
headerTop: 0,
|
|
17
|
+
isStickOnTop: false
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
computed: {
|
|
21
|
+
_scrollOffset() {
|
|
22
|
+
return -this.scrollOffset?.get() || 0
|
|
23
|
+
},
|
|
24
|
+
_refreshVersion() {
|
|
25
|
+
return this.refreshVersion?.get() || 0
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
watch: {
|
|
29
|
+
_scrollOffset: {
|
|
30
|
+
handler(newScrollOffset) {
|
|
31
|
+
const newIsStickOnTop = newScrollOffset > this.headerTop
|
|
32
|
+
if (newIsStickOnTop !== this.isStickOnTop) {
|
|
33
|
+
this.isStickOnTop = newIsStickOnTop
|
|
34
|
+
this.$emit('stickontopchange', getCustomEvent('stickontopchange', {
|
|
35
|
+
isStickOnTop: newIsStickOnTop
|
|
36
|
+
}, this))
|
|
37
|
+
}
|
|
38
|
+
const stickyHeader = this.$refs.stickyHeader
|
|
39
|
+
if (!stickyHeader) return
|
|
40
|
+
if (this.isStickOnTop) {
|
|
41
|
+
stickyHeader.style.transform = `translateY(${newScrollOffset - this.headerTop + this.offsetTop}px)`
|
|
42
|
+
} else {
|
|
43
|
+
stickyHeader.style.transform = 'none'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
immediate: true
|
|
47
|
+
},
|
|
48
|
+
_refreshVersion: {
|
|
49
|
+
handler() {
|
|
50
|
+
const parentElement = this.$el.parentElement
|
|
51
|
+
if (!parentElement) return
|
|
52
|
+
|
|
53
|
+
const parentClass = parentElement.className || ''
|
|
54
|
+
const isStickySection = /mpx-sticky-section/.test(parentClass)
|
|
55
|
+
const isScrollViewWrapper = /mpx-inner-wrapper/.test(parentClass)
|
|
56
|
+
|
|
57
|
+
if (!isStickySection && !isScrollViewWrapper) {
|
|
58
|
+
warn('sticky-header only supports being a direct child of a scroll-view or sticky-section component.')
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.headerTop = isStickySection
|
|
63
|
+
? this.$el.offsetTop + parentElement.offsetTop
|
|
64
|
+
: this.$el.offsetTop
|
|
65
|
+
|
|
66
|
+
const stickyHeader = this.$refs.stickyHeader
|
|
67
|
+
if (!stickyHeader) return
|
|
68
|
+
|
|
69
|
+
if (this._scrollOffset > this.headerTop) {
|
|
70
|
+
stickyHeader.style.transform = `translateY(${this._scrollOffset - this.headerTop + this.offsetTop}px)`
|
|
71
|
+
} else {
|
|
72
|
+
stickyHeader.style.transform = 'none'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
render(h) {
|
|
78
|
+
const style = {
|
|
79
|
+
width: '100%',
|
|
80
|
+
boxSizing: 'border-box',
|
|
81
|
+
position: 'relative',
|
|
82
|
+
zIndex: 10
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return h('div', {
|
|
86
|
+
style,
|
|
87
|
+
ref: 'stickyHeader'
|
|
88
|
+
}, this.$slots.default)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|