Created
December 10, 2021 21:44
-
-
Save EvanBacon/d148b2425c5a0bd11b6cecb5f4b72bb8 to your computer and use it in GitHub Desktop.
A stack based Expo component for setting the background color of the root view. Useful for changing the background color on certain screens or inside of native modals. Updates based on Appearance and AppState.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as SystemUI from 'expo-system-ui'; | |
import * as React from 'react'; | |
import { Appearance, AppState, AppStateStatus, ColorSchemeName, ColorValue } from 'react-native'; | |
type ThemedColorValue = { light: ColorValue, dark: ColorValue }; | |
type Props = { backgroundColor: ColorValue | ThemedColorValue } | |
const propsStack: Props[] = []; | |
const defaultProps = createStackEntry({ | |
backgroundColor: '#fff', | |
}); | |
// Timer for updating the native module values at the end of the frame. | |
let updateImmediate: any | null = null; | |
let appearanceListener: Appearance.AppearanceListener | null = null; | |
let appStateListener: ((state: AppStateStatus) => void) | null = null; | |
/** | |
* A stack based component for setting the background color of the root view. | |
* Useful for changing the background color on certain screens or inside of native modals. | |
* Updates based on Appearance and AppState. | |
* | |
* @example | |
* ```tsx | |
* function App() { | |
* return ( | |
* <> | |
* <RootViewBackgroundColor backgroundColor={{ light: '#fff', dark: '#000' }} /> | |
* <RootViewBackgroundColor backgroundColor={'#fff000'} /> | |
* </> | |
* ) | |
* } | |
* ``` | |
*/ | |
export function RootViewBackgroundColor(props: Props) { | |
let stack = React.useRef<Props | null>(null); | |
React.useEffect(() => { | |
// Create a stack entry on component mount | |
stack.current = RootViewBackgroundColor.pushStackEntry(props) | |
return () => { | |
if (stack.current) { | |
// Update on component unmount | |
RootViewBackgroundColor.popStackEntry(stack.current); | |
} | |
} | |
}, []) | |
React.useEffect(() => { | |
if (stack.current) { | |
// Update the current stack entry | |
stack.current = RootViewBackgroundColor.replaceStackEntry( | |
stack.current, | |
props, | |
); | |
} | |
}, [props.backgroundColor]); | |
return null; | |
} | |
function isThemedColor(color?: Props['backgroundColor']): color is ThemedColorValue { | |
return !!color && typeof color !== 'string' && ('light' in color) && ('dark' in color); | |
} | |
/** | |
* Merges the prop stack with the default values. | |
*/ | |
function mergePropsStack( | |
propsStack: Array<Props>, | |
defaultValues: Partial<Props>, | |
): Partial<Props> { | |
return propsStack.reduce((prev, cur) => { | |
for (const prop in cur) { | |
// @ts-ignore | |
if (cur[prop] != null) { | |
// @ts-ignore | |
prev[prop] = cur[prop]; | |
} | |
} | |
return prev; | |
}, Object.assign({}, defaultValues)); | |
} | |
function setBackgroundColorAsync(scheme: ColorSchemeName, backgroundColor: Props['backgroundColor']) { | |
if (isThemedColor(backgroundColor)) { | |
return SystemUI.setBackgroundColorAsync(scheme === 'dark' ? backgroundColor.dark ?? '#000' : backgroundColor.light ?? '#fff'); | |
} | |
return SystemUI.setBackgroundColorAsync(backgroundColor ?? '#fff'); | |
} | |
/** | |
* Returns an object to insert in the props stack from the props | |
* and the transition/animation info. | |
*/ | |
function createStackEntry(props: Props): Props { | |
return { | |
backgroundColor: props.backgroundColor | |
}; | |
} | |
/** | |
* Set the background color for the status bar | |
* @param color Background color. | |
* @param animated Animate the style change. | |
*/ | |
RootViewBackgroundColor.setBackgroundColor = (color: ThemedColorValue) => { | |
defaultProps.backgroundColor = color; | |
setBackgroundColorAsync(Appearance.getColorScheme(), color); | |
} | |
/** | |
* Push a RootViewBackgroundColor entry onto the stack. | |
* The return value should be passed to `popStackEntry` when complete. | |
* | |
* @param props Object containing the RootViewBackgroundColor props to use in the stack entry. | |
*/ | |
RootViewBackgroundColor.pushStackEntry = (props: Props): any => { | |
const entry = createStackEntry(props); | |
propsStack.push(entry); | |
// Ensure we only have one appearance change listener. | |
if (!appearanceListener) { | |
appearanceListener = ({ colorScheme }) => { | |
setBackgroundColorAsync(colorScheme, propsStack[propsStack.length - 1].backgroundColor); | |
} | |
Appearance.addChangeListener(appearanceListener); | |
} | |
if (!appStateListener) { | |
appStateListener = () => { | |
setBackgroundColorAsync(Appearance.getColorScheme(), propsStack[propsStack.length - 1].backgroundColor); | |
} | |
AppState.addEventListener('change', appStateListener); | |
} | |
RootViewBackgroundColor._updatePropsStack(); | |
return entry; | |
} | |
/** | |
* Pop a RootViewBackgroundColor entry from the stack. | |
* | |
* @param entry Entry returned from `pushStackEntry`. | |
*/ | |
RootViewBackgroundColor.popStackEntry = (entry: Props) => { | |
const index = propsStack.indexOf(entry); | |
if (index !== -1) { | |
propsStack.splice(index, 1); | |
} | |
if (propsStack.length === 0) { | |
if (appearanceListener) { | |
Appearance.removeChangeListener(appearanceListener); | |
appearanceListener = null; | |
} | |
if (appStateListener) { | |
AppState.removeEventListener('change', appStateListener); | |
appStateListener = null; | |
} | |
} | |
RootViewBackgroundColor._updatePropsStack(); | |
} | |
/** | |
* Replace an existing RootViewBackgroundColor stack entry with new props. | |
* | |
* @param entry Entry returned from `pushStackEntry` to replace. | |
* @param props Object containing the RootViewBackgroundColor props to use in the replacement stack entry. | |
*/ | |
RootViewBackgroundColor.replaceStackEntry = (entry: Props, props: Props): any => { | |
const newEntry = createStackEntry(props); | |
const index = propsStack.indexOf(entry); | |
if (index !== -1) { | |
propsStack[index] = newEntry; | |
} | |
RootViewBackgroundColor._updatePropsStack(); | |
return newEntry; | |
} | |
/** | |
* Updates the native status bar with the props from the stack. | |
*/ | |
RootViewBackgroundColor._updatePropsStack = () => { | |
// Send the update to the native module only once at the end of the frame. | |
clearImmediate(updateImmediate); | |
updateImmediate = setImmediate(() => { | |
const { backgroundColor } = mergePropsStack( | |
propsStack, | |
defaultProps, | |
); | |
if (backgroundColor) { | |
setBackgroundColorAsync(Appearance.getColorScheme(), backgroundColor); | |
} | |
}); | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I needed this for too many side projects so I published it under
@bacons/expo-background-color
-- this is not an official Expo package but there's no reason it should ever really break. Repo.