mirror of
https://github.com/zhigang1992/devhub.git
synced 2026-06-16 10:24:24 +08:00
Faster theme transition by using css variable when possible
This commit is contained in:
@@ -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};
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { AnchorHTMLAttributes } from 'react'
|
||||
import {
|
||||
Animated,
|
||||
StyleProp,
|
||||
TouchableOpacity,
|
||||
TouchableOpacityProps,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
import { Browser } from '../../libs/browser'
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
87
packages/components/src/hooks/use-animated-theme.shared.ts
Normal file
87
packages/components/src/hooks/use-animated-theme.shared.ts
Normal 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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
32
packages/components/src/hooks/use-animated-theme.web.ts
Normal file
32
packages/components/src/hooks/use-animated-theme.web.ts
Normal 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
|
||||
11
packages/components/src/utils/helpers/colors.ts
Normal file
11
packages/components/src/utils/helpers/colors.ts
Normal 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
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
* {
|
||||
transition: all 150ms linear !important;
|
||||
box-sizing: border-box;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
|
||||
Reference in New Issue
Block a user