1. 程式人生 > >popwindow 在安卓7.0上顯示位置錯誤以及在6.0下點選外部不能消失的情況解析

popwindow 在安卓7.0上顯示位置錯誤以及在6.0下點選外部不能消失的情況解析

popwindow在日常使用的過程中頻率很高 ,一般用起來也是得心應手,但是也是有很多坑存在的,在這個版本的迭代中就遇到了一些問題,解決起來很簡單,但是以後開發中肯定要注意的

popwindow 出現的View not attached to window manager

在這個版本的迭代中,產品要求了很多xx秒自動消失的需求,需求很簡單,洋洋灑灑寫了如下程式碼:

        PopupWindow popupWindow = new PopupWindow(context);
        View view = WorthbuyImageUtil.inflate(context,layoutId,null
); popupWindow.setContentView(view); popupWindow.setBackgroundDrawable(null); popupWindow.setWidth(DPIUtil.dip2px(302.5f)); popupWindow.setHeight(DPIUtil.dip2px(53)); popupWindow.setOutsideTouchable(false); // if (!((BaseActivity) context).isFinishing()) {
popupWindow.showAtLocation(anchorView, Gravity.BOTTOM, 0, DPIUtil.dip2px(60)); // } final Handler handler = new Handler(Looper.getMainLooper()); handler.postDelayed(new Runnable() { @Override![這裡寫圖片描述](http://img.blog.csdn.net/20170810194212157?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd3pseWQx/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)
public void run() { // if (!((BaseActivity) context).isFinishing() && popupWindow.isShowing()) { popupWindow.dismiss(); handler.removeCallbacks(this); // } } }, 3000); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (listener!=null){ listener.onClick(v); } } }); }

測試的時候都是ok的,畢竟等待事件不是很長,3s或者1s,但是上線後就出現了bug,雖然復現率很低,但是也要解決的,問題就在於,popwindow的顯示與消失是依賴於當前window的。引發這個的原因基本上都一致都是Dismiss對話方塊的時候,Activity已經不再存在。

所以問題的解決方式就很簡單了,就是新增上上面註釋掉的程式碼即可,在顯示或者消失的時候,判斷一下當前Activity是否已經存在,存在的話再做其他相應邏輯。

當然,除此之外,像Dialog也是需要依賴當前Activity的,平時開發也要注意

在Android7.0上popwindow顯示位置錯誤

這個版本需要彈一個Toast提示使用者自定義一些個性需求,但是這個需求這個Toast需要點選事件,本來一開始想的很簡單,new 一個TextView,然後給這個TextView設定點選監聽即可,但是洋洋灑灑寫完之後,並不生效。點選是沒有反應的。所以,以後別想著用Toast來處理點選事件了。建議用Snackbar。

當然,我用的popwindow。

本來一切都ok的,但是在安卓7.0 測試的時候,出現了問題,本來顯示的位置應該在螢幕的下方,但是在7.0手機上卻顯示在了上面。程式碼裡已經 設定了Gravity屬性,

popupWindow.showAtLocation(anchorView, Gravity.BOTTOM, 0, DPIUtil.dip2px(60));

其他手機沒有該問題。

然後通過調查後發現,是在安卓7.0上,update()方法寫錯了,不過這個bug在安卓7.1已經修復
安卓7.0原始碼如下:

public void update(int x, int y, int width, int height, boolean force) {
    if (width >= 0) {
        mLastWidth = width;
        setWidth(width);
    }

    if (height >= 0) {
        mLastHeight = height;
        setHeight(height);
    }

    if (!isShowing() || mContentView == null) {
        return;
    }

    final WindowManager.LayoutParams p =
            (WindowManager.LayoutParams) mDecorView.getLayoutParams();

    boolean update = force;

    final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
    if (width != -1 && p.width != finalWidth) {
        p.width = mLastWidth = finalWidth;
        update = true;
    }

    final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
    if (height != -1 && p.height != finalHeight) {
        p.height = mLastHeight = finalHeight;
        update = true;
    }

    if (p.x != x) {
        p.x = x;
        update = true;
    }

    if (p.y != y) {
        p.y = y;
        update = true;
    }

    final int newAnim = computeAnimationResource();
    if (newAnim != p.windowAnimations) {
        p.windowAnimations = newAnim;
        update = true;
    }

    final int newFlags = computeFlags(p.flags);
    if (newFlags != p.flags) {
        p.flags = newFlags;
        update = true;
    }

    final int newGravity = computeGravity();
    if (newGravity != p.gravity) {
        p.gravity = newGravity;
        update = true;
    }

    int newAccessibilityIdOfAnchor =
            (mAnchor != null) ? mAnchor.get().getAccessibilityViewId() : -1;
    if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) {
        p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor;
        update = true;
    }

    if (update) {
        setLayoutDirectionFromAnchor();
        mWindowManager.updateViewLayout(mDecorView, p);
    }
}

我們注意到 這個函式 computeGravity();跟蹤進去看

private int computeGravity() {
    int gravity = Gravity.START | Gravity.TOP;
    if (mClipToScreen || mClippingEnabled) {
        gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
    }
    return gravity;
}

可以明顯的看到,什麼鬼,原始碼把我們設定的gravity屬性給覆蓋了。只有Gravity.START | Gravity.TOP這個屬性了。所以問題顯而易見了。。
解決方式主要有兩種
1.不呼叫 update 方法即可
2.重寫 update 方法
最簡單是 dismiss,再調show
反射方法 把gravity那一段去掉
詳見 http://www.jianshu.com/p/0df10893bf5b
當然,在我們的需求裡面,只需要去掉update方法即可,沒必要反射這麼複雜。

popwindow在安卓6.0以下點選外部不消失情況

這個bug的出現還是很意外的,但是依然是google原始碼上的問題,我們知道,設定外部點選消失最簡單的方式就是設定 setOutsideTouchable(true);在開發初期都是ok的,因為手機都是6.0版本,包括我的測試機。

但是交給測試在測試相容性時,發現4.4版本的點選不能消失,很奇怪,以為是4.4獨有的,後來發現5.0也有,只有6.0以上版本沒有。所以解決問題,網上解決方案很多,但是很大一個原因都指向了popwindow的setBackgroundDrawable()方法。

是不是很奇怪,這個居然和點選消失掛鉤。

那我們看一下原始碼吧。

安卓4.4原始碼:

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || mContentView == null) {
            return;
        }

        registerForScrollChanged(anchor, xoff, yoff, gravity);

        mIsShowing = true;
        mIsDropdown = true;

        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        preparePopup(p);

        updateAboveAnchor(findDropDownPosition(anchor, p, xoff, yoff, gravity));

        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;

        p.windowAnimations = computeAnimationResource();

        invokePopup(p);
    }

跟蹤到preparePopup(p)這個方法,進去看一眼:
這裡寫圖片描述
注意到紅色方框區域,只有在backGround不為null的時候,才會建立一個叫PopViewContainer的類,而這個就是我們處理點選消失,按返回鍵消失的關鍵,跟蹤原始碼如下:

private class PopupViewContainer extends FrameLayout {
        private static final String TAG = "PopupWindow.PopupViewContainer";

        public PopupViewContainer(Context context) {
            super(context);
        }

        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            if (mAboveAnchor) {
                // 1 more needed for the above anchor state
                final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
                View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
                return drawableState;
            } else {
                return super.onCreateDrawableState(extraSpace);
            }
        }

        @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) {
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    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 dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @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);
            }
        }

        @Override
        public void sendAccessibilityEvent(int eventType) {
            // clinets are interested in the content not the container, make it event source
            if (mContentView != null) {
                mContentView.sendAccessibilityEvent(eventType);
            } else {
                super.sendAccessibilityEvent(eventType);
            }
        }
    }

}

所以原因就很明顯了,因為我並沒有設定它的backGround,因為沒這個必要,所以在安卓4.4上,就並沒有建立這個PopViewContainer類,所以事件處理機制失效,也就不能點選外部消失了。

在安卓6.0上,程式碼做了修改:

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.
        final int surfaceInset = (int) Math.ceil(mBackgroundView.getZ() * 2);
        p.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
        p.hasManualSurfaceInsets = true;

        mPopupViewInitialLayoutDirectionInherited =
                (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }

可以看到,在安卓6.0上無論 backGround是否為null,都會建立一個createDecorView的類,追蹤進去看:

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

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

        return decorView;
    }

會繼續建立一個 PopupDecorView,而這個類的功能,就和上面的PopupViewContainer 事件處理機制一樣了,所以,backGround是否有在安卓6.0以下不會影響點選外部是否消失邏輯。

哦了,這個版本遇到的問題先總結到這裡