如何在触摸窗口外部时取消类似Activity主题的对话框?

50

我有一个使用对话框主题的活动,当有人在此活动窗口之外的任何地方触摸屏幕时,我想关闭(结束)这个活动。我该如何实现?


Android 并没有真正支持这个功能。我不确定是否有任何方法可以实现,而且对于 Android 用户来说也不自然。这就是返回按钮的作用。 - Falmarri
2
对于一个对话框类来说,setCanceledOnTouchOutside方法是很自然的。那么为什么一个对话框主题的Activity不能有这个方法呢? - Alex
17个回答

101

仅想指出,从一个被主题化为对话框的Activity中获取类似“触摸屏幕外取消”的对话框行为是有方法的,尽管我还没有完全调查它是否具有不良副作用。

在您的Activity的onCreate()方法中,在创建视图之前,您将在窗口上设置两个标志:一个是使其“非模态”,以允许接收事件的视图不仅限于您的Activity的视图。第二个是接收通知其中发生了这些事件之一,这将向您发送ACTION_OUTSDIE移动事件。

如果您将主题设置在对话框主题上,则可以获得所需的行为。

大致如下:

public class MyActivity extends Activity {

 @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Make us non-modal, so that others can receive touch events.
    getWindow().setFlags(LayoutParams.FLAG_NOT_TOUCH_MODAL, LayoutParams.FLAG_NOT_TOUCH_MODAL);

    // ...but notify us that it happened.
    getWindow().setFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);

    // Note that flag changes must happen *before* the content view is set.
    setContentView(R.layout.my_dialog_view);
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // If we've received a touch notification that the user has touched
    // outside the app, finish the activity.
    if (MotionEvent.ACTION_OUTSIDE == event.getAction()) {
      finish();
      return true;
    }

    // Delegate everything else to Activity.
    return super.onTouchEvent(event);
  }
}

5
你好,这段代码看起来不错。但是在使用蜂窝网络时,如果我有一个覆盖在ListView上的Activity,并且我在视图外单击,触摸事件将被传递给ListView,即使我返回true也是如此。你有什么解决办法吗? - Alex
脑海中有两个选项,@alex - 在onPause/onResume期间禁用有问题的UI元素,或在启动时使用startActivityForResult()之前这样做,在返回结果代码时重新启用这些UI元素。 - Gregory Block
这很好,但它不能吞噬外部的触摸。例如,当用户关闭“对话框”时,我的视图后面的地图会被点击。 - Aiden Fry
5
WindowManager.LayoutParams - M. Usman Khan
9
@Alex:尝试使用 setFinishOnTouchOutside(false); - Mitesh Shah
谢谢,这对我有用 @MiteshShah Shah - MD.Riyaz

77

我发现了一个非常简单的答案,对我来说完美地解决了问题。如果你正在使用具有对话框主题的活动,则可以将this.setFinishOnTouchOutside(true);应用于活动的onCreate()方法。

@Override
protected void onCreate(Bundle savedInstanceState) 
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_yoptions);
    this.setFinishOnTouchOutside(true);
}

在我的情况下,唯一管用的方法是使用Theme.Holo.Dialog风格的活动。 - XMight
4
我在寻找相反的行为方式,这个答案同样适用于这种情况。我想要在用户点击外部区域时关闭对话框式的活动,比如加载对话框。将其设置为(false)就可以实现。 - Phil Ringsmuth

37

非常简单,只需将属性canceledOnTouchOutside = true设置为真。看这个例子:

Dialog dialog = new Dialog(context)
dialog.setCanceledOnTouchOutside(true);

5
这仅适用于实际对话,而不适用于使用对话主题的常规活动。 - eidylon
2
要使用对话框主题,请在当前对象上调用 setFinishOnTouchOutside(true),即 this.setFinishOnTouchOutside(true); - Ajay Takur
1
问题是要对话框主题的活动,而不是对话框! - Amir Hossein Ghasemi

19
很容易实现:
首先在 style.xml 中定义自己的主题:
<style name="DialogSlideAnim" parent="@android:style/Theme.Holo.Dialog">
    <item name="android:windowContentOverlay">@null</item>
    <item name="android:windowCloseOnTouchOutside">true</item>
</style>

然后在您的清单文件中将此主题应用于活动:

    <activity
        android:label="@string/app_name"
        android:name=".MiniModeActivity" 
        android:theme="@style/DialogSlideAnim" >
        <intent-filter >
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

4
该属性仅在API 11及以上版本可用。 - Longerian
1
如何访问低于API 11的此项API? - Satheesh

8

Gregory和Matt的答案结合起来对我最有帮助(适用于Honeycomb和其他版本)。这种方式可以确保当用户尝试触摸取消对话框时,外部视图不会收到触摸事件。

在主Activity中,在onCreate()方法中创建触摸拦截器:

    touchInterceptor = new FrameLayout(this);
    touchInterceptor.setClickable(true); // otherwise clicks will fall through

在onPause()中添加它:
    if (touchInterceptor.getParent() == null) {
        rootViewGroup.addView(touchInterceptor);
    }

(rootViewGroup可能必须是FrameLayout或RelativeLayout,LinearLayout可能无法正常工作。)

在onResume()中,将其删除:

    rootViewGroup.removeView(touchInterceptor);

接下来,对于对话框主题的Activity,请使用Gregory提供的代码(为了方便起见,在此复制):

public class MyActivity extends Activity {   

 @Override   
  protected void onCreate(Bundle savedInstanceState) {   
    super.onCreate(savedInstanceState);   

    // Make us non-modal, so that others can receive touch events.   
    getWindow().setFlags(LayoutParams.FLAG_NOT_TOUCH_MODAL, LayoutParams.FLAG_NOT_TOUCH_MODAL);   

    // ...but notify us that it happened.   
    getWindow().setFlags(LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);   

    // Note that flag changes must happen *before* the content view is set.   
    setContentView(R.layout.my_dialog_view);   
  }   

  @Override   
  public boolean onTouchEvent(MotionEvent event) {   
    // If we've received a touch notification that the user has touched   
    // outside the app, finish the activity.   
    if (MotionEvent.ACTION_OUTSIDE == event.getAction()) {   
      finish();   
      return true;   
    }   

    // Delegate everything else to Activity.   
    return super.onTouchEvent(event);   
  }   
}   

我不明白rootViewGroup应该是什么。如果我用活动的内容(例如android.R.content)来提供它,会发生什么? - Jacek Kwiecień
我不确定你的意思。但是,如果你通过setContentView()添加的根内容视图是ViewGroup的子类,我相信你可以使用rootViewGroup = (ViewGroup) findViewById(android.R.id.content)。 - Ken

6
如果使用像 android:theme="@style/Theme.AppCompat.Dialog" 或任何其它对话框主题,API 11及以上版本我们可以使用。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        setFinishOnTouchOutside(false);
    }

在活动的onCreate方法内调用此方法。


谢谢您。这对我帮助很大 +1 - Simon

4
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    Rect dialogBounds = new Rect();
    getWindow().getDecorView().getHitRect(dialogBounds);

    if (!dialogBounds.contains((int) ev.getX(), (int) ev.getY())) {
        return true;
    }
    return super.dispatchTouchEvent(ev);
}

这段代码解决了我的问题。

3

我看不懂最佳答案在三星平板3.1版本上的运行情况,因此我做了以下操作:

@Override
public boolean onTouchEvent(MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    int xmargin = (ViewUtils.getScreenWidth() - Constants.PRODUCT_DIALOG_WIDTH) / 2;
    int ymargin = (ViewUtils.getScreenHeight() - Constants.PRODUCT_DIALOG_HEIGHT) / 2;

    if (
            x < xmargin                              || 
            x > ViewUtils.getScreenWidth() - xmargin ||
            y < ymargin                              || 
            y > ViewUtils.getScreenHeight() - ymargin
        ) {
            finish();
            return true;
    }
    return super.onTouchEvent(event);
}

您需要将Constants.PRODUCT_DIALOG_WIDTH和Constants.PRODUCT_DIALOG_HEIGHT替换为您对话框的宽度/高度。我的代码中多次使用,因此我将其定义为常量。

您还需要实现自己的方法来获取屏幕宽度和高度,这在本网站上很容易找到。不要忘记考虑Android标题栏!

虽然代码看起来有些丑陋,但是它可以正常工作。


3
我唯一使这个工作的方法是:
alert = new AlertDialog.Builder(this)....

        alert.setOnDismissListener(new DialogInterface.OnDismissListener() {
        @Override
        public void onDismiss(final DialogInterface arg0) {
            Log.i(APP_NAME, "in OnDismissListener");
            // removeDialog(R.layout.dialog3);
            alert.dismiss();
            finish();
        }

即我需要明确地放置“dismiss”和“finish”,否则会在屏幕中央出现一个小白色矩形。

3

您可以从Android源代码中引用dialog.java代码:

public boolean onTouchEvent(MotionEvent event) {
    if (mCancelable && mCanceledOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(event)) {
        cancel();
        return true;
    }
    return false;
}

private boolean isOutOfBounds(MotionEvent event) {
    final int x = (int) event.getX();
    final int y = (int) event.getY();
    final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop();
    final View decorView = getWindow().getDecorView();
    return (x < -slop) || (y < -slop) || (x > (decorView.getWidth()+slop)) || (y > (decorView.getHeight()+slop));
}

只需将其修改为:
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(event)) {
        finish();
        return true;
    }
    return false;
}

private boolean isOutOfBounds(MotionEvent event) {
    final int x = (int) event.getX();
    final int y = (int) event.getY();
    final int slop = ViewConfiguration.get(this).getScaledWindowTouchSlop();
    final View decorView = getWindow().getDecorView();
    return (x < -slop) || (y < -slop) || (x > (decorView.getWidth() + slop)) || (y > decorView.getHeight() + slop));
}

可以解决您的问题。

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