@momo-kits/foundation 0.167.1-beta.12 → 0.167.1-beta.14
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/Application/BottomSheet.tsx +246 -42
- package/Application/BottomTab/BottomTabBar.tsx +6 -3
- package/Application/BottomTab/CustomBottomTabItem.tsx +3 -5
- package/Application/BottomTab/DuoBarCompression.tsx +135 -0
- package/Application/BottomTab/DuoTabBar.tsx +537 -0
- package/Application/BottomTab/index.tsx +145 -134
- package/Application/Components/DuoGlassBackground.tsx +158 -0
- package/Application/Components/DuoQuickReturnHeader.tsx +42 -0
- package/Application/Components/HeaderAnimated.tsx +9 -3
- package/Application/Components/HeaderExtendHeader.tsx +70 -28
- package/Application/Components/HeaderLeft.tsx +38 -0
- package/Application/Components/HeaderRight.tsx +144 -9
- package/Application/Components/HeaderTitle.tsx +43 -7
- package/Application/Components/SearchHeader.tsx +20 -15
- package/Application/Components/SideBarButton.tsx +134 -0
- package/Application/ModalScreen.tsx +20 -2
- package/Application/NavigationContainer.tsx +22 -38
- package/Application/Navigator.ts +1 -3
- package/Application/StackScreen.tsx +2 -5
- package/Application/device.ts +52 -0
- package/Application/types.ts +10 -1
- package/Application/utils.tsx +10 -7
- package/Badge/Badge.tsx +2 -3
- package/Badge/BadgeDotAnimation.tsx +3 -3
- package/Button/index.tsx +11 -11
- package/Consts/theme.ts +3 -4
- package/Input/Input.tsx +3 -4
- package/Input/InputMoney.tsx +123 -194
- package/Input/InputOTP.tsx +9 -12
- package/Input/InputPhoneNumber.tsx +3 -4
- package/Input/InputSearch.tsx +17 -14
- package/Input/InputTextArea.tsx +3 -4
- package/Input/SystemTextInput.tsx +2 -3
- package/Input/TextTyping.tsx +11 -6
- package/Input/styles.ts +9 -0
- package/Input/utils.ts +2 -3
- package/Layout/Card.tsx +8 -26
- package/Layout/FloatingButton.tsx +3 -3
- package/Layout/Item.tsx +1 -10
- package/Layout/ReservedRegions.tsx +78 -0
- package/Layout/Screen.tsx +242 -48
- package/Layout/Section.tsx +5 -18
- package/Layout/index.ts +4 -4
- package/Layout/styles.ts +5 -0
- package/Layout/utils.ts +478 -5
- package/Pagination/Dot.tsx +3 -6
- package/Popup/PopupNotify.tsx +49 -28
- package/Popup/PopupPromotion.tsx +21 -8
- package/Popup/utils.ts +26 -0
- package/Radio/index.tsx +2 -3
- package/Text/utils.ts +2 -3
- package/Title/index.tsx +8 -11
- package/package.json +5 -2
- package/Layout/GridSystem.tsx +0 -137
|
@@ -27,7 +27,11 @@ import StackScreen from '../StackScreen';
|
|
|
27
27
|
import { BottomTabProps } from '../types';
|
|
28
28
|
import { getOptions, getStackOptions, useAppState } from '../utils';
|
|
29
29
|
import BottomTabBar from './BottomTabBar';
|
|
30
|
+
import { DuoTabBar } from './DuoTabBar';
|
|
31
|
+
import { DuoBarCompressionProvider } from './DuoBarCompression';
|
|
30
32
|
import { ApplicationContext, MiniAppContext } from '../../Context';
|
|
33
|
+
import { useScreenLayout } from '../../Layout';
|
|
34
|
+
import { getHeaderRowOptions } from '../../Layout/utils';
|
|
31
35
|
|
|
32
36
|
const Tab = createBottomTabNavigator();
|
|
33
37
|
const Stack = createStackNavigator();
|
|
@@ -36,7 +40,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
36
40
|
const { navigator } = useContext(ApplicationContext);
|
|
37
41
|
const context = useContext<any>(MiniAppContext);
|
|
38
42
|
const { nested, options, screen, initialParams } = route?.params;
|
|
39
|
-
const
|
|
43
|
+
const headerStyleOptions = getHeaderRowOptions(useScreenLayout());
|
|
40
44
|
|
|
41
45
|
const opacityValue = 0.3;
|
|
42
46
|
const scaleValue = 0.97;
|
|
@@ -139,11 +143,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
139
143
|
return (
|
|
140
144
|
<Animated.View style={[styles.container, tabAnimatedStyle]}>
|
|
141
145
|
<Stack.Navigator
|
|
142
|
-
screenOptions={
|
|
143
|
-
headerStyle: {
|
|
144
|
-
height: 52 + insets.top,
|
|
145
|
-
},
|
|
146
|
-
}}
|
|
146
|
+
screenOptions={headerStyleOptions}
|
|
147
147
|
>
|
|
148
148
|
<Stack.Screen
|
|
149
149
|
name="Stack"
|
|
@@ -174,11 +174,7 @@ const TabScreen: React.FC<any> = ({ route, navigation }) => {
|
|
|
174
174
|
<NavigationIndependentTree>
|
|
175
175
|
<NavigationContainer>
|
|
176
176
|
<Stack.Navigator
|
|
177
|
-
screenOptions={
|
|
178
|
-
headerStyle: {
|
|
179
|
-
height: 52 + insets.top,
|
|
180
|
-
},
|
|
181
|
-
}}
|
|
177
|
+
screenOptions={headerStyleOptions}
|
|
182
178
|
>
|
|
183
179
|
<Stack.Screen
|
|
184
180
|
name="Stack"
|
|
@@ -205,8 +201,10 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
205
201
|
navigation,
|
|
206
202
|
initialRouteName,
|
|
207
203
|
floatingButton,
|
|
204
|
+
compressionBehavior,
|
|
208
205
|
}) => {
|
|
209
206
|
const { theme, navigator } = useContext(ApplicationContext);
|
|
207
|
+
const { isIPhoneDuo } = useScreenLayout();
|
|
210
208
|
const dimensions = useWindowDimensions();
|
|
211
209
|
const insets = useSafeAreaInsets();
|
|
212
210
|
const initialIndex = tabs.findIndex(i => i.name === initialRouteName);
|
|
@@ -279,139 +277,152 @@ const BottomTab: React.FC<BottomTabProps> = ({
|
|
|
279
277
|
},
|
|
280
278
|
};
|
|
281
279
|
|
|
280
|
+
const isIOS = Platform.OS === 'ios';
|
|
281
|
+
const bottomInset = isIOS ? Math.min(insets.bottom, 21) : insets.bottom;
|
|
282
|
+
|
|
282
283
|
return (
|
|
283
|
-
<
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
headerBackground: undefined,
|
|
288
|
-
tabBarLabelPosition: 'below-icon',
|
|
289
|
-
tabBarBackground: () => (
|
|
290
|
-
<View
|
|
291
|
-
style={[
|
|
292
|
-
styles.container,
|
|
293
|
-
{
|
|
294
|
-
backgroundColor: theme.colors.background.surface,
|
|
295
|
-
},
|
|
296
|
-
]}
|
|
297
|
-
>
|
|
298
|
-
<Animated.View style={[styles.indicatorContainer, indicatorStyle]}>
|
|
299
|
-
<View
|
|
300
|
-
style={[
|
|
301
|
-
styles.indicator,
|
|
302
|
-
{ backgroundColor: theme.colors.primary },
|
|
303
|
-
]}
|
|
304
|
-
/>
|
|
305
|
-
</Animated.View>
|
|
306
|
-
</View>
|
|
307
|
-
),
|
|
308
|
-
}}
|
|
309
|
-
tabBar={props => (
|
|
310
|
-
<BottomTabBar
|
|
311
|
-
{...props}
|
|
312
|
-
floatingButton={floatingButton}
|
|
313
|
-
inactiveTintColor={theme.colors.text.hint}
|
|
314
|
-
activeTintColor={theme.colors.primary}
|
|
315
|
-
style={[
|
|
316
|
-
{
|
|
317
|
-
height:
|
|
318
|
-
64 +
|
|
319
|
-
(Platform.OS === 'ios'
|
|
320
|
-
? Math.min(insets.bottom, 21)
|
|
321
|
-
: insets.bottom),
|
|
322
|
-
},
|
|
323
|
-
]}
|
|
324
|
-
/>
|
|
325
|
-
)}
|
|
326
|
-
initialRouteName={initialRouteName}
|
|
284
|
+
<DuoBarCompressionProvider
|
|
285
|
+
behavior={compressionBehavior}
|
|
286
|
+
tabCount={tabs.length}
|
|
287
|
+
hasQr={!!floatingButton}
|
|
327
288
|
>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
289
|
+
<Tab.Navigator
|
|
290
|
+
screenOptions={{
|
|
291
|
+
headerShown: false,
|
|
292
|
+
headerTransparent: true,
|
|
293
|
+
headerBackground: undefined,
|
|
294
|
+
tabBarLabelPosition: 'below-icon',
|
|
295
|
+
tabBarBackground: () => (
|
|
296
|
+
<View
|
|
297
|
+
style={[
|
|
298
|
+
styles.container,
|
|
299
|
+
{
|
|
300
|
+
backgroundColor: theme.colors.background.surface,
|
|
301
|
+
},
|
|
302
|
+
]}
|
|
303
|
+
>
|
|
304
|
+
<Animated.View
|
|
305
|
+
style={[styles.indicatorContainer, indicatorStyle]}
|
|
306
|
+
>
|
|
307
|
+
<View
|
|
308
|
+
style={[
|
|
309
|
+
styles.indicator,
|
|
310
|
+
{ backgroundColor: theme.colors.primary },
|
|
311
|
+
]}
|
|
312
|
+
/>
|
|
313
|
+
</Animated.View>
|
|
314
|
+
</View>
|
|
315
|
+
),
|
|
316
|
+
}}
|
|
317
|
+
tabBar={props =>
|
|
318
|
+
isIPhoneDuo ? (
|
|
319
|
+
<DuoTabBar
|
|
320
|
+
{...props}
|
|
321
|
+
floatingButton={floatingButton}
|
|
322
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
323
|
+
activeTintColor={theme.colors.primary}
|
|
324
|
+
/>
|
|
325
|
+
) : (
|
|
326
|
+
<BottomTabBar
|
|
327
|
+
{...props}
|
|
328
|
+
floatingButton={floatingButton}
|
|
329
|
+
inactiveTintColor={theme.colors.text.hint}
|
|
330
|
+
activeTintColor={theme.colors.primary}
|
|
331
|
+
style={[{ height: 64 + bottomInset }]}
|
|
332
|
+
/>
|
|
333
|
+
)
|
|
336
334
|
}
|
|
335
|
+
initialRouteName={initialRouteName}
|
|
336
|
+
>
|
|
337
|
+
{tabs.map((item, index) => {
|
|
338
|
+
const isNum = !isNaN(parseInt(`${item?.badgeLabel}`, 10));
|
|
339
|
+
const isDot =
|
|
340
|
+
typeof item?.badgeLabel === 'string' && !item?.badgeLabel;
|
|
341
|
+
const isNumZero = isNum && Number(item.badgeLabel) === 0;
|
|
337
342
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
width?: number;
|
|
343
|
-
} = {
|
|
344
|
-
marginRight: -8,
|
|
345
|
-
top: -4,
|
|
346
|
-
backgroundColor: Colors.orange_03,
|
|
347
|
-
};
|
|
343
|
+
let badgeLabel = item?.badgeLabel;
|
|
344
|
+
if (isNumZero) {
|
|
345
|
+
badgeLabel = undefined;
|
|
346
|
+
}
|
|
348
347
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
348
|
+
let badgeStyle: {
|
|
349
|
+
marginRight: number;
|
|
350
|
+
top: number;
|
|
351
|
+
backgroundColor: string;
|
|
352
|
+
width?: number;
|
|
353
|
+
} = {
|
|
354
|
+
marginRight: -8,
|
|
352
355
|
top: -4,
|
|
353
|
-
backgroundColor: Colors.red_03,
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
if (isDot) {
|
|
357
|
-
badgeStyle = {
|
|
358
|
-
marginRight: -2,
|
|
359
|
-
top: -2,
|
|
360
356
|
backgroundColor: Colors.orange_03,
|
|
361
|
-
width: 12,
|
|
362
357
|
};
|
|
363
|
-
}
|
|
364
358
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
alignSelf: 'center',
|
|
402
|
-
fontFamily: exportFontFamily('bold'),
|
|
359
|
+
if (isNum) {
|
|
360
|
+
badgeStyle = {
|
|
361
|
+
marginRight: -4,
|
|
362
|
+
top: -4,
|
|
363
|
+
backgroundColor: Colors.red_03,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
if (isDot) {
|
|
367
|
+
badgeStyle = {
|
|
368
|
+
marginRight: -2,
|
|
369
|
+
top: -2,
|
|
370
|
+
backgroundColor: Colors.orange_03,
|
|
371
|
+
width: 12,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<Tab.Screen
|
|
377
|
+
key={`BottomTab-${item.name}`}
|
|
378
|
+
name={item.name}
|
|
379
|
+
component={TabScreen}
|
|
380
|
+
initialParams={{ ...item, nested, index }}
|
|
381
|
+
listeners={handler}
|
|
382
|
+
options={{
|
|
383
|
+
tabBarLabel: ({ focused, color }) => {
|
|
384
|
+
return (
|
|
385
|
+
<Text
|
|
386
|
+
numberOfLines={1}
|
|
387
|
+
typography={
|
|
388
|
+
focused ? 'label_xs_medium' : 'description_xs_regular'
|
|
389
|
+
}
|
|
390
|
+
color={color}
|
|
391
|
+
>
|
|
392
|
+
{item.label}
|
|
393
|
+
</Text>
|
|
394
|
+
);
|
|
403
395
|
},
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
396
|
+
title: item.label,
|
|
397
|
+
tabBarStyle: [
|
|
398
|
+
{
|
|
399
|
+
height: 64,
|
|
400
|
+
},
|
|
401
|
+
styles.tabStyle,
|
|
402
|
+
],
|
|
403
|
+
tabBarBadge: badgeLabel,
|
|
404
|
+
tabBarBadgeStyle: [
|
|
405
|
+
{
|
|
406
|
+
borderColor: 'white',
|
|
407
|
+
borderWidth: 2,
|
|
408
|
+
fontSize: 10,
|
|
409
|
+
lineHeight: 14,
|
|
410
|
+
fontWeight: 'bold',
|
|
411
|
+
alignSelf: 'center',
|
|
412
|
+
fontFamily: exportFontFamily('bold'),
|
|
413
|
+
},
|
|
414
|
+
badgeStyle,
|
|
415
|
+
],
|
|
416
|
+
tabBarIcon: ({ color }) => (
|
|
417
|
+
<Icon source={item.icon} color={color} size={28} />
|
|
418
|
+
),
|
|
419
|
+
...item,
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
);
|
|
423
|
+
})}
|
|
424
|
+
</Tab.Navigator>
|
|
425
|
+
</DuoBarCompressionProvider>
|
|
415
426
|
);
|
|
416
427
|
};
|
|
417
428
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
import Animated, {
|
|
5
|
+
Extrapolation,
|
|
6
|
+
interpolate,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { ApplicationContext } from '../../Context';
|
|
10
|
+
import { Shadow } from '../../Consts';
|
|
11
|
+
import { Image } from '../../Image';
|
|
12
|
+
import { AnimatedCompatValue, useNormalizedSharedValue } from '../utils';
|
|
13
|
+
|
|
14
|
+
// extended headers run their gradient this far below the header, as off Duo
|
|
15
|
+
const EXTENDED_GRADIENT_EXTRA = 52;
|
|
16
|
+
// scroll distance over which the extended gradient collapses to the header
|
|
17
|
+
const EXTENDED_COLLAPSE_DISTANCE = 100;
|
|
18
|
+
// scroll distance over which the bar material fades in
|
|
19
|
+
const FADE_DISTANCE = 16;
|
|
20
|
+
const TRANSLUCENT_ALPHA = 'CC';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* iPhone Duo header background: translucent bar running edge to edge, behind the
|
|
24
|
+
* status bar column too, shown once content scrolls under it
|
|
25
|
+
*/
|
|
26
|
+
const DuoGlassBackground: React.FC<{
|
|
27
|
+
animatedValue?: AnimatedCompatValue;
|
|
28
|
+
// scroll offset where the bar starts to show, e.g. below an animated banner
|
|
29
|
+
fadeStart?: number;
|
|
30
|
+
}> = ({ animatedValue, fadeStart = 0 }) => {
|
|
31
|
+
const { theme } = useContext(ApplicationContext);
|
|
32
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
33
|
+
|
|
34
|
+
const fadeStyle = useAnimatedStyle(() => ({
|
|
35
|
+
opacity: interpolate(
|
|
36
|
+
sharedValue.value,
|
|
37
|
+
[fadeStart, fadeStart + FADE_DISTANCE],
|
|
38
|
+
[0, 1],
|
|
39
|
+
Extrapolation.CLAMP,
|
|
40
|
+
),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View pointerEvents={'none'} style={StyleSheet.absoluteFill}>
|
|
45
|
+
<Animated.View
|
|
46
|
+
style={[
|
|
47
|
+
StyleSheet.absoluteFill,
|
|
48
|
+
styles.translucentBar,
|
|
49
|
+
{
|
|
50
|
+
backgroundColor:
|
|
51
|
+
theme.colors.background.surface + TRANSLUCENT_ALPHA,
|
|
52
|
+
borderBottomColor: theme.colors.border.default,
|
|
53
|
+
},
|
|
54
|
+
fadeStyle,
|
|
55
|
+
]}
|
|
56
|
+
/>
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* pink header gradient drawn under the content, fading out on scroll as main's extended
|
|
63
|
+
* header does: header-high for default headers, 52 taller (collapsing) for extended
|
|
64
|
+
*/
|
|
65
|
+
const DuoScrollGradient: React.FC<{
|
|
66
|
+
animatedValue?: AnimatedCompatValue;
|
|
67
|
+
gradientColor?: string;
|
|
68
|
+
headerBackground?: string;
|
|
69
|
+
heightHeader: number;
|
|
70
|
+
extended?: boolean;
|
|
71
|
+
// quick return header: the gradient leaves with the content instead of fading
|
|
72
|
+
followScroll?: boolean;
|
|
73
|
+
}> = ({
|
|
74
|
+
animatedValue,
|
|
75
|
+
gradientColor,
|
|
76
|
+
headerBackground,
|
|
77
|
+
heightHeader,
|
|
78
|
+
extended,
|
|
79
|
+
followScroll,
|
|
80
|
+
}) => {
|
|
81
|
+
const { theme } = useContext(ApplicationContext);
|
|
82
|
+
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
83
|
+
const color = gradientColor ?? theme.colors.gradient;
|
|
84
|
+
const image = headerBackground ?? theme.assets?.headerBackground;
|
|
85
|
+
|
|
86
|
+
const scrollStyle = useAnimatedStyle(() => {
|
|
87
|
+
if (followScroll) {
|
|
88
|
+
return {
|
|
89
|
+
height: heightHeader + (extended ? EXTENDED_GRADIENT_EXTRA : 0),
|
|
90
|
+
transform: [{ translateY: -Math.max(sharedValue.value, 0) }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let height = heightHeader;
|
|
94
|
+
if (extended) {
|
|
95
|
+
height = interpolate(
|
|
96
|
+
sharedValue.value,
|
|
97
|
+
[0, EXTENDED_COLLAPSE_DISTANCE],
|
|
98
|
+
[heightHeader + EXTENDED_GRADIENT_EXTRA, heightHeader],
|
|
99
|
+
Extrapolation.CLAMP,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
height,
|
|
104
|
+
opacity: interpolate(
|
|
105
|
+
sharedValue.value,
|
|
106
|
+
[0, EXTENDED_GRADIENT_EXTRA],
|
|
107
|
+
[1, 0],
|
|
108
|
+
Extrapolation.CLAMP,
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!color) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Animated.View
|
|
119
|
+
pointerEvents={'none'}
|
|
120
|
+
style={[styles.gradient, scrollStyle]}
|
|
121
|
+
>
|
|
122
|
+
<LinearGradient
|
|
123
|
+
colors={[color, color + '00']}
|
|
124
|
+
style={StyleSheet.absoluteFill}
|
|
125
|
+
>
|
|
126
|
+
{!!image && (
|
|
127
|
+
<Image
|
|
128
|
+
style={styles.gradientFill}
|
|
129
|
+
source={{ uri: image }}
|
|
130
|
+
loading={false}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</LinearGradient>
|
|
134
|
+
</Animated.View>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const styles = StyleSheet.create({
|
|
139
|
+
translucentBar: {
|
|
140
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
141
|
+
...Shadow.Light,
|
|
142
|
+
},
|
|
143
|
+
gradient: {
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
top: 0,
|
|
146
|
+
left: 0,
|
|
147
|
+
right: 0,
|
|
148
|
+
overflow: 'hidden',
|
|
149
|
+
},
|
|
150
|
+
// banner image keeps its 375:154 shape, the gradient box clips it
|
|
151
|
+
gradientFill: {
|
|
152
|
+
width: '100%',
|
|
153
|
+
position: 'absolute',
|
|
154
|
+
aspectRatio: 375 / 154,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export { DuoGlassBackground, DuoScrollGradient };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import { StackHeaderProps, Header } from '@react-navigation/stack';
|
|
3
|
+
import Animated, {
|
|
4
|
+
SharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
} from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
// how far the header is pushed above the top edge
|
|
9
|
+
const DuoHeaderOffsetContext = createContext<SharedValue<number> | undefined>(
|
|
10
|
+
undefined,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* iPhone Duo header that leaves with the content and comes back when scrolling back
|
|
15
|
+
*/
|
|
16
|
+
const DuoQuickReturnHeader: React.FC<
|
|
17
|
+
StackHeaderProps & { offset: SharedValue<number> }
|
|
18
|
+
> = ({ offset, ...props }) => {
|
|
19
|
+
const style = useAnimatedStyle(() => ({
|
|
20
|
+
transform: [{ translateY: -offset.value }],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<DuoHeaderOffsetContext.Provider value={offset}>
|
|
25
|
+
<Animated.View pointerEvents={'box-none'} style={style}>
|
|
26
|
+
<Header {...props} />
|
|
27
|
+
</Animated.View>
|
|
28
|
+
</DuoHeaderOffsetContext.Provider>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* side bar controls render inside the header; this cancels its offset so they stay put
|
|
34
|
+
*/
|
|
35
|
+
const useSideBarCounterStyle = () => {
|
|
36
|
+
const offset = useContext(DuoHeaderOffsetContext);
|
|
37
|
+
return useAnimatedStyle(() => ({
|
|
38
|
+
transform: [{ translateY: offset ? offset.value : 0 }],
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export { DuoQuickReturnHeader, useSideBarCounterStyle };
|
|
@@ -28,9 +28,15 @@ const HeaderAnimated: React.FC<HeaderAnimatedProps> = ({
|
|
|
28
28
|
}) => {
|
|
29
29
|
const sharedValue = useNormalizedSharedValue(animatedValue);
|
|
30
30
|
const animatedStyle = useAnimatedStyle(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
let scale = 1;
|
|
32
|
+
if (useScale) {
|
|
33
|
+
scale = interpolate(
|
|
34
|
+
sharedValue.value,
|
|
35
|
+
[-300, 0, 300],
|
|
36
|
+
[4, 1, 1],
|
|
37
|
+
Extrapolation.CLAMP,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
34
40
|
const opacity = interpolate(
|
|
35
41
|
sharedValue.value,
|
|
36
42
|
[0, 150, 300],
|