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