From 8be6f2d8e42560e1137020a3799655e4880819de Mon Sep 17 00:00:00 2001 From: lijun Date: Mon, 1 Dec 2025 09:42:57 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0widget=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/.classpath | 2 +- .../org.eclipse.buildship.core.prefs | 2 +- android/.settings/org.eclipse.jdt.core.prefs | 4 + android/build.gradle | 26 ++- .../java/com/finogeeks/mop/MopPlugin.java | 163 ++++++++++-------- .../platformview/MopWidgetPlatformView.java | 118 +++++++++++++ .../MopWidgetPlatformViewFactory.java | 56 ++++++ ios/Classes/MopPlugin.m | 5 + ios/Classes/MopWidgetPlatformView.h | 9 + ios/Classes/MopWidgetPlatformView.m | 82 +++++++++ ios/Classes/MopWidgetPlatformViewFactory.h | 5 + ios/Classes/MopWidgetPlatformViewFactory.m | 32 ++++ lib/mop.dart | 1 + lib/platform_view.dart | 52 ++++++ 14 files changed, 474 insertions(+), 83 deletions(-) create mode 100644 android/.settings/org.eclipse.jdt.core.prefs create mode 100644 android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java create mode 100644 android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformViewFactory.java create mode 100644 ios/Classes/MopWidgetPlatformView.h create mode 100644 ios/Classes/MopWidgetPlatformView.m create mode 100644 ios/Classes/MopWidgetPlatformViewFactory.h create mode 100644 ios/Classes/MopWidgetPlatformViewFactory.m create mode 100644 lib/platform_view.dart diff --git a/android/.classpath b/android/.classpath index 4a04201c..0a3280e2 100644 --- a/android/.classpath +++ b/android/.classpath @@ -1,6 +1,6 @@ - + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index 5eb76e0e..ff1dcaa1 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -2,7 +2,7 @@ arguments=--init-script /var/folders/tx/98mydjws12b717spn4mx1wd80000gn/T/db3b08f auto.sync=false build.scans.enabled=false connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) -connection.project.dir= +connection.project.dir=../../flutter-demo-widget/android eclipse.preferences.version=1 gradle.user.home= java.home=/Users/fino/Library/Java/JavaVirtualMachines/corretto-11.0.23/Contents/Home diff --git a/android/.settings/org.eclipse.jdt.core.prefs b/android/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..e9186c3e --- /dev/null +++ b/android/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.source=21 diff --git a/android/build.gradle b/android/build.gradle index 58ba7db9..6360c2f2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' -apply plugin: 'kotlin-android-extensions' + group 'com.finogeeks.mop' version '1.0' @@ -31,8 +31,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61" + classpath 'com.android.tools.build:gradle:8.13.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0" } } @@ -62,8 +62,8 @@ rootProject.allprojects { android { - compileSdkVersion 28 - + compileSdkVersion 34 + namespace "com.finogeeks.mop" defaultConfig { minSdkVersion 21 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -75,9 +75,17 @@ android { } } + // 启用视图绑定 + buildFeatures { + viewBinding true + } + compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" } lintOptions { disable 'InvalidPackage' @@ -91,6 +99,6 @@ kapt { } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.finogeeks.lib:finapplet:2.50.1' - implementation 'com.finogeeks.mop:plugins:2.50.1' + implementation 'com.finogeeks.lib:finapplet:2.50.5' + implementation 'com.finogeeks.mop:plugins:2.50.5' } \ No newline at end of file diff --git a/android/src/main/java/com/finogeeks/mop/MopPlugin.java b/android/src/main/java/com/finogeeks/mop/MopPlugin.java index 75a9ce4b..de2f2fe5 100644 --- a/android/src/main/java/com/finogeeks/mop/MopPlugin.java +++ b/android/src/main/java/com/finogeeks/mop/MopPlugin.java @@ -3,11 +3,14 @@ import android.app.Activity; import android.content.Intent; import android.util.Log; + +import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; import com.finogeeks.mop.interfaces.Event; import com.finogeeks.mop.interfaces.FlutterInterface; import com.finogeeks.mop.interfaces.ICallback; +import com.finogeeks.mop.platformview.MopWidgetPlatformViewFactory; import com.finogeeks.mop.service.MopPluginService; import java.util.HashMap; @@ -22,8 +25,6 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** * MopPlugin @@ -38,115 +39,133 @@ public class MopPlugin implements FlutterPlugin, MethodCallHandler, ActivityAwar private final MopPluginDelegate delegate = new MopPluginDelegate(); private final MopEventStream mopEventStream = new MopEventStream(); - // These are null when not using v2 embedding. - private FlutterPluginBinding flutterPluginBinding; - private MethodChannel channel; + private MethodChannel methodChannel; private EventChannel eventChannel; private Lifecycle lifecycle; - - /** - * Plugin registration. - */ - @SuppressWarnings("deprecation") - public static void registerWith(Registrar registrar) { - MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL); - MopPluginDelegate delegate = new MopPluginDelegate(); - final MopPlugin plugin = new MopPlugin(); - channel.setMethodCallHandler(plugin); - registrar.addActivityResultListener(delegate); - - EventChannel eventChannel = new EventChannel(registrar.messenger(), EVENT_CHANNEL); - final MopEventStream mopEventStream = new MopEventStream(); - eventChannel.setStreamHandler(mopEventStream); - MopPluginService.getInstance().initialize(registrar.activity(), mopEventStream, channel); - } + private Activity activity; + // Unified platform view factory to create native views by external viewType + private MopWidgetPlatformViewFactory platformViewFactory; @Override - public void onMethodCall(MethodCall call, final Result result) { - ICallback callback = new ICallback() { - @Override - public void onSuccess(Object data) { - Map obj = new HashMap(); - - obj.put("success", true); - if (data != null) - obj.put("data", data); - obj.put("retMsg", "ok"); - result.success(obj); - } - - @Override - public void onFail(Object error) { - Map obj = new HashMap(); - obj.put("success", false); - obj.put("retMsg", error == null ? "" : error); - result.success(obj); - } - - @Override - public void onCancel(Object cancel) { - result.notImplemented(); - } - - @Override - public void startActivityForResult(Intent intent, int requestCode) { - - } + public void onMethodCall(MethodCall call, @NonNull final Result result) { + ICallback callback = new ICallback<>() { + @Override + public void onSuccess(Object data) { + Map obj = new HashMap<>(); + + obj.put("success", true); + if (data != null) + obj.put("data", data); + obj.put("retMsg", "ok"); + result.success(obj); + } + + @Override + public void onFail(Object error) { + Map obj = new HashMap<>(); + obj.put("success", false); + obj.put("retMsg", error == null ? "" : error); + result.success(obj); + } + + @Override + public void onCancel(Object cancel) { + result.notImplemented(); + } + + @Override + public void startActivityForResult(Intent intent, int requestCode) { + // 如果需要在 Activity 中启动,可以使用当前的 activity + if (activity != null && intent != null) { + activity.startActivityForResult(intent, requestCode); + } + } }; Log.d(LOG_TAG, "mopplugin: invoke " + call.method); Event event = new Event(call.method, call.arguments, callback); delegate.setEvent(event); this.flutterInterface.invokeHandler(event); - // if (call.method.equals("getPlatformVersion")) { - // result.success("Android " + android.os.Build.VERSION.RELEASE); - // } else { - // result.notImplemented(); - // } } @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - this.flutterPluginBinding = binding; - channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL); - channel.setMethodCallHandler(this); - EventChannel eventChannel = new EventChannel(binding.getFlutterEngine().getDartExecutor(), EVENT_CHANNEL); + // 使用 binding.getBinaryMessenger() 而不是 binding.getFlutterEngine().getDartExecutor() + methodChannel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL); + methodChannel.setMethodCallHandler(this); + + eventChannel = new EventChannel(binding.getBinaryMessenger(), EVENT_CHANNEL); eventChannel.setStreamHandler(mopEventStream); + + // Register unified PlatformView factory + platformViewFactory = new MopWidgetPlatformViewFactory(); + binding.getPlatformViewRegistry() + .registerViewFactory("com.finogeeks.mop/platform_view", platformViewFactory); + + Log.d(LOG_TAG, "MopPlugin attached to engine"); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - this.flutterPluginBinding = null; + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { + if (methodChannel != null) { + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + if (eventChannel != null) { + eventChannel.setStreamHandler(null); + eventChannel = null; + } + platformViewFactory = null; + Log.d(LOG_TAG, "MopPlugin detached from engine"); } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { + this.activity = binding.getActivity(); binding.addActivityResultListener(delegate); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); + setServicesFromActivity(binding.getActivity()); - channel.setMethodCallHandler(this); + // give activity to platform view factory so it can access FlutterFragmentActivity + if (platformViewFactory != null) { + platformViewFactory.setActivity(this.activity); + } + Log.d(LOG_TAG, "MopPlugin attached to activity"); } @Override public void onDetachedFromActivityForConfigChanges() { - lifecycle = null; + onDetachedFromActivity(); + Log.d(LOG_TAG, "MopPlugin detached from activity for config changes"); } @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - binding.addActivityResultListener(delegate); - lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); - setServicesFromActivity(binding.getActivity()); + onAttachedToActivity(binding); + Log.d(LOG_TAG, "MopPlugin reattached to activity for config changes"); } @Override public void onDetachedFromActivity() { + if (activity != null) { + activity = null; + } lifecycle = null; - channel.setMethodCallHandler(null); + if (platformViewFactory != null) { + platformViewFactory.setActivity(null); + } + Log.d(LOG_TAG, "MopPlugin detached from activity"); } private void setServicesFromActivity(Activity activity) { - if (activity == null) + if (activity == null) { + Log.w(LOG_TAG, "Cannot set services from null activity"); return; - MopPluginService.getInstance().initialize(activity, mopEventStream, channel); + } + try { + MopPluginService.getInstance().initialize(activity, mopEventStream, methodChannel); + Log.d(LOG_TAG, "MopPluginService initialized successfully"); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to initialize MopPluginService", e); + } } -} +} \ No newline at end of file diff --git a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java new file mode 100644 index 00000000..089029b5 --- /dev/null +++ b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java @@ -0,0 +1,118 @@ +package com.finogeeks.mop.platformview; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; + +import java.util.Map; + +import io.flutter.plugin.platform.PlatformView; + +import com.finogeeks.lib.applet.client.FinAppClient; +import com.finogeeks.lib.applet.sdk.api.request.IFinAppletRequest; +import com.finogeeks.lib.applet.sdk.component.ComponentCallback; +import com.finogeeks.lib.applet.sdk.component.ComponentHolder; + +/** + * A simple unified PlatformView which can branch by external viewType. + * For now, it creates a colored placeholder view; integrate real native views here. + */ +public class MopWidgetPlatformView implements PlatformView { + + private final FrameLayout container; + @Nullable + private final Activity activity; + private final int viewId; + private final String externalViewType; + @Nullable + private final Map creationParams; + private static final String TAG = "MOP-PlatformView"; + + private ComponentHolder holder; + + public MopWidgetPlatformView(@NonNull Context context, + int viewId, + @NonNull String externalViewType, + @Nullable Map creationParams, + @Nullable Activity activity) { + this.viewId = viewId; + this.externalViewType = externalViewType; + this.creationParams = creationParams; + this.activity = activity; + this.container = new FrameLayout(context); + if ( creationParams != null && creationParams.get("appId") != null) { + String appId = (String) creationParams.get("appId"); + boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; + buildContent(context, appId,enableMultiComponent); + } + } + + private void buildContent(Context context, String appId, boolean enableMultiComponent) { + // TODO: Replace with real native view creation based on externalViewType + + IFinAppletRequest request = IFinAppletRequest.Companion.fromAppId(appId); + request.setEnableMultiComponent(enableMultiComponent); + FinAppClient.INSTANCE.getAppletApiManager().startComponent((FragmentActivity) activity, request, new ComponentCallback() { + @Override + public void onSuccess(@Nullable String s) { + Log.d(TAG, "onSuccess: $result"); + } + + @Override + public void onError(int i, @Nullable String s) { + Log.d(TAG, "onError: code=$code, error=$error"); + } + + @Override + public void onComponentCreated(@NonNull ComponentHolder componentHolder) { + holder = componentHolder; + container.removeAllViews(); + + container.addView( + holder.getView(), + new ViewGroup.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + container.requestLayout(); + container.invalidate(); + + } + + @Override + public void onContentSizeChanged(int i, int i1) { + + } + + @Override + public void onContentLoaded() { + + } + }); + + } + + @NonNull + @Override + public View getView() { + Log.d(TAG, "getView: viewId=" + viewId); + return container; + } + + @Override + public void dispose() { + Log.d(TAG, "dispose: viewId=" + viewId); + if (holder != null) { + holder.destroy(); + holder = null; + } + container.removeAllViews(); + + } +} diff --git a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformViewFactory.java b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformViewFactory.java new file mode 100644 index 00000000..a1883dcc --- /dev/null +++ b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformViewFactory.java @@ -0,0 +1,56 @@ +package com.finogeeks.mop.platformview; + +import android.app.Activity; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; + +/** + * Unified PlatformView factory for MOP. + * Create different native views based on external "viewType" passed from Dart. + */ +public class MopWidgetPlatformViewFactory extends PlatformViewFactory { + + @Nullable + private Activity activity; + private static final String TAG = "MOP-PlatformView"; + + public MopWidgetPlatformViewFactory() { + super(StandardMessageCodec.INSTANCE); + } + + public void setActivity(@Nullable Activity activity) { + this.activity = activity; + Log.d(TAG, "Factory.setActivity: " + (activity == null ? "null" : activity.getClass().getName())); + } + + @Override + @NonNull + @SuppressWarnings("unchecked") + public PlatformView create(@NonNull Context context, int viewId, Object args) { + String externalViewType = "default"; + Map creationParams = null; + if (args instanceof Map) { + Map map = (Map) args; + Object vt = map.get("viewType"); + if (vt instanceof String) externalViewType = (String) vt; + Object p = map.get("params"); + if (p instanceof Map) creationParams = (Map) p; + } + Log.d(TAG, "Factory.create: viewId=" + viewId + + ", externalViewType=" + externalViewType + + ", params=" + (creationParams == null ? "null" : creationParams.toString()) + + ", activity=" + (activity == null ? "null" : activity.getClass().getName())); + return new MopWidgetPlatformView(context, viewId, externalViewType, creationParams, activity); + } + + +} diff --git a/ios/Classes/MopPlugin.m b/ios/Classes/MopPlugin.m index 21e38ea8..9a7cbb51 100644 --- a/ios/Classes/MopPlugin.m +++ b/ios/Classes/MopPlugin.m @@ -5,6 +5,7 @@ #import "MOPAppletDelegate.h" #import #import "MopShareView.h" +#import "MopWidgetPlatformViewFactory.h" #import @implementation MopEventStream { @@ -67,6 +68,10 @@ + (void)registerWithRegistrar:(NSObject*)registrar { [registrar addMethodCallDelegate:_instance channel:appletShareChannel]; _instance.shareAppletMethodChannel = appletShareChannel; + // Register unified PlatformView factory for iOS + MopWidgetPlatformViewFactory *factory = [[MopWidgetPlatformViewFactory alloc] initWithMessenger:[registrar messenger]]; + [registrar registerViewFactory:factory withId:@"com.finogeeks.mop/platform_view"]; + NSLog(@"finclip widget 000"); } diff --git a/ios/Classes/MopWidgetPlatformView.h b/ios/Classes/MopWidgetPlatformView.h new file mode 100644 index 00000000..f03f9281 --- /dev/null +++ b/ios/Classes/MopWidgetPlatformView.h @@ -0,0 +1,9 @@ +#import +#import + +@interface MopWidgetPlatformView : NSObject +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; +@end diff --git a/ios/Classes/MopWidgetPlatformView.m b/ios/Classes/MopWidgetPlatformView.m new file mode 100644 index 00000000..99bf2208 --- /dev/null +++ b/ios/Classes/MopWidgetPlatformView.m @@ -0,0 +1,82 @@ +#import "MopWidgetPlatformView.h" +#import + + +@interface MopWidgetPlatformView () { + UIView *_view; + NSString *_externalViewType; + NSDictionary *_params; + NSObject* _messenger; +} +@end + +@implementation MopWidgetPlatformView + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + _view = [[UIView alloc] initWithFrame:frame]; + + if ([args isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = (NSDictionary *)args; + id vt = dict[@"viewType"]; if ([vt isKindOfClass:[NSString class]]) { _externalViewType = vt; } + id p = dict[@"params"]; if ([p isKindOfClass:[NSDictionary class]]) { _params = p; } + + NSString *appId = p[@"appId"]; + NSString *apiServer; + if (p[@"apiServer"] != NULL) { + apiServer = p[@"apiServer"]; + } + + [self openWidget:appId apiServer:apiServer]; + } + } + return self; +} + + +- (UIView *)view { + return _view; +} + +- (void)openWidget:(NSString *)appId apiServer:(NSString *)apiserver { + FATWidgetRequest *widgetRequest = [[FATWidgetRequest alloc]init]; + widgetRequest.widgetId = appId; + widgetRequest.widgetServer = apiserver; + + + [[FATClient sharedClient].widgetManager createWidget:widgetRequest parentViewController:[[UIApplication sharedApplication] fat_topViewController] completion:^(FATWidgetView * _Nullable widgetView, FATError * _Nonnull error) { + NSLog(@"fincli 打开小组件"); + if (!error) { + if (widgetView) { + [self showWidgetView:widgetView]; + } + }else { + NSLog(@"fincli 打开小组件报错:"); + } + }]; +} + +- (void)showWidgetView:(FATWidgetView *)widgetView { + [_view addSubview:widgetView]; + + widgetView.translatesAutoresizingMaskIntoConstraints = NO; + NSLayoutConstraint *topLayoutConstraint = [NSLayoutConstraint constraintWithItem:widgetView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:_view attribute:NSLayoutAttributeTop multiplier:1 constant:0]; + + NSLayoutConstraint *leftLayoutConstraint = [NSLayoutConstraint constraintWithItem:widgetView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:_view attribute:NSLayoutAttributeLeft multiplier:1 constant:0]; + + NSLayoutConstraint *rightLayoutConstraint = [NSLayoutConstraint constraintWithItem:widgetView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:_view attribute:NSLayoutAttributeRight multiplier:1 constant:0]; + + NSLayoutConstraint *bottomLayoutConstraint = [NSLayoutConstraint constraintWithItem:widgetView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:_view attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; + + widgetView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50); + [_view addConstraints:@[topLayoutConstraint,leftLayoutConstraint,rightLayoutConstraint,bottomLayoutConstraint]]; + +} + + +@end diff --git a/ios/Classes/MopWidgetPlatformViewFactory.h b/ios/Classes/MopWidgetPlatformViewFactory.h new file mode 100644 index 00000000..6397e9bd --- /dev/null +++ b/ios/Classes/MopWidgetPlatformViewFactory.h @@ -0,0 +1,5 @@ +#import + +@interface MopWidgetPlatformViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject*)messenger; +@end diff --git a/ios/Classes/MopWidgetPlatformViewFactory.m b/ios/Classes/MopWidgetPlatformViewFactory.m new file mode 100644 index 00000000..ddc18b35 --- /dev/null +++ b/ios/Classes/MopWidgetPlatformViewFactory.m @@ -0,0 +1,32 @@ +#import "MopWidgetPlatformViewFactory.h" +#import "MopWidgetPlatformView.h" + +@interface MopWidgetPlatformViewFactory () { + NSObject* _messenger; +} +@end + +@implementation MopWidgetPlatformViewFactory + +- (instancetype)initWithMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[MopWidgetPlatformView alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; +} + +@end diff --git a/lib/mop.dart b/lib/mop.dart index d1b009a1..3ff13f65 100644 --- a/lib/mop.dart +++ b/lib/mop.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:mop/api.dart'; export 'package:mop/api.dart' show FinFilePathType; +export 'platform_view.dart' show MopPlatformView; typedef MopEventCallback = void Function(dynamic event); typedef MopEventErrorCallback = void Function(dynamic event); diff --git a/lib/platform_view.dart b/lib/platform_view.dart new file mode 100644 index 00000000..41098048 --- /dev/null +++ b/lib/platform_view.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// 统一平台视图入口 +/// +/// 使用方式: +/// MopPlatformView( +/// viewType: 'native-view-android', +/// creationParams: {'foo': 'bar'}, +/// ) +class MopPlatformView extends StatelessWidget { + final String viewType; + final Map? creationParams; + final MessageCodec? creationParamsCodec; + + const MopPlatformView({ + Key? key, + required this.viewType, + this.creationParams, + this.creationParamsCodec, + }) : super(key: key); + + static const String _unifiedRegistryViewType = 'com.finogeeks.mop/platform_view'; + + @override + Widget build(BuildContext context) { + final codec = creationParamsCodec ?? const StandardMessageCodec(); + final params = { + 'viewType': viewType, + 'params': creationParams, + }; + + if (defaultTargetPlatform == TargetPlatform.android) { + // 使用统一注册的 viewType,实际的原生视图类型通过 creationParams 传递 + return AndroidView( + viewType: _unifiedRegistryViewType, + creationParams: params, + creationParamsCodec: codec, + ); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + return UiKitView( + viewType: _unifiedRegistryViewType, + creationParams: params, + creationParamsCodec: codec, + ); + } + return const Center(child: Text('当前平台不支持原生视图')); + } +} From 463a815e9bb0221c66946ac931570a2e3c56ba0a Mon Sep 17 00:00:00 2001 From: lijun Date: Mon, 1 Dec 2025 11:11:59 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=9B=B4=E6=96=B0read.me?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 ++++++++++++++++++++ android/.classpath | 2 +- android/.settings/org.eclipse.jdt.core.prefs | 6 +-- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d775d18..9c2202cf 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,48 @@ class _MyAppState extends State { } ``` +## 🧩 MopPlatformView 使用说明 + +`MopPlatformView` 提供统一的平台视图接入方式,便于在 Flutter 页面中嵌入原生组件或 FinClip 提供的原生能力组件。 + +- 统一注册的 `viewType` 为:`com.finogeeks.mop/platform_view` +- 实际的原生视图类型通过 `creationParams` 中的 `viewType` 传递,由各平台原生侧进行分发创建 + +### 使用示例(Flutter) + +```dart +import 'package:mop/mop.dart'; + +Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 500, + child: MopPlatformView( + // 这里的 viewType 是“实际原生视图类型”,由原生侧根据该值创建对应视图 + viewType: 'native-widget', + creationParams: { + 'appId': 'your_app_id', + // 其他业务参数... + }, + // 默认使用 StandardMessageCodec,如需自定义可传入 creationParamsCodec + ), + ); +} +``` + +### 参数说明(Flutter) +- `viewType`:实际原生视图类型标识(例如 `native-widget`)。 +- `creationParams`:传递给原生视图的初始化参数,Map 结构。 +- `creationParamsCodec`:参数编解码器(默认 `StandardMessageCodec`)。 + +### 平台侧说明(原生) +- 插件已在 Android 与 iOS 侧统一注册 `com.finogeeks.mop/platform_view` 工厂。 + - Android:`MopPlugin` 中通过 `PlatformViewRegistry` 注册,并在工厂内根据 `params.viewType` 创建对应原生视图。 + - iOS:`MopPlugin` 中通过 `registerViewFactory` 注册,并在工厂内根据 `params.viewType` 创建对应原生视图。 +- 如需新增原生视图类型,请在各自平台的 `PlatformViewFactory` 中扩展分发逻辑,并约定一个新的 `creationParams.viewType` 值。 + +> 提示:若运行平台暂不支持平台视图,将显示“当前平台不支持原生视图”的占位文案。 + ## 📋 接口文档 ### 1. 初始化小程序 diff --git a/android/.classpath b/android/.classpath index 0a3280e2..bbe97e50 100644 --- a/android/.classpath +++ b/android/.classpath @@ -1,6 +1,6 @@ - + diff --git a/android/.settings/org.eclipse.jdt.core.prefs b/android/.settings/org.eclipse.jdt.core.prefs index e9186c3e..626e0e1d 100644 --- a/android/.settings/org.eclipse.jdt.core.prefs +++ b/android/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 -org.eclipse.jdt.core.compiler.compliance=21 -org.eclipse.jdt.core.compiler.source=21 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.source=17 From 54d3bbdcb6f5a6d7d1417202afc62ffb37329197 Mon Sep 17 00:00:00 2001 From: lijun Date: Wed, 10 Dec 2025 19:04:59 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E6=89=93=E5=BC=80=E5=B0=8F=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/.classpath | 2 +- android/.settings/org.eclipse.jdt.core.prefs | 6 +- .../platformview/MopWidgetPlatformView.java | 94 ++++++++++++++++++- ios/Classes/MopWidgetPlatformView.m | 36 +++++-- lib/platform_view.dart | 27 +++++- 5 files changed, 146 insertions(+), 19 deletions(-) diff --git a/android/.classpath b/android/.classpath index bbe97e50..0a3280e2 100644 --- a/android/.classpath +++ b/android/.classpath @@ -1,6 +1,6 @@ - + diff --git a/android/.settings/org.eclipse.jdt.core.prefs b/android/.settings/org.eclipse.jdt.core.prefs index 626e0e1d..e9186c3e 100644 --- a/android/.settings/org.eclipse.jdt.core.prefs +++ b/android/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 -org.eclipse.jdt.core.compiler.compliance=17 -org.eclipse.jdt.core.compiler.source=17 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.source=21 diff --git a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java index 089029b5..2ed0029e 100644 --- a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java +++ b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java @@ -6,6 +6,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -30,6 +31,7 @@ public class MopWidgetPlatformView implements PlatformView { @Nullable private final Activity activity; private final int viewId; + private final String externalViewType; @Nullable private final Map creationParams; @@ -43,14 +45,35 @@ public MopWidgetPlatformView(@NonNull Context context, @Nullable Map creationParams, @Nullable Activity activity) { this.viewId = viewId; + this.externalViewType = externalViewType; this.creationParams = creationParams; this.activity = activity; this.container = new FrameLayout(context); - if ( creationParams != null && creationParams.get("appId") != null) { - String appId = (String) creationParams.get("appId"); - boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; - buildContent(context, appId,enableMultiComponent); + if ( creationParams != null ) { + + if (creationParams.get("appId") != null) { + String appId = (String) creationParams.get("appId"); + boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; + buildContent(context, appId,enableMultiComponent); + } else if (creationParams.get("qrCode") != null) { + String qrCode = (String) creationParams.get("qrCode"); + boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; + buildContentWithQR(context, qrCode, enableMultiComponent); + } else { + + TextView textView = new TextView(context); + textView.setText("需要传入小组件的 appId 或者小组件的二维码"); + container.addView( + textView, + new ViewGroup.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT) + ); + + Log.d(TAG, "MopWidgetPlatformView: no appId or qrCode found"); + } + } } @@ -68,6 +91,14 @@ public void onSuccess(@Nullable String s) { @Override public void onError(int i, @Nullable String s) { Log.d(TAG, "onError: code=$code, error=$error"); + TextView textView = new TextView(context); + textView.setText("出错啦:" + s); + container.addView( + textView, + new ViewGroup.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT) + ); } @Override @@ -87,12 +118,65 @@ public void onComponentCreated(@NonNull ComponentHolder componentHolder) { @Override public void onContentSizeChanged(int i, int i1) { - + Log.d(TAG, "onContentSizeChanged:"); } @Override public void onContentLoaded() { + Log.d(TAG, "onLoad:"); + } + }); + + } + + + private void buildContentWithQR( Context context, String qrCode, boolean enableMultiComponent) { + // TODO: Replace with real native view creation based on externalViewType + + IFinAppletRequest request = IFinAppletRequest.Companion.fromQrCode(qrCode); + request.setEnableMultiComponent(enableMultiComponent); + FinAppClient.INSTANCE.getAppletApiManager().startComponent((FragmentActivity) activity, request, new ComponentCallback() { + @Override + public void onSuccess(@Nullable String s) { + Log.d(TAG, "onSuccess: $result"); + } + + @Override + public void onError(int i, @Nullable String s) { + Log.d(TAG, "onError: code=$code, error=$error"); + TextView textView = new TextView(context); + textView.setText("出错啦:" + s); + container.addView( + textView, + new ViewGroup.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT) + ); + } + + @Override + public void onComponentCreated(@NonNull ComponentHolder componentHolder) { + holder = componentHolder; + container.removeAllViews(); + + container.addView( + holder.getView(), + new ViewGroup.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT)); + container.requestLayout(); + container.invalidate(); + + } + @Override + public void onContentSizeChanged(int i, int i1) { + Log.d(TAG, "onContentSizeChanged:"); + } + + @Override + public void onContentLoaded() { + Log.d(TAG, "onLoad:"); } }); diff --git a/ios/Classes/MopWidgetPlatformView.m b/ios/Classes/MopWidgetPlatformView.m index 99bf2208..60996fc8 100644 --- a/ios/Classes/MopWidgetPlatformView.m +++ b/ios/Classes/MopWidgetPlatformView.m @@ -26,13 +26,19 @@ - (instancetype)initWithFrame:(CGRect)frame id vt = dict[@"viewType"]; if ([vt isKindOfClass:[NSString class]]) { _externalViewType = vt; } id p = dict[@"params"]; if ([p isKindOfClass:[NSDictionary class]]) { _params = p; } - NSString *appId = p[@"appId"]; - NSString *apiServer; - if (p[@"apiServer"] != NULL) { - apiServer = p[@"apiServer"]; + if (p[@"appId"]) { + + NSString *apiServer = p[@"apiServer"] != NULL ? p[@"apiServer"] : @""; + + [self openWidget:p[@"appId"] apiServer:apiServer]; + + } else if (p[@"qrCode"]) { + + [self openWidgetWithQR:p[@"qrCode"]]; + } else { + + NSLog(@"你需要传入小组件的 appId 或着小组件的二维码"); } - - [self openWidget:appId apiServer:apiServer]; } } return self; @@ -56,7 +62,23 @@ - (void)openWidget:(NSString *)appId apiServer:(NSString *)apiserver { [self showWidgetView:widgetView]; } }else { - NSLog(@"fincli 打开小组件报错:"); + NSLog(@"fincli 打开小组件报错:%@", error); + } + }]; +} + +- (void)openWidgetWithQR:(NSString *)qrCode { + FATWidgetQRCodeRequest *qrCodeRequest = [[FATWidgetQRCodeRequest alloc]init]; + qrCodeRequest.qrCode = qrCode; + + + [[FATClient sharedClient].widgetManager createWidgetWithQRCode:qrCodeRequest parentViewController:[[UIApplication sharedApplication] fat_topViewController] completion:^(FATWidgetView * _Nullable widgetView, FATError *error) { + if (!error) { + if (widgetView) { + [self showWidgetView:widgetView]; + } + } else { + NSLog(@"fincli 打开小组件报错:%@", error); } }]; } diff --git a/lib/platform_view.dart b/lib/platform_view.dart index 41098048..da5281a1 100644 --- a/lib/platform_view.dart +++ b/lib/platform_view.dart @@ -1,8 +1,10 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/rendering.dart'; /// 统一平台视图入口 /// @@ -35,10 +37,29 @@ class MopPlatformView extends StatelessWidget { if (defaultTargetPlatform == TargetPlatform.android) { // 使用统一注册的 viewType,实际的原生视图类型通过 creationParams 传递 - return AndroidView( + // 为了更好的兼容性与性能,这里采用 Hybrid Composition:PlatformViewLink + AndroidViewSurface + return PlatformViewLink( viewType: _unifiedRegistryViewType, - creationParams: params, - creationParamsCodec: codec, + surfaceFactory: (context, controller) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams creationParams0) { + final controller = PlatformViewsService.initSurfaceAndroidView( + id: creationParams0.id, + viewType: _unifiedRegistryViewType, + layoutDirection: TextDirection.ltr, + // 注意:这里的 creationParams 需与 surfaceFactory 同步 + creationParams: params, + creationParamsCodec: codec, + ); + controller.addOnPlatformViewCreatedListener(creationParams0.onPlatformViewCreated); + controller.create(); + return controller; + }, ); } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( From 38e2950021d3b3db423a889ade2cb43648e35206 Mon Sep 17 00:00:00 2001 From: lijun Date: Wed, 17 Dec 2025 14:03:50 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B0=8F=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E4=BA=8B=E4=BB=B6=E4=BB=A3=E7=90=86=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0forceUpdate=E5=B1=9E=E6=80=A7=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platformview/MopWidgetPlatformView.java | 171 +++++++++++++----- ios/Classes/MopWidgetPlatformView.m | 85 ++++++++- lib/api.dart | 11 ++ lib/mop.dart | 2 +- lib/platform_view.dart | 99 ++++++++-- 5 files changed, 300 insertions(+), 68 deletions(-) diff --git a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java index 2ed0029e..5c13fcda 100644 --- a/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java +++ b/android/src/main/java/com/finogeeks/mop/platformview/MopWidgetPlatformView.java @@ -6,12 +6,12 @@ import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import java.util.HashMap; import java.util.Map; import io.flutter.plugin.platform.PlatformView; @@ -20,6 +20,7 @@ import com.finogeeks.lib.applet.sdk.api.request.IFinAppletRequest; import com.finogeeks.lib.applet.sdk.component.ComponentCallback; import com.finogeeks.lib.applet.sdk.component.ComponentHolder; +import com.finogeeks.mop.service.MopPluginService; /** * A simple unified PlatformView which can branch by external viewType. @@ -32,9 +33,6 @@ public class MopWidgetPlatformView implements PlatformView { private final Activity activity; private final int viewId; - private final String externalViewType; - @Nullable - private final Map creationParams; private static final String TAG = "MOP-PlatformView"; private ComponentHolder holder; @@ -46,8 +44,6 @@ public MopWidgetPlatformView(@NonNull Context context, @Nullable Activity activity) { this.viewId = viewId; - this.externalViewType = externalViewType; - this.creationParams = creationParams; this.activity = activity; this.container = new FrameLayout(context); if ( creationParams != null ) { @@ -55,21 +51,29 @@ public MopWidgetPlatformView(@NonNull Context context, if (creationParams.get("appId") != null) { String appId = (String) creationParams.get("appId"); boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; - buildContent(context, appId,enableMultiComponent); + boolean forceUpdate = creationParams.get("forceUpdate") != null; + buildContent(context, appId,enableMultiComponent, forceUpdate); } else if (creationParams.get("qrCode") != null) { String qrCode = (String) creationParams.get("qrCode"); + boolean forceUpdate = creationParams.get("forceUpdate") != null; boolean enableMultiComponent = creationParams.get("enableMultiComponent") != null; - buildContentWithQR(context, qrCode, enableMultiComponent); + buildContentWithQR(context, qrCode, enableMultiComponent, forceUpdate); } else { - TextView textView = new TextView(context); - textView.setText("需要传入小组件的 appId 或者小组件的二维码"); - container.addView( - textView, - new ViewGroup.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT) - ); + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("errorMessage", "MopWidgetPlatformView: no appId or qrCode found"); + eventData.put("eventType", "onLoadError"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "onLoadError", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send onError event", e); + } Log.d(TAG, "MopWidgetPlatformView: no appId or qrCode found"); } @@ -77,11 +81,12 @@ public MopWidgetPlatformView(@NonNull Context context, } } - private void buildContent(Context context, String appId, boolean enableMultiComponent) { - // TODO: Replace with real native view creation based on externalViewType + private void buildContent(Context context, String appId, boolean enableMultiComponent, boolean forceUpdate) { - IFinAppletRequest request = IFinAppletRequest.Companion.fromAppId(appId); + IFinAppletRequest request = IFinAppletRequest.Companion.fromAppId(appId) + .setForceUpdate(forceUpdate); request.setEnableMultiComponent(enableMultiComponent); + FinAppClient.INSTANCE.getAppletApiManager().startComponent((FragmentActivity) activity, request, new ComponentCallback() { @Override public void onSuccess(@Nullable String s) { @@ -89,16 +94,24 @@ public void onSuccess(@Nullable String s) { } @Override - public void onError(int i, @Nullable String s) { - Log.d(TAG, "onError: code=$code, error=$error"); - TextView textView = new TextView(context); - textView.setText("出错啦:" + s); - container.addView( - textView, - new ViewGroup.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT) - ); + public void onError(int code, @Nullable String error) { + Log.d(TAG, "onError: " + error); + + // 发送加载失败事件 + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("errorMessage", "onError: " + error); + eventData.put("eventType", "onLoadError"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "onLoadError", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send onLoadError event", e); + } } @Override @@ -117,24 +130,56 @@ public void onComponentCreated(@NonNull ComponentHolder componentHolder) { } @Override - public void onContentSizeChanged(int i, int i1) { + public void onContentSizeChanged(int width, int height) { Log.d(TAG, "onContentSizeChanged:"); + // 发送内容尺寸变化事件 + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("width", width); + eventData.put("height", height); + eventData.put("eventType", "contentSizeChanged"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "contentSizeChanged", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send contentSizeChanged event", e); + } } @Override public void onContentLoaded() { Log.d(TAG, "onLoad:"); + // 发送内容加载完成事件 + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("message", "widget loaded successfully"); + eventData.put("eventType", "onContentLoaded"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "onContentLoaded", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send onContentLoaded event", e); + } } }); } - private void buildContentWithQR( Context context, String qrCode, boolean enableMultiComponent) { - // TODO: Replace with real native view creation based on externalViewType + private void buildContentWithQR( Context context, String qrCode, boolean enableMultiComponent, boolean forceUpdate) { - IFinAppletRequest request = IFinAppletRequest.Companion.fromQrCode(qrCode); + IFinAppletRequest request = IFinAppletRequest.Companion.fromQrCode(qrCode) + .setForceUpdate(forceUpdate); request.setEnableMultiComponent(enableMultiComponent); + FinAppClient.INSTANCE.getAppletApiManager().startComponent((FragmentActivity) activity, request, new ComponentCallback() { @Override public void onSuccess(@Nullable String s) { @@ -142,16 +187,23 @@ public void onSuccess(@Nullable String s) { } @Override - public void onError(int i, @Nullable String s) { - Log.d(TAG, "onError: code=$code, error=$error"); - TextView textView = new TextView(context); - textView.setText("出错啦:" + s); - container.addView( - textView, - new ViewGroup.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - FrameLayout.LayoutParams.WRAP_CONTENT) - ); + public void onError(int code, @Nullable String error) { + Log.d(TAG, "onError: " + error); + // 发送加载失败事件 + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("errorMessage", "onError: " + error); + eventData.put("eventType", "onError"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "onLoadError", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send onLoadError event", e); + } } @Override @@ -170,13 +222,44 @@ public void onComponentCreated(@NonNull ComponentHolder componentHolder) { } @Override - public void onContentSizeChanged(int i, int i1) { + public void onContentSizeChanged(int width, int height) { Log.d(TAG, "onContentSizeChanged:"); + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("width", width); + eventData.put("height", height); + eventData.put("eventType", "contentSizeChanged"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "contentSizeChanged", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send contentSizeChanged event", e); + } } @Override public void onContentLoaded() { Log.d(TAG, "onLoad:"); + + // 发送内容加载完成事件 + try { + Map eventData = new HashMap<>(); + eventData.put("viewId", viewId); + eventData.put("message", "widget loaded successfully"); + eventData.put("eventType", "onContentLoaded"); + + MopPluginService.getInstance().getMopEventStream().send( + "platform_view_events", + "onContentLoaded", + eventData + ); + } catch (Exception e) { + Log.e(TAG, "Failed to send onContentLoaded event", e); + } } }); diff --git a/ios/Classes/MopWidgetPlatformView.m b/ios/Classes/MopWidgetPlatformView.m index 60996fc8..800566c7 100644 --- a/ios/Classes/MopWidgetPlatformView.m +++ b/ios/Classes/MopWidgetPlatformView.m @@ -1,12 +1,14 @@ #import "MopWidgetPlatformView.h" #import +#import "MopPlugin.h" -@interface MopWidgetPlatformView () { +@interface MopWidgetPlatformView () { UIView *_view; NSString *_externalViewType; NSDictionary *_params; NSObject* _messenger; + int64_t _viewId; } @end @@ -19,8 +21,9 @@ - (instancetype)initWithFrame:(CGRect)frame self = [super init]; if (self) { _messenger = messenger; + _viewId = viewId; _view = [[UIView alloc] initWithFrame:frame]; - + if ([args isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)args; id vt = dict[@"viewType"]; if ([vt isKindOfClass:[NSString class]]) { _externalViewType = vt; } @@ -29,14 +32,22 @@ - (instancetype)initWithFrame:(CGRect)frame if (p[@"appId"]) { NSString *apiServer = p[@"apiServer"] != NULL ? p[@"apiServer"] : @""; - - [self openWidget:p[@"appId"] apiServer:apiServer]; + [self openWidget:p[@"appId"] apiServer:apiServer enableMultiWidget:p[@"enableMultiWidget"] != NULL forceUpdate:p[@"forceUpdate"] != NULL]; } else if (p[@"qrCode"]) { - [self openWidgetWithQR:p[@"qrCode"]]; + [self openWidgetWithQR:p[@"qrCode"] forceUpdate: p[@"forceUpdate"] != NULL]; } else { + NSLog(@"fincli 打开小组件报错:%@", error); + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(self->_viewId), + @"eventType": @"onLoadError", + @"errorMessage": @"MopWidgetPlatformView: no appId or qrCode found" + }; + + [eventStream send:@"platform_view_events" event:@"onLoadError" body:eventData]; NSLog(@"你需要传入小组件的 appId 或着小组件的二维码"); } } @@ -49,36 +60,81 @@ - (UIView *)view { return _view; } -- (void)openWidget:(NSString *)appId apiServer:(NSString *)apiserver { +- (void)openWidget:(NSString *)appId apiServer:(NSString *)apiserver enableMultiWidget:(Boolean)enableMultiWidget forceUpdate:(Boolean)forceUpdate{ FATWidgetRequest *widgetRequest = [[FATWidgetRequest alloc]init]; widgetRequest.widgetId = appId; widgetRequest.widgetServer = apiserver; + widgetRequest.enableMultiWidget = enableMultiWidget; + widgetRequest.forceUpdate = forceUpdate; [[FATClient sharedClient].widgetManager createWidget:widgetRequest parentViewController:[[UIApplication sharedApplication] fat_topViewController] completion:^(FATWidgetView * _Nullable widgetView, FATError * _Nonnull error) { NSLog(@"fincli 打开小组件"); if (!error) { if (widgetView) { + widgetView.delegate = self; [self showWidgetView:widgetView]; + + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(self->_viewId), + @"message": @"widget loaded successfully", + @"eventType": @"onContentLoaded" + }; + [eventStream send:@"platform_view_events" event:@"onContentLoaded" body:eventData]; + } }else { NSLog(@"fincli 打开小组件报错:%@", error); + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(self->_viewId), + @"eventType": @"onLoadError", + @"error": @"widget failed to load", + @"errorCode": @(error.code), + @"errorMessage": error.localizedDescription ?: @"Unknown error" + }; + + [eventStream send:@"platform_view_events" event:@"onLoadError" body:eventData]; } }]; } -- (void)openWidgetWithQR:(NSString *)qrCode { +- (void)openWidgetWithQR:(NSString *)qrCode forceUpdate:(Boolean)forceUpdate{ FATWidgetQRCodeRequest *qrCodeRequest = [[FATWidgetQRCodeRequest alloc]init]; qrCodeRequest.qrCode = qrCode; - + qrCodeRequest.forceUpdate = forceUpdate; [[FATClient sharedClient].widgetManager createWidgetWithQRCode:qrCodeRequest parentViewController:[[UIApplication sharedApplication] fat_topViewController] completion:^(FATWidgetView * _Nullable widgetView, FATError *error) { if (!error) { if (widgetView) { + widgetView.delegate = self; [self showWidgetView:widgetView]; + + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(self->_viewId), + @"message": @"widget loaded successfully", + @"eventType": @"onContentLoaded" + }; + + [eventStream send:@"platform_view_events" event:@"onContentLoaded" body:eventData]; + + } } else { NSLog(@"fincli 打开小组件报错:%@", error); + + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(self->_viewId), + @"eventType": @"onLoadError", + @"error": @"widget failed to load", + @"errorCode": @(error.code), + @"errorMessage": error.localizedDescription ?: @"Unknown error" + }; + + [eventStream send:@"platform_view_events" event:@"onLoadError" body:eventData]; } }]; } @@ -101,4 +157,17 @@ - (void)showWidgetView:(FATWidgetView *)widgetView { } +- (void)onWidgetView:(nonnull FATWidgetView *)widgetView contentSizeUpdate:(CGSize)size { + // 通过代理发送事件到Flutter + MopEventStream *eventStream = [[MopPlugin instance] mopEventStreamHandler]; + NSDictionary *eventData = @{ + @"viewId": @(_viewId), + @"width": @(size.width), + @"height": @(size.height), + @"eventType": @"contentSizeChanged" + }; + + [eventStream send:@"platform_view_events" event:@"contentSizeChanged" body:eventData]; +} + @end diff --git a/lib/api.dart b/lib/api.dart index 4d0c8db1..2e88da07 100644 --- a/lib/api.dart +++ b/lib/api.dart @@ -52,3 +52,14 @@ abstract class AppletHandler { ///getMobileNumber Future getMobileNumber(Function(dynamic params) param0); } + +abstract class WidgetHandler { + /// Widget内容尺寸变化回调 + void onContentSizeChanged(int viewId, double width, double height); + + /// 当小组件加载完成时调用 + void onContentLoaded(int viewId); + + /// 当小组件发生错误时调用 + void onError(int viewId, String error); +} diff --git a/lib/mop.dart b/lib/mop.dart index 3ff13f65..b83bf58e 100644 --- a/lib/mop.dart +++ b/lib/mop.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:mop/api.dart'; export 'package:mop/api.dart' show FinFilePathType; -export 'platform_view.dart' show MopPlatformView; +export 'platform_view.dart' show MopPlatformView, PlatformViewEventDelegate; typedef MopEventCallback = void Function(dynamic event); typedef MopEventErrorCallback = void Function(dynamic event); diff --git a/lib/platform_view.dart b/lib/platform_view.dart index da5281a1..f6af0cc0 100644 --- a/lib/platform_view.dart +++ b/lib/platform_view.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -6,38 +7,114 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/rendering.dart'; +/// 平台视图事件代理接口 +abstract class PlatformViewEventDelegate { + /// 当PlatformView的内容尺寸发生变化时调用 + void onContentSizeChanged(int viewId, double width, double height); + + /// 当PlatformView加载完成时调用 + void onContentLoaded(int viewId); + + /// 当PlatformView发生错误时调用 + void onError(int viewId, String error); +} + /// 统一平台视图入口 /// /// 使用方式: /// MopPlatformView( /// viewType: 'native-view-android', /// creationParams: {'foo': 'bar'}, +/// eventDelegate: MyPlatformViewDelegate(), /// ) -class MopPlatformView extends StatelessWidget { +class MopPlatformView extends StatefulWidget { final String viewType; final Map? creationParams; final MessageCodec? creationParamsCodec; + final PlatformViewEventDelegate? eventDelegate; const MopPlatformView({ Key? key, required this.viewType, this.creationParams, this.creationParamsCodec, + this.eventDelegate, }) : super(key: key); static const String _unifiedRegistryViewType = 'com.finogeeks.mop/platform_view'; + @override + State createState() => _MopPlatformViewState(); +} + +class _MopPlatformViewState extends State { + static const String _unifiedRegistryViewType = 'com.finogeeks.mop/platform_view'; + static const EventChannel _platformViewEventChannel = + EventChannel('plugins.mop.finogeeks.com/mop_event'); + + StreamSubscription? _eventSubscription; + + @override + void initState() { + super.initState(); + _listenToPlatformViewEvents(); + } + + @override + void dispose() { + _eventSubscription?.cancel(); + super.dispose(); + } + + void _listenToPlatformViewEvents() { + _eventSubscription = _platformViewEventChannel.receiveBroadcastStream().listen( + (event) { + if (event is Map && event['channel'] == 'platform_view_events') { + final eventData = event['body']; + final eventType = event['event']; + + switch (eventType) { + case 'contentSizeChanged': + widget.eventDelegate?.onContentSizeChanged( + eventData['viewId'] as int, + (eventData['width'] as num).toDouble(), + (eventData['height'] as num).toDouble(), + ); + break; + case 'onContentLoaded': + widget.eventDelegate?.onContentLoaded(eventData['viewId'] as int); + break; + case 'onLoadError': + widget.eventDelegate?.onError( + eventData['viewId'] as int, + eventData['errorMessage'] as String, + ); + break; + } + } + }, + onError: (error) { + debugPrint('PlatformView event error: $error'); + }, + ); + } + @override Widget build(BuildContext context) { - final codec = creationParamsCodec ?? const StandardMessageCodec(); + final codec = widget.creationParamsCodec ?? const StandardMessageCodec(); final params = { - 'viewType': viewType, - 'params': creationParams, + 'viewType': widget.viewType, + 'params': widget.creationParams, }; - if (defaultTargetPlatform == TargetPlatform.android) { - // 使用统一注册的 viewType,实际的原生视图类型通过 creationParams 传递 - // 为了更好的兼容性与性能,这里采用 Hybrid Composition:PlatformViewLink + AndroidViewSurface + if (defaultTargetPlatform == TargetPlatform.iOS) { + return UiKitView( + viewType: _unifiedRegistryViewType, + creationParams: params, + creationParamsCodec: codec, + ); + } else { + // Android端暂时保持现有实现 return PlatformViewLink( viewType: _unifiedRegistryViewType, surfaceFactory: (context, controller) { @@ -52,7 +129,6 @@ class MopPlatformView extends StatelessWidget { id: creationParams0.id, viewType: _unifiedRegistryViewType, layoutDirection: TextDirection.ltr, - // 注意:这里的 creationParams 需与 surfaceFactory 同步 creationParams: params, creationParamsCodec: codec, ); @@ -61,13 +137,6 @@ class MopPlatformView extends StatelessWidget { return controller; }, ); - } else if (defaultTargetPlatform == TargetPlatform.iOS) { - return UiKitView( - viewType: _unifiedRegistryViewType, - creationParams: params, - creationParamsCodec: codec, - ); } - return const Center(child: Text('当前平台不支持原生视图')); } }