Skip to content

Commit

Permalink
feat: adaptive and exact dark theme (#1367)
Browse files Browse the repository at this point in the history
  • Loading branch information
pan-pawel authored and Trancever committed Oct 11, 2019
1 parent abe187e commit d399733
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 54 deletions.
15 changes: 14 additions & 1 deletion docs/pages/2.theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const theme = {
...DefaultTheme.colors,
primary: '#3498db',
accent: '#f1c40f',
}
},
};

export default function Main() {
Expand All @@ -53,6 +53,7 @@ You can change the theme prop dynamically and all the components will automatica
A theme usually contains the following properties:

- `dark` (`boolean`): whether this is a dark theme or light theme.
- `mode` (`'adaptive' | 'exact'`): color mode for dark theme (See [Dark Theme](#dark-theme)).
- `roundness` (`number`): roundness of common elements, such as buttons.
- `colors` (`object`): various colors used throughout different elements.
- `primary` - primary color for your app, usually your brand color.
Expand Down Expand Up @@ -123,6 +124,18 @@ export default function FancyButton(props) {

Now you can use your `FancyButton` component everywhere instead of using `Button` from Paper.

## Dark Theme

Since 3.0 We adapt Dark theme to follow [Material design guidelines](https://material.io/design/color/dark-theme.html). </br>
In opposition to light theme, dark theme by default uses `surface` colour instead of `primary` on large components like `AppBar` or `BottomNavigation`.</br>
The Dark theme adds a white overlay with opacity depending on elevation of surfaces. It uses it for the better accentuation of surface elevation. Using only shadow is highly imperceptible on dark surfaces.

We are aware that users often use Dark theme in their own ways and they maybe don't want to use default dark theme features from guidelines</br>
That's why if you are using the dark mode you can switch between two types of dark theme `mode`:

`exact` where everything is like it was before. Appbar and BottomNavigation will still use primary colour by default</br>
`adaptive` where we follow [Material design guidelines](https://material.io/design/color/dark-theme.html), the surface will use white overlay with opacity to show elevation, `Appbar` and `BottomNavigation` will use surface colour as a background.

## Gotchas

The `Provider` exposes the theme to the components via [React's context API](https://reactjs.org/docs/context.html), which means that the component must be in the same tree as the `Provider`. Some React Native components will render a different tree such as a `Modal`, in which case the components inside the `Modal` won't be able to access the theme. The work around is to get the theme using the `withTheme` HOC and pass it down to the components as props, or expose it again with the exported `ThemeProvider` component.
Expand Down
26 changes: 16 additions & 10 deletions example/src/Examples/AppbarExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ const initialParams = {
showSubtitle: true,
showSearchIcon: true,
showMoreIcon: true,
showBottomPrimary: false,
showTopPrimary: false,
showExactTheme: false,
};

const MORE_ICON = Platform.OS === 'ios' ? 'dots-horizontal' : 'dots-vertical';
Expand All @@ -34,8 +33,10 @@ class AppbarExample extends React.Component<Props> {
return {
header: (
<Appbar.Header
primary={params.showTopPrimary}
style={params.showCustomColor ? { backgroundColor: '#ffff00' } : null}
theme={{
mode: params.showExactTheme ? 'exact' : 'adaptive',
}}
>
{params.showLeftIcon && (
<Appbar.BackAction onPress={() => navigation.goBack()} />
Expand Down Expand Up @@ -129,28 +130,33 @@ class AppbarExample extends React.Component<Props> {
/>
</View>
<View style={styles.row}>
<Paragraph>Bottom bar primary (dark theme)</Paragraph>
<Paragraph>Adaptive Dark Theme</Paragraph>
<Switch
value={params.showBottomPrimary}
value={!params.showExactTheme}
onValueChange={value =>
navigation.setParams({
showBottomPrimary: value,
showExactTheme: !value,
})
}
/>
</View>
<View style={styles.row}>
<Paragraph>Header bar primary (dark theme)</Paragraph>
<Paragraph>Exact Dark Theme</Paragraph>
<Switch
value={params.showTopPrimary}
value={params.showExactTheme}
onValueChange={value =>
navigation.setParams({
showTopPrimary: value,
showExactTheme: value,
})
}
/>
</View>
<Appbar style={styles.bottom} primary={params.showBottomPrimary}>
<Appbar
style={[styles.bottom]}
theme={{
mode: params.showExactTheme ? 'exact' : 'adaptive',
}}
>
<Appbar.Action icon="archive" onPress={() => {}} />
<Appbar.Action icon="email" onPress={() => {}} />
<Appbar.Action icon="label" onPress={() => {}} />
Expand Down
2 changes: 1 addition & 1 deletion example/src/Examples/BottomNavigationExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class BottomNavigationExample extends React.Component<
state = {
index: 0,
routes: [
{ key: 'album', title: 'Album', icon: 'image-album' },
{ key: 'album', title: 'Album', icon: 'image-album', color: '#272727' },
{
key: 'library',
title: 'Library',
Expand Down
27 changes: 15 additions & 12 deletions src/components/Appbar/Appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ type Props = Partial<React.ComponentProps<typeof View>> & {
* Whether the background color is a dark color. A dark appbar will render light text and vice-versa.
*/
dark?: boolean;
/**
* Pass `true` if you want Appbar to use theme primary color even in dark mode.
* By default in dark mode Appbar use surface color.
*/
primary?: boolean;
/**
* Content of the `Appbar`.
*/
Expand All @@ -44,6 +39,9 @@ export const DEFAULT_APPBAR_HEIGHT = 56;
* The top bar usually contains the screen title, controls such as navigation buttons, menu button etc.
* The bottom bar usually provides access to a drawer and up to four actions.
*
* By default Appbar uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.
* See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations
*
* <div class="screenshots">
* <img class="medium" src="screenshots/appbar.png" />
* </div>
Expand Down Expand Up @@ -88,18 +86,22 @@ class Appbar extends React.Component<Props> {
static Header = AppbarHeader;

render() {
const { children, dark, style, theme, primary, ...rest } = this.props;
const { children, dark, style, theme, ...rest } = this.props;

const { colors, dark: isDarkTheme } = theme;
const { colors, dark: isDarkTheme, mode } = theme;
const {
backgroundColor = isDarkTheme && !primary
? overlay(4, colors.surface)
: colors.primary,
backgroundColor: customBackground,
elevation = 4,
...restStyle
} = StyleSheet.flatten(style) || {};
}: ViewStyle = StyleSheet.flatten(style) || {};

let isDark: boolean;

const backgroundColor = customBackground
? customBackground
: isDarkTheme && mode === 'adaptive'
? overlay(elevation, colors.surface)
: colors.primary;
if (typeof dark === 'boolean') {
isDark = dark;
} else {
Expand Down Expand Up @@ -136,7 +138,8 @@ class Appbar extends React.Component<Props> {
}
return (
<Surface
style={[{ backgroundColor }, styles.appbar, restStyle]}
//@ts-ignore
style={[{ backgroundColor }, styles.appbar, { elevation }, restStyle]}
{...rest}
>
{shouldAddLeftSpacing ? <View style={styles.spacing} /> : null}
Expand Down
22 changes: 9 additions & 13 deletions src/components/Appbar/AppbarHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ type Props = React.ComponentProps<typeof Appbar> & {
* Pass `0` or a custom value to disable the default behaviour, and customize the height.
*/
statusBarHeight?: number;
/**
* Pass `true` if you want Appbar to use theme primary color even in dark mode.
* By default in dark mode Appbar use surface color.
*/
primary?: boolean;
/**
* Content of the header.
*/
Expand Down Expand Up @@ -96,21 +91,22 @@ class AppbarHeader extends React.Component<Props> {
// Don't use default props since we check it to know whether we should use SafeAreaView
statusBarHeight = APPROX_STATUSBAR_HEIGHT,
style,
primary,
dark,
...rest
} = this.props;
const { dark: isDarkTheme, colors } = rest.theme;
const { dark: isDarkTheme, colors, mode } = rest.theme;
const {
height = DEFAULT_APPBAR_HEIGHT,
elevation = 4,
backgroundColor = isDarkTheme && !primary
? overlay(4, colors.surface)
: colors.primary,
backgroundColor: customBackground,
zIndex = 0,
...restStyle
} = StyleSheet.flatten(style) || {};

}: ViewStyle = StyleSheet.flatten(style) || {};
const backgroundColor = customBackground
? customBackground
: isDarkTheme && mode === 'adaptive'
? overlay(elevation, colors.surface)
: colors.primary;
// Let the user override the behaviour
const Wrapper =
typeof this.props.statusBarHeight === 'number' ? View : SafeAreaView;
Expand All @@ -136,13 +132,13 @@ class AppbarHeader extends React.Component<Props> {
>
{/* $FlowFixMe: There seems to be conflict between Appbar's props and Header's props */}
<Appbar
//@ts-ignore
style={[
{ height, backgroundColor, marginTop: statusBarHeight },
styles.appbar,
restStyle,
]}
dark={dark}
primary={primary}
{...rest}
/>
</Wrapper>
Expand Down
20 changes: 13 additions & 7 deletions src/components/BottomNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ class SceneComponent extends React.PureComponent<any> {
*
* For integration with React Navigation, you can use [react-navigation-material-bottom-tab-navigator](https://github.com/react-navigation/react-navigation-material-bottom-tab-navigator).
*
* By default Bottom navigation uses primary color as a background, in dark theme with `adaptive` mode it will use surface colour instead.
* See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations
*
* <div class="screenshots">
* <img class="medium" src="screenshots/bottom-navigation.gif" />
* </div>
Expand Down Expand Up @@ -579,19 +582,22 @@ class BottomNavigation extends React.Component<Props, State> {

const { layout, loaded } = this.state;
const { routes } = navigationState;
const { colors, dark: isDarkTheme } = theme;
const { colors, dark: isDarkTheme, mode } = theme;

const shifting = this._isShifting();

const {
backgroundColor: approxBackgroundColor = isDarkTheme
? overlay(styles.bar.elevation)
: colors.primary,
} = StyleSheet.flatten(barStyle) || {};
const { backgroundColor: customBackground, elevation = 4 }: ViewStyle =
StyleSheet.flatten(barStyle) || {};

const approxBackgroundColor =
customBackground || (isDarkTheme && mode === 'adaptive')
? overlay(elevation, colors.surface)
: colors.primary;

const backgroundColor = shifting
? this.state.index.interpolate({
inputRange: routes.map((_, i) => i),
//@ts-ignore
outputRange: routes.map(
route => getColor({ route }) || approxBackgroundColor
),
Expand All @@ -611,7 +617,7 @@ class BottomNavigation extends React.Component<Props, State> {
.rgb()
.string();

const touchColor = color(activeColor)
const touchColor = color(activeColor || activeTintColor)
.alpha(0.12)
.rgb()
.string();
Expand Down
12 changes: 8 additions & 4 deletions src/components/Surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type Props = React.ComponentProps<typeof View> & {

/**
* Surface is a basic container that can give depth to an element with elevation shadow.
* On dark theme surface is constructed by also placing a semi-transparent white overlay over a component surface.
* A overlay and/or shadow can be applied by specifying the `elevation` property both on Android and iOS.
* On dark theme with `adaptive` mode, surface is constructed by also placing a semi-transparent white overlay over a component surface.
* See [Dark Theme](https://callstack.github.io/react-native-paper/theming.html#dark-theme) for more informations.
* Overlay and/or shadow can be applied by specifying the `elevation` property both on Android and iOS.
*
* <div class="screenshots">
* <img src="screenshots/surface-1.png" />
Expand Down Expand Up @@ -64,13 +65,16 @@ class Surface extends React.Component<Props> {
const { style, theme, ...rest } = this.props;
const flattenedStyles = StyleSheet.flatten(style) || {};
const { elevation = 4 }: ViewStyle = flattenedStyles;
const { dark: isDarkTheme, colors } = theme;
const { dark: isDarkTheme, mode, colors } = theme;
return (
<Animated.View
{...rest}
style={[
{
backgroundColor: isDarkTheme ? overlay(elevation) : colors.surface,
backgroundColor:
isDarkTheme && mode === 'adaptive'
? overlay(elevation, colors.surface)
: colors.surface,
},
elevation && shadow(elevation),
style,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2948,7 +2948,7 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom
style={
Object {
"alignItems": "center",
"backgroundColor": "#E96A82",
"backgroundColor": "#FFFFFF",
"overflow": "hidden",
}
}
Expand Down Expand Up @@ -3861,7 +3861,7 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav
style={
Object {
"alignItems": "center",
"backgroundColor": "rgba(233, 106, 130, 1)",
"backgroundColor": "rgba(255, 255, 255, 1)",
"overflow": "hidden",
}
}
Expand Down
1 change: 1 addition & 0 deletions src/styles/DarkTheme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Theme } from '../types';
const DarkTheme: Theme = {
...DefaultTheme,
dark: true,
mode: 'adaptive',
colors: {
...DefaultTheme.colors,
primary: '#BB86FC',
Expand Down
19 changes: 15 additions & 4 deletions src/styles/overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import color from 'color';
import { Animated } from 'react-native';
import DarkTheme from './DarkTheme';

export default function overlay(
elevation: number = 1,
elevation: number | Animated.Value = 1,
surfaceColor: string = DarkTheme.colors.surface
) {
if (elevation instanceof Animated.Value) {
const inputRange = [0, 1, 2, 3, 8, 24];
return elevation.interpolate({
inputRange,
outputRange: inputRange.map(elevation => {
return calculateColor(surfaceColor, elevation);
}),
});
}
return calculateColor(surfaceColor, elevation);
}
function calculateColor(surfaceColor: string, elevation: number) {
let overlayTransparency: number;
if (elevation >= 1 && elevation <= 24) {
overlayTransparency = elevationOverlayTransparency[elevation];
Expand All @@ -13,12 +26,10 @@ export default function overlay(
} else {
overlayTransparency = elevationOverlayTransparency[1];
}
const backgroundColor = color(surfaceColor)
return color(surfaceColor)
.mix(color('white'), overlayTransparency * 0.01)
.hex();
return backgroundColor;
}

const elevationOverlayTransparency: { [id: number]: number } = {
1: 5,
2: 7,
Expand Down
1 change: 1 addition & 0 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type Fonts = {

export type Theme = {
dark: boolean;
mode?: 'adaptive' | 'exact';
roundness: number;
colors: {
primary: string;
Expand Down

0 comments on commit d399733

Please sign in to comment.