Skip to content

Commit c2278b6

Browse files
refactor: move Surface iOS logic in separate component and memoize styles
1 parent bc8c001 commit c2278b6

File tree

1 file changed

+140
-139
lines changed

1 file changed

+140
-139
lines changed

src/components/Surface.tsx

Lines changed: 140 additions & 139 deletions
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,135 @@ 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+
if (
149+
process.env.NODE_ENV !== 'production' &&
150+
filteredStyle.overflow === 'hidden' &&
151+
elevation !== 0
152+
) {
153+
console.warn(
154+
'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.'
155+
);
156+
}
157+
158+
const outerLayerViewStyles = {
159+
...getStyleForShadowLayer(elevation, 0),
160+
...marginStyle,
161+
position,
162+
alignSelf,
163+
top,
164+
right,
165+
bottom,
166+
left,
167+
start,
168+
end,
169+
flex,
170+
width,
171+
height,
172+
transform,
173+
opacity,
174+
};
175+
176+
const innerLayerViewStyles = {
177+
...getStyleForShadowLayer(elevation, 1),
178+
...filteredStyle,
179+
flex: height ? 1 : undefined,
180+
backgroundColor: backgroundColorStyle || backgroundColor,
181+
};
182+
183+
return [outerLayerViewStyles, innerLayerViewStyles];
184+
}, [style, elevation, backgroundColor]);
185+
186+
return (
187+
<Animated.View
188+
ref={ref}
189+
style={outerLayerViewStyles}
190+
testID={`${testID}-outer-layer`}
191+
>
192+
<Animated.View {...props} style={innerLayerViewStyles} testID={testID}>
193+
{children}
194+
</Animated.View>
195+
</Animated.View>
196+
);
197+
});
198+
68199
/**
69200
* Surface is a basic container that can give depth to an element with elevation shadow.
70201
* On dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.
@@ -205,146 +336,16 @@ const Surface = forwardRef<View, Props>(
205336
);
206337
}
207338

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-
334339
return (
335-
<Animated.View
336-
ref={ref}
337-
style={[getStyleForShadowLayer(0), outerLayerViewStyles]}
338-
testID={`${testID}-outer-layer`}
340+
<SurfaceIOS
341+
{...props}
342+
elevation={elevation}
343+
backgroundColor={backgroundColor}
344+
style={style}
345+
testID={testID}
339346
>
340-
<Animated.View
341-
{...props}
342-
style={[getStyleForShadowLayer(1), innerLayerViewStyles]}
343-
testID={testID}
344-
>
345-
{children}
346-
</Animated.View>
347-
</Animated.View>
347+
{children}
348+
</SurfaceIOS>
348349
);
349350
}
350351
);

0 commit comments

Comments
 (0)