Android6.0 源碼修改之 仿IOS添加全屏可拖拽浮窗返回按鈕

来源:https://www.cnblogs.com/cczheng-666/archive/2019/04/20/10741082.html
-Advertisement-
Play Games

前言 之前寫過屏蔽系統導航欄功能的文章,具體可看 "Android6.0 源碼修改之屏蔽導航欄虛擬按鍵(Home和RecentAPP)/動態顯示和隱藏NavigationBar" 在某些特殊定製的版本中要求完全去掉導航欄,那麼當用戶點進一些系統自帶的應用界面如設置、聯繫人等,就沒法退出了,雖然可以在 ...


前言

之前寫過屏蔽系統導航欄功能的文章,具體可看Android6.0 源碼修改之屏蔽導航欄虛擬按鍵(Home和RecentAPP)/動態顯示和隱藏NavigationBar

在某些特殊定製的版本中要求完全去掉導航欄,那麼當用戶點進一些系統自帶的應用界面如設置、聯繫人等,就沒法退出了,雖然可以在actionBar中添加back按鈕,但總不能每一個app都去添加吧。所以靈機一動我們就給系統添加一個全屏可拖拽的浮窗按鈕,點擊的時候處理返回鍵的邏輯。它大概長這樣(審美可能醜了點,你們可以自由發揮)

在這裡插入圖片描述
圖1 最終效果圖

思路分析

  1. 通過分析之前的NavigationBar代碼,發現系統是通過WindowManager添加View的方式來實現,此處我們也可以模擬這種方法來添加
  2. 添加懸浮窗以後監聽觸摸事件,跟隨手指移動重新修改view的layoutParam
  3. 鬆手後獲取當前X坐標,小於屏幕width的一半則平移歸位至屏幕左邊
  4. 添加系統的返回按鍵功能

一、添加懸浮窗

private void showFloatingWindow() {
    DisplayMetrics outMetrics = new DisplayMetrics();
    mWindowManager.getDefaultDisplay().getMetrics(outMetrics);
    screenWidth = outMetrics.widthPixels;
    screenHeight = outMetrics.heightPixels;

    layoutParams = new WindowManager.LayoutParams();
    layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
    layoutParams.format = PixelFormat.RGBA_8888;
    layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
    layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    layoutParams.width = 100;
    layoutParams.height = 100;
    layoutParams.x = 200;
    layoutParams.y = 200;

    button = new ImageButton(mContext);
    button.setBackground(mContext.getResources().getDrawable(R.drawable.fab_background));//系統通訊錄里的藍色圓形圖標
    button.setImageResource(R.drawable.ic_sysbar_back);//系統本身的back圖標
    mWindowManager.addView(button, layoutParams);
    isShowFloatingView = true;
}

代碼很簡單,就是通過windowManager添加一個ImageButton,寬高都是100的,位置在屏幕左上角為原點的200,200。需要註意的是因為我們是在源碼里添加,而且是M的版本,所以type為WindowManager.LayoutParams.TYPE_PHONE。如果是在普通的app里註意事項可參考這篇

二、添加觸摸事件監聽

button.setOnTouchListener(new FloatingOnTouchListener());

private class FloatingOnTouchListener implements View.OnTouchListener {
    private int lastX;
    private int lastY;

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isDrag = false;
                lastX = (int) event.getRawX();
                lastY = (int) event.getRawY();
                break;

            case MotionEvent.ACTION_MOVE:
                isDrag = true;
                int nowX = (int) event.getRawX();
                int nowY = (int) event.getRawY();
                int movedX = nowX - lastX;
                int movedY = nowY - lastY;
                lastX = nowX;
                lastY = nowY;
                layoutParams.x = layoutParams.x + movedX;
                layoutParams.y = layoutParams.y + movedY;
                //獲取當前手指移動的x和y,通過updateViewLayout方法將改變後的x和y設置給button
                mWindowManager.updateViewLayout(view, layoutParams);
                break;

            case MotionEvent.ACTION_UP:
                if (isDrag) {
                    log("lastX=" + lastX + "  screenWidth=" + screenWidth);
                    //手指抬起時,判斷是需要滑動到屏幕左邊還是屏幕右邊
                    if (lastX >= screenWidth / 2) {
                        setAnimation(view, lastX, screenWidth);
                    } else {
                        setAnimation(view, -lastX, 0);
                    }
                }
                break;
        }
        //返回true則消費事件,返回false則傳遞事件,此處特殊處理是為了和點擊事件區分
        return isDrag || view.onTouchEvent(event);
    }

三、添加抬起滑動歸位動畫

private void setAnimation(final View view, int fromX, int toX) {
        final ValueAnimator animator = ValueAnimator.ofInt(fromX, toX);
        if (Math.abs(fromX) < screenWidth / 4 || fromX > screenWidth * 3 / 4)
            animator.setDuration(300);
        else
            animator.setDuration(600);

        animator.setInterpolator(new LinearInterpolator());
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {}
            @Override
            public void onAnimationEnd(Animator animation) {
                log("onAnimationEnd=");
                savePreValue(layoutParams.x, layoutParams.y);
            }
            @Override
            public void onAnimationCancel(Animator animation) {}
            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int current = (int) animator.getAnimatedValue();
                log("current=" + current);

                layoutParams.x = Math.abs(current);
                mWindowManager.updateViewLayout(view, layoutParams);
            }
        });
        animator.start();
    }
}

同樣是通過改變button的x和y值來達到滑動效果,只不過我只需要x平移,y為0,需要斜著滑的你們可自由發揮,為了使滑動看上去平滑,給動畫添加了一個線性插值器,設置滑動時間,監聽返回插值進度,這樣動態設置給button。為了保存button的最終位置,添加了一個動畫完成監聽,並將x和y寫入到SharedPreferences中保存。

四、添加點擊返回功能

通過列印日誌分析,系統導航欄的返回按鍵,發現其原理是通過KeyButtonView的觸摸事件發送一個KeyEvent事件給系統來實現返回功能

源碼位置frameworks\base\packages\SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java

public boolean onTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    int x, y;
    if (action == MotionEvent.ACTION_DOWN) {
        mGestureAborted = false;
    }
    if (mGestureAborted) {
        return false;
    }

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            //按下的時間
            mDownTime = SystemClock.uptimeMillis();
            setPressed(true);
            if (mCode != 0) {//按下事件
                sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
            } else {
                // Provide the same haptic feedback that the system offers for virtual keys.
                performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
            }
            removeCallbacks(mCheckLongPress);
            postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
            break;
        case MotionEvent.ACTION_MOVE:
            x = (int)ev.getX();
            y = (int)ev.getY();
            setPressed(x >= -mTouchSlop
                    && x < getWidth() + mTouchSlop
                    && y >= -mTouchSlop
                    && y < getHeight() + mTouchSlop);
            break;
        case MotionEvent.ACTION_CANCEL:
            setPressed(false);
            if (mCode != 0) {
                sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
            }
            removeCallbacks(mCheckLongPress);
            break;
        case MotionEvent.ACTION_UP:
            final boolean doIt = isPressed();
            setPressed(false);
            if (mCode != 0) {
                if (doIt) {//抬起事件
                    sendEvent(KeyEvent.ACTION_UP, 0);
                    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                    playSoundEffect(SoundEffectConstants.CLICK);
                } else {
                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                }
            } else {
                // no key code, just a regular ImageView
                if (doIt) {
                    performClick();
                }
            }
            removeCallbacks(mCheckLongPress);
            break;
    }

    return true;
}

//以下為我們給button添加的點擊事件
private void sendEvent(int action, int flags, long when) {
    int mCode = 4;
    Log.e(TAG, "mCode="+mCode + "  flags="+flags);
    final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
    final KeyEvent ev = new KeyEvent(when - 100, when, action, mCode, repeatCount,
            0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
            flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
            InputDevice.SOURCE_KEYBOARD);
    InputManager.getInstance().injectInputEvent(ev,
            InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.e(TAG,"click dragButton ...");
            final long mDownTime = SystemClock.uptimeMillis();
            //onBackPressed();
            sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);

            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    sendEvent(KeyEvent.ACTION_UP, 0, SystemClock.uptimeMillis());
                }
            }, 300);
        }
    });

需要註意的地方,系統返回鍵對應的code為4,所以mCode=4,KeyButtonView的觸摸事件包含按下和抬起,所以我們只需模擬發送按下和抬起事件,可以看到抬起事件加了300ms的延時發送,這是關鍵不然系統不會處理。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • sqli labs 下載、安裝 下載地址:https://github.com/Audi 1/sqli labs phpstudy:http://down.php.cn/PhpStudy20180211.zip 所需安裝環境支持包:http://www.pc6.com/softview/SoftVi ...
  • --constraint--not null 非空約束create table demo01(empno number(4),ename varchar2(10) not null,job varchar2(10))insert into demo01 values(1234,' ','SALES' ...
  • 寫了兩年的php,一直依賴於phpstorm,其他三方工具和框架集成的高效語句,造成的後果就是基礎愈發陌生,手寫原生sql和php代碼等卡殼嚴重。亡羊補牢!ps:原文是markdown,沒修改格式直接傳了 ...
  • 近期在給客戶做新數據交換方案調試時發現一處視圖創建語句帶不出數據。 精簡需求後如下:a部門從b部門獲取主體數據,由於a、b兩部門有些代碼標準不一致需要做轉換。於是開發寫了個對照表做轉換生成業務視圖。 主表zb數據如下,B_MD1和B_DM2是兩種類型代碼,分別是lx1和lx2(比如一個是證件類型代碼 ...
  • 觸發器時為執行業務規則和保持數據完整性而提供的一種機制,它可以在執行插入、更新,刪除等操作的前後自動觸發。觸發器與存儲過程類似,但是讓不接收輸入\輸出參數沒也不能被顯式調用,只能有伺服器事件自動觸發,根據一起執行觸發器語言的不同,可將其分為DML觸發器和DDL觸發器 1、DML觸發器 根據DML觸發 ...
  • DCL 數據控制語言 Data control language 之前說過的授權和收權利語句 grant, revoke DDL 數據定義語言 Data define language create alter,drop語句,創表,修改表,刪除 創建表 查看表結構 修改表 添加約束 約束用於限制加入 ...
  • 近些年,由於智能手機的迅速普及推動移動互聯網技術的蓬勃發展,全球數據呈現爆髮式的增長。2018年5月企鵝號的統計結果:互聯網每天新增的數據量達2.5*10^18位元組,而全球90%的數據都是在過去的兩年間創造出來的。隨著5G技術的商用,未來連接萬物的物聯網設備必將帶來更大量級的數據。大膽預期,我們即將 ...
  • 資料庫設計 資料庫設計的4個步驟: 需求分析: 資料庫存儲數據是什麼 數據有哪些屬性 數據和屬性的特點有那些 邏輯設計: 資料庫進行邏輯建模 物理設計: 根據資料庫自身特點把邏輯設計轉換成物理設計 維護優化: 新的需求建表 索引優化 大表拆分 需求分析: 邏輯設計: 物理設計: 維護優化: 需求分析 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...