在Android活动中创建弹出窗口的问题

56

我正在尝试创建一个弹出窗口,在应用程序启动时只出现一次。我想要显示一些文本,并有一个按钮关闭该弹窗。然而,我无法让PopupWindow正常工作。我尝试了两种不同的方法:

首先,我有一个名为popup.xml的XML文件,声明了弹出窗口的布局(一个线性布局内的文本视图),并在我的主Activity的OnCreate()中添加了它:

PopupWindow pw = new PopupWindow(findViewById(R.id.popup), 100, 100, true);
    pw.showAtLocation(findViewById(R.id.main), Gravity.CENTER, 0, 0);

然后我用完全相同的代码进行了这个操作:

final LayoutInflater inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    PopupWindow pw = new PopupWindow(inflater.inflate(R.layout.popup, (ViewGroup) findViewById(R.layout.main) ), 100, 100, true);
    pw.showAtLocation(findViewById(R.id.main_page_layout), Gravity.CENTER, 0, 0);

第一个抛出NullPointerException异常,第二个抛出BadTokenException异常,并显示“无法添加窗口--令牌为null无效”。

我到底做错了什么?我非常新手,请多包涵。

14个回答

188
为了避免BadTokenException异常,您需要推迟显示弹出窗口直到所有生命周期方法都被调用(->活动窗口已显示):
 findViewById(R.id.main_page_layout).post(new Runnable() {
   public void run() {
     pw.showAtLocation(findViewById(R.id.main_page_layout), Gravity.CENTER, 0, 0);
   }
});

5
不,它不会立即运行。必须在所有生命周期方法完成后运行。将上述代码放在onCreate或onStart中,并且它将在所有初始化生命周期方法被调用并设置完毕后,在UI线程上执行pw.showAtLocation(这就是post方法的目的-请阅读其javadoc以获取更多详细信息)。这应该可以正常工作。 - kordzik
1
这个答案太棒了!非常感谢。我长达一个月的搜索终于在这里结束 :) - Sudarshan Bhat
3
在onAttachedToWindow()方法中做什么? - Arnaud
1
嗨,我正在使用以下代码:layout.postDelayed(new Runnable() { @Override public void run() { // TODO Auto-generated method stub mWifiListing.showAsDropDown(mWIfiObj, convertDipToPixels(30), 0);
} }, 1000);但我仍然遇到相同的错误。你能帮我解决吗?
- dheeraj
1
这对于在屏幕旋转时恢复弹出窗口也非常有用。 - Rodney
显示剩余2条评论

36

Kordzik提供的解决方案在连续启动2个活动时将无法工作:

startActivity(ActivityWithPopup.class);
startActivity(ActivityThatShouldBeAboveTheActivivtyWithPopup.class);

如果你按照这种方式添加弹出窗口,在这种情况下,你将会遇到相同的崩溃,因为ActivityWithPopup在此情况下不会附加到Window上。

更通用的解决方法是使用onAttachedToWindowonDetachedFromWindow

而且没有必要使用postDelayed(Runnable, 100)。因为这100毫秒不能保证任何事情。

@Override
public void onAttachedToWindow() {
    super.onAttachedToWindow();
    Log.d(TAG, "onAttachedToWindow");

    showPopup();
}

@Override
public void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    Log.d(TAG, "onDetachedFromWindow");

    popup.dismiss();
}

1
这应该是答案!谢谢! - Jonathan ANTOINE
我已经和其他几十个答案奋斗了一个星期,但从来没有一致的解决方法。这个解决方案有效了。谢谢您。 - seekingStillness
完美的答案。谢谢。 - Visal Varghese
看起来,如果一个令牌为空,那么视图就没有附加到窗口上。可以在View类的源代码中验证这一点,链接在这里(https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#9742)。变量`mAttachInfo`包含窗口令牌,只有在将视图附加到窗口时才会设置。 - Bruno Bieri
看起来,如果一个令牌为空,那么视图就没有附加到窗口上。可以在View类的源代码中验证这一点,链接在这里(https://android.googlesource.com/platform/frameworks/base/+/a175a5b/core/java/android/view/View.java#9742)。变量`mAttachInfo`包含窗口令牌,只有在将视图附加到窗口时才会设置。 - undefined

18

接受的答案对我没用。我仍然收到BadTokenException异常。因此,我只需使用延迟从处理程序调用Runnable,如下所示:

new Handler().postDelayed(new Runnable() {
    public void run() {
        showPopup();
    }
}, 100);

所以你依赖于延迟不会太短...在我看来这不是一个很好的解决方法,但如果它对你有用的话... - shkschneider
我知道很笨,但要不然还能花多少时间去寻找其他可行的解决方案呢。:( - Todd Painton
这并不是一个普适的解决方案。你只是让竞争条件稍微不太可能发生了。 - Allison
即使没有延迟,它也能正常工作 new Handler().post(() -> showPopup();); - Johnny Five

10

使用Context类,例如MainActivity.this,而不是getApplicationContext()


4

4
解决方案是将Spinner模式设置为对话框,如下所示:
android:spinnerMode="dialog"

或者

Spinner(Context context, int mode)
tnxs RamallahDroid

See This.


这对我来说完美地解决了问题。我在 PopupWindow 中有一个 Spinner,在 Android 5 上出现了问题(在更新的版本上运行良好)。虽然 Spinner 的外观发生了变化,但这解决了问题。 - Rafał

1
根据使用情况,为了显示消息的弹出类型,将弹出类型设置为TYPE_TOAST并使用setWindowLayoutType()可以避免问题,因为这种弹出类型不依赖于底层活动。
编辑:其中一个副作用是对于API <=18的弹出窗口没有交互,因为可触摸/可聚焦事件会被系统移除。( http://www.jianshu.com/p/634cd056b90c )
最终我使用了TYPE_PHONE(因为应用程序恰好具有SYSTEM_ALERT_WINDOW权限,否则这也无法工作)。

1
你可以检查rootview是否有token。你可以从你的activity xml中获取定义的父布局,mRootView。
if (mRootView != null && mRootView.getWindowToken() != null) {
    popupWindow.showAtLocation();
}

0
您也可以尝试使用此检查:
  public void showPopupProgress (){
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            if (getWindow().getDecorView().getWindowVisibility() == View.GONE) {
                showPopupProgress();
                return;
            }
            popup.showAtLocation(.....);
        }
    });
}

0

检查findViewById是否返回了正确的内容——你可能在布局生成之前就调用它了。

此外,你可能想要发布异常的logcat输出。


我在onCreate()方法中调用它,不确定还能从哪里调用它。我已经更新了第一组代码的logcat输出。 - Amplify91
你能发一下你的onCreate方法吗?确保在使用setContentView填充和设置布局之后再调用findViewById - Asahi
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);//Admob广告 AdView ad = (AdView) findViewById(R.id.ad); ad.setAdListener(new AdMobListener());然后我有来自上面的PopupWindow代码。 - Amplify91

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接