Skip to content

Commit 485a239

Browse files
refactor: move Surface iOS logic in separate component and memoize styles
1 parent 6206c04 commit 485a239

File tree

1 file changed

+130
-129
lines changed

1 file changed

+130
-129
lines changed

src/components/Surface.tsx

+130-129
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import type { ThemeProp, MD3Elevation } from '../types';
1515
import { forwardRef } from '../utils/forwardRef';
1616
import { splitStyles } from '../utils/splitStyles';
1717

18+
type Elevation = 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
19+
1820
export type Props = React.ComponentPropsWithRef<typeof View> & {
1921
/**
2022
* Content of the `Surface`.
@@ -29,7 +31,7 @@ export type Props = React.ComponentPropsWithRef<typeof View> & {
2931
* Note: In version 2 the `elevation` prop was accepted via `style` prop i.e. `style={{ elevation: 4 }}`.
3032
* It's no longer supported with theme version 3 and you should use `elevation` property instead.
3133
*/
32-
elevation?: 0 | 1 | 2 | 3 | 4 | 5 | Animated.Value;
34+
elevation?: Elevation;
3335
/**
3436
* @optional
3537
*/
@@ -65,6 +67,125 @@ const MD2Surface = forwardRef<View, Props>(
6567
}
6668
);
6769

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

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-
const innerLayerViewStyles = [
246-
filteredStyle,
247-
{
248-
flex: height ? 1 : undefined,
249-
backgroundColor: backgroundColorStyle || backgroundColor,
250-
},
251-
];
252-
253-
const outerLayerViewStyles = {
254-
position,
255-
alignSelf,
256-
top,
257-
right,
258-
bottom,
259-
left,
260-
start,
261-
end,
262-
flex,
263-
width,
264-
height,
265-
transform,
266-
opacity,
267-
...marginStyle,
268-
};
269-
270-
if (isAnimatedValue(elevation)) {
271-
const inputRange = [0, 1, 2, 3, 4, 5];
272-
273-
const getStyleForAnimatedShadowLayer = (layer: 0 | 1) => {
274-
return {
275-
shadowColor,
276-
shadowOpacity: elevation.interpolate({
277-
inputRange: [0, 1],
278-
outputRange: [0, iOSShadowOutputRanges[layer].shadowOpacity],
279-
extrapolate: 'clamp',
280-
}),
281-
shadowOffset: {
282-
width: 0,
283-
height: elevation.interpolate({
284-
inputRange,
285-
outputRange: iOSShadowOutputRanges[layer].height,
286-
}),
287-
},
288-
shadowRadius: elevation.interpolate({
289-
inputRange,
290-
outputRange: iOSShadowOutputRanges[layer].shadowRadius,
291-
}),
292-
};
293-
};
294-
295-
return (
296-
<Animated.View
297-
style={[getStyleForAnimatedShadowLayer(0), outerLayerViewStyles]}
298-
testID={`${testID}-outer-layer`}
299-
>
300-
<Animated.View
301-
style={[getStyleForAnimatedShadowLayer(1), innerLayerViewStyles]}
302-
testID={testID}
303-
>
304-
{children}
305-
</Animated.View>
306-
</Animated.View>
307-
);
308-
}
309-
310-
const getStyleForShadowLayer = (layer: 0 | 1) => {
311-
return {
312-
shadowColor,
313-
shadowOpacity: elevation
314-
? iOSShadowOutputRanges[layer].shadowOpacity
315-
: 0,
316-
shadowOffset: {
317-
width: 0,
318-
height: iOSShadowOutputRanges[layer].height[elevation],
319-
},
320-
shadowRadius: iOSShadowOutputRanges[layer].shadowRadius[elevation],
321-
};
322-
};
323-
324329
return (
325-
<Animated.View
326-
ref={ref}
327-
style={[getStyleForShadowLayer(0), outerLayerViewStyles]}
328-
testID={`${testID}-outer-layer`}
330+
<SurfaceIOS
331+
{...props}
332+
elevation={elevation}
333+
backgroundColor={backgroundColor}
334+
style={style}
335+
testID={testID}
329336
>
330-
<Animated.View
331-
{...props}
332-
style={[getStyleForShadowLayer(1), innerLayerViewStyles]}
333-
testID={testID}
334-
>
335-
{children}
336-
</Animated.View>
337-
</Animated.View>
337+
{children}
338+
</SurfaceIOS>
338339
);
339340
}
340341
);

0 commit comments

Comments
 (0)