背景
游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑
方案一:应用外悬浮窗+栈顶权限/生命周期回调
1、弹出悬浮窗需要用到悬浮窗权限
<!--悬浮窗权限--> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>2、判断悬浮窗游戏内外显示
方式一:使用栈顶权限获取当前
//需要声明权限<uses-permission android:name="android.permission.GET_TASKS" />
//判断当前是否在后台private boolean isAppIsInBackground(Context context) { boolean isInBackground = true; ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT_WATCH) { List<ActivityManager.RunningAppProcessInfo> runningProcesses = am.getRunningAppProcesses(); for (ActivityManager.RunningAppProcessInfo processInfo : runningProcesses) { //前台程序 if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { for (String activeProcess : processInfo.pkgList) { if (activeProcess.equals(context.getPackageName())) { isInBackground = false; } } } } } else { List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1); ComponentName componentInfo = taskInfo.get(0).topActivity; if (componentInfo.getPackageName().equals(context.getPackageName())) { isInBackground = false; } }
return isInBackground; }这里考虑到这种方案网上有很多具体案例,在这里就不实现了。但是这种方案有如下缺点:
方案二:addContentView实现
原理:Activity 的接口中除了我们常用的 setContentView 接口外,还有 addContentView 接口。利用该接口可以在 Activity 上添加 View。
这里你可能会问:
1、那只能在一个 Activity 上添加吧?
没错,是只能在当前 Activity 上添加,但是由于游戏通常也就在一个 Activity 跑,因此基本上是可以接受的。
2、只 add 一个 view,那拖动怎么实现?
LayoutParams params = new LayoutParams(mWidth, mHeight);params.setMargins(mLeft, mTop, 0, 0);setLayoutParams(params);通过更新 LayoutParams 调整子 View 在父 View 中的位置就能实现
具体代码如下:
/** * @author zhuxiaoxin * 可拖拽贴边的view */public class DragViewLayout extends RelativeLayout {
//手指拖拽得到的位置 int mLeft, mRight, mTop, mBottom;
//view所在的位置 int mLastX, mLastY;
/** * 屏幕宽度|高度 */ int mScreenWidth, mScreenHeight;
/** * view的宽度|高度 */ int mWidth, mHeight;
/** * 是否在拖拽过程中 */ boolean isDrag = false;
/** * 系统最小滑动距离 * @param context */ int mTouchSlop = 0;
public DragViewLayout(Context context) { this(context, null); }
public DragViewLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScreenWidth = context.getResources().getDisplayMetrics().widthPixels; mScreenHeight = context.getResources().getDisplayMetrics().heightPixels; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); }
@Override protected void onFinishInflate() { super.onFinishInflate(); }
@Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mLeft = getLeft(); mRight = getRight(); mTop = getTop(); mBottom = getBottom(); mLastX = (int) event.getRawX(); mLastY = (int) event.getRawY(); break; case MotionEvent.ACTION_MOVE: int x = (int) event.getRawX(); int y = (int) event.getRawY(); int dx = x - mLastX; int dy = y - mLastY; if (Math.abs(dx) > mTouchSlop) { isDrag = true; } mLeft += dx; mRight += dx; mTop += dy; mBottom += dy; if (mLeft < 0) { mLeft = 0; mRight = mWidth; } if (mRight >= mScreenWidth) { mRight = mScreenWidth; mLeft = mScreenWidth - mWidth; } if (mTop < 0) { mTop = 0; mBottom = getHeight(); } if (mBottom > mScreenHeight) { mBottom = mScreenHeight; mTop = mScreenHeight - mHeight; } mLastX = x; mLastY = y; //根据拖动举例设置view的margin参数,实现拖动效果 LayoutParams params = new LayoutParams(mWidth, mHeight); params.setMargins(mLeft, mTop, 0, 0); setLayoutParams(params); break; case MotionEvent.ACTION_UP: //手指抬起,执行贴边动画 if (isDrag) { startAnim(); isDrag = false; } break; } return super.dispatchTouchEvent(event); }
//执行贴边动画 private void startAnim(){ ValueAnimator valueAnimator; if (mLeft < mScreenWidth / 2) { valueAnimator = ValueAnimator.ofInt(mLeft, 0); } else { valueAnimator = ValueAnimator.ofInt(mLeft, mScreenWidth - mWidth); } //动画执行时间 valueAnimator.setDuration(100); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mLeft = (int)animation.getAnimatedValue(); //动画执行依然是使用设置margin参数实现 LayoutParams params = new LayoutParams(mWidth, mHeight); params.setMargins(mLeft, getTop(), 0, 0); setLayoutParams(params); } }); valueAnimator.start(); }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mWidth == 0) { //获取view的高宽 mWidth = getWidth(); mHeight = getHeight(); } }
}/** * @author zhuxiaoxin * 37悬浮窗基础view */public class SqAddFloatView extends DragViewLayout {
private RelativeLayout mFloatContainer;
public SqAddFloatView(final Context context, final int floatImgId) { super(context); setClickable(true); final ImageView floatView = new ImageView(context); floatView.setImageResource(floatImgId); floatView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show(); } }); LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); addView(floatView, params); }
public void show(Activity activity) { FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); if(mFloatContainer == null) { mFloatContainer = new RelativeLayout(activity); } RelativeLayout.LayoutParams floatViewParams = new RelativeLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT); floatViewParams.setMargins(0, (int) (mScreenHeight * 0.4), 0, 0); mFloatContainer.addView(this, floatViewParams); activity.addContentView(mFloatContainer, params);
}}在 Activity 中使用
SqAddFloatView(this, R.mipmap.ic_launcher).show(this)方案三:WindowManager+应用内层级实现
WindowManger 中的层级有如下两个(其实是一样的~)可以实现在 Activity 上增加 View
/** * Start of types of sub-windows. The {@link #token} of these windows * must be set to the window they are attached to. These types of * windows are kept next to their attached window in Z-order, and their * coordinate space is relative to their attached window. */ public static final int FIRST_SUB_WINDOW = 1000;
/** * Window type: a panel on top of an application window. These windows * appear on top of their attached window. */ public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;具体实现时,WindowManger 相关的核心代码如下:
public void show() { floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, //最最重要的一句 WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.RGBA_8888); floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; floatLayoutParams.x = mMinWidth; floatLayoutParams.y = (int)(mScreenHeight * 0.4); mWindowManager.addView(this, floatLayoutParams); }添加完 view 如何更新位置?
使用 WindowManager 的 updateViewLayout 方法
mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);完整代码如下:
DragViewLayout:
public class DragViewLayout extends RelativeLayout {
//view所在位置 int mLastX, mLastY;
//屏幕高宽 int mScreenWidth, mScreenHeight;
//view高宽 int mWidth, mHeight;
/** * 是否在拖拽过程中 */ boolean isDrag = false;
/** * 系统最小滑动距离 * @param context */ int mTouchSlop = 0;
WindowManager.LayoutParams floatLayoutParams; WindowManager mWindowManager;
//手指触摸位置 private float xInScreen; private float yInScreen; private float xInView; public float yInView;
public DragViewLayout(Context context) { this(context, null); }
public DragViewLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public DragViewLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mScreenWidth = context.getResources().getDisplayMetrics().widthPixels; mScreenHeight = context.getResources().getDisplayMetrics().heightPixels; mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); }
@Override protected void onFinishInflate() { super.onFinishInflate(); }
@Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mLastX = (int) event.getRawX(); mLastY = (int) event.getRawY(); yInView = event.getY(); xInView = event.getX(); xInScreen = event.getRawX(); yInScreen = event.getRawY(); break; case MotionEvent.ACTION_MOVE: int dx = (int) event.getRawX() - mLastX; int dy = (int) event.getRawY() - mLastY; if (Math.abs(dx) > mTouchSlop || Math.abs(dy) > mTouchSlop) { isDrag = true; } xInScreen = event.getRawX(); yInScreen = event.getRawY(); mLastX = (int) event.getRawX(); mLastY = (int) event.getRawY(); //拖拽时调用WindowManager updateViewLayout更新悬浮球位置 updateFloatPosition(false); break; case MotionEvent.ACTION_UP: if (isDrag) { //执行贴边 startAnim(); isDrag = false; } break; default: break; } return super.dispatchTouchEvent(event); }
//更新悬浮球位置 private void updateFloatPosition(boolean isUp) { int x = (int) (xInScreen - xInView); int y = (int) (yInScreen - yInView); if(isUp) { x = isRightFloat() ? mScreenWidth : 0; } if(y < 0) { y = 0; } if(y > mScreenHeight - mHeight) { y = mScreenHeight - mHeight; } floatLayoutParams.x = x; floatLayoutParams.y = y; //更新位置 mWindowManager.updateViewLayout(this, floatLayoutParams); }
/** * 是否靠右边悬浮 * @return */ boolean isRightFloat() { return xInScreen > mScreenWidth / 2; }
//执行贴边动画 private void startAnim(){ ValueAnimator valueAnimator; if (floatLayoutParams.x < mScreenWidth / 2) { valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, 0); } else { valueAnimator = ValueAnimator.ofInt(floatLayoutParams.x, mScreenWidth - mWidth); } valueAnimator.setDuration(200); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { floatLayoutParams.x = (int)animation.getAnimatedValue(); mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams); } }); valueAnimator.start(); }
//悬浮球显示 public void show() { floatLayoutParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.FIRST_SUB_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.RGBA_8888); floatLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; floatLayoutParams.x = 0; floatLayoutParams.y = (int)(mScreenHeight * 0.4); mWindowManager.addView(this, floatLayoutParams); }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mWidth == 0) { //获取悬浮球高宽 mWidth = getWidth(); mHeight = getHeight(); } }}悬浮窗View
public class SqWindowManagerFloatView extends DragViewLayout {
public SqWindowManagerFloatView(final Context context, final int floatImgId) { super(context); setClickable(true); final ImageView floatView = new ImageView(context); floatView.setImageResource(floatImgId); floatView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(context, "点击了悬浮球", Toast.LENGTH_SHORT).show(); } }); LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); addView(floatView, params); }}使用:
SqWindowManagerFloatView(this, R.mipmap.float_icon).show()小结
1、方案一需要用到多个权限,显然是不合适的。
2、方案二简单方便,但是用到了 Activity 的 addContentView 方法,在某些游戏引擎上使用会有问题。因为有些游戏引擎不是在 Activity 上跑的,而是在NativeActivity 上跑
3、方案三是我们当前采用的方案,目前还暂未发现有显示不出来之类的问题~
4、本文讲述的方案只是 Demo 哈,实际使用还需要考虑刘海屏的问题,本文暂未涉及
如果觉得不错,素质三连、或者点个「赞」、「在看」都是对笔者莫大的支持,谢谢各位大佬啦~
推荐阅读