@mpxjs/webpack-plugin 2.10.4-beta.3 → 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.
@@ -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
- gestureRef: React.RefObject<any> | null
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 });
@@ -1,7 +1,6 @@
1
- import React, { useContext, useEffect, useMemo } from 'react';
1
+ import React, { useContext, useEffect } from 'react';
2
2
  import { Keyboard, View } from 'react-native';
3
3
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated';
4
- import { Gesture } from 'react-native-gesture-handler';
5
4
  import { KeyboardAvoidContext } from './context';
6
5
  import { isIOS } from './utils';
7
6
  const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
@@ -10,20 +9,10 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
10
9
  const offset = useSharedValue(0);
11
10
  const basic = useSharedValue('auto');
12
11
  const keyboardAvoid = useContext(KeyboardAvoidContext);
13
- const dismiss = () => {
14
- Keyboard.isVisible() && Keyboard.dismiss();
15
- };
16
- const gesture = useMemo(() => {
17
- return Gesture.Tap()
18
- .onEnd(() => {
19
- dismiss();
20
- }).runOnJS(true);
21
- }, []);
22
- const animatedStyle = useAnimatedStyle(() => {
23
- return Object.assign({
24
- transform: [{ translateY: -offset.value }]
25
- }, isIOS ? {} : { flexBasis: basic.value });
26
- });
12
+ const animatedStyle = useAnimatedStyle(() => ({
13
+ transform: [{ translateY: -offset.value }],
14
+ flexBasis: basic.value
15
+ }));
27
16
  const resetKeyboard = () => {
28
17
  if (keyboardAvoid?.current) {
29
18
  keyboardAvoid.current = null;
@@ -31,6 +20,9 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
31
20
  offset.value = withTiming(0, { duration, easing });
32
21
  basic.value = 'auto';
33
22
  };
23
+ const onTouchEnd = () => {
24
+ Keyboard.isVisible() && Keyboard.dismiss();
25
+ };
34
26
  useEffect(() => {
35
27
  let subscriptions = [];
36
28
  if (isIOS) {
@@ -46,7 +38,12 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
46
38
  const aboveValue = -aboveOffset >= cursorSpacing ? 0 : aboveOffset + cursorSpacing;
47
39
  const belowValue = Math.min(endCoordinates.height, aboveOffset + cursorSpacing);
48
40
  const value = aboveOffset > 0 ? belowValue : aboveValue;
49
- offset.value = withTiming(value, { duration, easing });
41
+ offset.value = withTiming(value, { duration, easing }, (finished) => {
42
+ if (finished) {
43
+ // Set flexBasic after animation to trigger re-layout and reset layout information
44
+ basic.value = '99.99%';
45
+ }
46
+ });
50
47
  });
51
48
  });
52
49
  }),
@@ -68,11 +65,7 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
68
65
  const value = aboveOffset > 0 ? belowValue : aboveValue;
69
66
  offset.value = withTiming(value, { duration, easing }, (finished) => {
70
67
  if (finished) {
71
- /**
72
- * In the Android environment, the layout information is not synchronized after the animation,
73
- * which results in the inability to correctly trigger element events.
74
- * Here, we utilize flexBasic to proactively trigger a re-layout
75
- */
68
+ // Set flexBasic after animation to trigger re-layout and reset layout information
76
69
  basic.value = '99.99%';
77
70
  }
78
71
  });
@@ -85,18 +78,14 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }) => {
85
78
  subscriptions.forEach(subscription => subscription.remove());
86
79
  };
87
80
  }, [keyboardAvoid]);
88
- return (
89
- // <GestureDetector gesture={gesture}>
90
- <View style={style}>
81
+ return (<View style={style} onTouchEnd={onTouchEnd}>
91
82
  <Animated.View style={[
92
83
  contentContainerStyle,
93
84
  animatedStyle
94
85
  ]}>
95
86
  {children}
96
87
  </Animated.View>
97
- </View>
98
- // </GestureDetector>
99
- );
88
+ </View>);
100
89
  };
101
90
  KeyboardAvoidingView.displayName = 'MpxKeyboardAvoidingView';
102
91
  export default KeyboardAvoidingView;
@@ -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 withRefresherScrollView = createElement(GestureDetector, { gesture: panGesture }, createElement(ScrollView, 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 }), {
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(ScrollView, extendObject(innerProps, {
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 = { x, y: y - navigationY, width, height, offsetLeft, offsetTop: offsetTop - navigationY };
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);
@@ -1,7 +1,6 @@
1
- import React, { ReactNode, useContext, useEffect, useMemo } from 'react'
2
- import { DimensionValue, EmitterSubscription, Keyboard, Platform, View, ViewStyle } from 'react-native'
3
- import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, runOnJS } from 'react-native-reanimated'
4
- import { GestureDetector, Gesture } from 'react-native-gesture-handler'
1
+ import React, { ReactNode, useContext, useEffect } from 'react'
2
+ import { DimensionValue, EmitterSubscription, Keyboard, View, ViewStyle } from 'react-native'
3
+ import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated'
5
4
  import { KeyboardAvoidContext } from './context'
6
5
  import { isIOS } from './utils'
7
6
 
@@ -19,25 +18,10 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
19
18
  const basic = useSharedValue('auto')
20
19
  const keyboardAvoid = useContext(KeyboardAvoidContext)
21
20
 
22
- const dismiss = () => {
23
- Keyboard.isVisible() && Keyboard.dismiss()
24
- }
25
-
26
- const gesture = useMemo(() => {
27
- return Gesture.Tap()
28
- .onEnd(() => {
29
- dismiss()
30
- }).runOnJS(true)
31
- }, [])
32
-
33
- const animatedStyle = useAnimatedStyle(() => {
34
- return Object.assign(
35
- {
36
- transform: [{ translateY: -offset.value }]
37
- },
38
- isIOS ? {} : { flexBasis: basic.value as DimensionValue }
39
- )
40
- })
21
+ const animatedStyle = useAnimatedStyle(() => ({
22
+ transform: [{ translateY: -offset.value }],
23
+ flexBasis: basic.value as DimensionValue
24
+ }))
41
25
 
42
26
  const resetKeyboard = () => {
43
27
  if (keyboardAvoid?.current) {
@@ -47,6 +31,10 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
47
31
  basic.value = 'auto'
48
32
  }
49
33
 
34
+ const onTouchEnd = () => {
35
+ Keyboard.isVisible() && Keyboard.dismiss()
36
+ }
37
+
50
38
  useEffect(() => {
51
39
  let subscriptions: EmitterSubscription[] = []
52
40
 
@@ -62,7 +50,12 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
62
50
  const aboveValue = -aboveOffset >= cursorSpacing ? 0 : aboveOffset + cursorSpacing
63
51
  const belowValue = Math.min(endCoordinates.height, aboveOffset + cursorSpacing)
64
52
  const value = aboveOffset > 0 ? belowValue : aboveValue
65
- offset.value = withTiming(value, { duration, easing })
53
+ offset.value = withTiming(value, { duration, easing }, (finished) => {
54
+ if (finished) {
55
+ // Set flexBasic after animation to trigger re-layout and reset layout information
56
+ basic.value = '99.99%'
57
+ }
58
+ })
66
59
  })
67
60
  })
68
61
  }),
@@ -82,11 +75,7 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
82
75
  const value = aboveOffset > 0 ? belowValue : aboveValue
83
76
  offset.value = withTiming(value, { duration, easing }, (finished) => {
84
77
  if (finished) {
85
- /**
86
- * In the Android environment, the layout information is not synchronized after the animation,
87
- * which results in the inability to correctly trigger element events.
88
- * Here, we utilize flexBasic to proactively trigger a re-layout
89
- */
78
+ // Set flexBasic after animation to trigger re-layout and reset layout information
90
79
  basic.value = '99.99%'
91
80
  }
92
81
  })
@@ -102,8 +91,7 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
102
91
  }, [keyboardAvoid])
103
92
 
104
93
  return (
105
- // <GestureDetector gesture={gesture}>
106
- <View style={style}>
94
+ <View style={style} onTouchEnd={onTouchEnd}>
107
95
  <Animated.View
108
96
  style={[
109
97
  contentContainerStyle,
@@ -113,7 +101,6 @@ const KeyboardAvoidingView = ({ children, style, contentContainerStyle }: Keyboa
113
101
  {children}
114
102
  </Animated.View>
115
103
  </View>
116
- // </GestureDetector>
117
104
  )
118
105
  }
119
106
 
@@ -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
- ScrollView,
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
- ScrollView,
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 = { x, y: y - navigationY, width, height, offsetLeft, offsetTop: offsetTop - navigationY }
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>
@@ -0,0 +1,15 @@
1
+ <script>
2
+ export default {
3
+ name: 'mpx-sticky-section',
4
+ render(h) {
5
+ const style = {
6
+ position: 'relative'
7
+ }
8
+
9
+ return h('div', {
10
+ style,
11
+ class: 'mpx-sticky-section'
12
+ }, this.$slots.default)
13
+ }
14
+ }
15
+ </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpxjs/webpack-plugin",
3
- "version": "2.10.4-beta.3",
3
+ "version": "2.10.4-beta.5",
4
4
  "description": "mpx compile core",
5
5
  "keywords": [
6
6
  "mpx"