Skip to content

Commit 46c6b21

Browse files
Merge branch 'app-bars' into 'main'
App bars See merge request react-native/react-native-material-components!16
2 parents b954525 + e87a5be commit 46c6b21

39 files changed

+806
-63
lines changed

README.md

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,137 @@ export const AppBodyLargeText: React.FC<PropsWithChildren> = ({children}) => {
212212
![linear activity indicator gif](https://ik.imagekit.io/Computools/rn-material-components/linear-indicator-gif.gif?updatedAt=1705066319092)
213213
</details>
214214
</details>
215-
<details><summary>Badge</summary>
215+
<details><summary>App Bars</summary>
216+
<br />
217+
<details><summary>Bottom App Bar</summary>
216218
<br />
217219

218220
**Properties**
219221

222+
| name | description | type | default |
223+
| ------ | ------ | ------ | ----|
224+
| iconButtons | required | IconButtonProps[] | - |
225+
| scrollDirection | UP or DOWN | sharedValue<ScrollDirection> | UP |
226+
| FabIcon | Pass an icon to show FAB | React.FC | - |
227+
| fabLabel | - | string | - |
228+
| animationDelay | - | number | 80 |
229+
| animationDumping | - | number | 20 |
230+
| onFabPress | - | () => void | - |
231+
232+
To enable animation on scroll use ScrollView from Animated, create a shared value with a ScrollDirection value, scrollContext with a number value and manage them onScroll.
233+
234+
See the example:
235+
```
236+
export const MyComponent: React.FC = () => {
237+
const scrollDirection = useSharedValue(ScrollDirection.UP);
238+
const scrollContext = useSharedValue(0);
239+
240+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
241+
const currentOffsetY = e.nativeEvent.contentOffset.y;
242+
243+
if (currentOffsetY <= 0 || currentOffsetY < scrollContext.value) {
244+
scrollDirection.value = ScrollDirection.UP;
245+
} else if (currentOffsetY >= scrollContext.value) {
246+
scrollDirection.value = ScrollDirection.DOWN;
247+
}
248+
249+
scrollContext.value = currentOffsetY;
250+
};
251+
252+
return (
253+
<>
254+
<Animated.ScrollView onScroll={onScroll}>
255+
<!-- scrollview content -->
256+
</Animated.ScrollView>
257+
<BottomAppBar scrollDirection={scrollDirection} />
258+
</>
259+
)
260+
}
261+
262+
```
263+
264+
![bottom app bar](https://ik.imagekit.io/Computools/rn-material-components/bottom-app-bar.gif?updatedAt=1734086950022)
265+
266+
</details>
267+
<details><summary>Top App Bars</summary>
268+
<br />
269+
270+
To enable animation on scroll use ScrollView from Animated, create a shared value with ScrollStatus (0 if top is reached, 1 if target offset reached) value and manage it onScroll.
271+
272+
See the example:
273+
```
274+
export const MyComponent: React.FC = () => {
275+
const scrollStatus = useSharedValue(0);
276+
277+
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
278+
if (e.nativeEvent.contentOffset.y > 10) { // 10 is offset treashold when top app bar changes background color
279+
scrollStatus.value = 1;
280+
} else if (e.nativeEvent.contentOffset.y <= 10) {
281+
scrollStatus.value = 0;
282+
}
283+
};
284+
285+
return (
286+
<>
287+
<Animated.ScrollView onScroll={onScroll}>
288+
<!-- scrollview content -->
289+
</Animated.ScrollView>
290+
<TopAppBar scrollStatus={scrollStatus} />
291+
</>
292+
)
293+
}
294+
```
295+
296+
![animated top app bar](https://ik.imagekit.io/Computools/rn-material-components/animated-top-app-bar.gif?updatedAt=1734088599114)
297+
298+
<details><summary>Center Aligned Top App Bar</summary>
299+
<br />
300+
301+
**Properties**
302+
303+
| name | description | type | default |
304+
| ------ | ------ | ------ | ----|
305+
| title | required | string | - |
306+
| StartIcon | - | React.FC | - |
307+
| EndIcon | - | React.FC | - |
308+
| scrollStatus | 1 - scrolled down (backgoround color will changed); 0 - non-scrolled, top is reached | SharedValue<number> | - |
309+
| iconProps | - | T | - |
310+
| titleStyle | - | TextStyle | - |
311+
| defaultColor | - | ColorValue | - |
312+
| scrolledColor | - | ColorValue | - |
313+
| animationDuration | - | number | - |
314+
315+
![center aligned top app bar](https://ik.imagekit.io/Computools/rn-material-components/center_aligned_top_app_bar.png?updatedAt=1734088249862)
316+
</details>
317+
<details><summary>Top App Bar</summary>
318+
<br />
319+
320+
**Properties**
321+
322+
| name | description | type | default |
323+
| ------ | ------ | ------ | ----|
324+
| title | required | string | - |
325+
| size | SMALL, MEDIUM, LARGE | TopAppBarSize | - |
326+
| StartIcon | - | React.FC | - |
327+
| actions | - | IconButtonProps<T>[] | - |
328+
| scrollStatus | 1 - scrolled down (backgoround color will changed); 0 - non-scrolled, top is reached | SharedValue<number> | - |
329+
| iconProps | - | T | - |
330+
| titleStyle | - | TextStyle | - |
331+
| defaultColor | - | ColorValue | - |
332+
| scrolledColor | - | ColorValue | - |
333+
| animationDuration | - | number | - |
334+
335+
![small top app bar](https://ik.imagekit.io/Computools/rn-material-components/small_top_app_bar.png?updatedAt=1734088346321)
336+
![medium top app bar](https://ik.imagekit.io/Computools/rn-material-components/medium_top_app_bar.png?updatedAt=1734088346249)
337+
![large top app bar](https://ik.imagekit.io/Computools/rn-material-components/large_top_app_bar.png?updatedAt=1734088346230)
338+
339+
</details>
340+
</details>
341+
</details>
342+
<details><summary>Badge</summary>
343+
</br>
344+
345+
**Properties**
220346
| name | description | type | default |
221347
| ------ | ------ | ------ | ----|
222348
| value | required | string | - |

example/ios/Podfile.lock

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -433,35 +433,10 @@ PODS:
433433
- RNGestureHandler (2.17.1):
434434
- RCT-Folly (= 2021.07.22.00)
435435
- React-Core
436-
- RNReanimated (3.5.1):
437-
- DoubleConversion
438-
- FBLazyVector
439-
- glog
440-
- hermes-engine
441-
- RCT-Folly
442-
- RCTRequired
443-
- RCTTypeSafety
444-
- React-callinvoker
436+
- RNReanimated (3.6.1):
437+
- RCT-Folly (= 2021.07.22.00)
445438
- React-Core
446-
- React-Core/DevSupport
447-
- React-Core/RCTWebSocket
448-
- React-CoreModules
449-
- React-cxxreact
450-
- React-hermes
451-
- React-jsi
452-
- React-jsiexecutor
453-
- React-jsinspector
454-
- React-RCTActionSheet
455-
- React-RCTAnimation
456-
- React-RCTAppDelegate
457-
- React-RCTBlob
458-
- React-RCTImage
459-
- React-RCTLinking
460-
- React-RCTNetwork
461-
- React-RCTSettings
462-
- React-RCTText
463439
- ReactCommon/turbomodule/core
464-
- Yoga
465440
- RNSVG (15.3.0):
466441
- React-Core
467442
- SocketRocket (0.6.1)
@@ -656,7 +631,7 @@ SPEC CHECKSUMS:
656631
React-utils: b79f2411931f9d3ea5781404dcbb2fa8a837e13a
657632
ReactCommon: 4b2bdcb50a3543e1c2b2849ad44533686610826d
658633
RNGestureHandler: bbb8e17b10afdafc5cc5746e8f60772236e7e2ca
659-
RNReanimated: 99aa8c96151abbc2d7e737a56ec62aca709f0c92
634+
RNReanimated: fdbaa9c964bbab7fac50c90862b6cc5f041679b9
660635
RNSVG: 7c3f3fac9de6d67eee5525a8bafffafaaf022991
661636
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
662637
Yoga: 3efc43e0d48686ce2e8c60f99d4e6bd349aff981

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"react": "18.2.0",
1313
"react-native": "0.72.4",
1414
"react-native-gesture-handler": "2.17.1",
15-
"react-native-reanimated": "3.5.1",
15+
"react-native-reanimated": "3.6.1",
1616
"react-native-safe-area-context": "4.10.7",
1717
"react-native-svg": "15.3.0"
1818
},

example/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4804,10 +4804,10 @@ react-native-gesture-handler@2.17.1:
48044804
invariant "^2.2.4"
48054805
prop-types "^15.7.2"
48064806

4807-
react-native-reanimated@3.5.1:
4808-
version "3.5.1"
4809-
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.5.1.tgz#72b2f84b522aad3aabcbc7c9fc2d718b82226339"
4810-
integrity sha512-ZBTOKibTn1IR5IaoQkpPCibuao5SjXR6Pzx+KHM50wrvBnL1PecsnQjmL2VEj8hbwshrzgRGgGt1XM82DKrykw==
4807+
react-native-reanimated@3.6.1:
4808+
version "3.6.1"
4809+
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-3.6.1.tgz#5add41efafac6d0befd9786e752e7f26dbe903b7"
4810+
integrity sha512-F4vG9Yf9PKmE3GaWtVGUpzj3SM6YY2cx1yRHCwiMd1uY7W0gU017LfcVUorboJnj0y5QZqEriEK1Usq2Y8YZqg==
48114811
dependencies:
48124812
"@babel/plugin-transform-object-assign" "^7.16.7"
48134813
"@babel/preset-typescript" "^7.16.7"

package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@
8282
"peerDependencies": {
8383
"react": "*",
8484
"react-native": "*",
85-
"react-native-reanimated": ">=3.0.0",
85+
"react-native-reanimated": ">=3.6.1",
8686
"react-native-gesture-handler": ">=2.14.1",
87-
"react-native-safe-area-context": ">=4.10.7"
87+
"react-native-safe-area-context": ">=4.10.7",
88+
"react-native-svg": ">=15.3.0"
8889
},
8990
"engines": {
9091
"node": ">= 16.0.0"
@@ -168,10 +169,10 @@
168169
]
169170
},
170171
"dependencies": {
171-
"@material/material-color-utilities": "^0.3.0",
172-
"react-native-gesture-handler": "^2.17.1",
173-
"react-native-reanimated": "^3.6.1",
174-
"react-native-safe-area-context": "^4.10.7",
175-
"react-native-svg": "^15.3.0"
172+
"@material/material-color-utilities": "0.3.0",
173+
"react-native-gesture-handler": "2.17.1",
174+
"react-native-reanimated": "3.6.1",
175+
"react-native-safe-area-context": "4.10.7",
176+
"react-native-svg": "15.3.0"
176177
}
177178
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {useSafeAreaInsets} from 'react-native-safe-area-context';
2+
import React, {useImperativeHandle, useState, forwardRef} from 'react';
3+
import {View, type ViewProps, type LayoutChangeEvent} from 'react-native';
4+
import Animated, {useAnimatedStyle, withSpring, type SharedValue} from 'react-native-reanimated';
5+
6+
import {useTheme} from '../../theme/useTheme.hook';
7+
import {type IconProps} from '../../icons/icon-props';
8+
import {BOTTOM_APP_BAR_PADDING_VARTICAL, styles} from './bottom-app-bar.styles';
9+
import {type IconButtonProps} from '../../buttons/icon-buttons/icon-button.types';
10+
import {FloatingActionButton, FloatingActionButtonSize} from '../../buttons/floating-action-button/FloatingActionButton.component';
11+
import {AnimatedBottomAppBarActionButton} from './animated-bottom-app-bar-action-button/AnimatedBottomAppBarActionButton.component';
12+
13+
export enum ScrollDirection {
14+
UP = 'UP',
15+
DOWN = 'DOWN',
16+
}
17+
18+
export interface BottomAppBarProps<T extends IconProps> extends ViewProps {
19+
iconButtons: IconButtonProps<T>[];
20+
21+
FabIcon?: React.FC;
22+
fabLabel?: string;
23+
scrollDirection?: SharedValue<ScrollDirection>;
24+
animationDelay?: number;
25+
animationDumping?: number;
26+
27+
onFabPress?: () => void;
28+
}
29+
30+
export interface BottomAppBarRef {
31+
getHeight: () => void;
32+
}
33+
34+
const FAB_BOTTOM_GAP = 12;
35+
36+
export const BottomAppBar = forwardRef(
37+
<T extends IconProps>(
38+
{iconButtons, scrollDirection, FabIcon, fabLabel, style, onFabPress, onLayout, animationDelay, animationDumping, ...props}: BottomAppBarProps<T>,
39+
ref: React.Ref<BottomAppBarRef>
40+
) => {
41+
const [bottomBarHeight, setBottombarHeight] = useState(0);
42+
43+
const insets = useSafeAreaInsets();
44+
const {surfaceContainer, secondaryContainer} = useTheme();
45+
46+
useImperativeHandle(ref, () => ({
47+
getHeight: () => bottomBarHeight,
48+
}));
49+
50+
const conteinerAnimatedStyle = useAnimatedStyle(() => ({
51+
transform: [{translateY: withSpring(scrollDirection?.value === ScrollDirection.DOWN ? bottomBarHeight : 0, {damping: 20})}],
52+
}));
53+
54+
const getBottomBarHeight = (e: LayoutChangeEvent) => {
55+
setBottombarHeight(e.nativeEvent.layout.height);
56+
57+
if (onLayout) {
58+
onLayout(e);
59+
}
60+
};
61+
62+
const renderIconButton = (iconButton: IconButtonProps<T>, index: number) => (
63+
<AnimatedBottomAppBarActionButton
64+
key={`${iconButton.Icon.toString()}-${index}`}
65+
buttonProps={iconButton}
66+
index={index}
67+
scrollDirection={scrollDirection}
68+
bottomBarHeight={bottomBarHeight}
69+
animationDelay={animationDelay}
70+
animationDumping={animationDumping}
71+
/>
72+
);
73+
74+
return (
75+
<>
76+
<Animated.View
77+
style={[
78+
styles.container,
79+
{backgroundColor: surfaceContainer.background, paddingBottom: insets.bottom + BOTTOM_APP_BAR_PADDING_VARTICAL},
80+
conteinerAnimatedStyle,
81+
style,
82+
]}
83+
onLayout={getBottomBarHeight}
84+
{...props}>
85+
<View style={styles.iconButtons}>{iconButtons.map(renderIconButton)}</View>
86+
</Animated.View>
87+
{FabIcon && (
88+
<FloatingActionButton
89+
Icon={FabIcon}
90+
label={fabLabel}
91+
onPress={onFabPress}
92+
size={FloatingActionButtonSize.BIG}
93+
iconProps={{color: secondaryContainer.text}}
94+
style={[styles.fab, {bottom: insets.bottom + FAB_BOTTOM_GAP}]}
95+
/>
96+
)}
97+
</>
98+
);
99+
}
100+
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React from 'react';
2+
import Animated, {FadeInDown, FadeOut, withDelay, useAnimatedStyle, withSpring, type SharedValue} from 'react-native-reanimated';
3+
4+
import {useTheme} from '../../../theme/useTheme.hook';
5+
import {type IconProps} from '../../../icons/icon-props';
6+
import {ScrollDirection} from '../BottomAppBar.component';
7+
import {type IconButtonProps} from '../../../buttons/icon-buttons/icon-button.types';
8+
import {StandartIconButton} from '../../../buttons/icon-buttons/standart-icon-button/StandardIconButton.component';
9+
10+
interface AnimatedBottomAppBarActionButtonProps<T extends IconProps> {
11+
index: number;
12+
bottomBarHeight: number;
13+
buttonProps: IconButtonProps<T>;
14+
15+
animationDelay?: number;
16+
animationDumping?: number;
17+
scrollDirection?: SharedValue<ScrollDirection>;
18+
}
19+
20+
export const AnimatedBottomAppBarActionButton = <T extends IconProps>({
21+
index,
22+
buttonProps,
23+
bottomBarHeight,
24+
scrollDirection,
25+
animationDumping = 20,
26+
animationDelay = 80,
27+
}: AnimatedBottomAppBarActionButtonProps<T>) => {
28+
const {surface} = useTheme();
29+
30+
const iconButtonAnimatedStyle = useAnimatedStyle(() => ({
31+
transform: [
32+
{
33+
translateY:
34+
scrollDirection?.value === ScrollDirection.DOWN
35+
? withSpring(bottomBarHeight, {damping: animationDumping})
36+
: withDelay(index * animationDelay, withSpring(0, {damping: animationDumping})),
37+
},
38+
],
39+
}));
40+
41+
return (
42+
<Animated.View
43+
exiting={FadeOut}
44+
entering={FadeInDown.damping(animationDumping).delay((index + 1) * animationDelay)}
45+
key={`${index}-${buttonProps.Icon.toString()}`}
46+
style={iconButtonAnimatedStyle}>
47+
<StandartIconButton iconProps={{color: surface.textVariant} as T} {...buttonProps} />
48+
</Animated.View>
49+
);
50+
};

0 commit comments

Comments
 (0)