[RN] Add color scheme support to header

This commit is contained in:
Bettenbuk Zoltan 2019-03-05 18:41:39 +01:00 committed by Zoltan Bettenbuk
parent 20c1b1cfae
commit 55a971c0fd
12 changed files with 398 additions and 147 deletions

View File

@ -17,6 +17,13 @@ export default {
icon: ColorPalette.white,
text: ColorPalette.white
},
'Header': {
background: ColorPalette.blue,
icon: ColorPalette.white,
statusBar: ColorPalette.blueHighlight,
statusBarContent: ColorPalette.white,
text: ColorPalette.white
},
'LargeVideo': {
background: ColorPalette.black
},

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react';
import { TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { Icon } from '../../../font-icons';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link BackButton}
*/
@ -20,13 +20,18 @@ type Props = {
/**
* An external style object passed to the component.
*/
style?: Object
style?: Object,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
* A component rendering a back button.
*/
export default class BackButton extends Component<Props> {
class BackButton extends Component<Props> {
/**
* Implements React's {@link Component#render()}, renders the button.
*
@ -41,10 +46,26 @@ export default class BackButton extends Component<Props> {
<Icon
name = { 'arrow_back' }
style = { [
styles.headerButtonIcon,
this.props._headerStyles.headerButtonIcon,
this.props.style
] } />
</TouchableOpacity>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default connect(_mapStateToProps)(BackButton);

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { translate } from '../../../i18n';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link ForwardButton}
*/
@ -35,7 +35,12 @@ type Props = {
/**
* The function to be used to translate i18n labels.
*/
t: Function
t: Function,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
@ -49,6 +54,8 @@ class ForwardButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _headerStyles } = this.props;
return (
<TouchableOpacity
accessibilityLabel = { 'Forward' }
@ -56,8 +63,8 @@ class ForwardButton extends Component<Props> {
onPress = { this.props.onPress } >
<Text
style = { [
styles.headerButtonText,
this.props.disabled && styles.disabledButtonText,
_headerStyles.headerButtonText,
this.props.disabled && _headerStyles.disabledButtonText,
this.props.style
] }>
{ this.props.t(this.props.labelKey) }
@ -67,4 +74,18 @@ class ForwardButton extends Component<Props> {
}
}
export default translate(ForwardButton);
/**
* Maps part of the Redux state to the props of the component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(ForwardButton));

View File

@ -2,14 +2,24 @@
import React, { Component, type Node } from 'react';
import { Platform, SafeAreaView, StatusBar, View } from 'react-native';
import { connect } from 'react-redux';
import styles, { HEADER_PADDING, STATUSBAR_COLOR } from './styles';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { isDarkColor } from '../../../styles';
import { HEADER_PADDING } from './headerstyles';
/**
* Compatibility header padding size for iOS 10 (and older) devices.
*/
const IOS10_PADDING = 20;
/**
* Constanst for the (currently) supported statusbar colors.
*/
const STATUSBAR_DARK = 'dark-content';
const STATUSBAR_LIGHT = 'light-content';
/**
* The type of the React {@code Component} props of {@link Header}
*/
@ -23,43 +33,18 @@ type Props = {
/**
* The component's external style
*/
style: Object
style: Object,
/**
* The color schemed style of the component.
*/
_styles: Object
}
/**
* A generic screen header component.
*/
export default class Header extends Component<Props> {
/**
* The style of button-like React {@code Component}s rendered in
* {@code Header}.
*
* @returns {Object}
*/
static get buttonStyle(): Object {
return styles.headerButtonIcon;
}
/**
* The style of a React {@code Component} rendering a {@code Header} as its
* child.
*
* @returns {Object}
*/
static get pageStyle(): Object {
return styles.page;
}
/**
* The style of text rendered in {@code Header}.
*
* @returns {Object}
*/
static get textStyle(): Object {
return styles.headerText;
}
class Header extends Component<Props> {
/**
* Initializes a new {@code Header} instance.
*
@ -78,20 +63,22 @@ export default class Header extends Component<Props> {
* @inheritdoc
*/
render() {
const { _styles } = this.props;
return (
<View
style = { [
styles.headerOverlay,
_styles.headerOverlay,
this._getIOS10CompatiblePadding()
] } >
<StatusBar
backgroundColor = { STATUSBAR_COLOR }
barStyle = 'light-content'
backgroundColor = { _styles.statusBar }
barStyle = { this._getStatusBarContentColor() }
translucent = { false } />
<SafeAreaView>
<View
style = { [
styles.screenHeader,
_styles.screenHeader,
this.props.style
] }>
{
@ -128,4 +115,54 @@ export default class Header extends Component<Props> {
return null;
}
/**
* Calculates the color of the statusbar content (light or dark) based on
* certain criterias.
*
* @returns {string}
*/
_getStatusBarContentColor() {
const { _styles } = this.props;
const { statusBarContent } = _styles;
if (statusBarContent) {
// We have the possibility to define the statusbar color in the
// color scheme feature, but since mobile devices (at the moment)
// only support two colors (light and dark) we need to normalize
// the value.
if (isDarkColor(statusBarContent)) {
return STATUSBAR_DARK;
}
return STATUSBAR_LIGHT;
}
// The statusbar color is not defined, so we need to base our choice
// on the header colors
const { statusBar, screenHeader } = _styles;
if (isDarkColor(statusBar || screenHeader.backgroundColor)) {
return STATUSBAR_LIGHT;
}
return STATUSBAR_DARK;
}
}
/**
* Maps part of the Redux state to the props of the component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _styles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default connect(_mapStateToProps)(Header);

View File

@ -2,11 +2,11 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { translate } from '../../../i18n';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link HeaderLabel}
*/
@ -20,7 +20,12 @@ type Props = {
/**
* The i18n translate function.
*/
t: Function
t: Function,
/**
* The color schemed style of the Header component.
*/
_headerStyles: Object
};
/**
@ -34,13 +39,15 @@ class HeaderLabel extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _headerStyles } = this.props;
return (
<View
pointerEvents = 'box-none'
style = { styles.headerTextWrapper }>
style = { _headerStyles.headerTextWrapper }>
<Text
style = { [
styles.headerText
_headerStyles.headerText
] }>
{ this.props.t(this.props.labelKey) }
</Text>
@ -49,4 +56,18 @@ class HeaderLabel extends Component<Props> {
}
}
export default translate(HeaderLabel);
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(HeaderLabel));

View File

@ -0,0 +1,87 @@
// @flex
import { StyleSheet } from 'react-native';
import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme';
import { BoxModel } from '../../../styles';
const HEADER_HEIGHT = 48;
export const HEADER_PADDING = BoxModel.padding / 2;
ColorSchemeRegistry.register('Header', {
/**
* Style of a disabled button in the header (e.g. Next).
*/
disabledButtonText: {
opacity: 0.6
},
/**
* Platform specific header button (e.g. back, menu, etc).
*/
headerButtonIcon: {
alignSelf: 'center',
color: schemeColor('icon'),
fontSize: 22,
marginRight: 12,
padding: 8
},
headerButtonText: {
color: schemeColor('text'),
fontSize: 20
},
/**
* Style of the header overlay to cover the unsafe areas.
*/
headerOverlay: {
backgroundColor: schemeColor('background')
},
/**
* Generic style for a label placed in the header.
*/
headerText: {
color: schemeColor('text'),
fontSize: 18
},
headerTextWrapper: {
alignItems: 'center',
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0
},
/**
* The top-level element of a page.
*/
page: {
...StyleSheet.absoluteFillObject,
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
overflow: 'hidden'
},
/**
* Base style of Header.
*/
screenHeader: {
alignItems: 'center',
backgroundColor: schemeColor('background'),
flexDirection: 'row',
height: HEADER_HEIGHT,
justifyContent: 'space-between',
paddingHorizontal: BoxModel.padding,
paddingVertical: HEADER_PADDING
},
statusBar: schemeColor('statusBar'),
statusBarContent: schemeColor('statusBarContent')
});

View File

@ -5,88 +5,13 @@ import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
const AVATAR_OPACITY = 0.4;
const HEADER_COLOR = ColorPalette.blue;
const HEADER_HEIGHT = 48;
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const AVATAR_SIZE = 65;
export const HEADER_PADDING = BoxModel.padding / 2;
export const STATUSBAR_COLOR = ColorPalette.blueHighlight;
export const SIDEBAR_WIDTH = 250;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
const HEADER_STYLES = {
disabledButtonText: {
opacity: 0.6
},
/**
* Platform specific header button (e.g. back, menu, etc).
*/
headerButtonIcon: {
alignSelf: 'center',
color: ColorPalette.white,
fontSize: 22,
marginRight: 12,
padding: 8
},
headerButtonText: {
color: ColorPalette.white,
fontSize: 20
},
/**
* Style of the header overlay to cover the unsafe areas.
*/
headerOverlay: {
backgroundColor: HEADER_COLOR
},
/**
* Generic style for a label placed in the header.
*/
headerText: {
color: ColorPalette.white,
fontSize: 18
},
headerTextWrapper: {
alignItems: 'center',
justifyContent: 'center',
left: 0,
position: 'absolute',
right: 0
},
/**
* The top-level element of a page.
*/
page: {
...StyleSheet.absoluteFillObject,
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
overflow: 'hidden'
},
/**
* Base style of Header.
*/
screenHeader: {
alignItems: 'center',
backgroundColor: HEADER_COLOR,
flexDirection: 'row',
height: HEADER_HEIGHT,
justifyContent: 'space-between',
paddingHorizontal: BoxModel.padding,
paddingVertical: HEADER_PADDING
}
};
/**
* Style classes of the PagedList-based components.
*/
@ -355,7 +280,6 @@ export const TINTED_VIEW_DEFAULT = {
* base/react.
*/
export default createStyleSheet({
...HEADER_STYLES,
...PAGED_LIST_STYLES,
...SECTION_LIST_STYLES,
...SIDEBAR_STYLES

View File

@ -23,6 +23,12 @@ const HEX_SHORT_COLOR_FORMAT
*/
const RGB_COLOR_FORMAT = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/i;
/**
* RegExp pattern for RGBA color format.
*/
const RGBA_COLOR_FORMAT
= /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*([0-9.]+)\)$/i;
/**
* The list of the well-known style properties which may not be numbers on Web
* but must be numbers on React Native.
@ -136,6 +142,23 @@ export function getRGBAFormat(color: string, alpha: number): string {
return color;
}
/**
* Decides if a color is light or dark based on the ITU-R BT.709 and W3C
* recommendations.
*
* NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef.
*
* @param {string} color - The color in rgb, rgba or hex format.
* @returns {boolean}
*/
export function isDarkColor(color: string): boolean {
const rgb = _getRGBObjectFormat(color);
return ((_getColorLuminance(rgb.r) * 0.2126)
+ (_getColorLuminance(rgb.g) * 0.7152)
+ (_getColorLuminance(rgb.b) * 0.0722)) <= 0.179;
}
/**
* Converts an [0..1] alpha value into HEX.
*
@ -147,6 +170,67 @@ function _getAlphaInHex(alpha: number): string {
.padStart(2, '0');
}
/**
* Calculated the color luminance component for an individual color channel.
*
* NOTE: Please see https://www.w3.org/TR/WCAG20/#relativeluminancedef.
*
* @param {number} c - The color which we need the individual luminance
* for.
* @returns {number}
*/
function _getColorLuminance(c: number): number {
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}
/**
* Parses a color string into an object containing the RGB values as numbers.
*
* NOTE: Object properties are not alpha-sorted for sanity.
*
* @param {string} color - The color to convert.
* @returns {{
* r: number,
* g: number,
* b: number
* }}
*/
function _getRGBObjectFormat(color: string): {r: number, g: number, b: number} {
let match = color.match(HEX_LONG_COLOR_FORMAT);
if (match) {
return {
r: parseInt(match[1], 16) / 255.0,
g: parseInt(match[2], 16) / 255.0,
b: parseInt(match[3], 16) / 255.0
};
}
match = color.match(HEX_SHORT_COLOR_FORMAT);
if (match) {
return {
r: parseInt(`${match[1]}${match[1]}`, 16) / 255.0,
g: parseInt(`${match[2]}${match[2]}`, 16) / 255.0,
b: parseInt(`${match[3]}${match[3]}`, 16) / 255.0
};
}
match = color.match(RGB_COLOR_FORMAT) || color.match(RGBA_COLOR_FORMAT);
if (match) {
return {
r: parseInt(match[1], 10) / 255.0,
g: parseInt(match[2], 10) / 255.0,
b: parseInt(match[3], 10) / 255.0
};
}
return {
r: 0,
g: 0,
b: 0
};
}
/**
* Shims style properties to work correctly on native. Allows us to minimize the
* number of style declarations that need to be set or overridden for specific

View File

@ -9,7 +9,7 @@ import { updateSettings } from '../../base/settings';
* The type of the React {@code Component} props of
* {@link AbstractSettingsView}.
*/
type Props = {
export type Props = {
/**
* The default URL for when there is no custom URL set in the settings.
@ -47,15 +47,15 @@ type Props = {
*
* @abstract
*/
export class AbstractSettingsView extends Component<Props> {
export class AbstractSettingsView<P: Props> extends Component<P> {
/**
* Initializes a new {@code AbstractSettingsView} instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* @param {P} props - The React {@code Component} props to initialize
* the component.
*/
constructor(props: Props) {
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.

View File

@ -11,12 +11,14 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { translate } from '../../../base/i18n';
import { BackButton, Header, Modal } from '../../../base/react';
import {
AbstractSettingsView,
_mapStateToProps
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractSettingsView';
import { setSettingsViewVisible } from '../../actions';
import FormRow from './FormRow';
@ -25,12 +27,20 @@ import { normalizeUserInputURL } from '../../functions';
import styles from './styles';
import { HeaderLabel } from '../../../base/react/components/native';
type Props = AbstractProps & {
/**
* Color schemed style of the header component.
*/
_headerStyles: Object
}
/**
* The native container rendering the app settings page.
*
* @extends AbstractSettingsView
*/
class SettingsView extends AbstractSettingsView {
class SettingsView extends AbstractSettingsView<Props> {
_urlField: Object;
/**
@ -60,7 +70,7 @@ class SettingsView extends AbstractSettingsView {
onRequestClose = { this._onRequestClose }
presentationStyle = 'overFullScreen'
visible = { this.props._visible }>
<View style = { Header.pageStyle }>
<View style = { this.props._headerStyles.page }>
{ this._renderHeader() }
{ this._renderBody() }
</View>
@ -239,4 +249,19 @@ class SettingsView extends AbstractSettingsView {
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(SettingsView));

View File

@ -4,8 +4,9 @@ import React, { Component } from 'react';
import { Switch, TouchableWithoutFeedback, View } from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { translate } from '../../base/i18n';
import { Header, Text } from '../../base/react';
import { Text } from '../../base/react';
import { updateSettings } from '../../base/settings';
import styles, { SWITCH_THUMB_COLOR, SWITCH_UNDER_COLOR } from './styles';
@ -25,6 +26,11 @@ type Props = {
*/
t: Function,
/**
* Color schemed style of the header component.
*/
_headerStyles: Object,
/**
* The current settings from redux.
*/
@ -55,15 +61,14 @@ class VideoSwitch extends Component<Props> {
* @inheritdoc
*/
render() {
const { t, _settings } = this.props;
const { textStyle } = Header;
const { t, _headerStyles, _settings } = this.props;
return (
<View style = { styles.audioVideoSwitchContainer }>
<TouchableWithoutFeedback
onPress = { this._onStartAudioOnlyFalse }>
<View style = { styles.switchLabel }>
<Text style = { textStyle }>
<Text style = { _headerStyles.headerText }>
{ t('welcomepage.audioVideoSwitch.video') }
</Text>
</View>
@ -77,7 +82,7 @@ class VideoSwitch extends Component<Props> {
<TouchableWithoutFeedback
onPress = { this._onStartAudioOnlyTrue }>
<View style = { styles.switchLabel }>
<Text style = { textStyle }>
<Text style = { _headerStyles.headerText }>
{ t('welcomepage.audioVideoSwitch.audio') }
</Text>
</View>
@ -132,6 +137,7 @@ class VideoSwitch extends Component<Props> {
*/
export function _mapStateToProps(state: Object) {
return {
_headerStyles: ColorSchemeRegistry.get(state, 'Header'),
_settings: state['features/base/settings']
};
}

View File

@ -10,6 +10,7 @@ import {
} from 'react-native';
import { connect } from 'react-redux';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { translate } from '../../base/i18n';
import { Icon } from '../../base/font-icons';
import { MEDIA_TYPE } from '../../base/media';
@ -21,7 +22,10 @@ import {
} from '../../base/tracks';
import { SettingsView } from '../../settings';
import { AbstractWelcomePage, _mapStateToProps } from './AbstractWelcomePage';
import {
AbstractWelcomePage,
_mapStateToProps as _abstractMapStateToProps
} from './AbstractWelcomePage';
import { setSideBarVisible } from '../actions';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
@ -90,18 +94,17 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {ReactElement}
*/
render() {
const { buttonStyle, pageStyle } = Header;
const roomnameAccLabel = 'welcomepage.accessibilityLabel.roomname';
const { t } = this.props;
const { _headerStyles, t } = this.props;
return (
<LocalVideoTrackUnderlay style = { styles.welcomePage }>
<View style = { pageStyle }>
<View style = { _headerStyles.page }>
<Header style = { styles.header }>
<TouchableOpacity onPress = { this._onShowSideBar } >
<Icon
name = 'menu'
style = { buttonStyle } />
style = { _headerStyles.headerButtonIcon } />
</TouchableOpacity>
<VideoSwitch />
</Header>
@ -269,4 +272,19 @@ class WelcomePage extends AbstractWelcomePage {
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
*/
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
};
}
export default translate(connect(_mapStateToProps)(WelcomePage));