首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >实战总结 Android 悬浮窗适配方案

实战总结 Android 悬浮窗适配方案

作者头像
展菲
发布于 2021-03-17 15:29:50
发布于 2021-03-17 15:29:50
2.4K0
举报
文章被收录于专栏:网罗开发网罗开发

背景


游戏内的悬浮窗通常情况下只出现在游戏内,用做切换账号、客服中心等功能的快速入口。本文将介绍几种实现方案,以及我们踩过的坑

方案一:应用外悬浮窗+栈顶权限/生命周期回调


通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用 WindowManager 在设备界面上 addView 实现( UI 层级较高,应用外显示)

1、弹出悬浮窗需要用到悬浮窗权限

代码语言:javascript
复制
    <!--悬浮窗权限-->    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
代码语言:javascript
复制

2、判断悬浮窗游戏内外显示

方式一:使用栈顶权限获取当前

代码语言:javascript
复制
//需要声明权限<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;  }

这里考虑到这种方案网上有很多具体案例,在这里就不实现了。但是这种方案有如下缺点:

  1. 适配问题,悬浮窗权限在不同设备上由于不同产商实现不同,适配难
  2. 向用户申请权限,打开率较低,体验较差

方案二:addContentView实现


原理:Activity 的接口中除了我们常用的 setContentView 接口外,还有 addContentView 接口。利用该接口可以在 Activity 上添加 View。

这里你可能会问:

1、那只能在一个 Activity 上添加吧?

没错,是只能在当前 Activity 上添加,但是由于游戏通常也就在一个 Activity 跑,因此基本上是可以接受的。

2、只 add 一个 view,那拖动怎么实现?

代码语言:javascript
复制
LayoutParams params = new LayoutParams(mWidth, mHeight);params.setMargins(mLeft, mTop, 0, 0);setLayoutParams(params);

通过更新 LayoutParams 调整子 View 在父 View 中的位置就能实现

代码语言:javascript
复制

具体代码如下:

代码语言:javascript
复制
/** * @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();        }    }
}
代码语言:javascript
复制
代码语言:javascript
复制
/** * @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 中使用

代码语言:javascript
复制
代码语言:javascript
复制
SqAddFloatView(this, R.mipmap.ic_launcher).show(this)
代码语言:javascript
复制

方案三:WindowManager+应用内层级实现


WindowManger 中的层级有如下两个(其实是一样的~)可以实现在 Activity 上增加 View

代码语言:javascript
复制
        /**         * 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 相关的核心代码如下:

代码语言:javascript
复制
代码语言:javascript
复制
    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 方法

代码语言:javascript
复制
mWindowManager.updateViewLayout(DragViewLayout.this, floatLayoutParams);
代码语言:javascript
复制

完整代码如下:

DragViewLayout:

代码语言:javascript
复制
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();        }    }}
代码语言:javascript
复制

悬浮窗View

代码语言:javascript
复制
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);    }}

使用:

代码语言:javascript
复制
代码语言:javascript
复制
SqWindowManagerFloatView(this, R.mipmap.float_icon).show()
代码语言:javascript
复制

小结


1、方案一需要用到多个权限,显然是不合适的。

2、方案二简单方便,但是用到了 Activity 的 addContentView 方法,在某些游戏引擎上使用会有问题。因为有些游戏引擎不是在 Activity 上跑的,而是在NativeActivity 上跑

3、方案三是我们当前采用的方案,目前还暂未发现有显示不出来之类的问题~

4、本文讲述的方案只是 Demo 哈,实际使用还需要考虑刘海屏的问题,本文暂未涉及

如果觉得不错,素质三连、或者点个「赞」、「在看」都是对笔者莫大的支持,谢谢各位大佬啦~

推荐阅读

Jetpack Compose 竟能写出如此炫酷的倒计时 App

化身面试官出 30+ Vue 面试题,超级干货(附答案)

了解「网罗开发」领书籍、源码

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 通常实现悬浮窗,首先考虑到的会是要使用悬浮窗权限,用 WindowManager 在设备界面上 addView 实现( UI 层级较高,应用外显示)
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档