【Android】PopupWindow中使用Spinner出錯

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官方文檔翻譯
視圖加載到窗口的過程分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容