Splash: reduce motion + dark mode (#2448)

* Don't use mask for android at all

* Handle reduced motion

* Add dark splash

* Add dark config

* Fix android version code

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
zio/stable
Eric Bailey 2024-01-08 15:47:25 -06:00 committed by GitHub
parent fcfebda469
commit 9eeff2cc5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 57 deletions

View File

@ -66,6 +66,12 @@ module.exports = function () {
'Used for profile pictures, posts, and other kinds of content', 'Used for profile pictures, posts, and other kinds of content',
}, },
associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'], associatedDomains: ['applinks:bsky.app', 'applinks:staging.bsky.app'],
splash: {
dark: {
image: './assets/splash-dark.png',
backgroundColor: '#001429',
},
},
}, },
androidStatusBar: { androidStatusBar: {
barStyle: 'dark-content', barStyle: 'dark-content',
@ -95,6 +101,12 @@ module.exports = function () {
category: ['BROWSABLE', 'DEFAULT'], category: ['BROWSABLE', 'DEFAULT'],
}, },
], ],
splash: {
dark: {
image: './assets/splash-dark.png',
backgroundColor: '#001429',
},
},
}, },
web: { web: {
favicon: './assets/favicon.png', favicon: './assets/favicon.png',

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 KiB

View File

@ -1,5 +1,11 @@
import React, {useCallback, useEffect} from 'react' import React, {useCallback, useEffect} from 'react'
import {View, StyleSheet, Image as RNImage} from 'react-native' import {
View,
StyleSheet,
Image as RNImage,
AccessibilityInfo,
useColorScheme,
} from 'react-native'
import * as SplashScreen from 'expo-splash-screen' import * as SplashScreen from 'expo-splash-screen'
import {Image} from 'expo-image' import {Image} from 'expo-image'
import Animated, { import Animated, {
@ -15,10 +21,17 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import Svg, {Path, SvgProps} from 'react-native-svg' import Svg, {Path, SvgProps} from 'react-native-svg'
import {isAndroid} from '#/platform/detection' import {isAndroid} from '#/platform/detection'
import {useColorMode} from 'state/shell'
import {colors} from '#/lib/styles'
// @ts-ignore // @ts-ignore
import splashImagePointer from '../assets/splash.png' import splashImagePointer from '../assets/splash.png'
// @ts-ignore
import darkSplashImagePointer from '../assets/splash-dark.png'
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
const darkSplashImageUri = RNImage.resolveAssetSource(
darkSplashImagePointer,
).uri
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) { export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
const width = 1000 const width = 1000
@ -29,9 +42,9 @@ export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
// @ts-ignore it's fiiiiine // @ts-ignore it's fiiiiine
ref={ref} ref={ref}
viewBox="0 0 64 66" viewBox="0 0 64 66"
style={{width, height}}> style={[{width, height}, props.style]}>
<Path <Path
fill="#fff" fill={props.fill || '#fff'}
d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z" d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
/> />
</Svg> </Svg>
@ -53,7 +66,19 @@ export function Splash(props: React.PropsWithChildren<Props>) {
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
const [isImageLoaded, setIsImageLoaded] = React.useState(false) const [isImageLoaded, setIsImageLoaded] = React.useState(false)
const [isLayoutReady, setIsLayoutReady] = React.useState(false) const [isLayoutReady, setIsLayoutReady] = React.useState(false)
const isReady = props.isReady && isImageLoaded && isLayoutReady const [reduceMotion, setReduceMotion] = React.useState<boolean | undefined>(
false,
)
const isReady =
props.isReady &&
isImageLoaded &&
isLayoutReady &&
reduceMotion !== undefined
const colorMode = useColorMode()
const colorScheme = useColorScheme()
const themeName = colorMode === 'system' ? colorScheme : colorMode
const isDarkMode = themeName === 'dark'
const logoAnimation = useAnimatedStyle(() => { const logoAnimation = useAnimatedStyle(() => {
return { return {
@ -73,6 +98,17 @@ export function Splash(props: React.PropsWithChildren<Props>) {
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
} }
}) })
const reducedLogoAnimation = useAnimatedStyle(() => {
return {
transform: [
{
scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'),
},
],
opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'),
}
})
const logoWrapperAnimation = useAnimatedStyle(() => { const logoWrapperAnimation = useAnimatedStyle(() => {
return { return {
opacity: interpolate( opacity: interpolate(
@ -137,71 +173,85 @@ export function Splash(props: React.PropsWithChildren<Props>) {
} }
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady]) }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
useEffect(() => {
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
}, [])
const logoAnimations =
reduceMotion === true ? reducedLogoAnimation : logoAnimation
return ( return (
<View style={{flex: 1}} onLayout={onLayout}> <View style={{flex: 1}} onLayout={onLayout}>
{!isAnimationComplete && ( {!isAnimationComplete && (
<Image <Image
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
onLoadEnd={onLoadEnd} onLoadEnd={onLoadEnd}
source={{uri: splashImageUri}} source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
style={StyleSheet.absoluteFillObject} style={StyleSheet.absoluteFillObject}
/> />
)} )}
{isAndroid ? ( {isReady &&
// Use a simple fade on older versions of android (work around a bug) (isAndroid || reduceMotion === true ? (
<> // Use a simple fade on older versions of android (work around a bug)
<Animated.View style={[{flex: 1}, appAnimation]}> <>
{props.children} <Animated.View style={[{flex: 1}, appAnimation]}>
</Animated.View> {props.children}
</Animated.View>
{!isAnimationComplete && ( {!isAnimationComplete && (
<Animated.View <Animated.View
style={[ style={[
StyleSheet.absoluteFillObject, StyleSheet.absoluteFillObject,
logoWrapperAnimation, logoWrapperAnimation,
{ {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
}, },
]}> ]}>
<AnimatedLogo style={[{opacity: 0}, logoAnimation]} /> <AnimatedLogo
fill={isDarkMode ? colors.blue3 : '#fff'}
style={[{opacity: 0}, logoAnimations]}
/>
</Animated.View>
)}
</>
) : (
<MaskedView
style={[StyleSheet.absoluteFillObject]}
maskElement={
<Animated.View
style={[
{
// Transparent background because mask is based off alpha channel.
backgroundColor: 'transparent',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<AnimatedLogo
fill={isDarkMode ? colors.blue3 : '#fff'}
style={[logoAnimations]}
/>
</Animated.View>
}>
{!isAnimationComplete && (
<View
style={[
StyleSheet.absoluteFillObject,
{backgroundColor: isDarkMode ? colors.blue3 : '#fff'},
]}
/>
)}
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View> </Animated.View>
)} </MaskedView>
</> ))}
) : (
<MaskedView
style={[StyleSheet.absoluteFillObject]}
maskElement={
<Animated.View
style={[
{
// Transparent background because mask is based off alpha channel.
backgroundColor: 'transparent',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
},
]}>
<AnimatedLogo style={[logoAnimation]} />
</Animated.View>
}>
{!isAnimationComplete && (
<View
style={[
StyleSheet.absoluteFillObject,
{backgroundColor: 'white'},
]}
/>
)}
<Animated.View style={[{flex: 1}, appAnimation]}>
{props.children}
</Animated.View>
</MaskedView>
)}
</View> </View>
) )
} }