ANTD mobile源碼分析 -- popover


最近的開發中要用到很多的各式各樣的組件。但是發現ant design mobile(后面簡稱ANTDM)里很多的資源。於是就分析一下,學習學習。

ANTDM直接使用了typescript,沒有用ES2015,不過這不會是障礙,反而是學習typescript的一個好機會。基本上可以學的開源項目里比這個好的也不多。

目錄結構

Popover組件在:

|
|--components
  |
  |--popover

我們要分析的組件全部都在components這個目錄下。

在這個目錄里還包含tests, demostyle。里面分別存放測試代碼、實例和樣式。其他的文件包括[component name]_native.tsx[component name].txs以及對應的index.native.tsxindex.tsx*,方便外部引入組件。

計算點擊組件的位置

這個是最核心的問題了!

實現React Native的彈出菜單,需要達到在界面上的某個可點擊組件上點擊了之后,就可以在被點擊的組件緊挨着的下方出現一個菜單(其他的計算,比如彈出菜單在左下、右下,左上,右上的位置計算暫時不提)。

用戶點擊了哪個組件(按鈕),哪個按鈕的下面就出現一個菜單(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組件作為練習。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM