Android游戲懸浮球,無(wú)視各種操蛋權(quán)限

老規(guī)矩播瞳,先上圖:


ezgif-3-857c576987.gif

懸浮球大家都知道狞谱,無(wú)非就是一個(gè)按鈕+N個(gè)子Item乃摹,會(huì)靠邊,會(huì)變小...
我大概看過(guò)網(wǎng)上的一些實(shí)現(xiàn)跟衅,用的最多的可能就是應(yīng)用加android.permission.SYSTEM_ALERT_WINDOW權(quán)限孵睬,然后windowManager.addView(懸浮個(gè)球)
多數(shù)手機(jī)上確實(shí)可以,我比著別人寫(xiě)的代碼也寫(xiě)了一次伶跷,模擬器上運(yùn)行也很好肪康,然后到我的小米手機(jī)上荚恶,誒!誒磷支!誒谒撼!我的球呢?雾狈?廓潜?
到哪兒去了現(xiàn)在我也沒(méi)找到,各位知道怎么解決的話(huà)也請(qǐng)不吝賜教善榛,指導(dǎo)指導(dǎo)辩蛋!
既然它不出來(lái),勞資這暴脾氣也不會(huì)慣著它移盆,卷起袖子換個(gè)姿勢(shì)擼:用PopupWindow實(shí)現(xiàn)的應(yīng)用內(nèi)懸浮球悼院!
首先,整體確定下我們要實(shí)現(xiàn)的功能:
1咒循、顯示一個(gè)球在頁(yè)面上方
2据途、這個(gè)球可以點(diǎn)擊
3、點(diǎn)擊之后能展開(kāi)子菜單
4叙甸、子菜單點(diǎn)擊之后能執(zhí)行相應(yīng)的操作
5颖医、球可以跟隨手指移動(dòng)
6、手指離開(kāi)屏幕之后球自動(dòng)靠邊
7裆蒸、靠邊5秒內(nèi)沒(méi)有操作自動(dòng)縮小

我們一個(gè)一個(gè)說(shuō):

  1. 新建一個(gè)PopupWindow熔萧,背景設(shè)置一張圓圖片,在需要顯示時(shí)調(diào)用showAtLocation(View parent, int gravity, int x, int y)方法僚祷。恩佛致,就這么簡(jiǎn)單。
  2. 額辙谜,這個(gè)放到后面晌杰,跟6一起解決。
  3. 最開(kāi)始我在糾結(jié)展開(kāi)的子項(xiàng)是跟懸浮球放在一起筷弦,還是另外單獨(dú)管理肋演,最后還是選擇了第二種方案。如果跟跟懸浮球放在一起烂琴,那么我要在同一個(gè)PopupWindow去處理懸浮球和子項(xiàng)的顯示狀態(tài)爹殊、業(yè)務(wù)邏輯,代碼看起來(lái)很冗余奸绷。分開(kāi)處理梗夸,只關(guān)注對(duì)象本身需要實(shí)現(xiàn)的功能和對(duì)外暴露的公共方法,這樣就簡(jiǎn)單了号醉。
    子項(xiàng)布局很簡(jiǎn)單反症,在一個(gè)LinearLayout里面循環(huán)添加了幾張圖片作為子項(xiàng)按鈕辛块,然后設(shè)置給PopupWindow做布局,當(dāng)然铅碍,你要用更優(yōu)美的布局替換來(lái)實(shí)現(xiàn)自己的需求润绵,這里只做示例。子項(xiàng)本身也是一個(gè)PopupWindow胞谈,所以不需要我們?nèi)?xiě)對(duì)應(yīng)的顯示和隱藏的方法尘盼,直接調(diào)用PopupWindow的showAtLocation(...)和dismiss()就可以了;子項(xiàng)按鈕的點(diǎn)擊事件需要我們自己來(lái)實(shí)現(xiàn)烦绳,這里我在子項(xiàng)里定義了一個(gè)接口卿捎,懸浮球繼承接口,實(shí)現(xiàn)子項(xiàng)的點(diǎn)擊径密,為什么要傳到懸浮球里處理呢午阵,主要是不想讓子項(xiàng)暴露,對(duì)外只展示懸浮球享扔,懸浮球?qū)ν馓幚硭械墓δ堋?/li>
  4. 懸浮球需要做的是定義一個(gè)變量底桂,控制子項(xiàng)的顯示狀態(tài)。注意伪很,在調(diào)用子項(xiàng)的showAtLocation(...)方法的時(shí)候,要計(jì)算自己的位置奋单,告訴子項(xiàng)锉试,TMD,跟著勞資览濒,別亂跑呆盖。
  5. 這里算是比較關(guān)鍵的實(shí)現(xiàn)了,我們?cè)趹腋∏驑?gòu)造方法里添加對(duì)觸摸事件的監(jiān)聽(tīng)setTouchInterceptor(OnTouchListener l)贷笛,對(duì)各種手勢(shì)進(jìn)行處理应又,思路大概是這樣的:手指移動(dòng)的時(shí)候調(diào)用PopupWindow的update(...)方法,對(duì)懸浮球的位置要不斷更新乏苦;手指抬起的時(shí)候要判斷是靠左邊近還是靠右邊近株扛,直接更改x的值,然后也是用update(...)方法讓?xiě)腋∏蚩窟呎?PS:update(...)是瞬時(shí)移動(dòng)的汇荐,由于時(shí)間較短洞就,肉眼看著也不算突兀,強(qiáng)迫癥自行修改實(shí)現(xiàn)滑動(dòng))掀淘,懸浮球靠邊之后旬蟋,hanler延時(shí)5s下一道圣旨,在handleMessage(...)方法里邊又是調(diào)用update(...)方法革娄,對(duì)懸浮球尺寸進(jìn)行操作倾贰,實(shí)現(xiàn)變小冕碟。
  6. 手指離開(kāi)屏幕之后球自動(dòng)靠邊的功能我們?cè)谏线呉呀?jīng)講了,在MotionEvent.ACTION_UP這種情況下可以處理匆浙。但是懸浮球的點(diǎn)擊事件怎么辦安寺,在onTouch(...)我們返回了true,點(diǎn)擊事件走不到onClick(...)了吞彤,那就另辟蹊徑吧:在手指按下的時(shí)候我們記錄了按下的位置我衬,手指抬起的時(shí)候我們記錄了抬起的位置,如果兩次位置之間的差值小于touchSlop饰恕,那么我們認(rèn)為這是一次點(diǎn)擊事件挠羔!之后就可以很愉快的在if(Math.abs(dx)<touchSlop && Math.abs(dy)<touchSlop)條件下處理點(diǎn)擊之后的事情了。
  7. 好吧埋嵌,這個(gè)在5里邊也已經(jīng)解釋過(guò)了破加。
    代碼里注釋非常非常詳細(xì)了,這里就不費(fèi)鍵盤(pán)了雹嗦,后面會(huì)貼出代碼和Github地址范舀。

好了,上面就是整個(gè)懸浮球的實(shí)現(xiàn)了罪,利用PopupWindow锭环,避開(kāi)了所有的權(quán)限申請(qǐng),尤其在這么多機(jī)型面前泊藕,申請(qǐng)權(quán)限還不一定能實(shí)現(xiàn)辅辩,操蛋!娃圆!

但是C捣妗!上面說(shuō)了讼呢,現(xiàn)在我在做游戲行業(yè)相關(guān)工作撩鹿,游戲整個(gè)頁(yè)面其實(shí)只是一個(gè)Activity(多activity反正我公司沒(méi)有,別抬杠)悦屏,上面實(shí)現(xiàn)的懸浮球也只是針對(duì)單Activity的(其實(shí)合理使用單 activity 配合 fragment节沦,頁(yè)面跳轉(zhuǎn)更流暢,管理更方便础爬,參考新版知乎)散劫,細(xì)節(jié)方面大家自行擴(kuò)展。

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

public class FloatPopup extends PopupWindow implements FloatPopupItem.OnItemClickListener {

    //設(shè)置懸浮按鈕尺寸
    private int size = Util.dp2px(50);
    private int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
    private int screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
    //觸發(fā)移動(dòng)事件的最小距離
    private int touchSlop = new ViewConfiguration().getScaledTouchSlop();
    //記錄當(dāng)前手指實(shí)時(shí)位置
    private float curX,curY;
    //記錄手指按下時(shí)的位置
    private float lastX,lastY;
    //設(shè)置當(dāng)前懸浮按鈕的顯示位置
    private float showX,showY;
    //記錄當(dāng)前懸浮按鈕顯示狀態(tài)
    private boolean showMenu = false;
    //記錄當(dāng)前懸浮按鈕顯示位置
    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);
                //接受到消息,說(shuō)明用戶(hù)在規(guī)定時(shí)間沒(méi)有操作懸浮按鈕幕帆,這個(gè)時(shí)候還要判斷下子選項(xiàng)是否展開(kāi)获搏,
                //選項(xiàng)沒(méi)有展開(kāi),那么就讓?xiě)腋“粹o變小,靠邊站
                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;
//                        }
                        /*如果當(dāng)前手指在Y軸上的位置小于按鈕的一半時(shí)常熙,這個(gè)時(shí)候按鈕的上邊沿已經(jīng)最靠邊了纬乍。
                        想想一下當(dāng)Y剛好等于臨界值size/2,按鈕會(huì)在什么位置裸卫,就會(huì)理解這里為什么做判斷了仿贬。*/
                        if(curY<size/2){
                           /*在MotionEvent.ACTION_MOVE里面去update按鈕的位置,是因?yàn)槭种该看蔚囊苿?dòng)都會(huì)消費(fèi)
                            move事件墓贿,你移動(dòng)很長(zhǎng)的一段路程茧泪,在move事件里就會(huì)分解成一小段一小段的位移。*/
                            update((int)event.getRawX() - size/2,0);
                        /*在M這里和上邊是一樣的道理聋袋,當(dāng)Y=臨界值screenHeight-size/2時(shí)队伟,說(shuō)明按鈕已經(jīng)接近下邊緣了*/
                        }else if(curY>screenHeight-size/2){
                            update((int)event.getRawX() - size/2,screenHeight-size);
                        }else{
                            /*常規(guī)情況。但是這里為什么要減去size/2呢(還有上邊)幽勒?
                            我們?cè)O(shè)置的位置對(duì)于按鈕來(lái)說(shuō)是它的左上角嗜侮,這里減去size/2只是為了讓我們的參考點(diǎn)移
                            動(dòng)到按鈕的中心位置,另外啥容,滑動(dòng)的時(shí)候會(huì)消除掉一頓一頓的情況锈颗,不信你試試沒(méi)有減掉
                            size/2時(shí)是什么樣子*/
                            update((int)event.getRawX() - size/2,(int)event.getRawY()-size/2);
                        }

                        /*當(dāng)開(kāi)始移動(dòng)的時(shí)候要判斷下子項(xiàng)是否展開(kāi),如果展開(kāi)咪惠,關(guān)閉之后再移動(dòng)*/
                        if(item.isShowing()){
                            item.dismiss();
                            showMenu = !showMenu;
                        }
                        break;
                    case MotionEvent.ACTION_UP:
                        /*這里就很好理解了击吱,當(dāng)我們手指抬起時(shí),如果抬起的位置靠近左邊(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);

                        /*這里是處理點(diǎn)擊事件的,當(dāng)手指按鈕和抬起之間的距離小于touchSlop時(shí)渠鸽,
                        我們認(rèn)為這是一次點(diǎn)擊事件叫乌,并根據(jù)showMenu的值處理顯示或者隱藏子項(xiàng)*/
                        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內(nèi)沒(méi)有操作的話(huà)柴罐,讓圖標(biāo)變小*/
                        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);
    }

    /**
     * 這里用了兩個(gè)接口把子項(xiàng)item的點(diǎn)擊事件傳到FloatPopup的onClick(int i)方法里面統(tǒng)一處理凿试,
     * 因?yàn)槲覀冎粚?duì)外暴露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;

    /**
     * 尼瑪,這里這么簡(jiǎn)單就別寫(xiě)備注了似芝,免得被人以為瞧不起拿刀砍
     * @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);
        }
    }
}

寫(xiě)到最后:
誰(shuí)要敢贊賞那婉,別怪我翻臉!哼党瓮!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末详炬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子寞奸,更是在濱河造成了極大的恐慌呛谜,老刑警劉巖在跳,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異隐岛,居然都是意外死亡猫妙,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)聚凹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)割坠,“玉大人,你說(shuō)我怎么就攤上這事妒牙”撕撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵单旁,是天一觀的道長(zhǎng)沪羔。 經(jīng)常有香客問(wèn)我,道長(zhǎng)象浑,這世上最難降的妖魔是什么蔫饰? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮愉豺,結(jié)果婚禮上篓吁,老公的妹妹穿的比我還像新娘。我一直安慰自己蚪拦,他們只是感情好杖剪,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著驰贷,像睡著了一般盛嘿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上括袒,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天次兆,我揣著相機(jī)與錄音,去河邊找鬼锹锰。 笑死芥炭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恃慧。 我是一名探鬼主播园蝠,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼痢士!你這毒婦竟也來(lái)了彪薛?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎善延,沒(méi)想到半個(gè)月后训唱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挚冤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年况增,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片训挡。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澳骤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澜薄,到底是詐尸還是另有隱情为肮,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布肤京,位于F島的核電站颊艳,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏忘分。R本人自食惡果不足惜棋枕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望妒峦。 院中可真熱鬧重斑,春花似錦、人聲如沸肯骇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)笛丙。三九已至漾脂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胚鸯,已是汗流浹背骨稿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蠢琳,地道東北人啊终。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓镜豹,卻偏偏與公主長(zhǎng)得像傲须,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子趟脂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容