美文网首页Android资源收录Android效果/自定义
【Android】PopupWindow中使用Spinner出错

【Android】PopupWindow中使用Spinner出错

作者: 紫豪 | 来源:发表于2016-12-28 15:36 被阅读637次

    1.问题背景

    示例效果.png
      如图,各位看到该效果时,第一反应是使用什么方式实现呢?相信有一部分童鞋会尝试使用PopupWindow+Spinner的方式来实现该效果,而Spinner控件的SpinnerMode属性默认是dialog样式,展现的显示效果不是很难理想,这时,为了更方便的达到与上图一致的效果,有人会尝试去修改SpinnerMode为dropDown,我所认识的一位童鞋就是这样操作的,就这样。。。在点击Spinner准备弹出下拉列表时,异常出现了。。。

    错误日志如下:

    FATAL EXCEPTION: main
       Process: com.zihao.spinnerdemo, PID: 28778
       android.view.WindowManager$BadTokenException: Unable to add window -- token android.view.ViewRootImpl$W@263c5a2 is not valid; is your activity running?
           at android.view.ViewRootImpl.setView(ViewRootImpl.java:567)
           at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
           at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
           at android.widget.PopupWindow.invokePopup(PopupWindow.java:1258)
           at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1110)
           at android.widget.ListPopupWindow.show(ListPopupWindow.java:658)
           at android.widget.Spinner$DropdownPopup.show(Spinner.java:1223)
           at android.widget.Spinner.performClick(Spinner.java:758)
           at android.support.v7.widget.AppCompatSpinner.performClick(AppCompatSpinner.java:438)
           at android.view.View$PerformClick.run(View.java:21153)
           at android.os.Handler.handleCallback(Handler.java:739)
           at android.os.Handler.dispatchMessage(Handler.java:95)
           at android.os.Looper.loop(Looper.java:148)
           at android.app.ActivityThread.main(ActivityThread.java:5417)
           at java.lang.reflect.Method.invoke(Native Method)
           at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
           at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
    
    

    产生背景:在PopupWindow中使用Spinner且SpinnerMode="dropDown".


    2.错误原因分析

    通过对问题产生背景的描述,我们可以很轻松的定位到问题产生的主要原因——即SpinnerMode为dropDown时引发的该异常(但是在SpinnerMode值为dialog时则会正常弹出下拉列表弹窗),那么SpinnerMode="dialog"与SpinnerMode="dropDown"究竟有什么不同?下面我们来看下Spinner源码,来仔细区分下两者的区别。

    • Spinner.java中关于下拉列表显示方式(样式)处理
    /**
     * Constructs a new spinner with the given context, the supplied attribute
     * set, default styles, popup mode (one of {@link #MODE_DIALOG} or
     * {@link #MODE_DROPDOWN}), and the theme against which the popup should be
     * inflated.
     *
     * @param context The context against which the view is inflated, which
     *                provides access to the current theme, resources, etc.
     * @param attrs The attributes of the XML tag that is inflating the view.
     * @param defStyleAttr An attribute in the current theme that contains a
     *                     reference to a style resource that supplies default
     *                     values for the view. Can be 0 to not look for
     *                     defaults.
     * @param defStyleRes A resource identifier of a style resource that
     *                    supplies default values for the view, used only if
     *                    defStyleAttr is 0 or can not be found in the theme.
     *                    Can be 0 to not look for defaults. * @param mode Constant describing how the user will select choices from
     *             the spinner.
     * @param popupTheme The theme against which the dialog or dropdown popup
     *                   should be inflated. May be {@code null} to use the
     *                   view theme. If set, this will override any value
     *                   specified by
     *                   {@link android.R.styleable#Spinner_popupTheme}.
     *
     * @see #MODE_DIALOG // SpinnerMode="dialog"
     * @see #MODE_DROPDOWN // SpinnerMode="dropDown"
     */
    public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode,
            Theme popupTheme) {
        super(context, attrs, defStyleAttr, defStyleRes);
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
        if (popupTheme != null) {
            mPopupContext = new ContextThemeWrapper(context, popupTheme);
        } else {
            final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
            if (popupThemeResId != 0) {
                mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
            } else {
                mPopupContext = context;
            }
        }
        if (mode == MODE_THEME) {
            mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG);
        }
        switch (mode) {
            case MODE_DIALOG: {// 当SpinnerMode为dialog时
                mPopup = new DialogPopup();
                mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt));
                break;
            }
            case MODE_DROPDOWN: {// 当SpinnerMode为dropDown时
                // 在这里新建了一个DropdownPopup用来展示下拉列表的内容,关于DropdownPopup源码请看下一片段
                final DropdownPopup popup = new DropdownPopup(
                        mPopupContext, attrs, defStyleAttr, defStyleRes);
                final TypedArray pa = mPopupContext.obtainStyledAttributes(
                        attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
                mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_dropDownWidth,
                        ViewGroup.LayoutParams.WRAP_CONTENT);
                if (pa.hasValueOrEmpty(R.styleable.Spinner_dropDownSelector)) {
                    popup.setListSelector(pa.getDrawable(
                            R.styleable.Spinner_dropDownSelector));
                }
                popup.setBackgroundDrawable(pa.getDrawable(R.styleable.Spinner_popupBackground));
                popup.setPromptText(a.getString(R.styleable.Spinner_prompt));
                pa.recycle();
                mPopup = popup;
                mForwardingListener = new ForwardingListener(this) {
                    @Override
                    public ShowableListMenu getPopup() {
                        return popup;
                    }
                    @Override
                    public boolean onForwardingStarted() {
                        if (!mPopup.isShowing()) {
                            mPopup.show(getTextDirection(), getTextAlignment());
                        }
                        return true;
                    }
                };
                break;
            }
        }
        mGravity = a.getInt(R.styleable.Spinner_gravity, Gravity.CENTER);
        mDisableChildrenWhenDisabled = a.getBoolean(
                R.styleable.Spinner_disableChildrenWhenDisabled, false);
        a.recycle();
        // Base constructor can call setAdapter before we initialize mPopup.
        // Finish setting things up if this happened.
        if (mTempAdapter != null) {
            setAdapter(mTempAdapter);
            mTempAdapter = null;
        }
    }
    
    • Spinner.java中DropdownPopup类源码
    private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
        private CharSequence mHintText;
        private ListAdapter mAdapter;
        public DropdownPopup(
                Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            setAnchorView(Spinner.this);
            setModal(true);
            setPromptPosition(POSITION_PROMPT_ABOVE);
            setOnItemClickListener(new OnItemClickListener() {
                public void onItemClick(AdapterView parent, View v, int position, long id) {
                    Spinner.this.setSelection(position);
                    if (mOnItemClickListener != null) {
                        Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
                    }
                    dismiss();
                }
            });
        }
        @Override
        public void setAdapter(ListAdapter adapter) {
            super.setAdapter(adapter);
            mAdapter = adapter;
        }
        public CharSequence getHintText() {
            return mHintText;
        }
        public void setPromptText(CharSequence hintText) {
            // Hint text is ignored for dropdowns, but maintain it here.
            mHintText = hintText;
        }
        void computeContentWidth() {
            final Drawable background = getBackground();
            int hOffset = 0;
            if (background != null) {
                background.getPadding(mTempRect);
                hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
            } else {
                mTempRect.left = mTempRect.right = 0;
            }
            final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
            final int spinnerPaddingRight = Spinner.this.getPaddingRight();
            final int spinnerWidth = Spinner.this.getWidth();
            if (mDropDownWidth == WRAP_CONTENT) {
                int contentWidth =  measureContentWidth(
                        (SpinnerAdapter) mAdapter, getBackground());
                final int contentWidthLimit = mContext.getResources()
                        .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
                if (contentWidth > contentWidthLimit) {
                    contentWidth = contentWidthLimit;
                }
                setContentWidth(Math.max(
                       contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
            } else if (mDropDownWidth == MATCH_PARENT) {
                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
            } else {
                setContentWidth(mDropDownWidth);
            }
            if (isLayoutRtl()) {
                hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
            } else {
                hOffset += spinnerPaddingLeft;
            }
            setHorizontalOffset(hOffset);
        }
        public void show(int textDirection, int textAlignment) {
            final boolean wasShowing = isShowing();
            computeContentWidth();
            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
            super.show();
            final ListView listView = getListView();
            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            listView.setTextDirection(textDirection);
            listView.setTextAlignment(textAlignment);
            setSelection(Spinner.this.getSelectedItemPosition());
            if (wasShowing) {
                // Skip setting up the layout/dismiss listener below. If we were previously
                // showing it will still stick around.
                return;
            }
            // Make sure we hide if our anchor goes away.
            // TODO: This might be appropriate to push all the way down to PopupWindow,
            // but it may have other side effects to investigate first. (Text editing handles, etc.)
            final ViewTreeObserver vto = getViewTreeObserver();
            if (vto != null) {
                final OnGlobalLayoutListener layoutListener = new OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        if (!Spinner.this.isVisibleToUser()) {
                            dismiss();
                        } else {
                            computeContentWidth();
                            // Use super.show here to update; we don't want to move the selected
                            // position or adjust other things that would be reset otherwise.
                            DropdownPopup.super.show();
                        }
                    }
                };
                vto.addOnGlobalLayoutListener(layoutListener);
                setOnDismissListener(new OnDismissListener() {
                    @Override
                    public void onDismiss() {
                        final ViewTreeObserver vto = getViewTreeObserver();
                        if (vto != null) {
                            vto.removeOnGlobalLayoutListener(layoutListener);
                        }
                    }
                });
            }
        }
    }
    

    通过对以上源码的分析,我们可以看出,当SpinnerMode为dialog时,Spinner内部选择用一个DialogPopup来显示下拉列表内容;当SpinnerMode为dropDown时,Spinner选择用一个DropdownPopup来显示下拉列表内容——即在两种不同的样式下,Spinner分别选择使用Dialog / PopupWindow来进行内容展示。
      看到这里,我们会疑惑,为什么PopupWindow中嵌套使用Dialog没问题,而嵌套使用PopupWindow就会出错?关于PopupWindow、Dialog窗口添加机制的不同之处推荐阅读Android 窗口添加机制系列2-Dialog,PopupWindow,Toast

    • PopupWindow
      PopupWindow本身依附的WindowToken实际上是也是Activity所依附的WindowToken,这也就是说PopupWindow与Activity所使用的WindowToken是一致的。
      PopupWindow内部不能再使用PopupWindow是因为它获取不到父PopupWindow的WindowToke,从这里我们也可以分析出,一个视图内部不能嵌套与之平级的视图。
    • Dialog
      Dialog在初始化视图时,在获取到Activity的WindowToken后,会重新new一个Window,它与Activity分属于不同的Window。

    3.解决方式

    搞明白PopupWindow、Dialog的区别以及与WindowsManager的关系后,我们可以通过以下方式来规避类似问题的发生:

    • 将外层PopupWindow换为Activity
    • 将外层PopupWindow替换为Dialog
    • 修改SpinnerMode为dialog
    • 使用自定义组件实现类Spinner效果
    • 使用Dialog+PopupWindow

    【拓展阅读】
    Android 窗口添加机制系列2-Dialog,PopupWindow,Toast
    Spinner SpinnerAPI官方文档翻译
    视图加载到窗口的过程分析

    相关文章

      网友评论

        本文标题:【Android】PopupWindow中使用Spinner出错

        本文链接:https://www.haomeiwen.com/subject/urgjvttx.html