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組件做爲練習。

相關文章
相關標籤/搜索