最近的開發中要用到很多的各式各樣的組件。但是發現ant design mobile(后面簡稱ANTDM)里很多的資源。於是就分析一下,學習學習。
ANTDM直接使用了typescript,沒有用ES2015,不過這不會是障礙,反而是學習typescript的一個好機會。基本上可以學的開源項目里比這個好的也不多。
目錄結構
Popover組件在:
|
|--components
|
|--popover
我們要分析的組件全部都在components這個目錄下。
在這個目錄里還包含tests, demo和style。里面分別存放測試代碼、實例和樣式。其他的文件包括[component name]_native.tsx和[component name].txs以及對應的index.native.tsx和index.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的前兩個參數是React和ReactNative。
回到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',
},
});
這個簡化版的實現思路就是:
- 點擊按鈕(
TouchableOpacity)的時候measure按鈕組件 - 把measure出來的按鈕組件的位置作為參數發送給父組件
- 父組件在計算后的位置顯示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組件作為練習。
