Android 7.0 PopupWindow 又引入新的問題,Google工程師也不夠仔細么


Android7.0 PopupWindow的兼容問題

  Android7.0 中對 PopupWindow 這個常用的控件又做了一些改動,修復了以前遺留的一些問題的同時貌似又引入了一些問題,本文通過在7.0設備上實測並且結合源碼分析,帶你了解關於 PopupWindow 的相關改動。

  Android7.0 中下面兩個問題解決了,這里強調一下,不是說從 Android7.0 開始才解決這兩個問題的,因為具體版本細節沒去深究。可能在其他的某些版本下面的問題也是被解決了的。

  1. PopupWindow 不響應點擊外部消失和返回鍵消失的解決方法,博文地址:
    http://www.cnblogs.com/popfisher/p/5608717.html
  2. 不得不吐槽的 Android PopupWindow 的幾個痛點(實現帶箭頭的上下文菜單遇到的坑),博文地址:
    http://www.cnblogs.com/popfisher/p/5944054.html

Android7.0 中又引入了新的問題(這就非常的尷尬了)

  1. 調用 update 方法,PopupWindowGravity 會改變

從源碼看7.0怎么解決遺留問題的

  解決 PopupWindow 不響應點擊外部消失和返回鍵消失的問題,我們是通過自己設置一個背景。Android7.0 中不設置背景也是可以的,那么它的代碼肯定做了處理。從 api24 的源碼中找到 PopupWindow.java 文件,我找到里面的 preparePopup 方法如下:

private void preparePopup(WindowManager.LayoutParams p) {
    if (mContentView == null || mContext == null || mWindowManager == null) {
        throw new IllegalStateException("You must specify a valid content view by "
                + "calling setContentView() before attempting to show the popup.");
    }

    // The old decor view may be transitioning out. Make sure it finishes
    // and cleans up before we try to create another one.
    if (mDecorView != null) {
        mDecorView.cancelTransitions();
    }

    // When a background is available, we embed the content view within
    // another view that owns the background drawable.
    if (mBackground != null) {
        mBackgroundView = createBackgroundView(mContentView);
        mBackgroundView.setBackground(mBackground);
    } else {
        mBackgroundView = mContentView;
    }

    mDecorView = createDecorView(mBackgroundView);

    // The background owner should be elevated so that it casts a shadow.
    mBackgroundView.setElevation(mElevation);

    // We may wrap that in another view, so we'll need to manually specify
    // the surface insets.
    p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);

    mPopupViewInitialLayoutDirectionInherited =
            (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
}

  重點只需要看 mDecorView = createDecorView(mBackgroundView); 可以看到不管 mBackground 變量是否為空,最終都執行了這句代碼,這句代碼會多加一層 ViewGroupmBackgroundView 包進去了,里面應該包含了對返回鍵的處理邏輯,我們再看看 createDecorView 方法源碼:

private PopupDecorView createDecorView(View contentView) {
    final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
    final int height;
    if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
        height = WRAP_CONTENT;
    } else {
        height = MATCH_PARENT;
    }

    final PopupDecorView decorView = new PopupDecorView(mContext);
    decorView.addView(contentView, MATCH_PARENT, height);
    decorView.setClipChildren(false);
    decorView.setClipToPadding(false);

    return decorView;
}

  createDecorView 里面還是沒有直接看出對事件的處理,但是里面有個 PopupDecorView 類,應該在里面了吧,繼續看:

    private class PopupDecorView extends FrameLayout {
    //......有代碼被省略

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            if (getKeyDispatcherState() == null) {
                return super.dispatchKeyEvent(event);
            }

            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                final KeyEvent.DispatcherState state = getKeyDispatcherState();
                if (state != null) {
                    state.startTracking(event, this);
                }
                return true;
            } else if (event.getAction() == KeyEvent.ACTION_UP) {
                final KeyEvent.DispatcherState state = getKeyDispatcherState();
                if (state != null && state.isTracking(event) && !event.isCanceled()) {
                    dismiss();
                    return true;
                }
            }
            return super.dispatchKeyEvent(event);
        } else {
            return super.dispatchKeyEvent(event);
        }
    }

    //......有代碼被省略

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int x = (int) event.getX();
        final int y = (int) event.getY();

        if ((event.getAction() == MotionEvent.ACTION_DOWN)
                && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
            dismiss();
            return true;
        } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
            dismiss();
            return true;
        } else {
            return super.onTouchEvent(event);
        }
    }

	//......有代碼被省略
}

  從上面的代碼中我們看到了KeyEvent.KEYCODE_BACKMotionEvent.ACTION_OUTSIDE,沒錯這里有對返回鍵和其他事件的處理。

  至於怎么解決 showAsDropDown 方法彈出位置不對的問題,也就是上文中描述的第二個問題,本文就不貼源碼了,感興趣的可以下載源碼去看看,本文只是提供一種解決問題的思路,希望大家能從源碼中找到解決問題的辦法,這才是作者希望達到的效果。 文章末尾會給出 Android7.0 PopupWindow.java 的 java 文件。

Android7.0引入的新問題

  調用 update 方法時,PopupWindowGravity 會改變,導致位置發生了改變,具體看下圖:

showAtLocation傳入Gravity.Bottom:從屏幕底部對齊彈出

調用update方法更新第5點中彈出PopupWindow,發現PopupWindow的Gravity發生了改變

關於這個問題還有篇文章可以參考, http://www.jianshu.com/p/0df10893bf5b

Android7.0 PopupWindow其他改動點,與Android5.1的對比

主界面

1. PopupWindow高寬都設置為match_parent:7.0(左邊)從屏幕左上角彈出,5.1(右邊)從anchorView下方彈出

2. 寬度wrap_content-高度match_parent:7.0(左邊)從屏幕左上角彈出,5.1(右邊)從anchorView下方彈出

3. 寬度match_parent-高度wrap_content:都從anchorView下方彈出

4. 寬度wrap_content-高度大於anchorView到屏幕底部的距離:7.0與5.1都從anchorView上方彈出,與anchorView左對齊

源碼地址

Github工程地址,收錄了 PopupWindow 相關使用問題:
https://github.com/PopFisher/SmartPopupWindow

Android 7.0 PopupWindow.java 文件:
https://github.com/PopFisher/SmartPopupWindow/blob/master/sourcecode/PopupWindow(7.0).java

總結

  Android PopupWindow 這個控件 Google 一直沒有優化好,使用時需要參考我之前的幾篇文章。本文是希望讀者善於從源碼的角度去分析和解決問題,加深自己對源碼的理解,對問題的理解,這樣印象要深刻一些。

  本來2017年回來還沒有時間寫寫文章,這篇文章也是巧合,同事在 Android7.0 中發現 PopupWindow 使用上有 bug,所以我就借此機會研究一下,雖然知識點簡單,但是也花費了幾個小時的時間整理出這樣一篇文章。如果讀者覺得有用,別忘記點擊推薦哦,總之也算是開了一個好頭吧,以后還是會堅持每個月寫些文章出來分享。


免責聲明!

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



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