【Android】PopupWindow的setOutsideTouchable和setFocusable的踩坑经历

参考文章:PopupWindow之踩坑(1) setFocusable与setOutsideTouchable问题

前言:

最近在准备一个下拉日期列表选择框的博客,里面就是基于PopupWindow实现的,如果把PopupWindow的一些知识点放在那篇博客,会把那篇文章的篇幅过长,所以,这里就单独写一篇出来,记录自己使用PopupWindow的一些坑:

内容概述:

一、PopupWindow的简单介绍
二、分析PopupWindow的属性(结合源码)
三、使用PopupWindow遇到的问题
四、解决办法
五、遗留问题

具体内容:

一、PopupWindow的介绍:

PopupWindow是一个和AlterDialog类似的弹窗式控件,他相对于AlterDialog的好处就是他能控制自己弹出的位置(比如在某个View的上面或下面弹出),灵活性较好。使用时,通过设置自己要展示的contentView,达到自己想要的效果。

二、PopupWindow的属性(结合源码)

首先我们得弄清楚,为什么点击PopupWindow以外的部分,会使PopupWindow消失(看完①②就能得出答案)

①setBackgroundDrawable

private class PopupDecorView extends FrameLayout {
    ......
    @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);
        }
    }
}

可以看到,PopupWindow的内部只有这个PopupDecorView类才有点击触摸事件的监听,而他的MotionEvent.ACTION_OUTSIDE就是对PopupWindow以外部分点击触摸事件的监听,当点击外部时,PopupWindow会调用dismiss()方法隐藏。同样,MotionEvent.ACTION_DOWN是用户点击到PopupWindow时调用的,也会使PopupWindow隐藏。

那么,如果点击触摸事件的监听要有效果,那么必须得设置PopupDecorView的实例对象在PopupWindow中,根据这点,我们可以找到mDecorView这个对象

/** View that handles event dispatch and content transitions. */
@UnsupportedAppUsage
private PopupDecorView mDecorView;

然后我们再看他是在哪里赋值,这里会涉及到Android版本的问题,Android系统创建popup的时候在5.0以下和以上会有不同的处理,5.0以下的系统在创建popup的时候会根据你是否通过调用popup的setBackgroundDrawable(Drawable background)方法来判断是否把你的popup放到一个PopupViewContainer里面,以下是5.0以下的源码

private void preparePopup(WindowManager.LayoutParams p) {
       ......
        if (mBackground != null) {
            //popupViewContainer 内部监听点击触摸事件
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackgroundDrawable(mBackground);
            popupViewContainer.addView(mContentView, listParams);   //重点,只有mBackground != null时才会执行

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }
    }

private class PopupViewContainer extends FrameLayout {
    private static final String TAG = "PopupWindow.PopupViewContainer";

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ......
    }
}

以下是5.0以上的源码

@UnsupportedAppUsage
private PopupDecorView mDecorView;

@UnsupportedAppUsage
private void preparePopup(WindowManager.LayoutParams p) {
    ......
    // 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);  //无论是否是mBackground,都会调用
}

private class PopupDecorView extends FrameLayout {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ....
    }
}

5.0以上系统的PopupDecorView就是5.0以下的PopupViewContainer(),它们是同一个东西(从上面的源码可以看出),在5.0以下的源码中,如果没有设置mBackground,那么就不会有PopupViewContainer的对象被创建,那么PopupWindow就不能监听到点击触摸事件。
【总结】:为了适配Android5.0以下的版本,在PopupWindow初始化的时候,要为其设置mBackground,否者PopupWindow会监听不到点击触摸事件

②setOutsideTouchable

对于setOutsideTouchable这个属性,我们点进去

//The default is false
public void setOutsideTouchable(boolean touchable) {
    mOutsideTouchable = touchable;
}

他默认为false,当设置其为true时,改变的时mOutsideTouchable这个变量

private int computeFlags(int curFlags) {
    ......
    if (mOutsideTouchable) {
        curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
    }
    ......
    return curFlags;
}

mOutsideTouchable变量影响的是WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH这个Flag,对于这个Flag的解释是

Window flag: if you have set {@link #FLAG_NOT_TOUCH_MODAL}, you
can set this flag to receive a single special MotionEvent with
the action
{@link MotionEvent#ACTION_OUTSIDE MotionEvent.ACTION_OUTSIDE} for
touches that occur outside of your window. Note that you will not
receive the full down/move/up gesture, only the location of the
first down as an ACTION_OUTSIDE.

这里表示,如果设置了这个Flag,那么 用户点击窗体以外的位置时,将会在窗体的MotionEvent中收到MotionEvetn.ACTION_OUTSIDE这个事件。

MotionEvetn.ACTION_OUTSIDE是不是感觉在哪里见过,我们上面在看PopupDecorView内部的onTouchEvent方法时,就是判断我们的点击事件是不是MotionEvetn.ACTION_OUTSIDE,如果是,就通过调用dismiss()方法隐藏自己。

【总结】在我们分析setBackgroundDrawable的时候,知道了,5.0系统以下,只要设置了mBackground就能监听点击触摸事件,而5.0以上的系统是直接就能监听的。那么,在能够监听点击触摸的事件的情况下,再设置以下他的setOutsideTouchable为true,去影响WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH就能接收到PopupWindow以外的监听事件了。

那么到这里,大家应该就能明白PopupWindow是如何通过点击除自己以外的部分,消失的了。

③ setFocusable

根据方法,可以直观的看出,这个方法是设置PopupWindow的焦点的。但是焦点这个概念比较抽象,举个例子来说:如果我们的PopupWindow内部是有EditText这种需要获取焦点的输入框,如果我们不设置setFocusable为true,那么我们的PopupWindow弹出来后,我们是不能点击输入的。而对于其他的TextView这些没有太大影响。下面来看他的过程:

public void setFocusable(boolean focusable) {
    mFocusable = focusable;
}

他和setOutsideTouchable的过程是类似的

private int computeFlags(int curFlags) {
    ......
    if (!mFocusable) {
        curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        if (mInputMethodMode == INPUT_METHOD_NEEDED) {
            curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
        }
    } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) {
        curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
    }
    ......
    return curFlags;
}

当我们设置setFocusable之后,影响的是WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE这个Flag,

Window flag: this window won't ever get key input focus, so the
user can not send key or other button events to it. Those will
instead go to whatever focusable window is behind it. This flag
will also enable {@link #FLAG_NOT_TOUCH_MODAL} whether or not that
is explicitly set.
Setting this flag also implies that the window will not need to
interact with
a soft input method, so it will be Z-ordered and positioned
independently of any active input method (typically this means it
gets Z-ordered on top of the input method, so it can use the full
screen for its content and cover the input method if needed. You
can use {@link #FLAG_ALT_FOCUSABLE_IM} to modify this behavior. */

这里明确指出,不设置这个这个Flag,那么键盘的输入和按钮的点击都将不会被PopupWindow接收,而是往PopupWindow后面可定焦的View去接收。

同时,如果设置了这个Flag,那么他能管ACTION_UP,ACTION_DOWN等Touch事件,由前面的PopupDecorView类的onTouchEvent方法可以看出,MotionEvent.ACTION_DOWN事件在MotionEvent.ACTION_OUTSIDE事件之前处理了,所以设置了setFoucus(true)后再去设置 setOutsideTouchable(false)没有作用了

if ((event.getAction() == MotionEvent.ACTION_DOWN)
        && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
    ......
} else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
    ......
} 

三、使用PopupWindow遇到的问题

从上面对属性的分析,可以看出,如果我们不设置setOutsideTouchablesetFocusable,那么他们会默认为false,那么我们的PopupWindow以外的触摸事件会往下传递,直到被其他的View接收。但是我们设置了setOutsideTouchablesetFocusable,会有一个问题,就是当我们点击PopupWindow以外的部分时,无法给PopupWindow设置隐藏动画(由上面的源码可知,是因为他内部onTouchEvent()中直接调用了dismiss()方法)。这就是我遇到的问题。

四、解决办法

由于设置了setOutsideTouchablesetFocusable不能再控制他内部去调用dismiss(),那么我们就让他的触摸事件穿透到下层,由下层的View去处理,这样就可以在监听到PopupWindow以外的触摸事件时,给他设置一个隐藏动画。

我这里是用到了一个View,把他作为灰色背景,当用户的点击事件穿透到View上时,使其用动画隐藏

//选择框出来时的灰色背景
public View mGrayLayout;

//对黑色半透明背景做监听,点击时开始退出动画并将popupwindow dismiss掉
mGrayLayout.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (isPopWindowShowing) {
            dismissPopupWindow();
        }
    }
});

private void dismissPopupWindow() {
    mPopupWindow.getContentView().startAnimation(AnimationUtil.createOutAnimation(MainActivity.this, fromYDelta));
    mPopupWindow.getContentView().postDelayed(new Runnable() {
        @Override
        public void run() {
            //popwindow隐藏
            mPopupWindow.dismiss();
        }
    }, AnimationUtil.ANIMATION_OUT_TIME);
}
public class AnimationUtil {

    public final static int ANIMATION_IN_TIME = 500;
    public final static int ANIMATION_OUT_TIME = 500;

    public static Animation createInAnimation(Context context, int fromYDelta) {
        AnimationSet set = new AnimationSet(context, null);
        //在动画链中,假定你有一个移动的动画紧跟一个淡出的动画,如果你不把移动的动画的setFillAfter置为true,
        // 那么移动动画结束后,View会回到原来的位置淡出,如果setFillAfter置为true, 就会在移动动画结束的位置淡出
        set.setFillAfter(false);
        TranslateAnimation animation = getTranslateAnimation(0,0,fromYDelta, 0,ANIMATION_IN_TIME);
        AlphaAnimation alphaAnimation = getAlphaAnimation(0, 1, ANIMATION_IN_TIME);
        //两个动画的结合,造成从上往下缓慢展开一个View
        set.addAnimation(animation);
        set.addAnimation(alphaAnimation);
        return set;
    }

    private static AlphaAnimation getAlphaAnimation(int fromAlpha, int toAlpha, int animationTime) {
        //从透明到完全显示
        AlphaAnimation alphaAnimation = new AlphaAnimation(fromAlpha, toAlpha);
        alphaAnimation.setDuration(animationTime);
        return alphaAnimation;
    }

    private static TranslateAnimation getTranslateAnimation(float fromXDelta, float toXDelta, float fromYDelta, float toYDelta,int animationTime) {
        //从view的y轴的最下面到最上面
        TranslateAnimation animation = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta);
        //设置动画时长
        animation.setDuration(animationTime);
        return animation;
    }

    public static Animation createOutAnimation(Context context, int toYDelta) {
        AnimationSet set = new AnimationSet(context, null);
        set.setFillAfter(false);
        TranslateAnimation animation = getTranslateAnimation(0, 0, 0, toYDelta,ANIMATION_OUT_TIME);
        set.addAnimation(animation);
        AlphaAnimation alphaAnimation = getAlphaAnimation(1, 0, ANIMATION_OUT_TIME);
        set.addAnimation(alphaAnimation);
        return set;
    }

}

呈现的效果:


PopupWindow的源码解析.gif

五、遗留问题

上面的解决办法确实奏效了,但是当我们的PopupWindow下面有复杂的View时,并不是每次的触摸事件都能传递到我们的灰色背景上,这个问题暂没想到怎么解决,所以我又把setOutsideTouchablesetFocusable设置上了,这样虽然不能给PopupWindow设置影藏动画,但是阻止了触摸事件往下传递造成的问题。这个坑只能先埋在这里了,等以后来填!

问题:


PopupWindow的问题.gif

最后

这是第一次写分析源码的文章,内容有点乱、也有点啰嗦,相信后面会变好的。

你可能感兴趣的:(【Android】PopupWindow的setOutsideTouchable和setFocusable的踩坑经历)