当前位置: 首页 > news >正文

【译】 如何使用 .NET MAUI 构建 Android 小部件

原文 | Toine de Boer

翻译 | 郑子铭

这是Toine de Boer的客座博文。

这篇博客将探讨上一篇关于iOS 小部件的博客中创建的交互式小部件的 Android 版本。Android 通常限制较少,也更容易上手,您可以在 Visual Studio 的 .NET MAUI 项目中直接构建所有内容。其复杂性在于完成任务的选项众多,以及需要考虑较旧的 Android 版本。

与 iOS 小部件博客一样,本文并非循序渐进的教程。相反,它将按照构建 Android 小部件时通常会遇到的障碍顺序,重点介绍最重要、最关键的部分。文章从创建一个简单的静态小部件开始,逐步构建一个可配置、完全交互式的小部件。GitHub 上有一个完整的小部件示例Maui.WidgetExample。

android-widgetexample

先决条件

对于 Android 小部件来说,并没有什么真正的先决条件。虽然 .NET MAUI 没有适用于 Android 小部件的组件,但我们可以使用原生 Android 方法自行构建。好处是所有操作都可以在 Visual Studio 中完成;当然,您也可以选择使用 Android Studio 及其可视化编辑器和预览工具来设计布局。由于我过去曾大量使用过 Android XML 布局,并且本博客侧重于功能而非外观,因此我将在 Copilot 的辅助下手动创建 XML 布局。

如果我想要更精致的界面,我会切换到 Android Studio,用 XML 创建布局。具体操作是:创建一个虚拟的 Android 项目,添加一个组件(例如,通过“文件”>“新建”>“组件”),然后开始设计 XML 布局。这样可以充分利用实时预览工具和属性面板,查看并编辑每个视图的所有可用选项。

当您将 Android 特有的资源文件按照类似于原生 Android 应用的文件夹结构进行组织时,.NET MAUI 可以很好地处理这些资源文件./Platforms/Resources。因此,创建小部件无需对项目文件进行任何更改.csproj——项目文件保持不变。我通常在 Visual Studio 之外创建或复制文件和文件夹,以防止 Visual Studio 修改项目文件.csproj。

对于 iOS 小部件,我已经创建了一个 .NET MAUI 项目来演示如何与小部件通信,我将在 Android 中复用该项目。大部分现有代码保持不变。所有新增的 Android 小部件代码都将放在相应的./Platforms/Android文件夹中。

创建小部件

Android 小部件并非像应用中的普通视图,但它们确实存在于 .NET MAUI 应用中。小部件仅限于 RemoteViews 提供的 Android 视图集;不支持自定义视图。您仍然可以对它们进行相当不错的样式设置,但需要巧妙地运用形状、矢量图形和其他可绘制对象。

Android 小部件的起点是 <div> AppWidgetProvider。它可以使用 <div>根据 ID 为小部件AppWidgetManager提供视图。Android使用 <div> 来显示来自其他进程的视图;它们使用与普通 Android 视图相同的 XML 布局样式,但它们被加载到一个 <div> 对象中。RemoteViewsRemoteViewsRemoteViews

[BroadcastReceiver(Label = "My Widget")]
[MetaData(AppWidgetManager.MetaDataAppwidgetProvider, Resource = "@xml/mywidget_provider_info")]
public class MyWidgetProvider : AppWidgetProvider
{public override void OnUpdate(Context? context, AppWidgetManager? appWidgetManager, int[]? appWidgetIds){if (context == null || appWidgetIds == null || appWidgetManager == null){return;}foreach (var appWidgetId in appWidgetIds){var views = new RemoteViews(context.PackageName, Resource.Layout.mywidget);views.SetTextViewText(Resource.Id.widgetText, "Count:5 (static)");appWidgetManager.UpdateAppWidget(appWidgetId, views);}}
}

该功能AppWidgetProvider依赖于位于文件夹中的配置文件,并通过字段中的属性Resources/xml引用该文件。此文件允许您配置小部件设置,例如预览图像、尺寸、调整大小限制和功能。MetaDataResource

<!-- Resources/xml/mywidget_provider_info.xml -->
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"android:minWidth="120dp"android:minHeight="80dp"android:maxResizeWidth="140dp"android:updatePeriodMillis="0"android:initialLayout="@layout/mywidget"android:resizeMode="horizontal|vertical"android:widgetCategory="home_screen"android:configure="widgetexample.WidgetConfigurationActivity"android:widgetFeatures="reconfigurable"android:previewImage="@drawable/mywidget_preview_image" />

其中最重要的条目之一是android:initialLayout,它指的是位于中的视图布局Resources/layout。此布局被加载到中RemoteView

<!-- Resources/layout/mywidget.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:id="@+id/widgetText"android:text="Static widget"android:textSize="16sp"android:layout_width="wrap_content"android:layout_height="wrap_content" />
</LinearLayout>

与普通 Android 视图的一个主要区别RemoteView在于,您无法直接从代码中操作视图。每个视图仅允许更新有限的属性集,所有更改都必须通过RemoteViews 对象及其辅助方法进行。例如,您可以传入要更新的视图的资源 ID:views.SetTextViewText(Resource.Id.widgetText, "Hello World");

此时,您可以构建应用程序,并且应该可以在手机的小部件库中看到该小部件。

应用与小部件之间的数据共享

在 Android 系统中,应用和组件之间的数据共享比 iOS 系统更简单,因为组件内部可以使用 C# .NET MAUI 代码。虽然由于生命周期问题,这种方式可能不太可靠,但您甚至可以在一定程度上在组件和应用之间共享内存数据。因此,我们建议采用持久化存储方案。由于我们已经为 iOS 组件设置了共享数据存储,SharedPreferences为了确保跨平台一致性,我们在 Android 系统上继续沿用现有的机制。

// example how to store data in .NET MAUI (Preferences)
Preferences.Set("MyDataKey", "my data to share", "group.com.enbyin.WidgetExample");// example how to store data on Android, on the same location (SharedPreferences)
var preferences = context.GetSharedPreferences("group.com.enbyin.WidgetExample", Context.MODE_PRIVATE);
var value = preferences.GetString("MyDataKey", null);

笔记

存储键区分大小写;请保持键名简洁且全部小写,以避免出现问题。

应用与小部件之间的通信

与 iOS 类似,Android 小部件无法感知应用何时更新数据,应用也无法自动感知小部件何时更新数据。在 Android 系统中,应用和小部件之间有多种通信方式;你甚至可以在应用运行时更新小部件视图。最可靠的机制是使用Intents……

原理很简单:创建一个对象Intent并为其指定一个操作字符串。Intent然后可以在需要时广播该对象;例如,在后台进程完成后或用户按下控件上的按钮时。

// Broadcast an Intent with action ‘ActionAppwidgetUpdate’
var intent = new Android.Content.Intent(AppWidgetManager.ActionAppwidgetUpdate);
Android.App.Application.Context.SendBroadcast(intent);

需要接收操作的组件Intent声明相同的操作字符串。在 .NET MAUI 中,Intent使用 @Occeptable 属性可以轻松订阅操作[IntentFilter]。如果组件需要Intent从其自身应用程序外部接收操作,则需要设置 @OcceptableExported = true属性。控件需要这样做,因为它们实际上存在于应用程序之外。

// Subscribing your AppWidgetProvider to listen to Intents
[BroadcastReceiver(Label = "My Widget", Exported = true)]
[IntentFilter(new[] { AppWidgetManager.ActionAppwidgetUpdate })]
[MetaData(AppWidgetManager.MetaDataAppwidgetProvider, Resource = "@xml/mywidget_provider_info")]
public class MyWidgetProvider : AppWidgetProvider
{public override void OnReceive(Context? context, Intent? intent){var myIntent = Intent;// ...}
}
// Subscribing a Service to listen to Intents
[Service(Exported = true)]
[IntentFilter(new[] { AppWidgetManager.ActionAppwidgetUpdate })]
public class WidgetListenerService : Service
{public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId){var myIntent = intent;return StartCommandResult.NotSticky;}
}
// Subscribing an Activity to listen to Intents
[IntentFilter(new[] { Intent.ActionView })]
public class MyActivity : Activity
{ protected override void OnCreate(Bundle savedInstanceState){base.OnCreate(savedInstanceState);var myIntent = Intent;}
}

手动刷新小部件

AppWidgetProvider默认情况下,该组件会监听Intent带有内置操作的事件AppWidgetManager.ActionAppwidgetUpdate。当Intent收到此类事件时,通常会触发组件刷新。您可以通过将事件作用域限定Intent在您的包中并包含特定的组件 ID 来限制哪些组件响应。

// Refreshing all Widgets of a specific AppWidgetProvider
public static void RefreshWidget(Context context)
{var appWidgetManager = AppWidgetManager.GetInstance(context);var componentName = new ComponentName(context, Java.Lang.Class.FromType(typeof(MyWidgetProvider)));var appWidgetIds = appWidgetManager?.GetAppWidgetIds(componentName);var intent = new Intent(AppWidgetManager.ActionAppwidgetUpdate);intent.SetPackage(context.PackageName);intent.PutExtra(AppWidgetManager.ExtraAppwidgetIds, appWidgetIds);context.SendBroadcast(intent);
}
// Handling incoming Intent on an AppWidgetProvider
[BroadcastReceiver(Label = "My Widget", Exported = true)]
[IntentFilter(new[]{ AppWidgetManager.ActionAppwidgetUpdate})]
[MetaData(AppWidgetManager.MetaDataAppwidgetProvider, Resource = "@xml/mywidget_provider_info")]
public class MyWidgetProvider : AppWidgetProvider
{public override void OnUpdate(Context? context, AppWidgetManager? appWidgetManager, int[]? appWidgetIds){if (context == null || appWidgetManager == null || appWidgetIds == null){return;}foreach (var appWidgetId in appWidgetIds){var views = BuildRemoteViews(context, appWidgetId);appWidgetManager.UpdateAppWidget(appWidgetId, views);}}public override void OnReceive(Context? context, Intent? intent){base.OnReceive(context, intent);}
}

按计划刷新小部件

Android 提供了多种安排小部件更新的方法,这些方法可以结合使用:

  1. updatePeriodMillis设置:在appwidget-providerXML 配置文件中定义。最简单的选项是使用固定的更新间隔,最短 30 分钟。
  2. AlarmManager能够重复广播警报Intent,最小间隔约为 60 秒;可能受到限制。设备重启后警报不会恢复。
  3. WorkManager可靠性高,设备重启后仍能正常工作,支持多种配置选项。最小重复间隔为15分钟。

使小部件具有交互性

小部件允许用户执行一些简单的操作,例如按下按钮。这些操作通过Intent事件触发,并且必须用 <div> 标签包裹,PendingIntent以便在小部件及其视图创建很久之后仍然可以执行。PendingIntent事件是可重用的;请保持其requestCode唯一性,以避免覆盖它们。

// Attach Intent to the increment button
var incrementIntent = new Intent(context, typeof(MyWidgetProvider));
incrementIntent.SetAction("com.enbyin.WidgetExample.INCREMENT_COUNTER");
var incrementPendingIntent = PendingIntent.GetBroadcast(context,101,incrementIntent,PendingIntentFlags.UpdateCurrent | (Build.VERSION.SdkInt >= BuildVersionCodes.S ? PendingIntentFlags.Mutable : 0)
);views.SetOnClickPendingIntent(Resource.Id.widgetIncrementButton, incrementPendingIntent);

通过监听自定义操作来处理按钮交互AppWidgetProvider:

[BroadcastReceiver(Label = "My Widget", Exported = true)]
[IntentFilter(new[] {AppWidgetManager.ActionAppwidgetUpdate,"com.enbyin.WidgetExample.INCREMENT_COUNTER"
})]
[MetaData(AppWidgetManager.MetaDataAppwidgetProvider, Resource = "@xml/mywidget_provider_info")]
public class MyWidgetProvider : AppWidgetProvider
{public override void OnReceive(Context? context, Intent? intent){if (intent == null || context == null){base.OnReceive(context, intent);return;}switch (intent.Action){case "com.enbyin.WidgetExample.INCREMENT_COUNTER":{var currentCount = Preferences.Get(MainPage.SharedStorageAppIncomingDataKey, 0);currentCount++;Preferences.Set(MainPage.SharedStorageAppIncomingDataKey, currentCount);UpdateAllWidgets(context);return;}}base.OnReceive(context, intent);}
}

从组件到应用程序的通信

所有触发器、数据传输以及其他与小部件之间的通信都通过消息流 (S) 处理Intent。诸如 BroadcastReceiver、Service、AppWidgetProvider 和 Activity 之类的组件可以指定Intent它们监听的消息流类型,也可以广播任何消息流Intent供其他组件接收。以下简要概述了数据流,展示了这些组件如何通过Intent消息流进行通信以管理交互式小部件。

android-widget-data-flow

您还可以使用 ActivityIntent来启动您的应用。无需Intent在后台广播 Activity,而是Activity使用 onBroadcast 直接将其发送到 Activity PendingIntent.GetActivity()。与任何 Activity 一样Intent,您可以向 Activity 附加数据。使用 Activity 启动应用时,一种常见的方法是使用深度链接(URL)将结构化数据传递到应用中。然后,您可以在onBroadcast(用于冷启动)或onBroadcast(当 Activity 已运行时)Intent中检索传入的数据。OnCreate()OnNewIntent()

// Example of making a PendingIntent using Deep Link / URL
var openAppIntent = new Intent(Intent.ActionView);
openAppIntent.SetData(global::Android.Net.Uri.Parse($"{App.UrlScheme}://{App.UrlHost}?counter={currentCount}"));
openAppIntent.SetFlags(ActivityFlags.NewTask | ActivityFlags.ClearTop);
var openAppPendingIntent = PendingIntent.GetActivity(context,103,openAppIntent,PendingIntentFlags.UpdateCurrent | (Build.VERSION.SdkInt >= BuildVersionCodes.S ? PendingIntentFlags.Immutable : 0)
);views.SetOnClickPendingIntent(Resource.Id.widgetText, openAppPendingIntent);
[Activity]
[IntentFilter(new[] { Intent.ActionView },Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },DataScheme = App.UrlScheme,DataHost = App.UrlHost)]
public class MainActivity : MauiAppCompatActivity
{protected override void OnCreate(Bundle? savedInstanceState){base.OnCreate(savedInstanceState);HandleIntent(Intent);}protected override void OnNewIntent(Intent? intent){base.OnNewIntent(intent);HandleIntent(intent);}private static void HandleIntent(Intent? intent){if (intent?.Data != null){var url = intent.Data.ToString();// handle the URL as needed}}
}

与 iOS 小部件相比,Android 小部件可以直接访问 C# 组件。IServiceProvider由于MauiProgram.CreateMauiApp()其调用方式与在应用程序中完全相同,因此也同样可用。请将小部件视为独立于应用程序之外的组件;将业务逻辑放在小部件之外AppWidgetProvider。如果您希望小部件Intent首先通过AppWidgetProviderC# 组件进行 UI 更新,请Intent从 C# 组件触发一个新的 C# 事件,并将其广播到应用程序以执行业务逻辑。

public override void OnReceive(Context? context, Intent? intent)
{if (intent == null || context == null){base.OnReceive(context, intent);return;}switch (intent.Action){case "com.enbyin.WidgetExample.INCREMENT_COUNTER":{var currentCount = Preferences.Get(MainPage.SharedStorageAppIncomingDataKey, 0);currentCount++;// Send silent trigger to app for background workvar silentIntent = new Intent(context, typeof(WidgetSilentReceiver));silentIntent.SetAction(WidgetToAppSilentIntentAction);silentIntent.PutExtra(WidgetToAppSilentExtraValueField, currentCount);silentIntent.SetPackage(context.PackageName);context.SendBroadcast(silentIntent);UpdateAllWidgets(context);return;}}base.OnReceive(context, intent);
}

当您需要响应事件执行短暂操作时Intent,标准接收器BroadcastReceiver非常适用。接收器只能短暂运行,之后会被 Android 系统自动停止。如果您需要更多时间,请使用后台接收器Service。

[BroadcastReceiver(Exported = true)]
[IntentFilter([ MyWidgetProvider.WidgetToAppSilentIntentAction ])]
public class WidgetSilentReceiver : BroadcastReceiver
{public override void OnReceive(Context? context, Intent? intent){if (context == null || intent == null || intent.Action != MyWidgetProvider.WidgetToAppSilentIntentAction){return;}var counterValue = intent.GetIntExtra(MyWidgetProvider.WidgetToAppSilentExtraValueField, int.MinValue);if (counterValue != int.MinValue){Preferences.Set(MainPage.SharedStorageAppIncomingDataKey, counterValue);}MainPage.RefreshWidget(); }
}

创建可配置的小部件

在小部件配置文件中,可以指定一个Activity作为小部件的用户配置屏幕。这样的小部件配置活动应该是一个小型活动,能够立即保存配置更改。关闭此屏幕后,小部件将自动更新一次。

<!-- Resources/xml/mywidget_provider_info.xml -->
<?xml version="1.0" encoding="utf-8" ?>
<appwidget-providerxmlns:android="http://schemas.android.com/apk/res/android"android:widgetFeatures="reconfigurable"android:configure="widgetexample.WidgetConfigurationActivity"><!-- other widget settings -->
</appwidget-provider>

该字段android:configure必须引用其Activity自身Name指定的值Activity。请注意该Name值。

[Activity(Label = "Configure Widget",Exported = true,Name = "widgetexample.WidgetConfigurationActivity",Theme = "@android:style/Theme.Material.Light.Dialog",ConfigurationChanges = ConfigChanges.UiMode)]
[IntentFilter([AppWidgetManager.ActionAppwidgetConfigure])]
public class WidgetConfigurationActivity : Activity
{protected override void OnCreate(Bundle? savedInstanceState){base.OnCreate(savedInstanceState);SetResult(Result.Canceled);var extras = Intent?.Extras;if (extras != null){_appWidgetId = extras.GetInt(AppWidgetManager.ExtraAppwidgetId, AppWidgetManager.InvalidAppwidgetId);}// Build the configuration View// The views used are NOT special widget views, so you can use event handlers// to store configuration changesSetContentView(layout);}
}

配置活动是一个标准的 Android 活动Activity,这意味着它使用常规的 Android 视图而不是RemoteView。您可以使用完整的 .NET MAUI 框架构建这些屏幕,但为了这篇博客,我使用了基本的 XML 布局和一个标准的非 .NET MAUI Activity。

利用上下文

关键在于使用正确的 Android 上下文Context。虽然总是使用容易访问的上下文很诱人Android.App.Application.Context,但在使用小部件时,这个上下文在很多情况下可能为空。当小部件触发后台服务时,这一点尤为重要,因为使用错误的上下文可能会导致服务静默崩溃。请使用 Android 提供给后台服务的上下文;如果该上下文不可用,至少要检查是否Platform.CurrentActivity可访问。

性能考量

每个 Android 应用只能有一个Application实例。Widgets(AppWidgetProviders)和 BroadcastReceiver 会自动在应用所在的同一应用程序内运行。使用它们会加载整个 .NET MAUI 堆栈,包括对 AppWidgetProviders 的调用MauiProgram.CreateMauiApp()。这可能会导致几秒钟的初始延迟,例如,当首次按下小部件按钮时。

您可以通过避免不必要的 UI 相关工作来减少这种延迟。一个简单的方法是创建一个最小化的 .NET MAUI 应用程序版本,该版本仅初始化小部件所需的基本要素。例如:

public static class MauiProgram
{public static MauiApp CreateMauiApp(){var builder = MauiApp.CreateBuilder();builder.UseMauiApp<App>().ConfigureFonts(fonts =>{fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");});#if DEBUGbuilder.Logging.AddDebug();
#endifreturn builder.Build();}public static MauiApp CreateMinimalMauiApp(){var builder = MauiApp.CreateBuilder();builder.UseMauiApp<App>();return builder.Build();}
}

为了充分利用最小化设置的优势,需要检测应用程序是作为小部件启动还是作为完整应用程序启动。在 Android 系统中,MauiApplication这可以通过检查ProcessInfo当前正在运行的应用程序进程来实现。

[Application]
public class MainApplication : MauiApplication
{public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { }protected override MauiApp CreateMauiApp(){bool isBackgroundOnly = IsBackgroundExecution();return isBackgroundOnly ? MauiProgram.CreateMinimalMauiApp() : MauiProgram.CreateMauiApp();}private bool IsBackgroundExecution(){try{var activityManager = (ActivityManager?)GetSystemService(ActivityService);if (activityManager == null){return false;}var runningAppProcesses = activityManager.RunningAppProcesses;if (runningAppProcesses == null){return false;}foreach (var processInfo in runningAppProcesses){if (processInfo.Pid == Process.MyPid()){bool isBackground = (int)processInfo.Importance > (int)Importance.Visible;return isBackground;}}}catch{// ignore errors and assume foreground}return false;}
}

在 iOS 上,您可以采用类似的方法,AppDelegate检查应用程序是否因静默推送通知而启动,如果是,则初始化一个最小的 .NET MAUI 应用程序。

笔记

Process = ":widget_process"在 Android 上,您可以使用属性在单独的进程中运行小部件BroadcastReceiver,但这会绕过 .NET MAUI 框架,从而阻止访问共享首选项和其他基本内容。

最后想说的话

与 iOS 小部件不同,Android 小部件提供了更多选项,并允许直接访问您的 C# 代码。因此,请谨慎对待,并实现一个能够在各种设备上可靠运行的解决方案,尤其是在跨平台场景下。一些 Android 设备制造商会限制或更改小部件的使用体验;本文主要讨论标准的 Android 小部件。

最后,还有几点建议:

  • 在新版 Android 设备和至少一台运行最低支持版本的设备上测试您的组件。
  • 始终使用 AndroidContext提供的AppWidgetProvider上下文BroadcastReceiver;服务应使用自己的上下文。
  • 避免使用过于复杂的 UI 结构来构建控件;优先选择简单的布局而不是基于适配器的视图,以防止闪烁。

原文链接

How to Build Android Widgets with .NET MAUI

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com)

http://www.jsqmd.com/news/482633/

相关文章:

  • 手把手教你用嘎嘎降AI处理毕业论文(附操作截图)
  • 当机器人学会“共情”:具身智能情感计算全解析
  • 某电车企业降薪四成,代表着电车行业的冬天真的来了
  • Qwen-Turbo-BF16部署教程:Kubernetes集群中Qwen-Turbo-BF16服务编排实践
  • 电车内幕,速成车,按着国标下限375公斤造车,车重高达2.6吨!还不如日本车飞度!
  • 告别爆显存!FLUX.1-dev优化版实测,24G显卡稳定运行,效果惊艳
  • Flux Sea Studio 海景摄影生成工具:操作系统选择与性能调优全攻略
  • MGeo中文地址解析在零售会员体系中的应用:地址清洗与分级管理实战
  • DeepSeek-OCR开源大模型教程:如何训练自己的Grounding定位微调模型
  • 具身智能:突破极限,重塑物理世界的“思想”与“身体”
  • Electron 应用打包实战:从 electron-builder.yml 配置到多平台部署
  • 分段处理vs整篇提交:降AI的正确打开方式
  • 代码实战:使用JavaScript前端调用Qwen-Image-Edit-F2P生成API
  • 详细步骤:Ubuntu服务器部署丹青幻境,支持多种画风生成
  • 机器人不再“饿肚子”:具身智能自主充电技术全解析
  • 基于CTC语音唤醒的零售业语音导购系统实战
  • 降AI后还要人工润色吗?最佳后处理流程详解
  • 为什么手动改论文降不了AI率?技术原理告诉你答案
  • Phi-3-vision-128k-instructGPU利用率优化:vLLM动态批处理提升吞吐300%
  • 基于JavaScript的StructBERT模型前端交互:构建实时文本相似度比对Demo
  • Phi-3-vision-128k-instruct作品集:128K上下文实现学术论文图表示意深度解析
  • 通义千问1.5-1.8B-Chat-GPTQ-Int4 WebUI开发扩展:集成Dify打造可视化AI工作流
  • iic/ofa_image-caption_coco_distilled_en效果展示:生成caption与COCO人工标注的语义相似度对比
  • 不踩雷!全行业通用的AI论文平台 —— 千笔ai写作
  • 2026年3月合肥异味治理公司实力盘点与选择建议 - 2026年企业推荐榜
  • ESP32联网电子时钟设计:RTC+NTP+MAX7219完整实现
  • Phi-3-vision-128k-instruct行业应用:保险理赔图片自动定损描述生成系统
  • 基于Cosmos-Reason1-7B的智能代码重构工具开发
  • 2026年侵权纠纷律师团队实力盘点与选型指南 - 2026年企业推荐榜
  • 对比一圈后,AI论文平台 千笔ai写作 VS Checkjie,继续教育首选