Android游戏悬浮球,无视各种操蛋权限

老规矩,先上图:


ezgif-3-857c576987.gif

悬浮球大家都知道,无非就是一个按钮+N个子Item,会靠边,会变小...
我大概看过网上的一些实现,用的最多的可能就是应用加android.permission.SYSTEM_ALERT_WINDOW权限,然后windowManager.addView(悬浮个球)
多数手机上确实可以,我比着别人写的代码也写了一次,模拟器上运行也很好,然后到我的小米手机上,诶!诶!诶!我的球呢???
到哪儿去了现在我也没找到,各位知道怎么解决的话也请不吝赐教,指导指导!
既然它不出来,劳资这暴脾气也不会惯着它,卷起袖子换个姿势撸:用PopupWindow实现的应用内悬浮球!
首先,整体确定下我们要实现的功能:
1、显示一个球在页面上方
2、这个球可以点击
3、点击之后能展开子菜单
4、子菜单点击之后能执行相应的操作
5、球可以跟随手指移动
6、手指离开屏幕之后球自动靠边
7、靠边5秒内没有操作自动缩小

我们一个一个说:

  1. 新建一个PopupWindow,背景设置一张圆图片,在需要显示时调用showAtLocation(View parent, int gravity, int x, int y)方法。恩,就这么简单。
  2. 额,这个放到后面,跟6一起解决。
  3. 最开始我在纠结展开的子项是跟悬浮球放在一起,还是另外单独管理,最后还是选择了第二种方案。如果跟跟悬浮球放在一起,那么我要在同一个PopupWindow去处理悬浮球和子项的显示状态、业务逻辑,代码看起来很冗余。分开处理,只关注对象本身需要实现的功能和对外暴露的公共方法,这样就简单了。
    子项布局很简单,在一个LinearLayout里面循环添加了几张图片作为子项按钮,然后设置给PopupWindow做布局,当然,你要用更优美的布局替换来实现自己的需求,这里只做示例。子项本身也是一个PopupWindow,所以不需要我们去写对应的显示和隐藏的方法,直接调用PopupWindow的showAtLocation(...)和dismiss()就可以了;子项按钮的点击事件需要我们自己来实现,这里我在子项里定义了一个接口,悬浮球继承接口,实现子项的点击,为什么要传到悬浮球里处理呢,主要是不想让子项暴露,对外只展示悬浮球,悬浮球对外处理所有的功能。
  4. 悬浮球需要做的是定义一个变量,控制子项的显示状态。注意,在调用子项的showAtLocation(...)方法的时候,要计算自己的位置,告诉子项,TMD,跟着劳资,别乱跑。
  5. 这里算是比较关键的实现了,我们在悬浮球构造方法里添加对触摸事件的监听setTouchInterceptor(OnTouchListener l),对各种手势进行处理,思路大概是这样的:手指移动的时候调用PopupWindow的update(...)方法,对悬浮球的位置要不断更新;手指抬起的时候要判断是靠左边近还是靠右边近,直接更改x的值,然后也是用update(...)方法让悬浮球靠边站(PS:update(...)是瞬时移动的,由于时间较短,肉眼看着也不算突兀,强迫症自行修改实现滑动),悬浮球靠边之后,hanler延时5s下一道圣旨,在handleMessage(...)方法里边又是调用update(...)方法,对悬浮球尺寸进行操作,实现变小。
  6. 手指离开屏幕之后球自动靠边的功能我们在上边已经讲了,在MotionEvent.ACTION_UP这种情况下可以处理。但是悬浮球的点击事件怎么办,在onTouch(...)我们返回了true,点击事件走不到onClick(...)了,那就另辟蹊径吧:在手指按下的时候我们记录了按下的位置,手指抬起的时候我们记录了抬起的位置,如果两次位置之间的差值小于touchSlop,那么我们认为这是一次点击事件!之后就可以很愉快的在if(Math.abs(dx)<touchSlop && Math.abs(dy)<touchSlop)条件下处理点击之后的事情了。
  7. 好吧,这个在5里边也已经解释过了。
    代码里注释非常非常详细了,这里就不费键盘了,后面会贴出代码和Github地址。

好了,上面就是整个悬浮球的实现,利用PopupWindow,避开了所有的权限申请,尤其在这么多机型面前,申请权限还不一定能实现,操蛋!!

但是!!上面说了,现在我在做游戏行业相关工作,游戏整个页面其实只是一个Activity(多activity反正我公司没有,别抬杠),上面实现的悬浮球也只是针对单Activity的(其实合理使用单 activity 配合 fragment,页面跳转更流畅,管理更方便,参考新版知乎),细节方面大家自行扩展。

附:
Github地址:https://github.com/StormFeng/FloatView.git
FloatPopup.java

public class FloatPopup extends PopupWindow implements FloatPopupItem.OnItemClickListener {

    //设置悬浮按钮尺寸
    private int size = Util.dp2px(50);
    private int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
    private int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
    //触发移动事件的最小距离
    private int touchSlop = new ViewConfiguration().getScaledTouchSlop();
    //记录当前手指实时位置
    private float curX,curY;
    //记录手指按下时的位置
    private float lastX,lastY;
    //设置当前悬浮按钮的显示位置
    private float showX,showY;
    //记录当前悬浮按钮显示状态
    private boolean showMenu = false;
    //记录当前悬浮按钮显示位置
    private boolean showLeft = true;
    private FloatPopupItem item;
    private Activity context;
    private OnClickListener onClickListener;
    private Handler handler;
    private Message message;

    private static FloatPopup floatPopup;

    public static FloatPopup getInstance(){
        if(floatPopup==null){
            floatPopup = new FloatPopup(Util.getContext());
        }
        return floatPopup;
    }

    public void show(){
        if(!floatPopup.isShowing()){
            floatPopup.showAtLocation(Util.getContext().getWindow().getDecorView(),
                    Gravity.NO_GRAVITY,0,0);
            floatPopup.setOnClickListener((OnClickListener) Util.getContext());
        }
    }

    @SuppressLint("HandlerLeak")
    public FloatPopup(Context context) {
        this.context = (Activity) context;
        item = new FloatPopupItem(context);
        item.setOnItemClickListener(this);
        handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //接受到消息,说明用户在规定时间没有操作悬浮按钮,这个时候还要判断下子选项是否展开,
                //选项没有展开,那么就让悬浮按钮变小,靠边站
                if(!showMenu){
                    toSmallIcon(msg.arg1,msg.arg2);
                }
            }
        };
        message = handler.obtainMessage();
        message.what = 0;

        ImageView iv = new ImageView(context);
        iv.setMinimumWidth(size);
        iv.setMinimumHeight(size);
        iv.setImageDrawable(context.getResources().getDrawable(R.mipmap.ic_launcher_round));
        setContentView(iv);
        setWidth(size);
        setHeight(size);
        setFocusable(false);
        setBackgroundDrawable(new ColorDrawable(0x00000000));
        setOutsideTouchable(false);
        setTouchInterceptor(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                curX = event.getRawX();
                curY = event.getRawY();
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        lastX = event.getRawX();
                        lastY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
//                        float ddx = lastX - curX;
//                        float ddy = lastY - curY;
//                        if(Math.abs(ddx)<touchSlop && Math.abs(ddy)<touchSlop){
//                            return true;
//                        }
                        /*如果当前手指在Y轴上的位置小于按钮的一半时,这个时候按钮的上边沿已经最靠边了。
                        想想一下当Y刚好等于临界值size/2,按钮会在什么位置,就会理解这里为什么做判断了。*/
                        if(curY<size/2){
                           /*在MotionEvent.ACTION_MOVE里面去update按钮的位置,是因为手指每次的移动都会消费
                            move事件,你移动很长的一段路程,在move事件里就会分解成一小段一小段的位移。*/
                            update((int)event.getRawX() - size/2,0);
                        /*在M这里和上边是一样的道理,当Y=临界值screenHeight-size/2时,说明按钮已经接近下边缘了*/
                        }else if(curY>screenHeight-size/2){
                            update((int)event.getRawX() - size/2,screenHeight-size);
                        }else{
                            /*常规情况。但是这里为什么要减去size/2呢(还有上边)?
                            我们设置的位置对于按钮来说是它的左上角,这里减去size/2只是为了让我们的参考点移
                            动到按钮的中心位置,另外,滑动的时候会消除掉一顿一顿的情况,不信你试试没有减掉
                            size/2时是什么样子*/
                            update((int)event.getRawX() - size/2,(int)event.getRawY()-size/2);
                        }

                        /*当开始移动的时候要判断下子项是否展开,如果展开,关闭之后再移动*/
                        if(item.isShowing()){
                            item.dismiss();
                            showMenu = !showMenu;
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        /*这里就很好理解了,当我们手指抬起时,如果抬起的位置靠近左边(curX<screenWidth/2),
                            抬起之后就让按钮滚到左边去,否则,到右边乖乖站好*/
                        if(curX<screenWidth/2){
                            showLeft = true;
                            showX = 0;
                            showY = event.getRawY()-size/2;
                        }else{
                            showLeft = false;
                            showX = screenWidth-size;
                            showY = event.getRawY()-size/2;
                        }
                        update((int) showX,(int) showY);

                        /*这里是处理点击事件的,当手指按钮和抬起之间的距离小于touchSlop时,
                        我们认为这是一次点击事件,并根据showMenu的值处理显示或者隐藏子项*/
                        float dx = lastX - curX;
                        float dy = lastY - curY;
                        if(Math.abs(dx)<touchSlop && Math.abs(dy)<touchSlop){
                            if(!showMenu){
                                showMenu();
                            }else{
                                hideMenu();
                            }
                            showMenu = !showMenu;
                        }

                        handler.removeMessages(0);

                        message = handler.obtainMessage();
                        message.what = 0;
                        message.arg1 = (int) showX;
                        message.arg2 = (int) showY;
                        /*手指抬起5s内没有操作的话,让图标变小*/
                        handler.sendMessageDelayed(message,5000);
                        break;
                }
                return true;
            }
        });
    }

    private void toSmallIcon(int curx,int cury){
        if(showLeft){
            update(curx,cury,size/2,size/2);
        }else{
            update(curx+size/2,cury,size/2,size/2);
        }
    }

    private void hideMenu() {
        if(item!=null){
            item.dismiss();
        }
    }

    private void showMenu(){
        /*这里为什么加,为什么减,自己拿尺子比着屏幕量吧,打字好累...*/
        if(showLeft){
            item.showAtLocation(context.getWindow().getDecorView(),Gravity.NO_GRAVITY,(int)(showX+size),(int)showY);
        }else{
            item.showAtLocation(context.getWindow().getDecorView(),Gravity.NO_GRAVITY,(int)(showX-item.width),(int)showY);
        }
    }

    @Override
    public void update(int x, int y) {
        this.update(x,y,size, size);
    }
 
    @Override
    public void update(int x, int y, int width, int height) {
        super.update(x, y, width, height);
    }

    /**
     * 这里用了两个接口把子项item的点击事件传到FloatPopup的onClick(int i)方法里面统一处理,
     * 因为我们只对外暴露FloatPopup
     * @param i
     */
    @Override
    public void onItemClick(int i) {
        if(onClickListener!=null){
            onClickListener.onClick(i);
        }
    }

    public void setOnClickListener(OnClickListener onClickListener) {
        this.onClickListener = onClickListener;
    }

    public interface OnClickListener{
        void onClick(int i);
    }

    public void release(){
        handler.removeMessages(0);
        message = null;
        handler = null;
        dismiss();
        floatPopup = null;
    }
}

FloatPopupItem.java


public class FloatPopupItem extends PopupWindow implements View.OnClickListener {

    public int width;
    private int height = Util.dp2px(50);
    private OnItemClickListener onItemClickListener;

    /**
     * 尼玛,这里这么简单就别写备注了,免得被人以为瞧不起拿刀砍
     * @param context
     */
    public FloatPopupItem(Context context) {
        LinearLayout layout = new LinearLayout(context);
        layout.setOrientation(LinearLayout.HORIZONTAL);
        layout.setGravity(Gravity.CENTER_VERTICAL);
        for (int i = 0; i < 3; i++) {
            ImageView iv = new ImageView(context);
            iv.setMinimumWidth(height);
            iv.setMinimumHeight(height);
            iv.setImageDrawable(context.getResources().getDrawable(R.mipmap.ic_launcher_round));
            iv.setTag(i);
            layout.addView(iv);
            width+=height;
            iv.setOnClickListener(this);
        }
        setContentView(layout);
        setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
        setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
        setBackgroundDrawable(new ColorDrawable(0x00000000));
        setOutsideTouchable(false);
    }

    interface OnItemClickListener{
        void onItemClick(int i);
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    @Override
    public void onClick(View v) {
        int tag = (int) v.getTag();
        if(onItemClickListener!=null){
            onItemClickListener.onItemClick(tag);
        }
    }
}

写到最后:
谁要敢赞赏,别怪我翻脸!哼!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,071评论 25 707
  • 本文的理论知识是基于:Android自定义ViewGroup神器-ViewDragHelper,如果你对ViewD...
    zhuhf阅读 6,700评论 6 58
  • 前言 去年用了一整年的MX4Pro,魅族留给我最大的印象就是悬浮球了(质量问题我就不说了),左右滑动切换应用、上拉...
    半栈工程师阅读 15,819评论 36 115
  • 最近最喜欢的诗是小时候最熟悉的那首 世界上有的是大树的种子,有的是小草的种子,有的可能一开始渺小,但很快就会强大,...
    嘿草草阅读 531评论 0 0
  • 有太多和标准的衡量的数据,个人的情绪就不重要了,被贴和太多的标签和身份之后你自己是谁已经不重要了,被物化太久之后,...
    qu溜达溜达阅读 143评论 0 0