????9.0App的時(shí)候很多布局的大小都是通過(guò)計(jì)算屏幕寬高煤辨、狀態(tài)欄高度裳涛、工具欄高度等計(jì)算得到的,這種計(jì)算方法在可動(dòng)態(tài)顯示或隱藏虛擬按鍵的Android手機(jī)的顯示上會(huì)有問(wèn)題众辨,最典型的有以下情況:
1.虛擬鍵顯示時(shí)端三,打開(kāi)全屏界面(如掃碼、批注等界面)鹃彻,隱藏虛擬鍵未填充界面郊闯,會(huì)留白。
2.先隱藏虛擬鍵再展開(kāi)時(shí),app底部或者右側(cè)可能會(huì)被遮擋团赁。
????一般采用自適應(yīng)寬高的View不受影響育拨,比如設(shè)置flex:1,這類(lèi)視圖會(huì)根據(jù)屏幕有效區(qū)域自動(dòng)刷新欢摄。
????但某些布局會(huì)有特殊要求熬丧,使用具體的寬高作為屬性,就需要考慮到虛擬鍵的因素怀挠。
解決方法
解決思路:
- 首先原生監(jiān)聽(tīng)虛擬按鍵的變化析蝴,計(jì)算出去除虛擬鍵尺寸后屏幕的可用寬高,然后將寬高發(fā)送給RN端绿淋。
-
RN端更新實(shí)際可用的屏幕尺寸闷畸,最后去通知各個(gè)需要適配虛擬鍵的頁(yè)面進(jìn)行更新。
去除虛擬鍵的區(qū)域
- 獲得實(shí)際虛擬鍵的高度
/**
* 獲取虛擬鍵高度(無(wú)論是否隱藏)
* @param context
* @return
*/
public static int getNavigationBarHeight(Context context){
int result = 0;
int resourceId = context.getResources().getIdentifier("navigation_bar_height","dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
/**
* 虛擬按鍵是否打開(kāi)
* @param activity
* @return
*/
public static boolean isNavigationBarShown(Activity activity){
//虛擬鍵的view,為空或者不可見(jiàn)時(shí)是隱藏狀態(tài)
View view = activity.findViewById(android.R.id.navigationBarBackground);
if(view == null){
return false;
}
int visible = view.getVisibility();
if(visible == View.GONE || visible == View.INVISIBLE){
return false ;
}else{
return true;
}
}
/**
* 獲取當(dāng)前虛擬鍵高度(隱藏后高度為0)
* @param activity
* @return
*/
public static int getCurrentNavigationBarHeight(Activity activity){
if(isNavigationBarShown(activity)){
return getNavigationBarHeight(activity);
}else{
return 0;
}
}
- 計(jì)算屏幕可用寬高(去除虛擬鍵顯示高度)
/**
* 獲取可用屏幕高度躬它,排除虛擬鍵
* @param context 上下文
* @return 返回高度
*/
public static int getContentHeight(Activity context){
int contentHeight;
if(isPortrait(context)){
contentHeight = getRealScreenHeight(context) - getCurrentNavigationBarHeight(context);
}else{
contentHeight = getRealScreenHeight(context);
}
return contentHeight;
}
/**
* 獲取可用屏幕寬度腾啥,無(wú)論是否有虛擬鍵
* @param context 上下文
* @return 返回高度
*/
public static int getContentWidth(Activity context){
int contentWidth;
if(isLandScape(context)){
contentWidth = getRealScreenWidth(context) - getCurrentNavigationBarHeight(context);
}else{
contentWidth = getRealScreenWidth(context);
}
return contentWidth;
}
- 監(jiān)聽(tīng)虛擬鍵的變化,并將最新屏幕可用寬高傳給RN
//監(jiān)聽(tīng)虛擬鍵變化
@Override
public void onGlobalLayout() {
if (!mLayoutComplete){
return;
}
Activity activity = getCurrentActivity();
if(activity == null){
return;
}
//嚴(yán)格保證是虛擬鍵的變化引起的onGlobalLayout時(shí)才去通知RN
if(IFAppUtils.isNavigationShow() != DeviceUtils.isNavigationBarShown(activity)){
updateContentSize();
}
IFAppUtils.setIsNavigationShow(DeviceUtils.isNavigationBarShown(activity));
}
//發(fā)送消息給RN
private void updateContentSize(boolean navigationBarChanged){
WritableMap writableMap = Arguments.createMap();
writableMap.putInt("width", getCurrentActivity() == null ? 0 : DeviceUtils.getContentWidth(getCurrentActivity()));
writableMap.putInt("height", getCurrentActivity() == null ? 0 : DeviceUtils.getContentHeight(getCurrentActivity()));
writableMap.putBoolean("navigationBarChanged", navigationBarChanged);
context.getJSModule(RCTNativeAppEventEmitter.class).emit(SCREENSIZE_CHANGE, writableMap);
}
4.RN端需要適配虛擬鍵的按鈕要先注冊(cè)監(jiān)聽(tīng):
addScreenSizeChangeListener(key, listener){
if(deviceType.android()){
screenChangeListenerMap.set(key,listener);
}
},
removeScreenSizeChangeListener(key) {
if(deviceType.android()) {
screenChangeListenerMap && screenChangeListenerMap.delete(key);
}
},
5.RN端接收虛擬鍵變化的消息冯吓,然后去通知所有注冊(cè)過(guò)的頁(yè)面去刷新布局倘待。
if(platform.isAndroidPlatform()){
let eventEmitter = new NativeEventEmitter(NativeModules.FCTScreenManager);
eventEmitter.addListener(SCREENSIZE_CHANGE,(sizeInfo)=>{
let newWidth = sizeInfo.width / PixelRatio.get();
let newHeight = sizeInfo.height / PixelRatio.get();
let navigationBarChanged = sizeInfo.navigationBarChanged;
//布局改變,更新布局
screenWidth = newWidth;
screenHeight = newHeight;
//是虛擬鍵引起的布局變化
if(navigationBarChanged){
screenChangeListenerMap.forEach((value, key)=>{
let listener = screenChangeListenerMap.get(key);
listener && listener();
});
}
});
}
6.刷新布局
componentDidMount(){
Device.addScreenSizeChangeListener(GUIDE, ()=>{
this._guideUI.forceUpdate();
});
}
componentWillUnmount() {
Device.removeScreenSizeChangeListener(GUIDE);
}
通過(guò)上述方法组贺,可以適配絕大多數(shù)頁(yè)面的虛擬鍵問(wèn)題凸舵。但仍然有一些特殊情況需要特殊處理。
特殊情況1:
????在CommonView/OfflinePage(常用頁(yè)/離線頁(yè))展開(kāi)虛擬鍵失尖,列表被擠壓成兩列啊奄。因?yàn)槊宽?xiàng)的寬度是屏幕寬度的1/3,虛擬鍵展開(kāi)后顯示不下被擠壓成兩行。
??????頁(yè)面添加了虛擬鍵監(jiān)聽(tīng)掀潮,虛擬鍵變化后執(zhí)行了forceUpdate()方法菇夸,重新計(jì)算每個(gè)item寬度進(jìn)行繪制,但此時(shí)列表并沒(méi)有重新繪制:
componentDidMount() {
Device.addScreenSizeChangeListener(OFFLINE_PAGE, ()=>{
//重新渲染
this.forceUpdate();
})
}
??????幾經(jīng)調(diào)試之后仪吧,發(fā)現(xiàn)ListView會(huì)判斷每行當(dāng)前數(shù)據(jù)和之前的數(shù)據(jù)是否有相同庄新,如果完全相同,則不會(huì)重新執(zhí)行renderRow()方法薯鼠。為了解決此問(wèn)題择诈,需要重新定義ListView的DataSource:
static getNewDataSource(): Object {
return new SwipeableListViewDataSource({
getRowData: (data, sectionID, rowID) => data[sectionID][rowID],
getSectionHeaderData: (data, sectionID) => data[sectionID],
//之前是row1!==row2
rowHasChanged: (row1, row2) => true,
sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
}
<List
ref={ref => this._view = ref}
ds={OfflinePage.getNewDataSource()}
/>
特殊情況2:
????pad的二級(jí)目錄頁(yè),同樣在虛擬鍵變化之后執(zhí)行forceUpdate()方法刷新目錄的整個(gè)控件出皇,但刷新之后羞芍,目錄的位置會(huì)出現(xiàn)偏差,可能左右偏移郊艘。
????調(diào)試發(fā)現(xiàn)pad二級(jí)目錄初始位置在屏幕外荷科,展示是通過(guò)動(dòng)畫(huà)移動(dòng)到屏幕顯示區(qū)域唯咬,最終從屏幕外展示到屏幕內(nèi)。動(dòng)畫(huà)的最終位置計(jì)算是通過(guò)屏幕尺寸等一系列參數(shù)進(jìn)行計(jì)算的步做。由于屏幕的尺寸在虛擬鍵變化后發(fā)生了變化副渴,動(dòng)畫(huà)的最終位置計(jì)算不準(zhǔn),導(dǎo)致目錄頁(yè)顯示位置有偏差全度。
????解決方法是,在二級(jí)目錄展示并且目錄更新之后斥滤,需要重新執(zhí)行動(dòng)畫(huà)調(diào)整目錄最終的位置将鸵。
componentDidUpdate() {
if(this.state.showSide){
this._subView.transitionTo({
translateX: - (this._width - this._rowWidth)
}, 0)
}
}
關(guān)于報(bào)表頁(yè)面的虛擬鍵適配:
問(wèn)題1:
??????放大報(bào)表時(shí),虛擬鍵變化佑颇,RN計(jì)算報(bào)表的縮放系數(shù)傳給原生重新繪制顶掉,導(dǎo)致報(bào)表恢復(fù)原本大小。
解決方法:
????判斷是否是虛擬鍵變化導(dǎo)致的報(bào)表重新繪制挑胸,并將該字段傳遞給原生代碼痒筒。
Device.addScreenSizeChangeListener(SINGLE_PAGE_VIEW,()=>{
this.causeByNavigationBarChanged = true;
});
UIManager.dispatchViewManagerCommand(
ReactNative.findNodeHandle(this.reportRef),
UIManager.FCTReportView.Commands.setGridZoomScale,
paramsArray
);
????原生如果是虛擬鍵導(dǎo)致的放大系數(shù)改變,則只保存放大縮放茬贵,不會(huì)真的進(jìn)行縮放簿透。如果不是虛擬鍵導(dǎo)致的放大系數(shù)改變,則進(jìn)行縮放解藻。
public void setGridZoomScale(float scale, float maxScale, float minScale, boolean causeByNavigationBarChanged) {
this.maxScale = maxScale;
this.minScale = minScale;
//虛擬鍵變化導(dǎo)致縮放系數(shù)的變化時(shí)不進(jìn)行縮放老充,只保存縮放系數(shù)
if(!causeByNavigationBarChanged){
scaleReport(scale);
}
}
問(wèn)題2:
????部分Android手機(jī)虛擬按鍵隱藏時(shí),打開(kāi)鍵盤(pán)會(huì)順便帶出虛擬按鍵螟左。報(bào)表點(diǎn)擊輸入框時(shí)會(huì)出現(xiàn)情況是:每次點(diǎn)擊輸入框控件鍵盤(pán)剛彈出來(lái)就會(huì)隱藏啡浊。
- 點(diǎn)擊輸入框控件,鍵盤(pán)彈出帶出虛擬按鍵
- 報(bào)表被虛擬按鍵擠壓胶背,需要調(diào)整大小進(jìn)行刷新巷嚣。
- 報(bào)表刷新導(dǎo)致輸入框控件失去焦點(diǎn),鍵盤(pán)隱藏钳吟。
解決方法:
????對(duì)鍵盤(pán)的顯示和隱藏進(jìn)行監(jiān)聽(tīng):在鍵盤(pán)打開(kāi)時(shí)廷粒,無(wú)論虛擬鍵是否變化,都不會(huì)調(diào)整報(bào)表大小砸抛。這樣也就避免了因?yàn)閳?bào)表的刷新導(dǎo)致的焦點(diǎn)失去评雌、鍵盤(pán)隱藏的問(wèn)題。只有鍵盤(pán)隱藏是直焙,才會(huì)根據(jù)虛擬鍵的變化調(diào)整報(bào)表大小景东。
//SinglePageView
_onLayout(e) {
const layout = e.nativeEvent.layout;
const {width, height} = layout;
//fixme MOBILE-8163 安卓虛擬按鍵會(huì)導(dǎo)致containerSize產(chǎn)生輕微變化,
// 尤其是小米 OPPO部分全面屏機(jī)型在隱藏虛擬按鍵的情況下奔誓,填報(bào)時(shí)輸入法會(huì)把虛擬按鍵帶出來(lái)導(dǎo)致height變小斤吐,觸發(fā)重新布局搔涝,重新發(fā)送options給原生,導(dǎo)致焦點(diǎn)立即失去和措,無(wú)法填報(bào)
// 補(bǔ)充:如果鍵盤(pán)處于彈出狀態(tài)庄呈,虛擬鍵變化時(shí),不觸發(fā)重新布局派阱。這個(gè)不影響MOBILE-8463中鍵盤(pán)彈出帶出虛擬鍵的情況诬留,同時(shí)解決報(bào)表不能根據(jù)虛擬鍵的變化進(jìn)行自適應(yīng)的情況。
if (Device.isAndroidPlatform() && this.orientation === orientation && this.keyboardDidShow) {
this.causeByNavigationBarChanged = false;
return;
}
this.setState({
containerSize: {width, height}
});
}
問(wèn)題2.1:
????解決問(wèn)題2的關(guān)鍵是監(jiān)聽(tīng)鍵盤(pán)的變化贫母。Android鍵盤(pán)是通過(guò)監(jiān)聽(tīng)布局被擠壓文兑,然后通過(guò)計(jì)算來(lái)獲得鍵盤(pán)彈出和鍵盤(pán)隱藏的事件的。
????但在橫屏的時(shí)候腺劣,鍵盤(pán)在全新的界面彈出绿贞,不會(huì)擠壓主布局,暫時(shí)無(wú)法監(jiān)聽(tīng)到鍵盤(pán)的彈出和隱藏事件橘原。
解決方法:
????暫時(shí)通過(guò)監(jiān)聽(tīng)輸入框焦點(diǎn)的變化來(lái)判斷鍵盤(pán)狀態(tài)籍铁。(此方法有bug的情況:橫屏->點(diǎn)擊輸入框->點(diǎn)鍵盤(pán)的回收按鈕隱藏鍵盤(pán),輸入框仍然有焦點(diǎn)->此時(shí)會(huì)被誤認(rèn)為鍵盤(pán)時(shí)彈出狀態(tài)趾断,報(bào)表不會(huì)根據(jù)虛擬鍵的變化伸縮)
onReceiveMessage({row, column, data = {}}) {
//Fixme 這里臨時(shí)處理android橫屏虛擬鍵問(wèn)題拒名,通過(guò)輸入框的焦點(diǎn)來(lái)判斷鍵盤(pán)是否打開(kāi),可以保證橫屏填報(bào)可用歼冰。但還存在隱藏鍵盤(pán)后報(bào)表的放大系數(shù)不對(duì)的問(wèn)題靡狞。
if(Device.android() && Orientation.isLandscape()){
this.resolveTextFocusEvent(data);
}
}
resolveTextFocusEvent (data){
//輸入框有焦點(diǎn)就認(rèn)為鍵盤(pán)彈出了
if(data.onTextFocus === true){
this.keyboardDidShow = true;
}else if(data.onTextFocus === false){
this.keyboardDidShow = false;
}
}