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

