windowManager之Popupwindow使用

1,942 阅读7分钟

前言

Popupwindow的使用概率之大,我想做开发的人员应该都知道,也是我们入门就该掌握的控件.但是使用该控件存在版本兼容性问题,也存在不少需要注意事项.这篇文章就按照我个人开发经验去梳理.

1.场景

在实际开发中,我们总会被要求实现可拖拽的悬浮窗,首先我们会想到使用WindowManager,但是直接使用这个会存在各种各样的问题,那替代方案呢?答案是popupWindow(其实该方案也是有问题的,后面我们会讲到).

2.popupWindow的使用

poupwindow的使用其实网上就比较多了,我就贴个其他人写的博客地址就好了,比较popupWindow的使用主要是在一些注意项上.
博客地址
1、PopUpWindow基本使用(全屏显示)
2、PopUpWindow使用详解系列(很全面)

3.popupWindow使用注意事项

注意事项一: 设置setOutsideTouchable (false)是没有作用的,点击区域外依然会dismiss

正常来说 如dialog设置setOutsideTouchable (false),点击区域外部不会消失,但是popupwindow不会,先说处理办法
需要也设置setFocusable(false)
原因参考源码解释

/**
     * <p>Controls whether the pop-up will be informed of touch events outside
     * of its window.  This only makes sense for pop-ups that are touchable
     * but not focusable, which means touches outside of the window will
     * be delivered to the window behind.  The default is false.</p>
     *
     * <p>If the popup is showing, calling this method will take effect only
     * the next time the popup is shown or through a manual call to one of
     * the {@link #update()} methods.</p>
     *
     * @param touchable true if the popup should receive outside
     * touch events, false otherwise
     *
     * @see #isOutsideTouchable()
     * @see #isShowing()
     * @see #update()
     */
    public void setOutsideTouchable(boolean touchable) {
        mOutsideTouchable = touchable;
    }

最上面部分翻译成中文是:控制是否向弹出窗口通知窗口外的触摸事件。这仅对可触摸但不能聚焦的弹出窗口有意义,这意味着窗口外部的触摸*将传递到后面的窗口。默认为false(其实感觉没有说清楚,先mark吧)

注意事项二:popupwindow.showAtLocation的时候报unable to add window -- token null is not valid; is your activity running

这种我碰到的是activty没有加载完,但是我们却调用了popupwindow.showAtLocation方法造成的,这是因为popupwindow控件需要绑定Activity的context
解决办法
个人采用的是 : 通过handler延迟加载 (会在acitvity加载完才调用这里面的回调)

private Handler popupHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case 0:
                popupWindow.showAtLocation(rootView, Gravity.CENTER,0, 0);
                popupWindow.update();
                break;
            }
        }
    };

还有一种场景可参考: unable to add window -- token null is not valid; is your activity running 错误解决办法

注意事项三: showAsDropDown和showAtLocation区别 以及方法参数意义

区别在于: showAsDropDown(Viewanchor,intxoff,intyoff)是以anchor的左下角坐标作为PopupWindow的原点坐标显示在anchor下方,showAtLocation指定弹窗相对于屏幕的精确位置,和具体的anchorView没有关系,后面三个参数来确定弹窗的位置\color{red}{showAsDropDown(View anchor, int xoff, int yoff)是以anchor的左下角坐标作为PopupWindow的原点坐标显示在anchor下方,而showAtLocation指定弹窗相对于屏幕的精确位置,和具体的anchorView没有关系,后面三个参数来确定弹窗的位置}
除此之外,其他参数的意义可参考
PopuWindow的showAsDropDown和showAtLocation参数详解

3.popupWindow实现拖动效果

正常view 我们可以通过重写onTouch方法在触屏的时候进行处理,但是popupWindow是一个不继承任何的类(可以看作view的管理器吧),那实现步骤呢?
1、设置popupWindow布局里面的根布局可点击setClickable(true)
2、设置popupWindow布局里面的根布局setOnTouchListener方法并重写该View.OnTouchListener方法
3、在case MotionEvent.ACTION_MOVE:场景下进行调用popupWindow.update(offsetX, offsetY, -1, -1, true)来进行位置的更新;
这样就将拖动功能的实现转嫁到view上了,具体讲解可参考Android Popupwindow 拖动

windowManager在不同版本注意事项

可以先查看这篇博客Android浮窗小球的特殊权限申请的变动和解决

1、SYSTEM_ALERT_WINDOW权限申请

正常只需要在AndroidMainfest.xml中声明,但是在6.0以上版本 这个权限并不能动态申请,可以通过跳转到系统设置里面去手动打开

  if (! Settings.canDrawOverlays(MainActivity.this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent,10);
        }
 
@RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (requestCode == 10) {
            if (!Settings.canDrawOverlays(this)) {
                // SYSTEM_ALERT_WINDOW permission not granted...
                Toast.makeText(MainActivity.this,"not granted",Toast.LENGTH_SHORT);
            }
        }
    }

2、悬浮窗如何悬浮窗在屏幕上?

windowmanager存在不同版本兼容性问题(不然会导致崩溃) 针对26以上 (但是这种方式会导致手机上不显示悬浮窗 如我自己的荣耀手机)

   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;(都是系统级别)
   } else {
        wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;(都是系统级别)
   }

4.实战1:windowManager实现悬浮按钮

------------------------------
package cn.com.essence.android.widget;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.graphics.Color;
import android.graphics.Point;
import android.os.Build;
import android.support.annotation.NonNull;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.ScrollView;
import android.widget.TextView;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.Queue;


/**
 * Debug窗口,用于将一些debug信息显示到UI界面,方面能直观看到调试信息。
 * @author lijh6@essence.com.cn
 * @date 2019.05.09 16:59
 * @description
 */
public class DebugPopupWindow {

    private static DebugPopupWindow sInstance;
    private static final String HH_MM_SS = "HH:mm:ss";
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(HH_MM_SS);
    private static final int DEBUG_MAX_LINE_COUNT = 100;
    private DebugPopupWindow() {
        
    }

    /**
     * 获取API实例。
     * @return
     */
    public static DebugPopupWindow getInstance() {
        if (sInstance == null) {
            synchronized (DebugPopupWindow.class) {
                if (sInstance == null) {
                    sInstance = new DebugPopupWindow();
                }
            }
        }
        return sInstance;
    }

    private PopupWindow popupWindow;
    private ScrollView scrollView;
    private TextView contentView;
    private boolean isShow = true;
    private Boolean isDebugable;
    private StringBuffer debugInfo = new StringBuffer("Debug Window:\n");
    private Queue<String> debugQueue = new LinkedList<String>();
    /**
     * 重置PopupWindow信息。
     * @param context context
     */
    public void resetPopupWindow(Activity context) {
        if (!isDebugable(context)) {
            return;
        }
        if (popupWindow != null) {
            destory();
        }
        scrollView = new ScrollView(context);
        scrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                scrollView.post(new Runnable() {
                    public void run() {
                        scrollView.fullScroll(View.FOCUS_DOWN);
                    }
                });
            }
        });
        scrollView.setPadding(40,100,40,100);
        contentView = new TextView(context);
        contentView.setTextColor(Color.parseColor("#CC0000FF"));
        contentView.setFocusable(true);
        contentView.setText(debugInfo);
        contentView.setBackgroundColor(Color.parseColor("#26FFFFFF"));
        contentView.setTextSize(10);
        contentView.setPadding(10,10,10,10);
        scrollView.addView(contentView);
        popupWindow = new PopupWindow(scrollView,
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT, false);
        popupWindow.setTouchable(false);

        contentView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK) {
                    return true;
                }
                return false;
            }
        });
        if (context.getWindow().isActive()) {
            if (!isShow) {
                popupWindow.dismiss();
            } else {
                popupWindow.showAtLocation(contentView.getRootView(), Gravity.CENTER,0,0);
            }
        }
    }

    private boolean isDebugable(Activity context) {
        if (isDebugable == null) {
            try {
                ApplicationInfo info = context.getApplicationInfo();
                isDebugable = (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return isDebugable == null ? false : isDebugable;
    }

    public void destory() {
        if (popupWindow != null) {
            popupWindow.dismiss();
            popupWindow = null;
            scrollView = null;
            contentView = null;
        }
    }

    /**
     * 切换显示和隐藏。
     */
    public void switchShowOrHidden() {
        isShow = !isShow;
        if (popupWindow != null) {
            if (popupWindow.isShowing()) {
                popupWindow.dismiss();
            } else {
                popupWindow.showAtLocation(contentView.getRootView(), Gravity.CENTER,0,0);
            }
        }
    }

    /**
     * 设置显示。
     * @param isShow
     */
    public void show(boolean isShow) {
        this.isShow = isShow;
        if (popupWindow == null) {
            return;
        }
        if (isShow && !popupWindow.isShowing()) {
            popupWindow.showAtLocation(contentView.getRootView(), Gravity.CENTER,0,0);
        } else if (!isShow && popupWindow.isShowing()) {
            popupWindow.dismiss();
        }
    }

    /**
     * 更新Debug信息。
     * @param info info
     */
    public void updateDebugInfo(String info) {
        this.debugInfo = new StringBuffer();
        this.debugInfo.append("Debug Info:\r\n" + info);
        if (contentView != null && popupWindow.isShowing()) {
            contentView.setText(this.debugInfo);
        }
    }

    /**
     * 添加日志信息。
     * @param info
     */
    public void appendDebugInfo(String info) {
        this.debugInfo = new StringBuffer();

        debugQueue.add("\n"+DATE_FORMAT.format(new Date())+"> "+info);
        if (debugQueue.size() > DEBUG_MAX_LINE_COUNT) {
            debugQueue.poll();
        }
        for (String d : debugQueue) {
            debugInfo.append(d);
        }
        if (contentView != null && popupWindow.isShowing()) {
            contentView.setText(this.debugInfo);
        }
    }
}

package cn.com.essence.android.widget.util;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;

import cn.com.essence.android.widget.R;

public class Float {

    WindowManager.LayoutParams wmParams;
    private WindowManager windowManager;
    private Button button;
    private static Float sInstance;

    private Float() {
    }
    /**
     * 获取API实例。
     * @return
     */
    public static Float getInstance() {
        if (sInstance == null) {
            synchronized (Float.class) {
                if (sInstance == null) {
                    sInstance = new Float();
                }
            }
        }
        return sInstance;
    }
    /**
     * 初始化windowManager
     */
    public void resetButton(Activity activity) {
        if (button != null) {
            return;
        }
        windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
        wmParams = getParams();//设置好悬浮窗的参数
        // 悬浮窗默认显示以左上角为起始坐标
        wmParams.gravity = Gravity.RIGHT | Gravity.BOTTOM;
        // 获取浮动窗口视图所在布局
        button = (Button) LayoutInflater.from(activity).inflate(R.layout.floating_layout,
                null);

    }
    public void show2(Activity activity) {
        //Looper.prepare();
        final WindowManager wm =
                (WindowManager) activity.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
        WindowManager.LayoutParams para = new WindowManager.LayoutParams();
        //设置弹窗的宽高
        para.height = WindowManager.LayoutParams.WRAP_CONTENT;
        para.width = WindowManager.LayoutParams.WRAP_CONTENT;
        //期望的位图格式。默认为不透明
        para.format = 1;
        //当FLAG_DIM_BEHIND设置后生效。该变量指示后面的窗口变暗的程度。
        //1.0表示完全不透明,0.0表示没有变暗。
        para.dimAmount = 0.6f;
        para.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        //设置为系统提示
        if (Build.VERSION.SDK_INT > 25) {
            para.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            if (! Settings.canDrawOverlays(activity)) {
                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                        Uri.parse("package:" + activity.getPackageName()));
                activity.startActivityForResult(intent, 10);
            }
            } else {
            para.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
        }

        //获取要显示的View
        button = (Button) LayoutInflater.from(activity).inflate(R.layout.floating_layout,
                null);
        //单击View是关闭弹窗
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                wm.removeView(button);
            }
        });
        //显示弹窗
        wm.addView(button, para);

        //Looper.loop();
    }
    public void show() {
        // 添加悬浮窗的视图
        windowManager.addView(button, wmParams);
    }
    public void hideButton() {
        if (button != null && windowManager != null) {
            button = null;
            onDestroy();
        }
    }

    /**
     * 对windowManager进行设置
     *
     * @return
     */
    public WindowManager.LayoutParams getParams() {
        wmParams = new WindowManager.LayoutParams();
        //设置window type 下面变量2002是在屏幕区域显示,2003则可以显示在状态栏之上
        //wmParams.type = LayoutParams.TYPE_PHONE;
        //wmParams.type = LayoutParams.TYPE_SYSTEM_ALERT;
        //wmParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
        //设置浮动窗口不可聚焦(实现操作除浮动窗口外的其他可见窗口的操作)
        //wmParams.flags = LayoutParams.FLAG_NOT_FOCUSABLE;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        } else {
            wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }
        //设置可以显示在状态栏上
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
                WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;

        //设置悬浮窗口长宽数据
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

        return wmParams;
    }

    private void onDestroy() {
        if (button != null) {
            // 移除悬浮窗口
            windowManager.removeView(button);
        }
    }

}


4. 实战2: popupwindow实现可拖动悬浮窗

package cn.com.essence.mobile.library.common.base.sensorplate;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.graphics.Color;
import android.graphics.Point;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;

import cn.com.essence.mobile.library.common.R;

import org.json.JSONObject;

/**
 * Debug窗口,用于将一些debug信息显示到UI界面,方面能直观看到调试信息
 * 神策埋点可视化工具面板。
 */
public class DragSensorWindow {
    private static DragSensorWindow instance;
    private final List<SensorBean> dataList = new ArrayList<>();
    private LinearLayout rootLayout;
    private ListAdapter listAdapter;
    private PopupWindow popupWindow;
    private Boolean isDebuggable;
    private Activity activity;
    //开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
    private int touchStartX;
    private int touchStartY;

    //拖拽控件偏移值
    private int offsetX = 0;
    private int offsetY = 0;

    /**
     * 是否显示弹窗。
     */
    private boolean showPop;
    private HandleDataListView lvData;

    private DragSensorWindow() {
    }

    /**
     * 获取API实例。
     *
     * @return 实例
     */
    public static DragSensorWindow getInstance() {
        if (instance == null) {
            synchronized (DragSensorWindow.class) {
                if (instance == null) {
                    instance = new DragSensorWindow();
                }
            }
        }
        return instance;
    }

    /**
     * 展示布局。
     *
     * @param context context
     */
    @SuppressLint("InflateParams")
    public void resetWindow(final Activity context) {
        if (!isDebuggable(context)) {
            return;
        }
        //说明当前activity还是存在 且与跳到下一个页面是同一个activity
        if (activity != null && activity.getClass().getName().equals(context.getClass().getName())
                && popupWindow != null) {
            return;
        }
        if (popupWindow != null) {
            popupWindow.dismiss();
            popupWindow = null;
        }
        activity = context;
        rootLayout = (LinearLayout) LayoutInflater.from(context)
                .inflate(R.layout.drag_debug_sensor_window_layout, null);
        
        popupWindow = new PopupWindow(rootLayout,
                getScreenW(), getScreenH() / 3, false);
        popupWindow.setTouchable(true);
        //注意:这个生效的前提是 focusable为false
        popupWindow.setOutsideTouchable(false);
        View rootView = rootLayout.findViewById(R.id.root_view);
        //设置布局可以点击
        rootView.setClickable(true);

        lvData = rootLayout.findViewById(R.id.lv_data);
        listAdapter = new ListAdapter();
        lvData.setAdapter(listAdapter);
        listAdapter.notifyDataSetChanged();
        TextView tvClearData = rootLayout.findViewById(R.id.tv_clear_data);
        ImageView ivClose = rootLayout.findViewById(R.id.iv_close);

        ivClose.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                hide();
            }
        });

        tvClearData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dataList.clear();
                listAdapter.notifyDataSetChanged();
                Toast.makeText(context, "清空数据成功", Toast.LENGTH_SHORT).show();
            }
        });

        //设置触摸监听器
        rootView.setOnTouchListener(new MyTouchListener());
        //针对back键监听
        rootView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                return keyCode == KeyEvent.KEYCODE_BACK;
            }
        });
        if (showPop) {
            show();
        }

    }

    /**
     * 只在测试环境才弹。
     *
     * @param context context
     * @return 结果
     */
    private boolean isDebuggable(Activity context) {
        try {
            ApplicationInfo info = context.getApplicationInfo();
            isDebuggable = (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return isDebuggable;
    }

    /**
     * 显示。
     */

    public void show() {
        if (popupWindow != null && activity != null) {
            showPop = true;
            if (popupWindow.isShowing()) {
                return;
            }
            new Handler().post(new Runnable() {

                public void run() {
                    //Gravity.BOTTOM 弹窗显示在屏幕下边界的中心位置,并以PopupWindow下边界中心为坐标原点(0,0)
                    // 来偏移,x + 表示向左偏移,y +表示向上偏移
                    popupWindow.showAtLocation(rootLayout.getRootView(), Gravity.BOTTOM, offsetX,
                            -offsetY);
                }

            });

        }
    }

    /**
     * 隐藏 同时销毁。
     */
    public void hide() {
        if (popupWindow != null && popupWindow.isShowing()) {
            popupWindow.dismiss();
            showPop = false;
            offsetX = 0;
            offsetY = 0;
        }
    }

    /**
     * 更新Debug信息。
     *
     * @param sensorEventType  埋点类型 页面埋点/按钮埋点/自定义埋点
     * @param dataJson  dataJson
     * @param labelJson labelJson
     */
    public void updateDebugInfo(String sensorEventType, JSONObject dataJson, JSONObject labelJson) {
        if (dataJson == null || labelJson == null) {
            return;
        }
        if (popupWindow != null && popupWindow.isShowing()) {
            SensorBean sensorBean = createSensorBean(sensorEventType,dataJson, labelJson);
            dataList.add(sensorBean);
            listAdapter.notifyDataSetChanged();
            lvData.setSelection(lvData.getBottom());
        }
    }

    /**
     * 创建神策埋点bean。
     *
     * @param sensorEventType  埋点类型 页面埋点/按钮埋点/自定义埋点
     * @param dataJson 数据json
     * @param labelJson 标签json
     * @return bean
     */
    private SensorBean createSensorBean(String sensorEventType, JSONObject dataJson,
                                        JSONObject labelJson) {
        SensorBean sensorBean = new SensorBean();
        Iterator<String> keys = dataJson.keys();
        StringBuilder content = new StringBuilder();
        while (keys.hasNext()) {
            String key = keys.next();
            String dataString = dataJson.optString(key);
            String labelString = labelJson.optString(key);
            if (!TextUtils.isEmpty(dataString) && !TextUtils.isEmpty(labelString)) {
                content.append(labelString).append(":").append(dataString).append("  ");
            }
        }
        sensorBean.setContent(content.toString());
        //埋点时间
        sensorBean.setTime(nowDate());
        //埋点类型
        sensorBean.setPageType(sensorEventType == null ? "" : sensorEventType);
        sensorBean.setAppType("");
        return sensorBean;
    }

    /**
     * 悬浮窗布局监听。
     */
    private class MyTouchListener implements View.OnTouchListener {

        @Override
        public boolean onTouch(View arg0, MotionEvent event) {

            int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    touchStartX = (int) event.getRawX();
                    touchStartY = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int touchCurrentX = (int) event.getRawX();
                    int touchCurrentY = (int) event.getRawY();
                    offsetX += touchCurrentX - touchStartX;
                    offsetY += touchCurrentY - touchStartY;
                    //width 为-1 表示不会更改本身布局大小
                    //兼容7.0版本及以下
                    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
                        popupWindow.update(offsetX, getScreenH() / 3 * 2 + offsetY, -1, -1);
                    } else {
                        //7.0以上版本
                        popupWindow.update(offsetX, -offsetY, -1, -1);
                    }
                    touchStartX = touchCurrentX;
                    touchStartY = touchCurrentY;
                    break;
                case MotionEvent.ACTION_UP:
                default:
                    break;
            }
            return false;
        }

    }

    /**
     * 埋点adapter。
     */
    class ListAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return dataList.size();
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public Object getItem(int position) {
            return dataList.get(position);
        }

        @SuppressLint("InflateParams")
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = LayoutInflater.from(activity).inflate(R.layout.sensor_item_pop_layout,
                        null);
                viewHolder.tvItemTime = convertView.findViewById(R.id.tv_item_time);
                viewHolder.tvItemPageType = convertView.findViewById(R.id.tv_item_page_type);
                viewHolder.tvItemAppType = convertView.findViewById(R.id.tv_item_app_type);
                viewHolder.tvItemContent = convertView.findViewById(R.id.tv_item_content);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            SensorBean sensorBean = dataList.get(position);
            if (sensorBean != null) {
                viewHolder.tvItemTime.setText(sensorBean.getTime());
                viewHolder.tvItemPageType.setText(sensorBean.getPageType());
                viewHolder.tvItemAppType.setText(sensorBean.getAppType());
                String content = sensorBean.getContent();
                if (content  == null) {
                    viewHolder.tvItemContent.setText("");
                } else {
                    viewHolder.tvItemContent.setText(setColorByContent(content));
                }
            }
            return convertView;
        }

        /**
         * 主要是为了指定字段文案变色。
         * @param content 上下文
         * @return
         */
        private SpannableString setColorByContent(String content) {
            int index = -1;
            if (content.contains("安信页面名称")) {
                index = content.indexOf("安信页面名称");
            } else if (content.contains("安信按钮名称")) {
                index = content.indexOf("安信按钮名称");
            }
            SpannableString spanString = new SpannableString(content);
            if (index >= 0) {
                ForegroundColorSpan span = new ForegroundColorSpan(Color.BLUE);
                spanString.setSpan(span, index, content.length() - 1,
                        Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
            return spanString;
        }

        //自定义ViewHolder类
        class ViewHolder {
            TextView tvItemTime;  //时间
            TextView tvItemPageType;  //页面类型
            TextView tvItemAppType;  //App类型
            TextView tvItemContent;  //内容
        }
    }

    /**
     * 清空数据。
     */
    public void clearData() {
        if (dataList != null) {
            dataList.clear();
            listAdapter.notifyDataSetChanged();
        }
    }

    /**
     * 获取屏幕高度。
     */
    private int getScreenH() {
        Point outPoint = getPoint();
        return outPoint.y;
    }

    /**
     * 获取屏幕宽度。
     */
    private int getScreenW() {
        Point outPoint = getPoint();
        return outPoint.x;
    }

    /**
     * 获取屏幕Point。
     */
    @NonNull
    private Point getPoint() {
        Point outPoint = new Point();
        try {
            WindowManager windowManager =
                    (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
            final Display display = windowManager.getDefaultDisplay();
            display.getRealSize(outPoint);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return outPoint;
    }

    /**
     * 获取当前日期。
     * @return 日期字符串
     */
    private String nowDate() {
        String dateTime;
        Date dt = new Date();
        //最后的aa表示“上午”或“下午”    HH表示24小时制    如果换成hh表示12小时制
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
        dateTime = sdf.format(dt);
        return dateTime;
    }

}

5.windowManager跟Popupwindow还存在的问题

问题1: Popupwindow是依赖activity的 如果跳转下一个activity pop会被覆盖 这就会导致悬浮窗不能一直存在于整个应用内,相当于acitivty级别的悬浮窗,暂时的办法是存了一个静态pop(不合理)

问题2: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY 这个属性会导致悬浮窗不止存在于应用内,更是存在于整个手机屏幕,相当于是系统级别弹窗(不合理)

思考:如何实现一个应用级别的悬浮窗????(如有读者实现了 可提供思路)

Mark:

1、有些悬浮窗里面是一个列表 可以参考listview的使用
2、WindowManager.LayoutParams.type属性