Android實(shí)現(xiàn)懸浮按鈕功能
一、項(xiàng)目概述
在很多場景中,我們希望在應(yīng)用或系統(tǒng)任意界面上都能看到一個(gè)小的“懸浮按鈕”(Floating Button),用來快速啟動(dòng)工具、展示未讀信息或快捷操作。它的特點(diǎn)是:
始終懸浮:在其他應(yīng)用之上顯示,不被當(dāng)前 Activity 覆蓋;
可拖拽:用戶可以長按拖動(dòng)到屏幕任意位置;
點(diǎn)擊響應(yīng):點(diǎn)擊后執(zhí)行自定義邏輯;
自動(dòng)適配:適應(yīng)不同屏幕尺寸和屏幕旋轉(zhuǎn)。
本項(xiàng)目演示如何使用 Android 的 WindowManager
+ Service
+ SYSTEM_ALERT_WINDOW
權(quán)限,在 Android 8.0+(O)及以上通過 TYPE_APPLICATION_OVERLAY
實(shí)現(xiàn)一個(gè)可拖拽、可點(diǎn)擊的懸浮按鈕。
二、相關(guān)技術(shù)知識
懸浮窗權(quán)限
從 Android 6.0 開始需用戶授予“在其他應(yīng)用上層顯示”權(quán)限(
ACTION_MANAGE_OVERLAY_PERMISSION
);
WindowManager
用于在系統(tǒng)窗口層級中添加自定義 View,
LayoutParams
可指定位置、大小、類型等;
Service
利用前臺
Service
保證懸浮窗在后臺或應(yīng)用退出后仍能繼續(xù)顯示;
觸摸事件處理
在懸浮 View 的
OnTouchListener
中處理ACTION_DOWN
/ACTION_MOVE
事件,實(shí)現(xiàn)拖拽;
兼容性
Android O 及以上需使用
TYPE_APPLICATION_OVERLAY
;以下使用TYPE_PHONE
或TYPE_SYSTEM_ALERT
。
三、實(shí)現(xiàn)思路
申請懸浮窗權(quán)限
在
MainActivity
中檢測Settings.canDrawOverlays()
,若未授權(quán)則跳轉(zhuǎn)系統(tǒng)設(shè)置請求;
創(chuàng)建前臺 Service
FloatingService
繼承Service
,在onCreate()
時(shí)初始化并向WindowManager
添加懸浮按鈕 View;在
onDestroy()
中移除該 View;
懸浮 View 布局
floating_view.xml
包含一個(gè)ImageView
(可替換為任何 View);設(shè)置合適的背景和尺寸;
拖拽與點(diǎn)擊處理
對懸浮按鈕設(shè)置
OnTouchListener
,記錄按下時(shí)的坐標(biāo)與初始布局參數(shù),響應(yīng)移動(dòng);在
ACTION_UP
且位移較小的情況下視為點(diǎn)擊,觸發(fā)自定義邏輯(如Toast
);
啟動(dòng)與停止 Service
在
MainActivity
的“啟動(dòng)懸浮”按鈕點(diǎn)擊后啟動(dòng)FloatingService
;在“停止懸浮”按鈕點(diǎn)擊后停止 Service。
四、整合代碼
4.1 Java 代碼(MainActivity.java,含兩個(gè)類)
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:用于申請權(quán)限并啟動(dòng)/停止 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); // 啟動(dòng)懸浮按鈕 findViewById(R.id.btn_start).setOnClickListener(v -> { if (Settings.canDrawOverlays(this)) { startService(new Intent(this, FloatingService.class)); finish(); // 可選:關(guān)閉 Activity,懸浮按鈕仍會(huì)顯示 } else { // 請求懸浮窗權(quán)限 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, "未授予懸浮窗權(quán)限", 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("懸浮按鈕已啟動(dòng)") .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; } // 默認(rèn)初始位置 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, "懸浮按鈕服務(wù)", 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)聽:支持拖拽與點(diǎ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í)數(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; // 如果按下和抬起位置變化不大且時(shí)間短,則視為點(diǎn)擊 if (clickDuration < 200 && Math.hypot(event.getRawX() - initialTouchX, event.getRawY() - initialTouchY) < 10) { Toast.makeText(FloatingService.this, "懸浮按鈕被點(diǎn)擊!", Toast.LENGTH_SHORT).show(); // 這里可啟動(dòng) Activity 或其他操作 } return true; } return false; } } }
4.2 XML 與 Manifest
<!-- =================================================================== AndroidManifest.xml — 入口、權(quán)限與 Service 聲明 =================================================================== --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.floatingbutton"> <!-- 懸浮窗權(quán)限 --> <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 — 包含啟動(dòng)/停止按鈕 =================================================================== --> <?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="啟動(dòng)懸浮按鈕"/> <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 中設(shè)置 --> </shape>
五、代碼解讀
MainActivity
檢查并請求“在其他應(yīng)用上層顯示”權(quán)限;
點(diǎn)擊“啟動(dòng)”后啟動(dòng)
FloatingService
;點(diǎn)擊“停止”后停止 Service。
FloatingService
創(chuàng)建前臺通知以提高進(jìn)程優(yōu)先級;
使用
WindowManager
+TYPE_APPLICATION_OVERLAY
(O 及以上)或TYPE_PHONE
(以下),向系統(tǒng)窗口層添加floating_view
;在
OnTouchListener
中處理拖拽與點(diǎn)擊:短點(diǎn)擊觸發(fā)Toast
,長拖拽更新LayoutParams
并調(diào)用updateViewLayout()
。
布局與資源
floating_view.xml
定義按鈕視圖;float_bg.xml
定義圓形背景;AndroidManifest.xml
聲明必要權(quán)限和 Service。
六、項(xiàng)目總結(jié)
本文介紹了在 Android 8.0+ 環(huán)境下,如何通過前臺 Service
與 WindowManager
實(shí)現(xiàn)一個(gè)可拖拽、可點(diǎn)擊、始終懸浮在其他應(yīng)用之上的按鈕。核心優(yōu)勢:
系統(tǒng)懸浮窗:不依賴任何 Activity,無論在任何界面都可顯示;
靈活拖拽:用戶可自由拖動(dòng)到屏幕任意位置;
點(diǎn)擊回調(diào):可在點(diǎn)擊時(shí)執(zhí)行自定義邏輯(啟動(dòng) Activity、切換頁面等);
前臺 Service:保證在后臺也能持續(xù)顯示,不易被系統(tǒng)回收。
七、實(shí)踐建議與未來展望
美化與動(dòng)畫
為按鈕添加
ShadowLayer
或elevation
提升立體感;在顯示/隱藏時(shí)添加淡入淡出動(dòng)畫;
自定義布局
氣泡菜單、多按鈕懸浮菜單、可擴(kuò)展為多種操作;
權(quán)限引導(dǎo)
自定義更友好的權(quán)限申請界面,檢查失敗后提示用戶如何開啟;
資源兼容
針對深色模式、自適應(yīng)布局等場景優(yōu)化;
Compose 方案
在 Jetpack Compose 中可用
AndroidView
或WindowManager
同樣實(shí)現(xiàn),結(jié)合Modifier.pointerInput
處理拖拽。
以上就是Android實(shí)現(xiàn)懸浮按鈕功能的詳細(xì)內(nèi)容,更多關(guān)于Android懸浮按鈕的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android編程設(shè)置全屏的方法實(shí)例詳解
這篇文章主要介紹了Android編程設(shè)置全屏的方法,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android設(shè)置全屏的兩種常見技巧,非常簡單實(shí)用,需要的朋友可以參考下2015-11-11Android Studio使用USB真機(jī)調(diào)試詳解
這篇文章主要為大家詳細(xì)介紹了Android Studio使用USB真機(jī)調(diào)試的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05Android Retrofit 2.0框架上傳圖片解決方案
這篇文章主要介紹了Android Retrofit 2.0框架上傳一張與多張圖片解決方案,感興趣的小伙伴們可以參考一下2016-03-03Android之Intent附加數(shù)據(jù)的兩種實(shí)現(xiàn)方法
這篇文章主要介紹了Android之Intent附加數(shù)據(jù)的兩種實(shí)現(xiàn)方法,以實(shí)例形式較為詳細(xì)的分析了添加數(shù)據(jù)到Intent的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09Android自定義View繪圖實(shí)現(xiàn)漸隱動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了Android自定義View繪圖實(shí)現(xiàn)漸隱動(dòng)畫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Android?ViewModel創(chuàng)建不受橫豎屏切換影響原理詳解
這篇文章主要為大家介紹了Android?ViewModel創(chuàng)建不受橫豎屏切換影響原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03android POST數(shù)據(jù)遇到的UTF-8編碼(亂碼)問題解決辦法
這篇文章主要介紹了android POST數(shù)據(jù)遇到的UTF-8編碼(亂碼)問題解決辦法,需要的朋友可以參考下2014-04-04