Android實現(xiàn)懸浮按鈕功能
一、項目概述
在很多場景中,我們希望在應用或系統(tǒng)任意界面上都能看到一個小的“懸浮按鈕”(Floating Button),用來快速啟動工具、展示未讀信息或快捷操作。它的特點是:
始終懸浮:在其他應用之上顯示,不被當前 Activity 覆蓋;
可拖拽:用戶可以長按拖動到屏幕任意位置;
點擊響應:點擊后執(zhí)行自定義邏輯;
自動適配:適應不同屏幕尺寸和屏幕旋轉。
本項目演示如何使用 Android 的 WindowManager
+ Service
+ SYSTEM_ALERT_WINDOW
權限,在 Android 8.0+(O)及以上通過 TYPE_APPLICATION_OVERLAY
實現(xiàn)一個可拖拽、可點擊的懸浮按鈕。
二、相關技術知識
懸浮窗權限
從 Android 6.0 開始需用戶授予“在其他應用上層顯示”權限(
ACTION_MANAGE_OVERLAY_PERMISSION
);
WindowManager
用于在系統(tǒng)窗口層級中添加自定義 View,
LayoutParams
可指定位置、大小、類型等;
Service
利用前臺
Service
保證懸浮窗在后臺或應用退出后仍能繼續(xù)顯示;
觸摸事件處理
在懸浮 View 的
OnTouchListener
中處理ACTION_DOWN
/ACTION_MOVE
事件,實現(xiàn)拖拽;
兼容性
Android O 及以上需使用
TYPE_APPLICATION_OVERLAY
;以下使用TYPE_PHONE
或TYPE_SYSTEM_ALERT
。
三、實現(xiàn)思路
申請懸浮窗權限
在
MainActivity
中檢測Settings.canDrawOverlays()
,若未授權則跳轉系統(tǒng)設置請求;
創(chuàng)建前臺 Service
FloatingService
繼承Service
,在onCreate()
時初始化并向WindowManager
添加懸浮按鈕 View;在
onDestroy()
中移除該 View;
懸浮 View 布局
floating_view.xml
包含一個ImageView
(可替換為任何 View);設置合適的背景和尺寸;
拖拽與點擊處理
對懸浮按鈕設置
OnTouchListener
,記錄按下時的坐標與初始布局參數(shù),響應移動;在
ACTION_UP
且位移較小的情況下視為點擊,觸發(fā)自定義邏輯(如Toast
);
啟動與停止 Service
在
MainActivity
的“啟動懸浮”按鈕點擊后啟動FloatingService
;在“停止懸浮”按鈕點擊后停止 Service。
四、整合代碼
4.1 Java 代碼(MainActivity.java,含兩個類)
package com.example.floatingbutton; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.*; import android.graphics.PixelFormat; import android.net.Uri; import android.os.Build; import android.os.IBinder; import android.provider.Settings; import android.view.*; import android.widget.ImageView; import android.widget.Toast; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import androidx.core.app.NotificationCompat; /** * MainActivity:用于申請權限并啟動/停止 FloatingService */ public class MainActivity extends AppCompatActivity { private static final int REQ_OVERLAY = 1000; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 啟動懸浮按鈕 findViewById(R.id.btn_start).setOnClickListener(v -> { if (Settings.canDrawOverlays(this)) { startService(new Intent(this, FloatingService.class)); finish(); // 可選:關閉 Activity,懸浮按鈕仍會顯示 } else { // 請求懸浮窗權限 Intent intent = new Intent( Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQ_OVERLAY); } }); // 停止懸浮按鈕 findViewById(R.id.btn_stop).setOnClickListener(v -> { stopService(new Intent(this, FloatingService.class)); }); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQ_OVERLAY) { if (Settings.canDrawOverlays(this)) { startService(new Intent(this, FloatingService.class)); } else { Toast.makeText(this, "未授予懸浮窗權限", Toast.LENGTH_SHORT).show(); } } } } /** * FloatingService:前臺 Service,添加可拖拽懸浮按鈕 */ public class FloatingService extends Service { private WindowManager windowManager; private View floatView; private WindowManager.LayoutParams params; @Override public void onCreate() { super.onCreate(); // 1. 創(chuàng)建前臺通知 String channelId = createNotificationChannel(); Notification notification = new NotificationCompat.Builder(this, channelId) .setContentTitle("Floating Button") .setContentText("懸浮按鈕已啟動") .setSmallIcon(R.drawable.ic_floating) .setOngoing(true) .build(); startForeground(1, notification); // 2. 初始化 WindowManager 與 LayoutParams windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); params = new WindowManager.LayoutParams(); params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; // 不同 SDK 對懸浮類型的支持 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { params.type = WindowManager.LayoutParams.TYPE_PHONE; } // 默認初始位置 params.gravity = Gravity.TOP | Gravity.START; params.x = 100; params.y = 300; // 3. 載入自定義布局 floatView = LayoutInflater.from(this) .inflate(R.layout.floating_view, null); ImageView iv = floatView.findViewById(R.id.iv_float); iv.setOnTouchListener(new FloatingOnTouchListener()); // 4. 添加到窗口 windowManager.addView(floatView, params); } // 前臺通知 Channel private String createNotificationChannel() { String channelId = "floating_service"; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel chan = new NotificationChannel( channelId, "懸浮按鈕服務", NotificationManager.IMPORTANCE_NONE); ((NotificationManager)getSystemService(NOTIFICATION_SERVICE)) .createNotificationChannel(chan); } return channelId; } @Override public void onDestroy() { super.onDestroy(); if (floatView != null) { windowManager.removeView(floatView); floatView = null; } } @Nullable @Override public IBinder onBind(Intent intent) { return null; } /** * 觸摸監(jiān)聽:支持拖拽與點擊 */ private class FloatingOnTouchListener implements View.OnTouchListener { private int initialX, initialY; private float initialTouchX, initialTouchY; private long touchStartTime; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 記錄按下時數(shù)據(jù) initialX = params.x; initialY = params.y; initialTouchX = event.getRawX(); initialTouchY = event.getRawY(); touchStartTime = System.currentTimeMillis(); return true; case MotionEvent.ACTION_MOVE: // 更新懸浮位置 params.x = initialX + (int)(event.getRawX() - initialTouchX); params.y = initialY + (int)(event.getRawY() - initialTouchY); windowManager.updateViewLayout(floatView, params); return true; case MotionEvent.ACTION_UP: long clickDuration = System.currentTimeMillis() - touchStartTime; // 如果按下和抬起位置變化不大且時間短,則視為點擊 if (clickDuration < 200 && Math.hypot(event.getRawX() - initialTouchX, event.getRawY() - initialTouchY) < 10) { Toast.makeText(FloatingService.this, "懸浮按鈕被點擊!", Toast.LENGTH_SHORT).show(); // 這里可啟動 Activity 或其他操作 } return true; } return false; } } }
4.2 XML 與 Manifest
<!-- =================================================================== AndroidManifest.xml — 入口、權限與 Service 聲明 =================================================================== --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.floatingbutton"> <!-- 懸浮窗權限 --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <application ...> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <!-- 聲明 Service --> <service android:name=".FloatingService" android:exported="false"/> </application> </manifest>
<!-- =================================================================== activity_main.xml — 包含啟動/停止按鈕 =================================================================== --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layout_root" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp"> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="啟動懸浮按鈕"/> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="停止懸浮按鈕" android:layout_marginTop="16dp"/> </LinearLayout>
<!-- =================================================================== floating_view.xml — 懸浮按鈕布局 =================================================================== --> <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="48dp" android:layout_height="48dp"> <ImageView android:id="@+id/iv_float" android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/ic_float" android:background="@drawable/float_bg" android:padding="8dp"/> </FrameLayout>
<!-- =================================================================== float_bg.xml — 按鈕背景(圓形 + 陰影) =================================================================== --> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> <solid android:color="#FFFFFF"/> <size android:width="48dp" android:height="48dp"/> <corners android:radius="24dp"/> <padding android:all="4dp"/> <stroke android:width="1dp" android:color="#CCCCCC"/> <!-- 陰影需在代碼中或 ShadowLayer 中設置 --> </shape>
五、代碼解讀
MainActivity
檢查并請求“在其他應用上層顯示”權限;
點擊“啟動”后啟動
FloatingService
;點擊“停止”后停止 Service。
FloatingService
創(chuàng)建前臺通知以提高進程優(yōu)先級;
使用
WindowManager
+TYPE_APPLICATION_OVERLAY
(O 及以上)或TYPE_PHONE
(以下),向系統(tǒng)窗口層添加floating_view
;在
OnTouchListener
中處理拖拽與點擊:短點擊觸發(fā)Toast
,長拖拽更新LayoutParams
并調(diào)用updateViewLayout()
。
布局與資源
floating_view.xml
定義按鈕視圖;float_bg.xml
定義圓形背景;AndroidManifest.xml
聲明必要權限和 Service。
六、項目總結
本文介紹了在 Android 8.0+ 環(huán)境下,如何通過前臺 Service
與 WindowManager
實現(xiàn)一個可拖拽、可點擊、始終懸浮在其他應用之上的按鈕。核心優(yōu)勢:
系統(tǒng)懸浮窗:不依賴任何 Activity,無論在任何界面都可顯示;
靈活拖拽:用戶可自由拖動到屏幕任意位置;
點擊回調(diào):可在點擊時執(zhí)行自定義邏輯(啟動 Activity、切換頁面等);
前臺 Service:保證在后臺也能持續(xù)顯示,不易被系統(tǒng)回收。
七、實踐建議與未來展望
美化與動畫
為按鈕添加
ShadowLayer
或elevation
提升立體感;在顯示/隱藏時添加淡入淡出動畫;
自定義布局
氣泡菜單、多按鈕懸浮菜單、可擴展為多種操作;
權限引導
自定義更友好的權限申請界面,檢查失敗后提示用戶如何開啟;
資源兼容
針對深色模式、自適應布局等場景優(yōu)化;
Compose 方案
在 Jetpack Compose 中可用
AndroidView
或WindowManager
同樣實現(xiàn),結合Modifier.pointerInput
處理拖拽。
以上就是Android實現(xiàn)懸浮按鈕功能的詳細內(nèi)容,更多關于Android懸浮按鈕的資料請關注腳本之家其它相關文章!
相關文章
Android Studio使用USB真機調(diào)試詳解
這篇文章主要為大家詳細介紹了Android Studio使用USB真機調(diào)試的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05Android Retrofit 2.0框架上傳圖片解決方案
這篇文章主要介紹了Android Retrofit 2.0框架上傳一張與多張圖片解決方案,感興趣的小伙伴們可以參考一下2016-03-03Android之Intent附加數(shù)據(jù)的兩種實現(xiàn)方法
這篇文章主要介紹了Android之Intent附加數(shù)據(jù)的兩種實現(xiàn)方法,以實例形式較為詳細的分析了添加數(shù)據(jù)到Intent的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09Android?ViewModel創(chuàng)建不受橫豎屏切換影響原理詳解
這篇文章主要為大家介紹了Android?ViewModel創(chuàng)建不受橫豎屏切換影響原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03android POST數(shù)據(jù)遇到的UTF-8編碼(亂碼)問題解決辦法
這篇文章主要介紹了android POST數(shù)據(jù)遇到的UTF-8編碼(亂碼)問題解決辦法,需要的朋友可以參考下2014-04-04