You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter实现类似WhatsApp的FCM悬浮弹窗通知方案问询

实现Flutter FCM悬浮弹窗通知(类似WhatsApp样式)

我已经基于FCM实现了Flutter推送通知,该通知在应用前台、后台及关闭状态下均可正常工作。现希望实现类似WhatsApp的悬浮弹窗通知(非通知栏展示),即当用户不在本应用内时,通知能以悬浮形式显示在屏幕上方。

现有实现代码

Flutter代码

FirebaseMessaging.instance.getInitialMessage().then((message) {
  print(message);
});
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
  print(message);
});

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print(message);
});

AndroidManifest配置

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app_name">
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
 

    <application
        android:allowBackup="false"
        android:label="app_name"
        android:icon="@mipmap/ic_launcher">
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
        <receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"></action>
            </intent-filter>
        </receiver>

        <service
            android:name="MyNavigationService"
            android:foregroundServiceType="location" >
        </service>
        <receiver
            android:name="app_name.FirebaseBroadcastReceiver"
            android:exported="false"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
            </intent-filter>
        </receiver>

        <!--service
            android:name="app_name.java.MyFirebaseMessagingService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>
        </service-->

        <service
            android:name="app_name.service.MyFirebaseInstanceIDService"
            android:exported="false">
            <intent-filter>
                <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
            </intent-filter>
        </service>

        <meta-data android:name="com.google.android.geo.API_KEY"
            android:value="A......"/>

        <meta-data
            android:name="com.google.firebase.messaging.default_notification_channel_id"
            android:value="@string/default_notification_channel_id" />
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:exported="true"
            android:showWhenLocked="false"
            android:turnScreenOn="false"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
       
            <meta-data
                android:name="io.flutter.embedding.android.SplashScreenDrawable"
                android:resource="@drawable/launch_background" />
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>

        </activity>
         <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

依赖版本

firebase_messaging: ^13.0.4


实现悬浮弹窗的具体步骤

1. 补充必要权限

在AndroidManifest.xml中添加悬浮窗和Android 13+通知权限:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

2. 启用自定义Firebase消息服务

取消AndroidManifest.xml中注释的MyFirebaseMessagingService节点:

<service
    android:name="app_name.java.MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

然后实现该服务类,用于拦截后台消息并触发悬浮弹窗:

package app_name.java;

import android.app.ActivityManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.os.Build;
import android.view.Gravity;
import android.view.WindowManager;
import android.widget.RemoteViews;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        super.onMessageReceived(remoteMessage);
        
        // 仅当应用在后台时显示悬浮弹窗
        if (!isAppForeground()) {
            showFloatingWindow(remoteMessage);
        }
    }

    // 判断应用是否处于前台
    private boolean isAppForeground() {
        ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        for (ActivityManager.RunningAppProcessInfo processInfo : am.getRunningAppProcesses()) {
            if (processInfo.processName.equals(getPackageName()) 
                && processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
                return true;
            }
        }
        return false;
    }

    // 显示悬浮弹窗
    private void showFloatingWindow(RemoteMessage remoteMessage) {
        // 加载自定义悬浮窗布局
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.floating_notification);
        remoteViews.setTextViewText(R.id.tv_title, remoteMessage.getNotification().getTitle());
        remoteViews.setTextViewText(R.id.tv_content, remoteMessage.getNotification().getBody());

        // 点击弹窗打开应用
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
        remoteViews.setOnClickPendingIntent(R.id.root_layout, pendingIntent);

        // 配置悬浮窗参数
        WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
                        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
                        WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
                android.graphics.PixelFormat.TRANSLUCENT
        );
        params.gravity = Gravity.TOP | Gravity.END;
        params.x = 20;
        params.y = 100;

        // 添加悬浮窗到屏幕
        try {
            wm.addView(remoteViews.apply(this, null), params);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 创建悬浮窗布局文件

在res/layout目录下新建floating_notification.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/root_layout"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:background="@drawable/floating_bg"
    android:orientation="vertical"
    android:padding="12dp">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/tv_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:textColor="@android:color/darker_gray"
        android:textSize="14sp" />
</LinearLayout>

可以在res/drawable下创建floating_bg.xml作为弹窗背景:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@android:color/white" />
    <corners android:radius="8dp" />
    <stroke android:width="1dp" android:color="@android:color/darker_gray" />
</shape>

4. 处理前台悬浮弹窗(可选)

如果需要前台也显示悬浮弹窗,通过MethodChannel从Flutter调用Android端方法:

Flutter端代码:

import 'package:flutter/services.dart';

final MethodChannel _floatingChannel = MethodChannel('app_name/floating_notification');

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print(message);
  // 调用Android端显示悬浮窗
  _floatingChannel.invokeMethod('showFloating', {
    'title': message.notification?.title,
    'body': message.notification?.body,
  });
});

// 请求悬浮窗权限
Future<void> requestOverlayPermission() async {
  try {
    await _floatingChannel.invokeMethod('requestPermission');
  } on PlatformException catch (e) {
    print(e.message);
  }
}

Android端MainActivity中配置MethodChannel:

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;

public class MainActivity extends FlutterActivity {
    private static final String CHANNEL = "app_name/floating_notification";

    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        super.configureFlutterEngine(flutterEngine);
        new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
                .setMethodCallHandler((call, result) -> {
                    switch (call.method) {
                        case "requestPermission":
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                if (!Settings.canDrawOverlays(this)) {
                                    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                                            Uri.parse("package:" + getPackageName()));
                                    startActivityForResult(intent, 100);
                                    result.success(false);
                                } else {
                                    result.success(true);
                                }
                            } else {
                                result.success(true);
                            }
                            break;
                        case "showFloating":
                            String title = call.argument("title");
                            String body = call.argument("body");
                            // 调用之前写的showFloatingWindow方法,建议封装成工具类复用
                            break;
                        default:
                            result.notImplemented();
                    }
                });
    }
}

内容的提问来源于stack exchange,提问作者YourDeveloper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 17:15:36