弹窗

出现位置飘忽不定的弹窗在Android应用并不少见, 比如这种:

还有这种:

这些窗口的特点是位置不固定, 神出鬼没. 这种情况下, 我们可以使用PopupWindow来实现.

示例1

我们可以尝试着使用PopupWindow来实现图1的效果. 整个页面的载体是一个ListView, 每一个条目都有一个分享按钮, 点击分享按钮需要在按钮的上方显示一个悬浮窗. 难点就是悬浮窗显示的位置. 其实使用PopupWindow很容易实现, 毕竟PopupWindow就是为这种情况而设计的, 直接上代码:

public class SharePopupWindow extends PopupWindow {

    private OnShareItemClickedListener listener;
    private int contentHeight;

    public SharePopupWindow(Context context) {
        init(context);
    }

    private void init(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.view_share_window, null);
        contentHeight = DisplayUtil.dip2px(context, 60);

        setContentView(view); // 设置内容视图
        setWidth(WindowManager.LayoutParams.WRAP_CONTENT); // 设置宽度为WRAP_CONTENT
        setHeight(WindowManager.LayoutParams.WRAP_CONTENT); // 设置高度为WRAP_CONTENT

        setFocusable(true); // 设置能被选中
        setOutsideTouchable(true); // 设置外部可被触摸
        setBackgroundDrawable(new BitmapDrawable(null, (Bitmap) null)); // 加了此代码能响应返回键
        setAnimationStyle(R.style.anim_style); // 设置动画效果

        // 省略findViewById...

        // 省略设置onClick...
    }

    public void showUpon(View view) {
        int[] location = new int[2];
        view.getLocationOnScreen(location); // 获取需要显示在上方的view在屏幕中的位置

        // 根据view的位置显示PopupWindow
        showAtLocation(view, Gravity.NO_GRAVITY, 0, location[1] - contentHeight); 
    }

    public void setOnShareItemClickedListener(OnShareItemClickedListener l) {
        listener = l;
    }
}
public interface OnShareItemClickedListener {
    void onShareItemClicked(View view, ShareType type);

    enum ShareType {
        QQ, PENGYOUQUAN, WEIXIN, WEIBO, QQZONE
    }
}

布局就不贴了, 就是一个.9的背景图片加上几个TextView. 加上注释应该很容易理解上面的代码. 我们可以通过setAnimationStyle来为PopupWindow添加动画, setAnimationStyle需要一个style id, style中注明显示与隐藏的动画资源id:

<style name="anim_style">
    <item name="android:windowEnterAnimation">@anim/alpha_in</item>
    <item name="android:windowExitAnimation">@anim/alpha_out</item>
</style>

使用方法:

private SharePopupWindow window;

if (window == null)
    window = new SharePopupWindow(context);
window.showUpon(view);

PopupWindow最核心的方法就是showAtLocation, showAsDropDown以及他们的各种重载方法:

  • showAtLocation(View parent, int gravity, int x, int y) 显示在屏幕中指定的x,y的位置

  • showAtLocation(IBinder token, int gravity, int x, int y) 显示在屏幕中指定的x,y的位置

  • showAsDropDown(View anchor) 显示在指定view的下方

  • showAsDropDown(View anchor, int xoff, int yoff) 显示在指定view的下方, 并在x轴偏移x, y轴偏移y

  • showAsDropDown(View anchor, int xoff, int yoff, int gravity) 显示在指定view的下方, 并在x轴偏移x, y轴偏移y

示例二

看了示例一再来看示例二会觉得非常简单. 无非就是一个输入框加一个按钮. 然后有n中方法可以让弹窗显示在屏幕底部. 我使用的则是showAtLocation(view, Gravity.BOTTOM, 0, 0), 并且PopupWindow的宽高均是MATCH_PARENT. 布局则是有一个半透明的灰色的MATCH_PARENT的LinearLayout, 输入框和按钮都显示在LinearLayout的底部. 为什么这么做呢? 因为评论框弹出的时候背景要变灰, 所以使用了这种取巧的方式.

但是, ui效果虽然简单, 想要把评论框的用户体验做好, 不是那么简单:

  1. 首先, 想要用户体验好就需要在弹窗弹出的同时, 打开软键盘, 参考微信的朋友圈评论.

  2. 其次, 弹窗在显示状态下, 按返回键, 或者触摸输入框其他空白位置的时候, 不仅要隐藏弹窗, 还需要收回软键盘

  3. 默认情况下, 软键盘会遮盖一部分的输入框, 不同手机可能有不同的效果.

如何解决上面三个问题, 才是示例二的难点所在. 接下来一个个的看:

自动弹出软键盘

这个问题应该是最简单的了, 直接在show方法中使用IMEUtil来打开软键盘即可.

public void showAtBottom(View view) {
    showAtLocation(view, Gravity.BOTTOM, 0, 0);
    IMEUtil.openIME(comment, comment.getContext());
}

自动隐藏软键盘

首先, 触摸输入框其他空白位置的时候收回软键盘同时隐藏弹窗, 这个问题也不难, 同样借助IMEUtil:

View.OnClickListener dismiss = new View.OnClickListener() {
    @Override public void onClick(View v) {
        IMEUtil.closeIME(comment, comment.getContext());
        dismiss();
    }
};

View view = LayoutInflater.from(context).inflate(R.layout.view_comment, null);
view.setOnClickListener(dismiss);

我们只需要在整个布局上设置OnClickListener即可. 但是, 点击返回键的时候需要收回键盘并且dismiss就不那么容易了. 看到这个问题我们第一反应就是监听返回键. 监听返回键只能在Activity里去做, PopupWindow本身不提供此API. 监听返回键之后, 需要判断PopupWindow是否是显示状态, 如果是, 则隐藏, 不是, 则要将事件传递出去. 这就意味这Activity要持有PopupWindow. 如果PopupWindow本身就在Activity中, 这么做没问题. 但是, 如果PopupWindow是在Fragment中, 这样做就不太好了. 虽然有很多方法可以将PopupWindow传递给持有Fragment的Activity, 但是不建议这么做, 类不要持有与其无直接关联的引用.

还有第二种方法, 自定义EditText. EditText中有一个可被覆写的方法dispatchKeyEventPreIme. 看看此方法的注释:

Dispatch a key event before it is processed by any input method 
associated with the view hierarchy. This can be used to intercept 
key events in special situations before the IME consumes them;
a typical example would be handling the BACK key to update 
the application's UI instead of allowing the IME to see it and 
close itself.

大意就是, 由于有软键盘存在的情况下, 按返回键会默认先收回软键盘, 此方法可以拦截按返回键收回软键盘这种事件. 代码如下:

@Override
public boolean dispatchKeyEventPreIme(KeyEvent event) {
    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK
                && event.getAction() == KeyEvent.ACTION_UP
                && !event.isCanceled()) {

        IMEUtil.closeIME(this, getContext());
        RxBus.getDefault().send(new Object(), EVENT_BACK_PRESSED);

        return true;
    }
    return super.dispatchKeyEventPreIme(event);
}

代码很简单, 首先如果监听到返回键, 会去收回软键盘, 然后借助RxBus发送一个不带数据的事件. 在PopupWindow监听此事件, 一旦接受到, 就dismiss自己. 达到点击返回键的时候需要收回键盘并且dismiss的效果.

    private void registerBackPressedEvent() {
        if (subscription != null) subscription.unsubscribe();

        subscription = RxBus.getDefault().toObservable(Object.class,
                CloseIMEEditText.EVENT_BACK_PRESSED)
                .subscribe(new RxBusSubscriber<Object>() {
                    @Override public void receive(Object data) {
                        if (isShowing()) dismiss();
                    }
                });
    }

软键盘覆盖部分输入框

在某些机型上, 默认情况下软键盘覆盖弹窗的部分输入框. 要解决这个问题首先想到的就是Activity的windowSoftInputMode属性. 但是每种属性都试过了还是有这种情况. 面向百度或谷歌也没有发现是什么问题. 后来无意中发现在PopupWindow中一个方法: setSoftInputMode. 这个方法是不是跟Activity的windowSoftInputMode类似? 试验发现, 如果这么写, 问题就解决了:

setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

至此, 三个问题就全部解决.

Last updated