2017-09-09 13:55:23

React Native:安卓控件的iOS兼容(其一)

RN在控件的跨平台上做的并不好。在v0.30之后,RN不再有原生控件加入,甚至于在几个版本中删除了平台独占的数个控件。本系列记录一下博主在iOS上兼容安卓控件的一些办法。

伪造全局控件

RN通过Object.defineProperty导出内部组件,因此我们也可以用同样的方法,将指定组件伪造为自定义组件

import ReactNative from 'react-native'
Object.defineProperty(ReactNative, 'TouchableNativeFeedback', {
  get: function () {
    return CustormTouchableNativeFeedback
  }
})

循环引用

import ReactNative, { ActivityIndicator  } from 'react-native'

class IOSActivityIndicator extends Component<any, any> {
  constructor(props) {
    super(props)
  }

  render() {
    const { size, ...props } = this.props
    return (
      <ActivityIndicator  {...props} />
    )
  }
}

如果伪造组件时使用如上代码,那么渲染时很容易发现render函数被调用数百次,说明此时循环引用了。

此时需要改写为如下的组件导入形式:

import ReactNative from 'react-native'

const { ActivityIndicator } = ReactNative

伪造静态成员

以Image组件为例,它有好几个静态方法,我们知道静态方法和实例并没有关系,因此只需要遍历赋值一遍给新组件即可

for (const i in Image) CachedImageWrapper[i] = Image[i]

去除多余属性

以ActivityIndicator为例,在iOS的实现中,给size属性赋值时会直接抛错,而size在安卓中则是经常使用的参数,因此可以用对象散列操作符简单去除:

class IOSActivityIndicator extends Component<any, any> {
  constructor(props) {
    super(props)
  }

  render() {
    const { size, ...props } = this.props
    return (
      <ActivityIndicator  {...props} />
    )
  }
}

兼容ToolbarAndroid

ToolbarAndroid有一些特性,比如它可以自定义标题组件,因此比较麻烦。另外,图标和标题组件之间的距离也需要固定的,必须提前计算好并使用绝对布局,否则会造成标题组件覆盖了图标按钮。兼容的代码如下:

export default class ToolbarIOS extends React.Component<any, any> {
  constructor(props) {
    super(props)
  }

  renderActions({
    backgroundColor,
    shouldShow,
    shouldNotShow
  }) {
    const { onActionSelected, iconColor: color } = this.props

    return (
      <View style={{flexDirection: 'row', marginTop: 2, position: 'absolute', right: 0}}>
        {shouldShow.map((item, index) => {
          return (
            <TouchableOpacity key={index} onPress={() => onActionSelected(item.index)}>
              <View style={{padding: 15, width: 50, alignItems: 'center'}}>
                <global.Icons name={item.iconName} size={20} color={color} />
              </View>
            </TouchableOpacity>
          )
        })}
        {shouldNotShow.length !== 0 && (
          <TouchableOpacity onPress={() => this.actionSheet(shouldNotShow, onActionSelected)}>
            <View style={{padding: 15, width: 50, alignItems: 'center'}}>
              <global.Icons name='md-more' size={20} color={color} />
            </View>
          </TouchableOpacity>
        )}
      </View>
    )
  }

  actionSheet = (arr, onActionSelected) => {
    ActionSheetIOS.showActionSheetWithOptions({
      options: arr.map(item => item.title).concat('取消'),
      cancelButtonIndex: arr.length,
      title: '选择操作'
    }, (index) => {
      if (index !== arr.length) {
        onActionSelected(arr[index].index)
      }
    })
  }

  render() {
    const { renderStatusBar, children,
      navIconName, onIconClicked, iconColor: color, title, actions = [], subtitle, subtitleColor } = this.props
    // const color = modeInfo.backgroundColor
    const style: any = StyleSheet.flatten(this.props.style)
    const shouldShow = actions.map((item, index) => (item.index = index, item)).filter(item => item.show !== 'never')
    const shouldNotShow = actions.map((item, index) => (item.index = index, item)).filter(item => item.show === 'never')
    const maxWidth = Dimensions.get('window').width - 20 - 15 - 30 - shouldShow.length * 50 - (shouldNotShow.length === 0 ? 0 : 1) * 50
    const childrenStyle: any = { zIndex: -1, position: 'absolute', left: 80, top: 3 }
    if (!subtitle) { delete childrenStyle.position; delete childrenStyle.left; delete childrenStyle.top; childrenStyle.marginLeft = -12 }
    return (
      <View style={{
        height: typeof renderStatusBar !== 'undefined' ? 44 : 44 + 20,
        width: style.width || undefined,
        backgroundColor: style.backgroundColor
      }}>
        <View style={{
          height: 44,
          marginTop: typeof renderStatusBar !== 'undefined'  ? 0 : 18,
          alignItems: 'center',
          flexDirection: 'row'
        }}>
          <TouchableOpacity onPress={onIconClicked}>
            <View style={{padding: 15, marginRight: 30, marginTop: 2, width: 50, alignItems: 'center'}}>
              <global.Icons name={navIconName} size={20} color={color}/>
            </View>
          </TouchableOpacity>
            { children || (
              <View style={[childrenStyle]}>
                <Text numberOfLines={1} ellipsizeMode='tail' style={{color, fontSize: 20,
                fontWeight: '500', maxWidth }}>{title}</Text>
                { subtitle && <Text numberOfLines={1} ellipsizeMode='tail' style={{color: subtitleColor || color, fontSize: 14,
                fontWeight: '300', maxWidth }}>{subtitle}</Text> || undefined }
              </View>
            )}
          {this.renderActions({
            backgroundColor: style.backgroundColor,
            shouldNotShow,
            shouldShow
          })}
        </View>
      </View>
    )
  }
}

本文链接:https://smallpath.me/post/react-native-android-control-to-ios-1

-- EOF --