欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android實(shí)現(xiàn)懸浮按鈕功能

 更新時(shí)間:2025年04月20日 16:15:12   作者:Katie。  
在很多場景中,我們希望在應(yīng)用或系統(tǒng)任意界面上都能看到一個(gè)小的“懸浮按鈕”(Floating Button),用來快速啟動(dòng)工具、展示未讀信息或快捷操作,所以本文給大家介紹了如何在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ù)知識

  1. 懸浮窗權(quán)限

    • 從 Android 6.0 開始需用戶授予“在其他應(yīng)用上層顯示”權(quán)限(ACTION_MANAGE_OVERLAY_PERMISSION);

  2. WindowManager

    • 用于在系統(tǒng)窗口層級中添加自定義 View,LayoutParams 可指定位置、大小、類型等;

  3. Service

    • 利用前臺 Service 保證懸浮窗在后臺或應(yīng)用退出后仍能繼續(xù)顯示;

  4. 觸摸事件處理

    • 在懸浮 View 的 OnTouchListener 中處理 ACTION_DOWN/ACTION_MOVE 事件,實(shí)現(xiàn)拖拽;

  5. 兼容性

    • Android O 及以上需使用 TYPE_APPLICATION_OVERLAY;以下使用 TYPE_PHONE 或 TYPE_SYSTEM_ALERT。

三、實(shí)現(xiàn)思路

  1. 申請懸浮窗權(quán)限

    • 在 MainActivity 中檢測 Settings.canDrawOverlays(),若未授權(quán)則跳轉(zhuǎn)系統(tǒng)設(shè)置請求;

  2. 創(chuàng)建前臺 Service

    • FloatingService 繼承 Service,在 onCreate() 時(shí)初始化并向 WindowManager 添加懸浮按鈕 View;

    • 在 onDestroy() 中移除該 View;

  3. 懸浮 View 布局

    • floating_view.xml 包含一個(gè) ImageView(可替換為任何 View);

    • 設(shè)置合適的背景和尺寸;

  4. 拖拽與點(diǎn)擊處理

    • 對懸浮按鈕設(shè)置 OnTouchListener,記錄按下時(shí)的坐標(biāo)與初始布局參數(shù),響應(yīng)移動(dòng);

    • 在 ACTION_UP 且位移較小的情況下視為點(diǎn)擊,觸發(fā)自定義邏輯(如 Toast);

  5. 啟動(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>

五、代碼解讀

  1. MainActivity

    • 檢查并請求“在其他應(yīng)用上層顯示”權(quán)限;

    • 點(diǎn)擊“啟動(dòng)”后啟動(dòng) FloatingService;點(diǎn)擊“停止”后停止 Service。

  2. 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()。

  3. 布局與資源

    • 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í)踐建議與未來展望

  1. 美化與動(dòng)畫

    • 為按鈕添加 ShadowLayer 或 elevation 提升立體感;

    • 在顯示/隱藏時(shí)添加淡入淡出動(dòng)畫;

  2. 自定義布局

    • 氣泡菜單、多按鈕懸浮菜單、可擴(kuò)展為多種操作;

  3. 權(quán)限引導(dǎo)

    • 自定義更友好的權(quán)限申請界面,檢查失敗后提示用戶如何開啟;

  4. 資源兼容

    • 針對深色模式、自適應(yīng)布局等場景優(yōu)化;

  5. 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)文章

最新評論