主页面布局很简单,只有一个RelativelyLayout
悬浮窗中只有一个TextView
主界面代码
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
/** * 获取sdk版本号 */
private static final int SDKVERSION = Build.VERSION.SDK_INT;
/** * 窗口管理器 */
private WindowManager windowManager;
/** * 浮动按钮布局 */
private View floatingButtonView;
/** * 浮动按钮布局参数 */
private WindowManager.LayoutParams floatingButtonParams;
/** * 顶部状态栏高度 */
private int top;
/** * 浮动窗原始位置 */
private float startPositionX = 0;
private float startPositionY = 0;
/** * 屏幕宽高 */
private int contentWidth;
private int contentHeight;
private float lastX;
private float lastY;
private float mTouchStartX;
private float mTouchStartY;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
initFloatingButton();
}
private void initFloatingButton() {
//浮动按钮布局
floatingButtonView = LayoutInflater.from(this).inflate(R.layout.floating_view, null);
floatingButtonParams = new WindowManager.LayoutParams();
if (SDKVERSION >= 19) {
floatingButtonParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
floatingButtonParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
floatingButtonParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams
.FLAG_NOT_FOCUSABLE;
floatingButtonParams.format = PixelFormat.TRANSLUCENT;
floatingButtonParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
floatingButtonParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
floatingButtonParams.gravity = Gravity.TOP| Gravity.LEFT;
floatingButtonParams.x = 0;
floatingButtonParams.y = 0;
Log.d(TAG, "initFloatingButton: " + floatingButtonParams.x + " " + floatingButtonParams.y);
windowManager.addView(floatingButtonView, floatingButtonParams);
floatingButtonView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
lastX = event.getRawX();
lastY = event.getRawY() - top;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mTouchStartX = event.getX();
mTouchStartY = event.getY();
//记录悬浮窗原始位置
startPositionX = floatingButtonParams.x;
startPositionY = floatingButtonParams.y;
Log.d(TAG, "onTouch down : m " + mTouchStartX + " " + mTouchStartY);
Log.d(TAG, "onTouch down : last " + lastX + " " + lastY);
Log.d(TAG, "onTouch down : start " + startPositionX + " " + startPositionY);
Log.d(TAG, "onTouch down : Params " + floatingButtonParams.x + " " + floatingButtonParams
.y);
break;
case MotionEvent.ACTION_MOVE:
//计算新的位置
floatingButtonParams.x = (int) (lastX - mTouchStartX);
floatingButtonParams.y = (int) (lastY - mTouchStartY);
//如果原始位置在中间,所以需要减去屏幕宽高的一半
// floatingButtonParams.x = (int) (lastX - mTouchStartX - contentWidth / 2);
// floatingButtonParams.y = (int) (lastY - mTouchStartY - contentHeight / 2);
Log.d(TAG, "onTouch move : m " + mTouchStartX + " " + mTouchStartY);
Log.d(TAG, "onTouch move : last " + lastX + " " + lastY);
Log.d(TAG, "onTouch move : start " + startPositionX + " " + startPositionY);
Log.d(TAG, "onTouch move : Params " + floatingButtonParams.x + " " + floatingButtonParams
.y);
//
windowManager.updateViewLayout(floatingButtonView, floatingButtonParams);
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "onTouch up : m " + mTouchStartX + " " + mTouchStartY);
Log.d(TAG, "onTouch up : last " + lastX + " " + lastY);
Log.d(TAG, "onTouch up : start " + startPositionX + " " + startPositionY);
Log.d(TAG, "onTouch up : Params " + floatingButtonParams.x + " " + floatingButtonParams.y);
if (Math.abs(floatingButtonParams.x - startPositionX) < 20 && Math.abs(floatingButtonParams.y
- startPositionY) < 20) {
Toast.makeText(MainActivity.this, "click", Toast.LENGTH_LONG);
}
break;
}
return false;
}
});
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
//获取整个布局的宽高
Point size = new Point();
windowManager.getDefaultDisplay().getSize(size);
contentWidth = size.x;
contentHeight = size.y;
//下面这两个方法已经不建议使用了
// windowManager.getDefaultDisplay().getWidth();
// windowManager.getDefaultDisplay().getHeight();
Rect rect = new Rect();
// /取得整个视图部分,注意,如果你要设置标题样式,这个必须出现在标题样式之后,否则会出错
getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
top = rect.top;//状态栏的高度,所以rect.height,rect.width分别是系统的高度的宽度
Log.d(TAG, "onWindowFocusChanged: " + contentWidth + " " + contentHeight + " " + top);
}
}
但是上述代码有点问题,如果将悬浮窗的初始位置设为Gravity.CENTER,在拖动的最开始会有个抖动。如果将主题设为没有title的主题,然后出去界面的宽高,在拖动的时候减去宽高的一半,则没有抖动。可是加上有title的主题后,会有微小的抖动。这一bug暂未修复。
为什么悬浮窗的属性要设置成TYPE_TOAST,请看这里
1、使用 type 值为 WindowManager.LayoutParams.TYPE_PHONE 和 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT 需要申请 android.permission.SYSTEM_ALERT_WINDOW 权限,否则无法显示,报错:
E/AndroidRuntime: android.view.WindowManagerBadTokenException:Unabletoaddwindowandroid.view.ViewRootW@b64b5458 – permission denied for this window type
2、type 值为 WindowManager.LayoutParams.TYPE_TOAST 显示的 System overlay view 不需要权限,即可在任何平台显示。
但仅在 API level >= 19 时可以达到目的。API level 19 以下因无法接收无法接收触摸(点击)和按键事件,故无法达到目的。
3、对于 API level < 19 的机器(MIUI除外),想要达到目的,需要:
要有 android.permission.SYSTEM_ALERT_WINDOW 权限
将 type 设置为 WindowManager.LayoutParams.TYPE_PHONE 或者 WindowManager.LayoutParams.TYPE_SYSTEM_ALERT