你们好,我叫小鑫,也能够叫我蜡笔小鑫😊;java
本人17年毕业于中山大学,于2018年7月加入37手游安卓团队,曾经就任于久邦数码担任安卓开发工程师;android
目前是37手游安卓团队的海外负责人,负责相关业务开发;同时兼顾一些基础建设相关工做。windows
游戏内的悬浮窗一般状况下只出如今游戏内,用作切换帐号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及咱们踩过的坑markdown
一般实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用WindowManager在设备界面上addView实现(UI层级较高,应用外显示)app
一、弹出悬浮窗须要用到悬浮窗权限ide
<!--悬浮窗权限-->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
复制代码
二、判断悬浮窗游戏内外显示动画
方式一:使用栈顶权限获取当前ui
//须要声明权限
<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;
}
复制代码
这里考虑到这种方案网上有不少具体案例,在这里就不实现了。可是这种方案有以下缺点:this
一、适配问题,悬浮窗权限在不一样设备上因为不一样产商实现不一样,适配难。spa
二、向用户申请权限,打开率较低,体验较差
原理:Activity的接口中除了咱们经常使用的setContentView接口外,还有addContentView接口。利用该接口能够在Activity上添加View。
这里你可能会问:
一、那只能在一个Activity上添加吧?
没错,是只能在当前Activity上添加,可是因为游戏一般也就在一个Activity跑,所以基本上是能够接受的。
二、只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)
复制代码
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()
复制代码
一、方案一须要用到多个权限,显然是不合适的。
二、方案二简单方便,可是用到了Activity的addContentView方法,在某些游戏引擎上使用会有问题。由于有些游戏引擎不是在Activity上跑的,而是在NativeActivity上跑
三、方案三是咱们当前采用的方案,目前还暂未发现有显示不出来之类的问题~
四、本文讲述的方案只是Demo哈,实际使用还须要考虑刘海屏的问题,本文暂未涉及