一、原理
1、添加独立的View
我们在APP中想不依靠Activity中的布局添加View时,可以通过WindowManager.addView()的方式,创建一个window,并体现添加的View。
2、Window可分为三类
应用window:一样寻常位于最底层,对应一个Activity;
子window:不能单独存在,须要附属在父window上,如Dialog;
体系window:一样寻常位于最顶层,不会被其他window遮住,如Toast。
二、悬浮窗
1、查抄权限
// 查抄悬浮窗Settings.canDrawOverlays(mContext)// 跳转到悬浮窗设置页面activity.startActivityForResult(new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity.getPackageName())), requestCode);2、添加悬浮窗
此处须要注意的是flag对于悬浮窗的影响:
FLAG_NOT_TOUCH_MODAL: 悬浮窗外部可相应变乱
FLAG_NOT_FOCUSABLE : 不处置惩罚体系按钮变乱
FLAG_LAYOUT_NO_LIMITS: 悬浮窗可以延伸到屏幕外
if (Settings.canDrawOverlays(mContext)) { // 获取WindowManager服务 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); mLayoutParams = new WindowManager.LayoutParams(); // 设置LayoutParam if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; } // 悬浮窗外部可相应变乱 mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | // 不处置惩罚体系按钮变乱 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | // 悬浮窗可以延伸到屏幕外 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mLayoutParams.format = PixelFormat.RGBA_8888; mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.gravity = Gravity.TOP | Gravity.START; mLayoutParams.x = 0; mLayoutParams.y = 300; // 将悬浮窗控件添加到WindowManager mWindowManager.addView(mRootView, mLayoutParams);}// 移除悬浮窗try { mWindowManager.removeView(mRootView);} catch (Exception e) { //nothing}3、相应点击变乱
// 可以给view直接设置mRootView.setOnClickListener(onClickListener);4、拖动
起首, 在ACTION_DOWN中记载当前的x,y位置;
然后, 在ACTION_MOVE中判定滑动隔断,如果触发了滑动,算出滑动后的x,y位置,通过updateViewLayout举行更新;
末了, 在ACTION_UP中处置惩罚贴边,点击等逻辑。
// 拖动可以给view设置时间监听if (canDrag) { mRootView.setOnTouchListener(new EFWindowOnTouchListener());}private class EFWindowOnTouchListener implements View.OnTouchListener { private boolean isMoved; private int oX; private int oY; private int x; private int y; @Override public boolean onTouch(View view, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 初始化滑动的标记 isMoved = false; oX = (int) event.getRawX(); oY = (int) event.getRawY(); x = (int) event.getRawX(); y = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int nowX = (int) event.getRawX(); int nowY = (int) event.getRawY(); int movedX = nowX - x; int movedY = nowY - y; x = nowX; y = nowY; // 如果没有触发滑动,则举行滑动判定 if (!isMoved && (Math.abs(oX - x) > mMinScrollDistance || Math.abs(oY - y) > mMinScrollDistance)) { isMoved = true; } // 拖动中不能用贴边盘算 mLayoutParams.x = getRealX(mLayoutParams.x + movedX, false, false); mLayoutParams.y = getRealY(mLayoutParams.y + movedY, false); mWindowManager.updateViewLayout(view, mLayoutParams); break; case MotionEvent.ACTION_UP: if (mIsSticky) { int viewWidth = view.getMeasuredWidth(); int screenWidth = ScreenUtils.getScreenWidth(mContext); // 贴边尽头只有左、右两种状态 if ((mLayoutParams.x + viewWidth / 2) > screenWidth / 2) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } mLayoutParams.x = screenWidth - viewWidth - mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } mLayoutParams.x = mXEdgeSize; } mWindowManager.updateViewLayout(view, mLayoutParams); } // 处置惩罚移动后还会相应点击变乱的环境 if (isMoved) { isMoved = false; return true; } break; default: break; } return false; } }5、横竖屏切换适配
监听到屏幕厘革后,由于屏幕的长宽发生了厘革,重新盘算当前的x,y,并举行更新。
// 添加屏幕厘革监听DisplayManager displayManager = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);displayManager.registerDisplayListener(new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { // 监听到屏幕发生厘革 if (mLayoutParams != null && mWindowManager != null && mRootView != null && mRootView.isAttachedToWindow()) { mLayoutParams.x = getRealX(mLayoutParams.x, false, true); mLayoutParams.y = getRealY(mLayoutParams.y, false); mWindowManager.updateViewLayout(mRootView, mLayoutParams); } }}, new Handler(Looper.getMainLooper()));6、贴边,缩进等处置惩罚
private int getRealX(int xPosition, boolean isInit, boolean considerSticky) { if (isInit) { // 此时view还没有触发测量,主动测量一次 getRootView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); } int viewWidth = getRootView().getMeasuredWidth(); int screenWidth = ScreenUtils.getScreenWidth(mContext); int screenHeight = ScreenUtils.getScreenHeight(mContext); // 部分手机,如:vivo R9s,横竖屏切换,获取的值不会发生厘革,此处做一个适配 screenWidth = isHorizontalScreen(mContext) ? Math.max(screenWidth, screenHeight) : Math.min(screenWidth, screenHeight); // 贴边初始只有左和右,没有中心态 if (considerSticky && mIsSticky) { if ((xPosition + viewWidth / 2) > screenWidth / 2) {// 处置惩罚缩进 if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } // 贴右边体现,真实坐标为(屏幕宽度 - view的宽度 - x边距) return screenWidth - viewWidth - mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } // 贴左边体现,真实坐标为(左出发点 + x边距),左边的出发点为0。 return mXEdgeSize; } } else { if (xPosition > (screenWidth - viewWidth - mXEdgeSize)) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } // 当前位置已经超过允许的右边距,返回最右边距 return screenWidth - viewWidth - mXEdgeSize; } else if (xPosition < mXEdgeSize) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } // 当前位置小于允许的左边距,返回最左边距 return mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_MIDDLE); } // 由于不吸边,以是展示真实位置 return xPosition; } }}private int getRealY(int yPosition, boolean isInit) { // 此时view还没有触发测量,主动测量一次 if (isInit) { getRootView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); } int viewHeight = getRootView().getMeasuredHeight(); int screenWidth = ScreenUtils.getScreenWidth(mContext); int screenHeight = ScreenUtils.getScreenHeight(mContext); // 部分手机,如:vivo R9s,横竖屏切换,获取的值不会发生厘革,此处做一个适配 screenHeight = isHorizontalScreen(mContext) ? Math.min(screenWidth, screenHeight) : Math.max(screenWidth, screenHeight); if (yPosition > (screenHeight - viewHeight - mYBottomEdgeSize)) { // 当前位置已经超过允许的下边距,返回最下边距 return screenHeight - viewHeight - mYBottomEdgeSize; } else if (yPosition < mYTopEdgeSize) { // 当前位置小于允许的上边距,返回最上边距 return mYTopEdgeSize; } else { // 展示真实位置 return yPosition; }}private boolean isHorizontalScreen(Context context) { int angle = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); //屏幕旋转90°或270°,体现横屏 return angle == Surface.ROTATION_90 || angle == Surface.ROTATION_270;}三、注意
部分场景须要点击回到当前的Activity,可以参考如下代码:
// 方法一:跳转到app展示的上一个页面public void onClick(View v) { Intent intent = BaseApplication.getGlobalContext().getPackageManager().getLaunchIntentForPackage(BaseApplication.getGlobalContext().getPackageName()); BaseApplication.getGlobalContext().startActivity(intent);}// 方法二:当地缓存页面,判定登录状态跳转不同的页面public void onClick(View v) { // 注意,这里的跳转没有参数通报,在一些activity的onNewIntent中会对intent举行再次分析 // 因此,须要判定是否是从悬浮窗跳已往的,举行拦截,防止出现非常 // 也可以将准确的参数通报已往,本钱较高,用作备选项 // FLAG_ACTIVITY_SINGLE_TOP,如果当前activity已经在栈顶,不再创建,直接跳转 // FLAG_ACTIVITY_REORDER_TO_FRONT,如果activity已经创建了,如:ABCD,跳转到B,不会重新创建,且变为ACDB if (ACCOUNT_CONTROLLER.hasLogin()) { if (mCurrentActivity != null) { // 正常环境都不是null,除非长时间被体系采取了 Intent targetIntent = new Intent(BaseApplication.getGlobalContext(), mCurrentActivity.getClass()); targetIntent.putExtra(SpName.INTENT_FROM, FROM_EFWINDOW); targetIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); mCurrentActivity.startActivity(targetIntent); } else { // 跳转首页,然后通过规复接口举行再次跳转 Intent targetIntent = new Intent(BaseApplication.getGlobalContext(), HomeActivity.class); targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); targetIntent.putExtra(SpName.INTENT_FROM, FROM_EFWINDOW); BaseApplication.getGlobalContext().startActivity(targetIntent); } } else { // 登录信息失效了 Intent targetIntent = new Intent(BaseApplication.getGlobalContext(), LoginActivity.class); targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); targetIntent.putExtra(SpName.INTENT_FROM, FROM_EFWINDOW); BaseApplication.getGlobalContext().startActivity(targetIntent); ToastUtil.showMessage("登录信息失效,请重新登录"); }}四、悬浮窗代码
//try {// if (mEfWindow == null) {// View view = LayoutInflater.from(BaseApplication.getGlobalContext()).inflate(R.layout.layout_edj_floating_window, null);// mEfWindow = new EFWindow.Builder(BaseApplication.getGlobalContext())// .view(view)// .yPosition(ScreenUtils.getScreenHeight(BaseApplication.getGlobalContext()) / 3)// .isSticky(true)// .canDrag(true)// .xEdgeSize(-UIUtils.dp2px(BaseApplication.getGlobalContext(), 20))// .onClickListener(this)// .build();// }// mEfWindow.show();//} catch (Exception e) {// e.printStackTrace();//}public class EFWindow { private final int mMinScrollDistance; private WindowManager mWindowManager; private WindowManager.LayoutParams mLayoutParams; private final View mRootView; private boolean mIsShow; private boolean mIsSticky; private int mXEdgeSize; private int mYTopEdgeSize; private int mYBottomEdgeSize; private OnPositionListener mOnPositionListener; private final Context mContext; @RequiresApi(api = Build.VERSION_CODES.M) private EFWindow(Builder builder) { // 赋值 mContext = builder.mContext; mMinScrollDistance = ViewConfiguration.get(mContext).getScaledTouchSlop(); mIsSticky = builder.mIsSticky; mXEdgeSize = builder.mXEdgeSize; mYTopEdgeSize = builder.mYTopEdgeSize; mYBottomEdgeSize = builder.mYBottomEdgeSize; mOnPositionListener = builder.mOnPositionListener; mRootView = builder.mRootView; boolean canDrag = builder.mCanDrag; int xPosition = builder.mXPosition; int yPosition = builder.mYPosition; View.OnClickListener onClickListener = builder.mOnClickListener; // 添加屏幕厘革监听 DisplayManager displayManager = (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE); displayManager.registerDisplayListener(new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { } @Override public void onDisplayRemoved(int displayId) { } @Override public void onDisplayChanged(int displayId) { // 监听到屏幕发生厘革 if (mLayoutParams != null && mWindowManager != null && mRootView != null && mRootView.isAttachedToWindow()) { mLayoutParams.x = getRealX(mLayoutParams.x, false, true); mLayoutParams.y = getRealY(mLayoutParams.y, false); mWindowManager.updateViewLayout(mRootView, mLayoutParams); } } }, new Handler(Looper.getMainLooper())); if (Settings.canDrawOverlays(mContext)) { // 获取WindowManager服务 mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); if (canDrag) { mRootView.setOnTouchListener(new EFWindowOnTouchListener()); } mRootView.setOnClickListener(onClickListener); mLayoutParams = new WindowManager.LayoutParams(); // 设置LayoutParam if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE; } // 悬浮窗外部可相应变乱 mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | // 不处置惩罚体系按钮变乱 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | // 悬浮窗可以延伸到屏幕外 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; mLayoutParams.format = PixelFormat.RGBA_8888; mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT; mLayoutParams.gravity = Gravity.TOP | Gravity.START; mLayoutParams.x = getRealX(xPosition, true, true); mLayoutParams.y = getRealY(yPosition, true); } } private int getRealX(int xPosition, boolean isInit, boolean considerSticky) { if (isInit) { // 此时view还没有触发测量,主动测量一次 getRootView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); } int viewWidth = getRootView().getMeasuredWidth(); int screenWidth = ScreenUtils.getScreenWidth(mContext); int screenHeight = ScreenUtils.getScreenHeight(mContext); // 部分手机,如:vivo R9s,横竖屏切换,获取的值不会发生厘革,此处做一个适配 screenWidth = isHorizontalScreen(mContext) ? Math.max(screenWidth, screenHeight) : Math.min(screenWidth, screenHeight); // 贴边初始只有左和右,没有中心态 if (considerSticky && mIsSticky) { if ((xPosition + viewWidth / 2) > screenWidth / 2) {// 处置惩罚缩进 if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } // 贴右边体现,真实坐标为(屏幕宽度 - view的宽度 - x边距) return screenWidth - viewWidth - mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } // 贴左边体现,真实坐标为(左出发点 + x边距),左边的出发点为0。 return mXEdgeSize; } } else { if (xPosition > (screenWidth - viewWidth - mXEdgeSize)) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } // 当前位置已经超过允许的右边距,返回最右边距 return screenWidth - viewWidth - mXEdgeSize; } else if (xPosition < mXEdgeSize) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } // 当前位置小于允许的左边距,返回最左边距 return mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_MIDDLE); } // 由于不吸边,以是展示真实位置 return xPosition; } } } private int getRealY(int yPosition, boolean isInit) { // 此时view还没有触发测量,主动测量一次 if (isInit) { getRootView().measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); } int viewHeight = getRootView().getMeasuredHeight(); int screenWidth = ScreenUtils.getScreenWidth(mContext); int screenHeight = ScreenUtils.getScreenHeight(mContext); // 部分手机,如:vivo R9s,横竖屏切换,获取的值不会发生厘革,此处做一个适配 screenHeight = isHorizontalScreen(mContext) ? Math.min(screenWidth, screenHeight) : Math.max(screenWidth, screenHeight); if (yPosition > (screenHeight - viewHeight - mYBottomEdgeSize)) { // 当前位置已经超过允许的下边距,返回最下边距 return screenHeight - viewHeight - mYBottomEdgeSize; } else if (yPosition < mYTopEdgeSize) { // 当前位置小于允许的上边距,返回最上边距 return mYTopEdgeSize; } else { // 展示真实位置 return yPosition; } } public View getRootView() { return mRootView; } public boolean isShow() { return mIsShow; } public void show() { mIsShow = true; // 先实行移除 try { mLayoutParams.x = getRealX(mLayoutParams.x, false, true); mLayoutParams.y = getRealY(mLayoutParams.y, false); mWindowManager.removeView(mRootView); } catch (Exception e) { //nothing } // 将悬浮窗控件添加到WindowManager mWindowManager.addView(mRootView, mLayoutParams); } public void dismiss() { // 实行移除 try { mWindowManager.removeView(mRootView); } catch (Exception e) { //nothing } mIsShow = false; } private class EFWindowOnTouchListener implements View.OnTouchListener { private boolean isMoved; private int oX; private int oY; private int x; private int y; @Override public boolean onTouch(View view, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 初始化滑动的标记 isMoved = false; oX = (int) event.getRawX(); oY = (int) event.getRawY(); x = (int) event.getRawX(); y = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int nowX = (int) event.getRawX(); int nowY = (int) event.getRawY(); int movedX = nowX - x; int movedY = nowY - y; x = nowX; y = nowY; // 如果没有触发滑动,则举行滑动判定 if (!isMoved && (Math.abs(oX - x) > mMinScrollDistance || Math.abs(oY - y) > mMinScrollDistance)) { isMoved = true; } // 拖动中不能用贴边盘算 mLayoutParams.x = getRealX(mLayoutParams.x + movedX, false, false); mLayoutParams.y = getRealY(mLayoutParams.y + movedY, false); mWindowManager.updateViewLayout(view, mLayoutParams); break; case MotionEvent.ACTION_UP: if (mIsSticky) { int viewWidth = view.getMeasuredWidth(); int screenWidth = ScreenUtils.getScreenWidth(mContext); // 贴边尽头只有左、右两种状态 if ((mLayoutParams.x + viewWidth / 2) > screenWidth / 2) { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_RIGHT); } mLayoutParams.x = screenWidth - viewWidth - mXEdgeSize; } else { if (mOnPositionListener != null) { mOnPositionListener.onPositionChanged(OnPositionListener.POSITION_LEFT); } mLayoutParams.x = mXEdgeSize; } mWindowManager.updateViewLayout(view, mLayoutParams); } // 处置惩罚移动后还会相应点击变乱的环境 if (isMoved) { isMoved = false; return true; } break; default: break; } return false; } } public static class Builder { private final Context mContext; private View mRootView; private int mXPosition; private int mYPosition; private boolean mCanDrag; private boolean mIsSticky; private int mXEdgeSize; private int mYTopEdgeSize; private int mYBottomEdgeSize; private View.OnClickListener mOnClickListener; private OnPositionListener mOnPositionListener; public Builder(Context context) { mContext = context; } public Builder onPositionListener(OnPositionListener onPositionListener) { mOnPositionListener = onPositionListener; return this; } public Builder onClickListener(View.OnClickListener onClickListener) { mOnClickListener = onClickListener; return this; } public Builder xPosition(int xPosition) { mXPosition = xPosition; return this; } public Builder yPosition(int yPosition) { mYPosition = yPosition; return this; } public Builder view(View view) { mRootView = view; return this; } public Builder canDrag(boolean canDrag) { mCanDrag = canDrag; return this; } public Builder isSticky(boolean isSticky) { mIsSticky = isSticky; return this; } public Builder xEdgeSize(int xEdgeSize) { mXEdgeSize = xEdgeSize; return this; } public Builder yTopEdgeSize(int yTopEdgeSize) { mYTopEdgeSize = yTopEdgeSize; return this; } public Builder yBottomEdgeSize(int yBottomEdgeSize) { mYBottomEdgeSize = yBottomEdgeSize; return this; } public Builder view(int viewRes) throws Exception { if (mContext == null) { throw new Exception("EFWindow:请传入准确的context."); } mRootView = LayoutInflater.from(mContext).inflate(viewRes, null); return this; } @RequiresApi(api = Build.VERSION_CODES.M) public EFWindow build() throws Exception { if (mContext == null) { throw new Exception("EFWindow:请传入准确的context."); } if (mRootView == null) { throw new Exception("EFWindow:请先设置Window布局."); } return new EFWindow(this); } } public interface OnPositionListener { int POSITION_LEFT = 0; int POSITION_MIDDLE = 1; int POSITION_RIGHT = 2; void onPositionChanged(int positionStatus); } private boolean isHorizontalScreen(Context context) { int angle = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); //屏幕旋转90°或270°,体现横屏 return angle == Surface.ROTATION_90 || angle == Surface.ROTATION_270; }} |