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
相关产品推荐
相关产品推荐

