Skip to content

Commit

Permalink
feat: allow elevation to be animated
Browse files Browse the repository at this point in the history
  • Loading branch information
Drakeoon committed Mar 3, 2022
1 parent eef8391 commit f483228
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 49 deletions.
5 changes: 3 additions & 2 deletions src/components/Appbar/Appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Surface from '../Surface';
import { withTheme } from '../../core/theming';
import { black, white } from '../../styles/themes/v2/colors';
import overlay from '../../styles/overlay';
import type { Theme } from '../../types';
import type { MD3Elevation, Theme } from '../../types';

type Props = Partial<React.ComponentPropsWithRef<typeof View>> & {
/**
Expand Down Expand Up @@ -127,7 +127,8 @@ const Appbar = ({ children, dark, style, theme, ...rest }: Props) => {
}
return (
<Surface
style={[{ backgroundColor }, styles.appbar, { elevation }, restStyle]}
style={[{ backgroundColor }, styles.appbar, restStyle]}
elevation={elevation as MD3Elevation}
{...rest}
>
{shouldAddLeftSpacing ? <View style={styles.spacing} /> : null}
Expand Down
9 changes: 2 additions & 7 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,8 @@ const Button = ({
return (
<Surface
{...rest}
style={[
styles.button,
compact && styles.compact,
{ elevation: elevationRes } as ViewStyle,
buttonStyle,
style,
]}
style={[styles.button, compact && styles.compact, buttonStyle, style]}
elevation={elevationRes}
>
<TouchableRipple
borderless
Expand Down
137 changes: 99 additions & 38 deletions src/components/Surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
} from 'react-native';
import shadow from '../styles/shadow';
import { useTheme } from '../core/theming';
import overlay from '../styles/overlay';
import type { MD3Elevation, Theme } from '../types';
import overlay, { isAnimatedValue } from '../styles/overlay';
import type { MD3Colors, MD3Elevation, Theme } from '../types';

type MD2Props = React.ComponentPropsWithRef<typeof View> & {
/**
Expand All @@ -25,7 +25,7 @@ type MD2Props = React.ComponentPropsWithRef<typeof View> & {
};

type Props = MD2Props & {
elevation?: MD3Elevation;
elevation?: MD3Elevation | Animated.Value;
};

/**
Expand Down Expand Up @@ -93,6 +93,40 @@ const MD2Surface = ({ style, theme: overrideTheme, ...rest }: MD2Props) => {
);
};

const getIOSShadows = (
elevation: MD3Elevation | Animated.Value,
colors: MD3Colors
) => {
const shadows = (() => {
if (isAnimatedValue(elevation)) {
return colors.elevationShadows?.[
`level${elevation as unknown as MD3Elevation}`
]?.map((shadow) => shadow.split(' '));
}

return colors.elevationShadows?.[`level${elevation}`]?.map((shadow) =>
shadow.split(' ')
);
})();

return shadows?.map(([width, height, size, ...colorArr]: any) => {
const shadowWidth = parseInt(width?.replace('px', ''));
const shadowHeight = parseInt(height?.replace('px', ''));
const shadowRadius = parseInt(size.replace('px', ''));
const shadowColor = colorArr.join('');

return {
shadowColor,
shadowOffset: {
width: shadowWidth,
height: shadowHeight + 0.5,
},
shadowOpacity: 1,
shadowRadius: shadowRadius,
};
});
};

const Surface = ({
elevation = 1,
children,
Expand All @@ -108,8 +142,62 @@ const Surface = ({
</MD2Surface>
);

if (elevation === 0) {
return <View {...props}>{children}</View>;
}

const { colors } = theme;

const elevationStyles = [
{ elevation: 0 },
{ elevation: 3 },
{ elevation: 6 },
{ elevation: 9 },
{ elevation: 12 },
{ elevation: 15 },
];

if (isAnimatedValue(elevation)) {
const inputRange = [0, 1, 2, 3, 4, 5];

const backgroundColor = elevation.interpolate({
inputRange,
outputRange: inputRange.map((elevation) => {
return colors.elevation?.[`level${elevation as MD3Elevation}`];
}),
});

const elevationAndroid = elevation.interpolate({
inputRange,
outputRange: elevationStyles.map(({ elevation }) => elevation),
});

const sharedStyle = [
{ backgroundColor, elevation: elevationAndroid },
props.style,
];

if (Platform.OS === 'android') {
return (
<Animated.View {...props} style={sharedStyle}>
{children}
</Animated.View>
);
}

const shadowStyles = getIOSShadows(elevation, colors);

return (
<Animated.View style={shadowStyles?.[0]}>
<Animated.View style={shadowStyles?.[1]}>
<Animated.View {...props} style={sharedStyle}>
{children}
</Animated.View>
</Animated.View>
</Animated.View>
);
}

const backgroundColor = colors.elevation?.[`level${elevation}`];

const sharedStyle = [{ backgroundColor }, props.style];
Expand All @@ -122,52 +210,25 @@ const Surface = ({
);
}

const elevationStyles = [
{ elevation: 0 },
{ elevation: 3 },
{ elevation: 6 },
{ elevation: 9 },
{ elevation: 12 },
{ elevation: 15 },
];

if (Platform.OS === 'android') {
return (
<View {...props} style={[elevationStyles[elevation], ...sharedStyle]}>
<Animated.View
{...props}
style={[elevationStyles[elevation], ...sharedStyle]}
>
{children}
</View>
</Animated.View>
);
}

const shadows = colors.elevationShadows?.[`level${elevation}`]?.map(
(shadow) => shadow.split(' ')
);

const shadowStyles = shadows?.map(
([width, height, size, ...colorArr]: any) => {
const shadowWidth = parseInt(width?.replace('px', ''));
const shadowHeight = parseInt(height?.replace('px', ''));
const shadowRadius = parseInt(size.replace('px', ''));
const shadowColor = colorArr.join('');

return {
shadowColor,
shadowOffset: {
width: shadowWidth,
height: shadowHeight + 0.5,
},
shadowOpacity: 1,
shadowRadius: shadowRadius,
};
}
);
const shadowStyles = getIOSShadows(elevation, colors);

return (
<View style={shadowStyles?.[0]}>
<View style={shadowStyles?.[1]}>
<View {...props} style={sharedStyle}>
<Animated.View {...props} style={sharedStyle}>
{children}
</View>
</Animated.View>
</View>
</View>
);
Expand Down
4 changes: 2 additions & 2 deletions src/styles/overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import color from 'color';
import { Animated } from 'react-native';
import DarkTheme from './themes/v2/DarkTheme';

const isAnimatedValue = (
it: number | Animated.AnimatedInterpolation
export const isAnimatedValue = (
it: number | string | Animated.AnimatedInterpolation
): it is Animated.Value => it instanceof Animated.Value;

export default function overlay<T extends Animated.Value | number>(
Expand Down

0 comments on commit f483228

Please sign in to comment.