Skip to content

Commit 65e99b2

Browse files
refactor: move Surface iOS logic in separate component and memoize styles
1 parent 0c957ba commit 65e99b2

File tree

1 file changed

+144
-139
lines changed

1 file changed

+144
-139
lines changed

src/components/Surface.tsx

Lines changed: 144 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import {
33
Animated,
44
Platform,
5+
ShadowStyleIOS,
56
StyleProp,
67
StyleSheet,
78
View,
@@ -15,6 +16,8 @@ import type { ThemeProp, MD3Elevation } from '../types';
1516
import { forwardRef } from '../utils/forwardRef';
1617
import { splitStyles } from '../utils/splitStyles';
1718

19+
type Elevation = 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
20+
1821
export type Props = React.ComponentPropsWithRef<typeof View> & {
1922
/**
2023
* Content of the `Surface`.
@@ -29,7 +32,7 @@ export type Props = React.ComponentPropsWithRef<typeof View> & {
2932
* Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.
3033
* It's no longer supported with theme version 3 and you should use `elevation` property instead.
3134
*/
32-
elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
35+
elevation?: Elevation;
3336
/**
3437
* @optional
3538
*/
@@ -65,6 +68,138 @@ const MD2Surface = forwardRef<View, Props>(
6568
}
6669
);
6770

71+
const shadowColor = '#000';
72+
const iOSShadowOutputRanges = [
73+
{
74+
shadowOpacity: 0.15,
75+
height: [0, 1, 2, 4, 6, 8],
76+
shadowRadius: [0, 3, 6, 8, 10, 12],
77+
},
78+
{
79+
shadowOpacity: 0.3,
80+
height: [0, 1, 1, 1, 2, 4],
81+
shadowRadius: [0, 1, 2, 3, 3, 4],
82+
},
83+
];
84+
const inputRange = [0, 1, 2, 3, 4, 5];
85+
function getStyleForShadowLayer(
86+
elevation: Elevation,
87+
layer: 0 | 1
88+
): Animated.WithAnimatedValue<ShadowStyleIOS> {
89+
if (isAnimatedValue(elevation)) {
90+
return {
91+
shadowColor,
92+
shadowOpacity: elevation.interpolate({
93+
inputRange: [0, 1],
94+
outputRange: [0, iOSShadowOutputRanges[layer].shadowOpacity],
95+
extrapolate: 'clamp',
96+
}),
97+
shadowOffset: {
98+
width: 0,
99+
height: elevation.interpolate({
100+
inputRange,
101+
outputRange: iOSShadowOutputRanges[layer].height,
102+
}),
103+
},
104+
shadowRadius: elevation.interpolate({
105+
inputRange,
106+
outputRange: iOSShadowOutputRanges[layer].shadowRadius,
107+
}),
108+
};
109+
}
110+
111+
return {
112+
shadowColor,
113+
shadowOpacity: elevation ? iOSShadowOutputRanges[layer].shadowOpacity : 0,
114+
shadowOffset: {
115+
width: 0,
116+
height: iOSShadowOutputRanges[layer].height[elevation],
117+
},
118+
shadowRadius: iOSShadowOutputRanges[layer].shadowRadius[elevation],
119+
};
120+
}
121+
122+
const SurfaceIOS = forwardRef<
123+
View,
124+
Omit<Props, 'elevation'> & {
125+
elevation: Elevation;
126+
backgroundColor?: string | Animated.AnimatedInterpolation<string | number>;
127+
}
128+
>(({ elevation, style, backgroundColor, testID, children, ...props }, ref) => {
129+
const [outerLayerViewStyles, innerLayerViewStyles] = React.useMemo(() => {
130+
const {
131+
position,
132+
alignSelf,
133+
top,
134+
left,
135+
right,
136+
bottom,
137+
start,
138+
end,
139+
flex,
140+
backgroundColor: backgroundColorStyle,
141+
width,
142+
height,
143+
transform,
144+
opacity,
145+
...restStyle
146+
} = (StyleSheet.flatten(style) || {}) as ViewStyle;
147+
148+
const [filteredStyles, marginStyles] = splitStyles(restStyle, (style) =>
149+
style.startsWith('margin')
150+
);
151+
152+
if (
153+
process.env.NODE_ENV !== 'production' &&
154+
filteredStyles.overflow === 'hidden' &&
155+
elevation !== 0
156+
) {
157+
console.warn(
158+
'When setting overflow to hidden on Surface the shadow will not be displayed correctly. Wrap the content of your component in a separate View with the overflow style.'
159+
);
160+
}
161+
162+
const outerLayerViewStyles = {
163+
...getStyleForShadowLayer(elevation, 0),
164+
...marginStyles,
165+
position,
166+
alignSelf,
167+
top,
168+
right,
169+
bottom,
170+
left,
171+
start,
172+
end,
173+
flex,
174+
width,
175+
height,
176+
transform,
177+
opacity,
178+
};
179+
180+
const innerLayerViewStyles = {
181+
...getStyleForShadowLayer(elevation, 1),
182+
...filteredStyles,
183+
flex: height ? 1 : undefined,
184+
backgroundColor: backgroundColorStyle || backgroundColor,
185+
};
186+
187+
return [outerLayerViewStyles, innerLayerViewStyles];
188+
}, [style, elevation, backgroundColor]);
189+
190+
return (
191+
<Animated.View
192+
ref={ref}
193+
style={outerLayerViewStyles}
194+
testID={`${testID}-outer-layer`}
195+
>
196+
<Animated.View {...props} style={innerLayerViewStyles} testID={testID}>
197+
{children}
198+
</Animated.View>
199+
</Animated.View>
200+
);
201+
});
202+
68203
/**
69204
* Surface is a basic container that can give depth to an element with elevation shadow.
70205
* On dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.
@@ -205,146 +340,16 @@ const Surface = forwardRef<View, Props>(
205340
);
206341
}
207342

208-
const iOSShadowOutputRanges = [
209-
{
210-
shadowOpacity: 0.15,
211-
height: [0, 1, 2, 4, 6, 8],
212-
shadowRadius: [0, 3, 6, 8, 10, 12],
213-
},
214-
{
215-
shadowOpacity: 0.3,
216-
height: [0, 1, 1, 1, 2, 4],
217-
shadowRadius: [0, 1, 2, 3, 3, 4],
218-
},
219-
];
220-
221-
const shadowColor = '#000';
222-
223-
const {
224-
position,
225-
alignSelf,
226-
top,
227-
left,
228-
right,
229-
bottom,
230-
start,
231-
end,
232-
flex,
233-
backgroundColor: backgroundColorStyle,
234-
width,
235-
height,
236-
transform,
237-
opacity,
238-
...restStyle
239-
} = (StyleSheet.flatten(style) || {}) as ViewStyle;
240-
241-
const [filteredStyle, marginStyle] = splitStyles(restStyle, (style) =>
242-
style.startsWith('margin')
243-
);
244-
245-
if (
246-
process.env.NODE_ENV !== 'production' &&
247-
filteredStyle.overflow === 'hidden' &&
248-
elevation !== 0
249-
) {
250-
console.warn(
251-
'When setting overflow to hidden on Surface the shadow will not be displayed correctly. Wrap the content of your component in a separate View with the overflow style.'
252-
);
253-
}
254-
255-
const innerLayerViewStyles = [
256-
filteredStyle,
257-
{
258-
flex: height ? 1 : undefined,
259-
backgroundColor: backgroundColorStyle || backgroundColor,
260-
},
261-
];
262-
263-
const outerLayerViewStyles = {
264-
position,
265-
alignSelf,
266-
top,
267-
right,
268-
bottom,
269-
left,
270-
start,
271-
end,
272-
flex,
273-
width,
274-
height,
275-
transform,
276-
opacity,
277-
...marginStyle,
278-
};
279-
280-
if (isAnimatedValue(elevation)) {
281-
const inputRange = [0, 1, 2, 3, 4, 5];
282-
283-
const getStyleForAnimatedShadowLayer = (layer: 0 | 1) => {
284-
return {
285-
shadowColor,
286-
shadowOpacity: elevation.interpolate({
287-
inputRange: [0, 1],
288-
outputRange: [0, iOSShadowOutputRanges[layer].shadowOpacity],
289-
extrapolate: 'clamp',
290-
}),
291-
shadowOffset: {
292-
width: 0,
293-
height: elevation.interpolate({
294-
inputRange,
295-
outputRange: iOSShadowOutputRanges[layer].height,
296-
}),
297-
},
298-
shadowRadius: elevation.interpolate({
299-
inputRange,
300-
outputRange: iOSShadowOutputRanges[layer].shadowRadius,
301-
}),
302-
};
303-
};
304-
305-
return (
306-
<Animated.View
307-
style={[getStyleForAnimatedShadowLayer(0), outerLayerViewStyles]}
308-
testID={`${testID}-outer-layer`}
309-
>
310-
<Animated.View
311-
style={[getStyleForAnimatedShadowLayer(1), innerLayerViewStyles]}
312-
testID={testID}
313-
>
314-
{children}
315-
</Animated.View>
316-
</Animated.View>
317-
);
318-
}
319-
320-
const getStyleForShadowLayer = (layer: 0 | 1) => {
321-
return {
322-
shadowColor,
323-
shadowOpacity: elevation
324-
? iOSShadowOutputRanges[layer].shadowOpacity
325-
: 0,
326-
shadowOffset: {
327-
width: 0,
328-
height: iOSShadowOutputRanges[layer].height[elevation],
329-
},
330-
shadowRadius: iOSShadowOutputRanges[layer].shadowRadius[elevation],
331-
};
332-
};
333-
334343
return (
335-
<Animated.View
336-
ref={ref}
337-
style={[getStyleForShadowLayer(0), outerLayerViewStyles]}
338-
testID={`${testID}-outer-layer`}
344+
<SurfaceIOS
345+
{...props}
346+
elevation={elevation}
347+
backgroundColor={backgroundColor}
348+
style={style}
349+
testID={testID}
339350
>
340-
<Animated.View
341-
{...props}
342-
style={[getStyleForShadowLayer(1), innerLayerViewStyles]}
343-
testID={testID}
344-
>
345-
{children}
346-
</Animated.View>
347-
</Animated.View>
351+
{children}
352+
</SurfaceIOS>
348353
);
349354
}
350355
);

0 commit comments

Comments
 (0)