Faster theme transition by using css variable when possible

This commit is contained in:
Bruno Lemos
2018-12-06 08:33:43 -02:00
parent 86a54b3d75
commit 443767c8fb
11 changed files with 171 additions and 112 deletions

View File

@@ -4,10 +4,24 @@ import { Theme } from '@devhub/core/src/types'
import { useTheme } from './context/ThemeContext'
function getStyles(params: { theme: Theme }) {
const t = params.theme
return `
::-webkit-scrollbar-thumb
{
background-color: ${params.theme.backgroundColorDarker08};
background-color:${t.backgroundColorDarker08};
}
body {
--theme_backgroundColor:${t.backgroundColor};
--theme_backgroundColorDarker08:${t.backgroundColorDarker08};
--theme_backgroundColorLess08:${t.backgroundColorLess08};
--theme_backgroundColorLighther08:${t.backgroundColorLighther08};
--theme_backgroundColorMore08:${t.backgroundColorMore08};
--theme_backgroundColorTransparent10:${t.backgroundColorTransparent10};
--theme_foregroundColor:${t.foregroundColor};
--theme_foregroundColorMuted50:${t.foregroundColorMuted50};
--theme_foregroundColorTransparent50:${t.foregroundColorTransparent50};
--theme_foregroundColorTransparent80:${t.foregroundColorTransparent80};
}
`
}

View File

@@ -7,4 +7,6 @@ export interface AnimatedLinkProps extends Omit<LinkProps, 'animated'> {
style: any
}
export const AnimatedLink = (props: AnimatedLinkProps) => <Link {...props} />
export const AnimatedLink = (props: AnimatedLinkProps) => (
<Link {...props} animated />
)

View File

@@ -1,11 +1,9 @@
import React, { AnchorHTMLAttributes } from 'react'
import {
Animated,
StyleProp,
TouchableOpacity,
TouchableOpacityProps,
View,
ViewStyle,
} from 'react-native'
import { Browser } from '../../libs/browser'

View File

@@ -6,6 +6,7 @@ import SplashScreen from 'react-native-splash-screen'
import { AnimatedSafeAreaView } from '../../components/animated/AnimatedSafeAreaView'
import { AnimatedStatusBar } from '../../components/animated/AnimatedStatusBar'
import { useAnimatedTheme } from '../../hooks/use-animated-theme'
import { useTheme } from '../context/ThemeContext'
let isSplashScreenVisible = true
@@ -24,7 +25,8 @@ const styles = StyleSheet.create({
})
export function Screen(props: ScreenProps) {
const theme = useAnimatedTheme()
const theme = useTheme()
const animatedTheme = useAnimatedTheme()
useEffect(() => {
if (isSplashScreenVisible && SplashScreen) {
@@ -42,7 +44,9 @@ export function Screen(props: ScreenProps) {
<>
<AnimatedStatusBar
barStyle={theme.isDark ? 'light-content' : 'dark-content'}
backgroundColor={statusBarBackgroundColor || theme.backgroundColor}
backgroundColor={
statusBarBackgroundColor || animatedTheme.backgroundColor
}
/>
<View style={{ flex: 1 }}>

View File

@@ -3,6 +3,7 @@ import React, { ReactNode } from 'react'
import { StyleProp, View, ViewStyle } from 'react-native'
import { LinearGradient } from '../../libs/linear-gradient'
import { computeThemeColor } from '../../utils/helpers/colors'
export type From = 'top' | 'bottom' | 'left' | 'right'
export type FromWithVH = 'vertical' | 'horizontal' | From
@@ -88,7 +89,11 @@ function getProps(from: From, size: number) {
function GradientLayerOverlay(
props: TransparentTextOverlayProps & { from: From },
) {
const { color, from, radius, size, style, ...otherProps } = props
const { color: _color, from, radius, size, style, ...otherProps } = props
const color = computeThemeColor(_color)
if (!color) return null
return (
<LinearGradient
colors={[rgba(color, 0), color]}

View File

@@ -19,9 +19,11 @@ import { H2 } from '../common/H2'
import { H3 } from '../common/H3'
import { Spacer } from '../common/Spacer'
import { Switch } from '../common/Switch'
import { useTheme } from '../context/ThemeContext'
export function ThemePreference() {
const appTheme = useAnimatedTheme()
const appTheme = useTheme()
const appAnimatedTheme = useAnimatedTheme()
const currentThemeId = useReduxState(selectors.themePairSelector).id
const preferredDarkTheme = useReduxState(
selectors.preferredDarkThemePairSelector,
@@ -97,7 +99,7 @@ export function ThemePreference() {
</Text>
</View>
<Animated.Text style={{ color: appTheme.foregroundColor }}>
<Animated.Text style={{ color: appAnimatedTheme.foregroundColor }}>
{theme.displayName}
</Animated.Text>
</View>

View File

@@ -0,0 +1,87 @@
import _ from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { Animated, Easing } from 'react-native'
import { useReduxStore } from '../redux/context/ReduxStoreContext'
import { themeSelector } from '../redux/selectors'
export function useAnimatedTheme() {
const store = useReduxStore()
const themeRef = useRef(themeSelector(store.getState()))
const animatedValueRef = useRef(new Animated.Value(0))
const [animatedTheme, setAnimatedTheme] = useState(() => {
const getInterpolate = (value: string) =>
animatedValueRef.current.interpolate({
inputRange: [0, 0],
outputRange: [value, value],
})
return {
backgroundColor: getInterpolate(themeRef.current.backgroundColor),
backgroundColorDarker08: getInterpolate(
themeRef.current.backgroundColorDarker08,
),
backgroundColorLess08: getInterpolate(
themeRef.current.backgroundColorLess08,
),
backgroundColorLighther08: getInterpolate(
themeRef.current.backgroundColorLighther08,
),
backgroundColorMore08: getInterpolate(
themeRef.current.backgroundColorMore08,
),
backgroundColorTransparent10: getInterpolate(
themeRef.current.backgroundColorTransparent10,
),
foregroundColor: getInterpolate(themeRef.current.foregroundColor),
foregroundColorMuted50: getInterpolate(
themeRef.current.foregroundColorMuted50,
),
foregroundColorTransparent50: getInterpolate(
themeRef.current.foregroundColorTransparent50,
),
foregroundColorTransparent80: getInterpolate(
themeRef.current.foregroundColorTransparent80,
),
}
})
useEffect(
() => {
return store.subscribe(() => {
const newTheme = themeSelector(store.getState())
if (newTheme === themeRef.current) return
animatedValueRef.current.setValue(0)
const keys = Object.keys(animatedTheme) as Array<
keyof typeof animatedTheme
>
keys.forEach(key => {
const currentValue = themeRef.current[key]
const newValue = newTheme[key]
animatedTheme[key] = animatedValueRef.current.interpolate({
inputRange: [0, 1],
outputRange: [currentValue, newValue],
})
})
themeRef.current = newTheme
Animated.timing(animatedValueRef.current, {
easing: Easing.linear,
toValue: 1,
duration: 150,
}).start()
setAnimatedTheme({ ...animatedTheme })
})
},
[store, animatedTheme],
)
return animatedTheme
}

View File

@@ -1,103 +1 @@
import _ from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { Animated, Easing } from 'react-native'
import { useReduxStore } from '../redux/context/ReduxStoreContext'
import { themeSelector } from '../redux/selectors'
export function useAnimatedTheme() {
const store = useReduxStore()
const themeRef = useRef(themeSelector(store.getState()))
const animatedValueRef = useRef(new Animated.Value(0))
const [animatedTheme, setAnimatedTheme] = useState(() => {
const getInterpolate = (value: string) =>
animatedValueRef.current.interpolate({
inputRange: [0, 0],
outputRange: [value, value],
})
return {
id: themeRef.current.id,
displayName: themeRef.current.displayName,
isDark: themeRef.current.isDark,
invert: themeRef.current.invert,
backgroundColor: getInterpolate(themeRef.current.backgroundColor),
backgroundColorDarker08: getInterpolate(
themeRef.current.backgroundColorDarker08,
),
backgroundColorLess08: getInterpolate(
themeRef.current.backgroundColorLess08,
),
backgroundColorLighther08: getInterpolate(
themeRef.current.backgroundColorLighther08,
),
backgroundColorMore08: getInterpolate(
themeRef.current.backgroundColorMore08,
),
backgroundColorTransparent10: getInterpolate(
themeRef.current.backgroundColorTransparent10,
),
foregroundColor: getInterpolate(themeRef.current.foregroundColor),
foregroundColorMuted50: getInterpolate(
themeRef.current.foregroundColorMuted50,
),
foregroundColorTransparent50: getInterpolate(
themeRef.current.foregroundColorTransparent50,
),
foregroundColorTransparent80: getInterpolate(
themeRef.current.foregroundColorTransparent80,
),
}
})
useEffect(
() => {
return store.subscribe(() => {
const newTheme = themeSelector(store.getState())
if (newTheme === themeRef.current) return
animatedValueRef.current.setValue(0)
const keys = Object.keys(animatedTheme) as Array<
keyof typeof animatedTheme
>
keys.forEach(key => {
const currentValue = themeRef.current[key]
const newValue = newTheme[key]
if (
key.toLowerCase().includes('color') &&
currentValue &&
typeof currentValue === 'string' &&
newValue &&
typeof newValue === 'string'
) {
if (animatedTheme[key] && typeof animatedTheme[key] === 'object') {
animatedTheme[key] = animatedValueRef.current.interpolate({
inputRange: [0, 1],
outputRange: [currentValue, newValue],
})
}
} else {
animatedTheme[key] = newValue
}
})
themeRef.current = newTheme
Animated.timing(animatedValueRef.current, {
easing: Easing.linear,
toValue: 1,
duration: 250,
}).start()
setAnimatedTheme({ ...animatedTheme })
})
},
[store, animatedTheme],
)
return animatedTheme
}
export { useAnimatedTheme } from './use-animated-theme.shared'

View File

@@ -0,0 +1,32 @@
import _ from 'lodash'
import { Platform } from '../libs/platform'
const cssVariablesTheme = {
backgroundColor: 'var(--theme_backgroundColor)',
backgroundColorDarker08: 'var(--theme_backgroundColorDarker08)',
backgroundColorLess08: 'var(--theme_backgroundColorLess08)',
backgroundColorLighther08: 'var(--theme_backgroundColorLighther08)',
backgroundColorMore08: 'var(--theme_backgroundColorMore08)',
backgroundColorTransparent10: 'var(--theme_backgroundColorTransparent10)',
foregroundColor: 'var(--theme_foregroundColor)',
foregroundColorMuted50: 'var(--theme_foregroundColorMuted50)',
foregroundColorTransparent50: 'var(--theme_foregroundColorTransparent50)',
foregroundColorTransparent80: 'var(--theme_foregroundColorTransparent80)',
}
function useCSSVariableTheme() {
return cssVariablesTheme
}
const _window = typeof window !== 'undefined' ? (window as any) : undefined
const supportsCSSVariables =
Platform.OS === 'web' &&
_window &&
_window.CSS &&
_window.CSS.supports &&
_window.CSS.supports('--fake-var', 0)
export const useAnimatedTheme = (supportsCSSVariables
? () => useCSSVariableTheme
: () => require('./use-animated-theme.shared').useAnimatedTheme)() // tslint:disable-line

View File

@@ -0,0 +1,11 @@
import { Platform } from '../../libs/platform'
export function computeThemeColor(color: string) {
return Platform.OS === 'web' && color && color.includes('var(--')
? typeof getComputedStyle === 'function'
? getComputedStyle(document.body)
.getPropertyValue(color.replace(/var\((.+)\)$/, '$1'))
.trim()
: undefined
: color
}

View File

@@ -1,3 +1,9 @@
* {
transition: all 150ms linear !important;
box-sizing: border-box;
cursor: default;
}
::-webkit-scrollbar {
width: 2px;
height: 2px;