Android创建应用全局小窗口

需求描述
类似微信视频、语音时点击返回会形成一个App小窗口浮动在界面上,点击继续是通通话,如下图:
Android创建应用全局小窗口
文章图片

效果展示

技术分析
其实实现这个功能只需要你细心分析一下就有思路了:首先这个小窗口是浮动在app最上层的视图,其次所有触屏事件需先由该小窗口处理,还有就是小窗口的生命周期和Application也能虽可能不能同生,但是确是可以共死。所以可以在Application中创建一个view添加到WindowManage,这里将视图为view的window的type设置成系统级别的窗口,这样这个window可以在在全局呈现。另外,还需要让这个window可以随手指拖动而滑动,手指释放后会回弹到距离这个释放点最近的屏幕侧边,所以需要重写view 的OnTouch事件。
代码细节实现
  • 创建全局Application,在Application创建的时候初始化一个view,以及一个WindowManager.LayoutParams,并设置get方法,方便外部调用:
private SmallWindowView windowView; private WindowManager wm; private WindowManager.LayoutParams mLayoutParams; public SmallWindowView getWindowView() { return windowView; } public WindowManager getWm() { return wm; } public WindowManager.LayoutParams getmLayoutParams() { return mLayoutParams; } @Override public void onCreate() { super.onCreate(); initSmallViewLayout(); } public void initSmallViewLayout() { windowView = (SmallWindowView) LayoutInflater.from(this).inflate(R.layout.small_window, null); wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); mLayoutParams = new WindowManager.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); mLayoutParams.gravity = Gravity.NO_GRAVITY; //使用非CENTER时,可以通过设置XY的值来改变View的位置 windowView.setWm(wm); windowView.setWmParams(mLayoutParams); }

  • 编写一个BaseActivity 实现可供子类来显示隐藏窗口的方法:
private WindowManager wm; private SmallWindowView windowView; private WindowManager.LayoutParams mLayoutParams; private int OVERLAY_PERMISSION_REQ_CODE = 2; private boolean isRange = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); wm = ((MyApplication)getApplication()).getWm(); windowView = ((MyApplication)getApplication()).getWindowView(); mLayoutParams = ((MyApplication)getApplication()).getmLayoutParams(); }public void alertWindow() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // 7.0 以上需要引导用去设置开启窗口浮动权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 8.0 以上type需要设置成这个 mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } requestDrawOverLays(); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // 6.0 动态申请 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW}, 1); } }@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (wm != null && windowView.getWm() == null) { wm.addView(windowView, mLayoutParams); } } else { Toast.makeText(this, "权限申请失败", Toast.LENGTH_SHORT).show(); } }private int[] location = new int[2]; // 小窗口位置坐标@Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { isRange = calcPointRange(event); } if (isRange) { windowView.dispatchTouchEvent(event); } return super.onTouchEvent(event); }/** *计算当前点击事件坐标是否在小窗口内 * @param event * @return */ private boolean calcPointRange(MotionEvent event) { windowView.getLocationOnScreen(location); int width = windowView.getMeasuredWidth(); int height = windowView.getMeasuredHeight(); float curX = event.getRawX(); float curY = event.getRawY(); if (curX >= location[0] && curX <= location[0] + width && curY >= location[1] && curY <= location[1] + height) { return true; } return false; }private static final String TAG = "BaseActivity"; // android 23 以上先引导用户开启这个权限 该权限动态申请不了 @TargetApi(Build.VERSION_CODES.M) public void requestDrawOverLays() { if (!Settings.canDrawOverlays(BaseActivity.this)) { Toast.makeText(this, "can not DrawOverlays", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + BaseActivity.this.getPackageName())); startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); } else { if (wm != null && windowView.getWindowId() == null) { wm.addView(windowView, mLayoutParams); } Toast.makeText(this, "权限已经授予", Toast.LENGTH_SHORT).show(); } }@TargetApi(Build.VERSION_CODES.M) @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { if (!Settings.canDrawOverlays(this)) { Toast.makeText(this, "设置权限拒绝", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "设置权限成功", Toast.LENGTH_SHORT).show(); } } } // 移除window public void dismissWindow() { if (wm != null && windowView != null && windowView.getWindowId() != null) { wm.removeView(windowView); } }

  • 自定义一个处理Window内滑动事件的ViewGroup
/** * Author: luweicheng on 2018/8/24 11:22 * E-mail:1769005961@qq.com * GitHub: https://github.com/luweicheng24 * function: **/public class SmallWindowView extends LinearLayout { private final int screenHeight; private final int screenWidth; private int statusHeight; private float mTouchStartX; private float mTouchStartY; private float x; private float y; private WindowManager wm; public WindowManager.LayoutParams wmParams; public SmallWindowView(Context context) { this(context, null); }public WindowManager getWm() { return wm; }public void setWm(WindowManager wm) { this.wm = wm; }public WindowManager.LayoutParams getWmParams() { return wmParams; }public void setWmParams(WindowManager.LayoutParams wmParams) { this.wmParams = wmParams; this.wmParams.x = screenWidth; // 窗口先贴附在右边 }public SmallWindowView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); }public SmallWindowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); statusHeight = getStatusHeight(context); DisplayMetrics dm = getResources().getDisplayMetrics(); screenHeight = dm.heightPixels; screenWidth = dm.widthPixels; }/** * 获得状态栏的高度 * * @param context * @return */ public static int getStatusHeight(Context context) { int statusHeight = -1; try { Class clazz = Class.forName("com.android.internal.R$dimen"); Object object = clazz.newInstance(); int height = Integer.parseInt(clazz.getField("status_bar_height") .get(object).toString()); statusHeight = context.getResources().getDimensionPixelSize(height); } catch (Exception e) { e.printStackTrace(); } return statusHeight; }boolean isRight = true; @Override public boolean onTouchEvent(MotionEvent event) { x = event.getRawX(); // 触摸点相对屏幕的x坐标 y = event.getRawY() - statusHeight; // 触摸点相对于屏幕的y坐标 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (wmParams.x > 0) { isRight = true; } if (wmParams.x < 0) { isRight = false; } mTouchStartX = event.getX(); // 触摸点在View内的相对x坐标 mTouchStartY = event.getY(); // 触摸点在View内的相对Y坐标 Log.i("startP", "startX" + mTouchStartX + "====startY" + mTouchStartY); break; case MotionEvent.ACTION_MOVE: updateViewPosition(); //跟新window布局参数 break; case MotionEvent.ACTION_UP: if (wmParams.x <= 0) {//窗口贴附在左边 wmParams.x = Math.abs(wmParams.x) <= screenWidth / 2 ? -screenWidth : screenWidth; } else {// 窗口贴附在右边 wmParams.x = wmParams.x <= screenWidth / 2 ? screenWidth : -screenWidth; }// wmParams.x = screenWidth; wmParams.y = (int) (y - screenHeight / 2); // 跟新y坐标 wm.updateViewLayout(this, wmParams); break; } return true; } private void updateViewPosition() { wmParams.gravity = Gravity.NO_GRAVITY; //更新浮动窗口位置参数 int dx = (int) (mTouchStartX - x); int dy = (int) (y-screenHeight / 2); if (isRight) { wmParams.x = screenWidth / 2 - dx; } else { wmParams.x = -dx - screenWidth / 2; } wmParams.y = dy; Log.i("winParams", "x : " + wmParams.x + "y :" + wmParams.y + "dy :" + dy); wm.updateViewLayout(this, wmParams); //刷新显示 } }

【Android创建应用全局小窗口】以上就能实现一个应用内小窗口了,这里windowManager的布局参数有坑要踩:
  • 不设置Gravity属性,window的坐标是以屏幕左上角为(0,0)原点,而当第一次接受到触摸事件之后就会以默认原点更改为屏幕中心,Github源码,给个小星星

    推荐阅读