ANTD mobile源码分析 -- popover

最近的开发中要用到不少的各式各样的组件。可是发现ant design mobile(后面简称ANTDM)里不少的资源。因而就分析一下,学习学习。html

ANTDM直接使用了typescript,没有用ES2015,不过这不会是障碍,反而是学习typescript的一个好机会。基本上能够学的开源项目里比这个好的也很少。react

目录结构

Popover组件在:git

|
|--components
  |
  |--popover

咱们要分析的组件所有都在components这个目录下。github

在这个目录里还包含tests, demostyle。里面分别存放测试代码、实例和样式。其余的文件包括[component name]_native.tsx[component name].txs以及对应的index.native.tsxindex.tsx*,方便外部引入组件。typescript

计算点击组件的位置

这个是最核心的问题了!react-native

实现React Native的弹出菜单,须要达到在界面上的某个可点击组件上点击了以后,就能够在被点击的组件紧挨着的下方出现一个菜单(其余的计算,好比弹出菜单在左下、右下,左上,右上的位置计算暂时不提)。app

用户点击了哪一个组件(按钮),哪一个按钮的下面就出现一个菜单(View)。这就须要肯定点击组件的位置。源码分析

咱们看一下index.native.tsx这个文件。文件里基本上没几行代码,直接看render方法里返回的是MenuContext等。也就是,这个文件没实现什么pop over须要的什么东西。都在import里了:学习

import Menu, { MenuContext, MenuOptions, MenuOption, MenuTrigger }from 'react-native-menu';

因此ANTDM的源码分析到此为止。测试

咱们要跳到react-native-menu。咱们分析代码的方式就是无限递归,一直找到实现功能的代码为止。那么咱们就能够分析react-native-menu了。

react-native-menu

这个项目的写法也是很不一样。用的是比较老的ES5的React版本。github地址在这里

这个项目里不少的文件,各位能够后面慢慢看。咱们来看makeMenuContext.js

在这个项目里,除了index.js以外都是叫作makeXXX.js。里面都是HOC的实现方式。并且更加Trick的是HOC的前两个参数是ReactReactNative

回到makeMenuContext.js,在openMenu()这个方法里就有实现的方式。这就是咱们寻找代码递归跳出的地方。咱们来看一下实现方式:

openMenu(name) {
  const handle = ReactNative.findNodeHandle(this._menus[name].ref);
  UIManager.measure(handle, (x, y, w, h, px, py) => {
    this._menus[name].measurements = { x, y, w, h, px, py };

    this.setState({
      openedMenu: name,
      menuOptions: this._makeAndPositionOptions(name, this._menus[name].measurements),
      backdropWidth: this._ownMeasurements.w
    });

    this._activeMenuHooks = this._menus[name];
    this._activeMenuHooks && this._activeMenuHooks.didOpen();
  });
},

这里使用了UIManager,来自:

const {
    UIManager,
    TouchableWithoutFeedback,
    ScrollView,
    View,
    BackHandler
  } = ReactNative

用现代一点的写法的话就是:import { UIManager } from 'react-native';

使用的时候是这么用的:

const handle = ReactNative.findNodeHandle(this._menus[name].ref);
  UIManager.measure(handle, (x, y, w, h, px, py) => {
    // x, y, width, height, pageX, pageY
  });

measure()方法的回调里获得的就是该组件对于Screen的位置。还有其余的measureXXX()方法在这里能够看到。

measure获得的x,y,w,h,px,py是这个组件的左上角坐标(x,y)和宽、高。在这个measure方法里获得的px和py与这个组件的左上角坐标值同样。

注意:measure的时候,只有在原生视图完成绘制以后才会返回值。

因此,若是要快点获得一个组件在screen上的坐标值的话,那么能够这样:

<View onLayout={this.onLayout}>
  
</View>

// onLayout
onLayout() {
  const handle = ReactNative.findNodeHandle(this.refs.Container);
  UIManager.measure(handle, (x, y, w, h, px, py) => {
    this._ownMeasurements = {x, y, w, h, px, py};
  });
}

因此,在弹出菜单的组件上使用onLayoutprops获得它的位置。

注意

they(measureXXX方法) are not available on composite components that aren't directly backed by a native view.

大意是,若是组合组件的最外层不是一个原生view的话,measureXXX()方法是无法用的!!

那么measure方法的第一个参数,也就是measure的目标组件如何得到呢?代码在这里:const handle = ReactNative.findNodeHandle(this._menus[name].ref);。在findNodeHandle()方法的参数是组件的ref。那么,经过组件的ref能够获得组件的handle。在经过这个handle就能够来measure组件,获得这个组件的位置、宽高等数据。

到这里咱们就知道如何来算出触发组件的位置了。可是,这个直接使用UIManager的方法太复杂了。

基本上,组件能够直接调用measure方法。咱们来简单的实现一下这个弹出菜单的功能。

Reimplement

无论单词对错了。总之是重写一次。简化版的!为了篇幅足够长,我就把代码都贴出来了。哈哈~

/**
 * Created by Uncle Charlie, 2018/03/01
 * @flow
 */

import React from 'react';
import { TouchableOpacity, Text, View, StyleSheet } from 'react-native';

type Prop = {
  text: ?string,
  onPress: (e?: any) => void,
  styles?: { button: any, text: any },
};

export default class Button extends React.Component<Prop, {}> {
  static defaultProps = {
    text: 'Show Menu',
  };

  handlePress = () => {
    const { onPress } = this.props;

    if (!this.container) {
      console.error('container view is empty');
      return;
    }

    this.container.measure((x, y, w, h, px, py) => {
      console.log('===>measure', { x, y, w, h, px, py });
      onPress && onPress({ left: x, top: y + h });
    });
  };

  onLayout = () => {};

  render() {
    const { text, styles } = this.props;
    const wrapper =
      styles && styles.wrapper ? styles.wrapper : innerStyles.wrapper;
    return (
      <View
        style={wrapper}
        onLayout={this.onLayout}
        ref={container => (this.container = container)}
      >
        <TouchableOpacity onPress={this.handlePress}>
          <View>
            <Text>{text}</Text>
          </View>
        </TouchableOpacity>
      </View>
    );
  }
}

const innerStyles = StyleSheet.create({
  wrapper: {
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'green',
  },
});

这个简化版的实现思路就是:

  1. 点击按钮(TouchableOpacity)的时候measure按钮组件
  2. 把measure出来的按钮组件的位置做为参数发送给父组件
  3. 父组件在计算后的位置显示menu

measure

在measure组件以前,首先要得到这个组件的ref。

render() {
    // ...
    return (
      <View ref={container => (this.container = container)}
      >
      // ...
      </View>
    );
  }

获得的ref就是this.container

handlePress = () => {
    const { onPress } = this.props;

    if (!this.container) {
      console.error('container view is empty');
      return;
    }

    this.container.measure((x, y, w, h, px, py) => {
      console.log('===>measure', { x, y, w, h, px, py });
      onPress && onPress({ left: x, top: y + h });
    });
  };

在点击按钮以后开始measure。直接在得到的ref上调用measure方法就能够:this.container.measure。得到measure的结果以后,调用props传过来的方法onPress把须要用到的数据传过去。

绘制Menu

renderMenu = () => {
    const { top, left, open } = this.state;
    if (!open) {
      return null;
    }

    return (
      <View
        style={{
          position: 'absolute',
          left,
          top,
          width: 100,
          height: 200,
          backgroundColor: 'rgba(52, 52, 52, 0.8)',
        }}
      >
        <Text>Menu</Text>
      </View>
    );
  };

咱们要View显示在一个特定的位置的时候,须要在style里设置位置模式为position: 'absolute',也就是启用绝对定位。

上面的left、和top就是菜单的具体位置。宽、高暂时hard code了(简化版。。。)。

这样就一个popover,超级简化版的,就完成了。所有的代码在这里

最后

咱们在前文中说道过一个更好的得到触发组件的位置的方式,onLayout。这个方法是空的。各位能够试着完成这个方法,或者所有完成这个popover组件做为练习。

相关文章
相关标签/搜索