• PopupWindow 点击外部和返回键无法消失背后的真相(setBackgroundDrawable(Drawable background))


    刚接手PopupWindow的时候,我们都可能觉得很简单,因为它确实很简单,不过运气不好的可能就会踩到一个坑:

    点击PopupWindow最外层布局以及点击返回键PopupWindow不会消失

    新手在遇到这个问题的时候可能会折腾半天,最后通过强大的网络找到一个解决方案,那就是跟PopupWindow设置一个背景

    popupWindow.setBackgroundDrawable(drawable),这个drawable随便一个什么类型的都可以,只要不为空。

    Demo地址:https://github.com/PopFisher/SmartPopupWindow

    下面从源码(我看的是android-22)上看看到底发生了什么事情导致返回键不能消失弹出框:

    先看看弹出框显示的时候代码showAsDropDown,里面有个preparePopup方法。

     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>Prepare the popup by embedding in into a new ViewGroup if the
         * background drawable is not null. If embedding is required, the layout
         * parameters' height is modified to take into account the background's
         * padding.</p>
         *
         * @param p the layout parameters of the popup's content view
         */
        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.");
            }
    
            if (mBackground != null) {
                final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
                int height = ViewGroup.LayoutParams.MATCH_PARENT;
                if (layoutParams != null &&
                        layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                    height = ViewGroup.LayoutParams.WRAP_CONTENT;
                }
    
                // when a background is available, we embed the content view
                // within another view that owns the background drawable
                PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
                PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT, height
                );
                popupViewContainer.setBackground(mBackground);
                popupViewContainer.addView(mContentView, listParams);
    
                mPopupView = popupViewContainer;
            } else {
                mPopupView = mContentView;
            }
    
            mPopupView.setElevation(mElevation);
            mPopupViewInitialLayoutDirectionInherited =
                    (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
            mPopupWidth = p.width;
            mPopupHeight = p.height;
        }

    上面可以看到mBackground不为空的时候,会PopupViewContainer作为mContentView的Parent,下面看看PopupViewContainer到底干了什么

        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) {  // 这个方法里面实现了返回键处理逻辑,会调用dismiss
                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);
                }
            }
        }

    看到上面红色部分的标注可以看出,这个内部类里面封装了处理返回键退出和点击外部退出的逻辑,但是这个类对象的构造过程中(preparePopup方法中)却有个mBackground != null的条件才会创建

    而mBackground对象在setBackgroundDrawable方法中被赋值,看到这里应该就明白一切了。

       /**
         * Specifies the background drawable for this popup window. The background
         * can be set to {@code null}.
         *
         * @param background the popup's background
         * @see #getBackground()
         * @attr ref android.R.styleable#PopupWindow_popupBackground
         */
        public void setBackgroundDrawable(Drawable background) {
            mBackground = background;
            // 省略其他的
        }
        

    setBackgroundDrawable方法除了被外部调用,构造方法中也会调用,默认是从系统资源中取的

        /**
         * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p>
         * 
         * <p>The popup does not provide a background.</p>
         */
        public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            mContext = context;
            mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
    
            final TypedArray a = context.obtainStyledAttributes(
                    attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
            final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
            mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
            mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
    
            final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, -1);
            mAnimationStyle = animStyle == R.style.Animation_PopupWindow ? -1 : animStyle;
    
            a.recycle();
    
            setBackgroundDrawable(bg);
        }

    有些版本没有,android6.0版本preparePopup如下:

        /**
         * Prepare the popup by embedding it into a new ViewGroup if the background
         * drawable is not null. If embedding is required, the layout parameters'
         * height is modified to take into account the background's padding.
         *
         * @param p the layout parameters of the popup's content view
         */
        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;
        }

    这里实现返回键监听的代码是mDecorView = createDecorView(mBackgroundView),这个并没有受到那个mBackground变量的控制,所以这个版本应该没有我们所描述的问题,感兴趣的可以自己去尝试一下

    分析到此为止

  • 相关阅读:
    Effective Java 19 Use interfaces only to define types
    Effective Java 18 Prefer interfaces to abstract classes
    Effective Java 17 Design and document for inheritance or else prohibit it
    Effective Java 16 Favor composition over inheritance
    Effective Java 15 Minimize mutability
    Effective Java 14 In public classes, use accessor methods, not public fields
    Effective Java 13 Minimize the accessibility of classes and members
    Effective Java 12 Consider implementing Comparable
    sencha touch SortableList 的使用
    sencha touch dataview 中添加 button 等复杂布局并添加监听事件
  • 原文地址:https://www.cnblogs.com/popfisher/p/5608717.html
Copyright © 2020-2023  润新知