Android 中使用 PopupWindow 弹出取消提示框浅析

3,039 阅读7分钟
原文链接: www.jianshu.com

Demo下载地址

一、什么是PopupWindow

  • 官方文档中对PopupWindow的解释是:一个弹出窗口,可以用来显示任意视图,并且这个弹出窗口是浮动在当前activity上面的容器。

二、PopupWindow长什么样子?

  • 先来看一下一个关于PopupWindow的案例效果图。从图中我们可以知道PopupWindow就是一个类似于对话框的弹出框,这个弹出框可以自定义视图。它也相当于一个容器,这个容器里可以存放自己定义的控件,如图中的PopupWindow就定义四个ImageView,并且给每一个ImageView添加了点击事件。

PopupWindow案例效果图

三、PopupWindow用法详解

  • 这里以我做的Demo为材料,主要给大家讲解五个方面,包括自定义ProgressBar样式,ListView数据的获取,点击条目后PopupWindow的的弹出、点击容器外部区域使弹出框消失,以及弹出框容器控件的点击事件。

1. 自定义ProgressBar样式

  • 通过翻阅ProgressBar的源代码,我们可以知道,ProgressBar显示的样式其实主要是使用了系统自己定义的旋转动画,既然这样那我们就可以使用自己定义的ProgressBar样式替换掉系统的。

  • 自定义ProgressBar样式主要代码如下:

// 1. 布局文件中申明ProgressBar控件


// 2. 样式文件中申明自定义的样式,样式指向drawable文件
 

    <item name="android:indeterminateDrawable">@drawable/progress_medium</item>


// 3. 自定义旋转动画,指向自己定义的旋转图片
// 一进来就显示加载进度条
mViewPb.setVisibility(View.VISIBLE);

new Thread() {

    public void run() {
        // 加载数据
        mList = AppInfoUtil.getAppInfo(MainActivity.this);

        // 子线程睡一会儿
        SystemClock.sleep(1200);

        // 在主线程中显示数据
        runOnUiThread(new Runnable() {
            public void run() {
                // 加载进度条消失
                mViewPb.setVisibility(View.GONE);

                mAdapter = new AppInfoAdapter(MainActivity.this, mList);
                // 设置数据
                mLvApp.setAdapter(mAdapter);
            }
        });
    };
}.start();

2. ListView数据的获取

  • 通过Android提供的一个PackageManager类可以获取到android系统中应用的一些基本信息,我们可以把这些信息封装在一个JavaBean里,然后存入集合。这里把获取App信息的逻辑放在一个工具类里。而使用的时候时候直接调用工具类的方法就能拿到集合数据,这样我们就可以用ListView显示这些应用的基本信息。

  • AppInfoUtil类主要代码如下:

    /**
     * 获取系统安装的APP信息
     * 
     * @param context
     */
    public static List getAppInfo(Context context) {
        // 存储信息的list集合
        List list = new ArrayList();
        // 包管理器
        PackageManager pm = context.getPackageManager();

        // 获取到应用程序的信息集合
        List appInfos = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES);

        for (ApplicationInfo info : appInfos) {
            // 创建App实体类
            AppBean bean = new AppBean();

            bean.icon = info.loadIcon(pm);
            bean.label = (String) info.loadLabel(pm);
            bean.packageName = info.packageName;
            bean.apkSize = Formatter.formatFileSize(context, new File(info.sourceDir).length());

            list.add(bean);
        }
        return list;
    }

3. 点击条目后PopupWindow的弹出

  • 给ListView的条目设置点击监听,点击之后会弹出PopupWindow提示框。
  • 使用PopupWindow第一步就是创建对象,所以我先来说一下PopupWindow的构造方法,Demo中的构造代码如下。PopupWindow中有好几个构造方法,你可以传入一个自定义的View,然后让系统默认提示框的宽高,也可以自己去指定弹出框的宽高。这里指定弹出框的宽高都是包裹内容。

     /**
       *  创建popupwindow对象
       *  参数一:自定义的View布局,用来填充弹出框的显示内容
       *  参数二和参数三:指定弹出框的宽和高,不写时表示使用系统默认的宽高,这里使用的是包裹内容。
       */
       PopupWindow popupWindow = new PopupWindow(convertView,LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
  • 创建了PopupWindow对象后,就可以进行显示,我们看一下PopupWindow的显示方法,Demo中的显示代码如下。PopupWindow类提供了四个显示的API,其中三个重载方法的显示是围绕在指定的View旁边,还有一个是指定具体的位置显示。这里要根据不同的需要调用不同的显示方法。

    /** 
       * 弹出popupwindow
       * 这个方法有三个重载,这里说一下有三个参数的重载
       * 第一次参数:表示popupwindow显示在一个参照物View的周围
       * 第二个参数:离参照物x方向的距离
       * 第三个参数:离参照物y方向的距离
       * 
       */
    popupWindow.showAsDropDown(view, mBean.icon.getMinimumWidth() + 20, -view.getHeight() + 10);
  • 能弹出提示框之后,我们继续研究一下弹出的动画。PopupWindow其实给我们提供了一个设置弹出动画的API,我们只需要指定弹出的样式动画就好啦。指定动画样式的代码如下,具体的样式代码请翻阅Demo中的源代码。

    // 设置弹出动画
    popupWindow.setAnimationStyle(R.style.PopupWindowStyle);

4. PopupWindow弹出框的消失

  • PopupWindow比较特殊,像对话框点击返回键或者提示框的外部区域都能miss掉,但是PopupWindow不能。查看官方文档可以知道,让PopupWindow弹出框消失主要代码有两行。

      // 设置获得焦点
      popupWindow.setFocusable(true);
      // 设置背景
      popupWindow.setBackgroundDrawable(new ColorDrawable());

    这里第一行代码让PopupWindow失去焦点很容易理解,但是第二行代码又怎么理解呢?查看PopupWindow的源代码我们可以知道,在PopupWindow弹出的时候,就对背景进行了相关的设置,代码如下:

    /**
    * 

    Prepare the popup by embedding in into a new ViewGroup if the * background drawable is not null. If embedding is required, the layout * parameters' height is modified to take into account the background's * padding.

    * * @param p the layout parameters of the popup's content view */ private void preparePopup(WindowManager.LayoutParams p) { if (mContentView == null || mContext == null || mWindowManager == null) { throw new IllegalStateException("You must specify a valid content view by " + "calling setContentView() before attempting to show the popup."); } if (mBackground != null) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); int height = ViewGroup.LayoutParams.MATCH_PARENT; if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { height = ViewGroup.LayoutParams.WRAP_CONTENT; } // when a background is available, we embed the content view // within another view that owns the background drawable PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height ); popupViewContainer.setBackground(mBackground); popupViewContainer.addView(mContentView, listParams); mPopupView = popupViewContainer; } else { mPopupView = mContentView; } mPopupView.setElevation(mElevation); mPopupViewInitialLayoutDirectionInherited = (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT); mPopupWidth = p.width; mPopupHeight = p.height; }

    由源码可知当有背景的时候,PopupWindow会使用一个PopupViewContainer容器包裹自定义的View作为mPopupView,如果没有背景就会直接用自定义的View作为mPopupView。
    而系统对触摸和点击取消的事件都是在PopupViewContainer容器处理的,所以当没有背景的时候,是执行不到具体的触摸代码逻辑的。

5. PopupWindow容器具体控件的点击事件

  • 通过我们自定义的View可以获取到具体的控件。
  • 首先先做打开应用图片的点击事件,这个其实就是在点击之后,直接跳转到该条目对应应用的首页,但是我们怎么获取到对应的跳转的Intent对象呢。实际上,PackageManager提供了一个API,通过这个API可以获取到打开某个应用的Intent对象。

    // 打开应用
    convertView.findViewById(R.id.tv_open).setOnClickListener(new OnClickListener() {
    
          @Override
          public void onClick(View v) {
    
              PackageManager pm = getPackageManager();
              // 通过包管理器获取打开应用的意图对象
              Intent intent = pm.getLaunchIntentForPackage(mBean.packageName);
              if(intent != null){
                  startActivity(intent);
              }
    
              popupWindow.dismiss();
          }
      });
  • 其次做一下卸载应用的点击事件,同理卸载一个应用也是通过意图来完成。这里使用的是系统提供的卸载Intent意图。但是我们还需要考虑一个问题,就是卸载完应用了怎么刷新ListView列表?这里使用的方式是监听系统安装和卸载应用的广播,当有应用卸载时,我们接收到广播了,就可以在onReceiver()方法里将集合里对应的应用对象移除掉,然后Adapter的notifyDataSetChanged()方法刷新一下数据。

      // 卸载应用
      convertView.findViewById(R.id.tv_uninstall).setOnClickListener(new OnClickListener() {
    
          @Override
          public void onClick(View v) {
              Intent intent = new Intent();
              intent.setAction("android.intent.action.DELETE");
    
              intent.setData(Uri.parse("package:"+ mBean.packageName));
    
              startActivity(intent);
    
              popupWindow.dismiss();
          }
      });
  • 接下来做一下跳转到应用信息界面的点击事件,同理也是通过Activity的跳转。

      // 应用信息
      convertView.findViewById(R.id.tv_info).setOnClickListener(
          new OnClickListener() {
    
              @Override
              public void onClick(View v) {
                  // 点击跳转到app信息界面
                  Intent intent = new Intent();
                  intent.setAction("android.settings.APPLICATION_DETAILS_SETTINGS");
    
                  System.out.println(mBean.packageName);
                  intent.setData(Uri.parse("package:"+ mBean.packageName));
                  startActivity(intent);
    
                  // 消失popupwindow
                  popupWindow.dismiss();
              }
      });
  • 最后讲一下分享的点击实现,Demo中点击之后会真正的跳转到分享页面,然后进行分享。其实这里是使用了Mob提供的ShareSDK。具体的继承SDK方式请查看Mob提供的Android集成文档

Demo下载地址