UNPKG

tuya-panel-kit

Version:

a functional component library for developing tuya device panels!

440 lines (425 loc) 11.7 kB
/* eslint-disable no-bitwise */ import React from 'react'; import PropTypes from 'prop-types'; import { View, Text, Image, ViewPropTypes, ColorPropType, StyleSheet } from 'react-native'; import { Rect } from 'react-native-svg'; import LinearGradient from '../gradient/linear-gradient'; import RadialGradient from '../gradient/radial-gradient'; import { SIZE_MAP, StyledBtnWrapper, StyledBtnContainer, StyledBtn, StyledBtnText, StyledBadge, StyledBadgeText, StyledIconFont, } from './styled'; import { RatioUtils } from '../../utils'; const { convertX: cx } = RatioUtils; class CircleBtn extends React.PureComponent { static propTypes = { /** * 按钮是否拉伸,跟随父容器 */ stretch: PropTypes.bool, /** * 按钮是否禁用 */ disabled: PropTypes.bool, /** * 按钮背景尺寸,默认为`noSet`,即默认不设置, * large -> 48, * normal -> 40, * small -> 32, */ size: PropTypes.oneOfType([ PropTypes.oneOf(['large', 'normal', 'small', 'noSet']), PropTypes.number, ]), /** * 按钮背景类型,默认为`normal`,即默认背景色为`transparent`, * 若为`primary`,则跟随主色 */ type: PropTypes.oneOf(['primary', 'normal']), /** * 按钮背景,默认为`null`,可为颜色值或渐变值 */ background: PropTypes.oneOfType([ColorPropType, PropTypes.object]), /** * 按钮内的文字内容 */ text: PropTypes.string, /** * 按钮内的文字是否只显示一行,即超出显示省略号,默认为`true` */ textSingleLine: PropTypes.bool, /** * 按钮内的文字排列方向,默认放置文字位于按钮底部 */ textDirection: PropTypes.oneOf(['left', 'top', 'right', 'bottom', 'center']), /** * 按钮内的 IconFont`名称` */ icon: PropTypes.string, /** * 按钮内的 IconFont `path` */ iconPath: PropTypes.string, /** * 按钮内的 IconFont 尺寸 */ iconSize: PropTypes.number, /** * 按钮内的 IconFont 颜色 */ iconColor: ColorPropType, /** * 按钮内的 image 图片资源路径 */ image: Image.propTypes.source, /** * 按钮内的 image 图片颜色 */ imageColor: ColorPropType, /** * 按钮内的 image 样式 */ imageStyle: ViewPropTypes.style, /** * 徽标字体内容,即按钮右上角徽标 */ badgeText: PropTypes.string, /** * 按钮内容的禁用透明度比例 */ disabledOpacity: PropTypes.number, /** * 按钮的样式 */ style: ViewPropTypes.style, /** * 最外层容器的样式 */ wrapperStyle: ViewPropTypes.style, /** * 按钮背景的边框值,安卓有瑕疵,暂时不用 */ border: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]), /** * 按钮内字体样式 */ textStyle: Text.propTypes.style, /** * 按钮内徽标容器的样式 */ badgeStyle: ViewPropTypes.style, /** * 按钮内徽标字体的样式 */ badgeTextStyle: Text.propTypes.style, /** * 按钮点击回调 */ onPress: PropTypes.func, /** * 按钮布局完毕回调 */ onLayout: PropTypes.func, /** * 是否使用 ART 实现版本 */ useART: PropTypes.bool, /** * 测试标志 */ textAccessibilityLabel: PropTypes.string, /** * 测试标志 */ badgeAccessibilityLabel: PropTypes.string, /** * 测试标志 */ badgeTextAccessibilityLabel: PropTypes.string, children: PropTypes.element, wrapperProps: PropTypes.any, }; static defaultProps = { stretch: false, border: true, size: 'noSet', type: 'normal', background: null, text: '', textSingleLine: true, textDirection: 'bottom', icon: undefined, iconPath: null, iconSize: null, iconColor: null, image: null, badgeText: '', style: {}, wrapperStyle: {}, textStyle: {}, badgeStyle: {}, badgeTextStyle: {}, disabled: false, disabledOpacity: 0.2, imageColor: null, imageStyle: null, onPress: () => {}, onLayout: () => {}, useART: false, textAccessibilityLabel: 'Button_Text', badgeAccessibilityLabel: 'Button_Badge', badgeTextAccessibilityLabel: 'Button_Badge_Text', children: null, wrapperProps: {}, }; constructor(props) { super(props); this.state = { borderLayout: null, iconLayout: null, }; this.badgePosition = null; } // TODO getBorderStyle = /* istanbul ignore next */ () => { const { border } = this.props; if (!border) { // border: false; border: 0; border: ''; border: undefined, mean no border return { borderWidth: 0 }; } if (this.state.borderLayout && this.state.badgeLayout) { const { x, y, width } = this.state.borderLayout; this.badgePosition = { left: x + width - this.state.badgeLayout.width / 2, top: y, }; } if (typeof border === 'boolean') { // border: true, use default return null; } else if (typeof border === 'number') { // border: numer, set width return { borderWidth: border }; } // border: string const borderParamsArray = border.split(' '); if (borderParamsArray.length === 1) { // only set width return { borderWidth: parseFloat(border) }; } if (borderParamsArray.length === 2) { // set width and style return { borderWidth: parseFloat(borderParamsArray[0]), borderStyle: borderParamsArray[1], }; } if (borderParamsArray.length === 3) { // set all params return { borderWidth: parseFloat(borderParamsArray[0]), borderStyle: borderParamsArray[1], borderColor: borderParamsArray[2], }; } }; getChild = () => { const { type, children, icon, image, iconPath, imageColor, iconSize, iconColor, disabled, disabledOpacity, imageStyle, size, useART, } = this.props; const childCount = React.Children.count(children); let hasChild = true; let child = null; if (childCount > 1) { /* istanbul ignore next */ throw new Error('only contain one elements'); } else if (childCount === 1) { child = children; } else if (icon || image || iconPath) { const cImageStyle = { resizeMode: 'stretch', tintColor: imageColor, }; let customIconSize = cx(40); if (SIZE_MAP[size]) { customIconSize = cx(SIZE_MAP[size]); } else if (typeof size === 'number') { customIconSize = size; } if (iconSize) { customIconSize = iconSize; } child = image ? ( <Image source={image} style={[cImageStyle, imageStyle]} /> ) : ( <StyledIconFont type={type} name={icon} d={iconPath} size={customIconSize} color={iconColor} useART={useART} /> ); } else { hasChild = false; child = this.renderText(); } child = React.cloneElement(child, { style: [child.props.style, { opacity: disabled ? disabledOpacity : 1 }], }); return { hasChild, child }; }; // TODO borderLayout = /* istanbul ignore next */ e => { this.setState({ borderLayout: e.nativeEvent.layout, }); }; iconLayout = e => { this.setState({ iconLayout: e.nativeEvent.layout, }); }; badgeLayout = e => { this.setState({ badgeLayout: e.nativeEvent.layout, }); }; renderBackground = () => { const { size, background } = this.props; if (typeof background === 'string') { return <View style={[StyleSheet.absoluteFill, { backgroundColor: background }]} />; } else if (background && typeof background === 'object' && background.stops) { const { width, height } = StyleSheet.flatten([style]); const dimension = typeof size === 'number' ? size : SIZE_MAP[size]; const style = { width: width || dimension, height: height || dimension }; const { x1 = '0%', y1 = '0%', x2 = '0%', y2 = '100%', stops } = background; if (Array.isArray(stops)) { return <RadialGradient style={style} stops={stops} />; } return ( <LinearGradient style={style} stops={stops} x1={x1} y1={y1} x2={x2} y2={y2}> <Rect x="0" y="0" {...style} /> </LinearGradient> ); } return null; }; renderText = textDirection => { const { disabled, text, textAccessibilityLabel, textSingleLine, textStyle } = this.props; return ( <StyledBtnText style={textStyle} disabled={disabled} accessibilityLabel={textAccessibilityLabel} numberOfLines={textSingleLine ? 1 : null} textDirection={textDirection} > {text} </StyledBtnText> ); }; renderButton = () => { const { stretch, type, size, disabled, badgeText, text, badgeStyle, badgeTextStyle, style, textDirection, badgeAccessibilityLabel, badgeTextAccessibilityLabel, ...otherProps } = this.props; // borderWidth bug on Andiord, so borderWidth must be 0, keep smile // const customBorder = this.getBorderStyle(); // badge if ( !this.badgePosition && this.state.iconLayout && // this.state.borderLayout && this.state.badgeLayout ) { const { iconLayout } = this.state; this.badgePosition = { left: iconLayout.x + iconLayout.width - this.state.badgeLayout.width / 2, top: iconLayout.y - this.state.badgeLayout.height / 2, }; } const customBadgeStyle = [ this.badgePosition && { left: this.badgePosition.left, top: this.badgePosition.top }, badgeStyle, ]; // text const { hasChild, child } = this.getChild(); const direction = !!text && (textDirection === 'left' || textDirection === 'right') ? 'row' : 'column'; const isTextBefore = ~['left', 'top'].indexOf(textDirection); return ( <StyledBtnContainer style={{ flexDirection: direction }} disabled={disabled} activeOpacity={0.6} stretch={stretch} {...otherProps} > {hasChild && !!text && isTextBefore ? this.renderText(textDirection) : null} <StyledBtn type={type} size={size} onLayout={this.iconLayout} style={style}> {this.renderBackground()} {child} </StyledBtn> {badgeText ? ( <StyledBadge style={customBadgeStyle} onLayout={this.badgeLayout} accessibilityLabel={badgeAccessibilityLabel} > <StyledBadgeText style={badgeTextStyle} accessibilityLabel={badgeTextAccessibilityLabel} > {badgeText} </StyledBadgeText> </StyledBadge> ) : null} {hasChild && !!text && !isTextBefore ? this.renderText(textDirection) : null} </StyledBtnContainer> ); }; render() { const { stretch, onLayout, wrapperStyle, wrapperProps = {} } = this.props; return ( <StyledBtnWrapper {...wrapperProps} style={wrapperStyle} onLayout={onLayout} stretch={stretch} > {this.renderButton()} </StyledBtnWrapper> ); } } export default CircleBtn;