SeaHawks Wearable: 第 12 人支持
适用于 Android 可穿戴设备的 Seahawk-12 手表。
引言
这是我连续第二篇关于 Android 可穿戴设备应用学习的文章。这次,我将为我最喜欢的球队西雅图海鹰队(NFL 球队)制作一个 Android Wear 模拟手表。我是我球队的忠实支持者和第12人。我们在2014年获胜,2015年获得亚军。我想,如果我做一个非常简单的模拟手表来纪念我的球队,当然也为了像我一样的所有第12人,那将是非常有趣的。:D
背景
再次,我使用了 Xamarin 平台,正如我之前提到的。它比使用 Android Studio 或 Eclipse 进行传统的 Android 开发有许多优势。作为一名 .NET 开发者,可以说我对 C# 相当着迷。目前有很多编程语法只有 C# 才能提供,而 Xamarin 平台的优点在于,你可以在移动开发中使用所有这些语法。如果您想了解更多关于 Xamarin 的信息,请点击这里。
为什么选择 Xamarin?
这可能是一个很好的问题,为什么选择 Xamarin,如果我们已经有了 Android Studio 或 Eclipse?我将根据我的个人经验给您一个答案。首先,我非常了解 Eclipse,在我开始 Android 开发时,它对我来说非常熟悉,但后来我主要在 Visual Studio 和 C# 上花费时间。这是我的主要语言,可以说我对 C# 语言非常着迷。
由于我对 C# 已经很熟悉,起初我只能开发 Windows 移动应用程序,但直到我开始为最流行的移动平台 Android 和 iOS 制作原生应用程序,我才算得上一个完整的移动开发者。对于 Android,由于 Java 的存在,开发起来很容易,但 Objective-C 对我来说从头开始学习真的非常头疼。
因此,所有的困境都由 Xamarin - Mono 框架解决了。
您不仅可以用 C# 制作原生应用程序,还可以为所有三种设备重用大量组件,这真的很棒。有些人可能会考虑 Mono 的性能问题,但我到目前为止还没有遇到过这样的问题。最近,我使用了 Xamarin Forms,它在未来确实具有巨大的潜力。总之,我将 Xamarin 的优缺点总结如下:
优点
- 实时代码更新和应用程序的当天渲染
- C# - 伟大的语言
- Xamarin Forms – 我喜欢 Xamarin 的这项功能,因为您可以为所有三个主要的移动平台创建跨平台 UI。开发与 WPF 类似。它为制作同一应用程序的多个平台节省了很多时间。
- 内置的高级 C# 语法支持
- 跨平台 – 您可以在可移植项目中使用业务逻辑的核心,它将共享到所有三个平台 - iOS、Android 和 Windows 移动版。
缺点 - 对我来说,唯一的缺点是它不是免费的。
表盘开发 -
让我们开始编写代码。打开 Visual Studio,在项目浏览器中创建一个可穿戴设备表盘项目,如下图所示:让我们一步步开始表盘开发。
注意 - 正如我在上一篇文章中已经提到的,Xamarin 中的 Android 开发与 Android Studio 或 Eclipse 中的原生 Android 开发完全相同,除了伟大的 C# 语法与 Java 的对比:)
- 您的项目结构看起来像这样
- 首先,您必须添加一个 XML 文件夹和一个名为
watch_face
的 XML 文件(对于表盘,我们必须添加此 XML 并在 AndroidManifest.xml 中配置)。我目前还不清楚它的具体用途。也许这是 Android 定义的标准。<?xml version="1.0" encoding="UTF-8"?> <wallpaper xmlns:android="http://schemas.android.com/apk/res/android" />
- 现在您必须为圆形和方形手表添加表盘预览图像。我附上了一张预览图,以防您是第12人,并且以不同的方式支持。
Preview_analaog
图像用于 Android manifest 文件。当用户想要在 Wear 手表上设置表盘时,此图像将在设置选项中显示预览。您的表盘至少需要一张预览图,这一点很重要。 - 现在是配置 Android manifest 以支持手表服务的时候了,在 Xamarin 中,这非常简单,您可以在主类的属性中定义它。
[Service(Label = "Xamarin Analog Watchface", Permission = "android.permission.BIND_WALLPAPER")] [MetaData("android.service.wallpaper", Resource = "@xml/watch_face")] [MetaData("com.google.android.wearable.watchface.preview", Resource = "@drawable/preview_analog")] [IntentFilter(new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" })] public class NFLWatch : CanvasWatchFaceService {
然而,在 Android Studio 或 Eclipse 中,您必须修改 AndroidManifest.xml 并为手表服务添加 service 元素的配置。让我简要描述一下 manifest 中的这些设置。在 Xamarin 中,您可以在 MainActivity 中定义 manifest 属性,这很容易做到。
正如您所见,
Service
标签用于为您的表盘命名。您可以在此处配置您的项目表盘名称。接下来,手表服务需要一些权限,例如它需要 android.permission.BIND_WALLPAPER。
IntentFilter
让系统知道您的服务是为了显示手表。在步骤2中,Preview image
用于在 manifest 文件中定义,这是一个非常重要的定义属性。 - 现在创建一个手表服务,
WatchFaceService
是 Android 手表应用的基础类,可以对其进行扩展以创建自定义表盘。因此,基本上,有两种方法可以扩展手表服务。
CanvasWatchFaceService
:您可以使用 Canvas 创建自定义视图。这类似于 Android 应用的 Canvas 应用开发。您可以创建多个形状和画笔等。Gles2WatchFaceService
:如果您是 OpenGL 专家,并且更倾向于创建 2D 或 3D UI,那么您可以使用此服务来创建手表。
为了简单起见,我们将只关注
CanvasWatchService
。如果您想了解更多关于手表服务的信息,可以点击这里。回到项目。
首先,删除 MainActivity 中的所有代码,并将其重命名为
NFLWatch
,它继承自CanvasWatchFaceService
。[Service(Label = "Xamarin Analog Watchface", Permission = "android.permission.BIND_WALLPAPER")] [MetaData("android.service.wallpaper", Resource = "@xml/watch_face")] [MetaData("com.google.android.wearable.watchface.preview", Resource = "@drawable/preview_analog")] [IntentFilter(new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" })] public class NFLWatch : CanvasWatchFaceService { const string Tag = "AnalogNFLWatchFaceService"; /** * Update rate in milliseconds for interactive mode. We update once a second to advance the * second hand. */ static long InteractiveUpdateRateMs = TimeUnit.Seconds.ToMillis(1); public override Android.Service.Wallpaper.WallpaperService.Engine OnCreateEngine() { return new NFLWatchEngine(this); }
在上面的代码中,我像下面这样使用了一个常量 Tag 来在 Xamarin IDE 中进行日志记录
Log.Debug (Tag, "update time");
这里,我特别提到了这个常量,因为如果我真的想使用超过23个字符的长字符串,我会遇到运行时错误。我必须弄清楚这个限制,但我必须说,为了弄清楚 Xamarin 中的这个问题,我花了大量时间。所以,为了避免这个问题,请保持您的字符串简短。
- 现在您必须添加另一个子类,它继承自
CanvasWatchFaceService.Engine
。这是实际实现表盘的类,它在Canvas
上进行绘制。您必须实现onCreateEngine()
来返回您的具体Engine
实现。private class NFLWatchEngine:CanvasWatchFaceService.Engine { const int MsgUpdateTime = 0; // base class object CanvasWatchFaceService NFLWatchService; //Initialize Analogue niddles Paint hourPaint; Paint minutePaint; Paint secondPaint; Paint tickPaint; bool mute; Time time; //used for get currenttime Timer timerSeconds; TimeZoneReceiver timeZoneReceiver; bool registeredTimezoneReceiver = false; // Whether the display supports fewer bits for each color in ambient mode. // When true, we disable anti-aliasing in ambient mode. bool lowBitAmbient; Bitmap backgroundBitmap; Bitmap backgroundScaledBitmap; public NFLWatchEngine(CanvasWatchFaceService NFLWatchService) : base(NFLWatchService) { this.NFLWatchService = NFLWatchService; }
在上面的代码中,
Timer
对象会跟踪手表引擎启动时的当前时间。lowBitAmbient
值支持在 Ambient 模式下的颜色,如果为 true,我们将显示抗锯齿。 - 接下来初始化您的手表引擎,如下所示:
public override void OnCreate (ISurfaceHolder surfaceHolder) { this.SetWatchFaceStyle (new WatchFaceStyle.Builder (this.NFLWatchService) .SetCardPeekMode (WatchFaceStyle.PeekModeShort) .SetBackgroundVisibility (WatchFaceStyle.BackgroundVisibilityInterruptive) .SetShowSystemUiTime (false) .Build () );
您必须设置
watchfacestyle
,以便系统在表盘处于活动状态时如何交互。此外,您还可以定义通知的 peek 模式,以防您的表盘在后台处于活动状态。NFLWatchService
用于定义您的表盘背景,我们在上面的步骤中已经讨论过。 - 更重要的是,您必须重写
draw
方法,您可以在其中在canvas
中创建任何类型的watch
布局。如果您之前在 Android 应用中绘制过形状,那么这会很容易理解。//Actual Draw of analogue niddle in Canvas which will show in watch surface public override void OnDraw (Canvas canvas, Rect bounds) { time.SetToNow (); int width = bounds.Width (); int height = bounds.Height (); // Draw the background, scaled to fit. if (backgroundScaledBitmap == null || backgroundScaledBitmap.Width != width || backgroundScaledBitmap.Height != height) { backgroundScaledBitmap = Bitmap.CreateScaledBitmap (backgroundBitmap, width, height, true /* filter */); } canvas.DrawColor (Color.Black); canvas.DrawBitmap (backgroundScaledBitmap, 0, 0, null); float centerX = width / 2.0f; float centerY = height / 2.0f; // Draw the ticks. float innerTickRadius = centerX - 10; float outerTickRadius = centerX; for (int tickIndex = 0; tickIndex < 12; tickIndex++) { float tickRot = (float)(tickIndex * Math.PI * 2 / 12); float innerX = (float)Math.Sin (tickRot) * innerTickRadius; float innerY = (float)-Math.Cos (tickRot) * innerTickRadius; float outerX = (float)Math.Sin (tickRot) * outerTickRadius; float outerY = (float)-Math.Cos (tickRot) * outerTickRadius; canvas.DrawLine (centerX + innerX, centerY + innerY, centerX + outerX, centerY + outerY, tickPaint); } float secRot = time.Second / 30f * (float)Math.PI; int minutes = time.Minute; float minRot = minutes / 30f * (float)Math.PI; float hrRot = ((time.Hour + (minutes / 60f)) / 6f) * (float)Math.PI; float secLength = centerX - 20; float minLength = centerX - 40; float hrLength = centerX - 80; if (!IsInAmbientMode) { float secX = (float)Math.Sin (secRot) * secLength; float secY = (float)-Math.Cos (secRot) * secLength; canvas.DrawLine (centerX, centerY, centerX + secX, centerY + secY, secondPaint); } float minX = (float)Math.Sin (minRot) * minLength; float minY = (float)-Math.Cos (minRot) * minLength; canvas.DrawLine (centerX, centerY, centerX + minX, centerY + minY, minutePaint); float hrX = (float)Math.Sin (hrRot) * hrLength; float hrY = (float)-Math.Cos (hrRot) * hrLength; canvas.DrawLine (centerX, centerY, centerX + hrX, centerY + hrY, hourPaint); }
在上面的代码中,我使用了标准的模拟图形实现。您可以根据自己的需求进行自定义。您可以在 canvas 上绘制任何内容,从简单的 UI 到高级的。
- 此步骤是配置设备状态的一个非常重要的步骤。这意味着当用户切换表盘时,手表将如何表现。您必须在
engine
类中重写一些方法来处理表盘。public override void OnVisibilityChanged (bool visible) { base.OnVisibilityChanged (visible); if (Log.IsLoggable (Tag, LogPriority.Debug)) { Log.Debug (Tag, "OnVisibilityChanged: " + visible); } if (visible) { RegisterTimezoneReceiver (); time.Clear (Java.Util.TimeZone.Default.ID); time.SetToNow (); } else { UnregisterTimezoneReceiver (); } UpdateTimer (); }
正如方法中清楚显示的那样,当表盘可见时,它会调用
RegisterTimezoneReceiver
方法,该方法基本上用于检查broadcastReceiver
是否已注册。在 Android manifest 文件中,IntentFilter
用于获取手表服务的时区,例如:(intent.GetStringExtra ("time-zone")
如果表盘不可见,此方法将检查
BroadcastReceiver
是否可以取消注册。一旦BroadcastReceiver
已处理,就会调用updateTimer
来触发表盘的无效化并重新绘制表盘。updateTimer
会停止任何待处理的Handler
操作,并检查是否应发送另一个。private void UpdateTimer () { if (Log.IsLoggable (Tag, LogPriority.Debug)) { Log.Debug (Tag, "update time"); } if (timerSeconds == null) { timerSeconds = new Timer ((state) => { Invalidate (); }, null, TimeSpan.FromMilliseconds (InteractiveUpdateRateMs), TimeSpan.FromMilliseconds (InteractiveUpdateRateMs)); } else { if (ShouldTimerBeRunning ()) { timerSeconds.Change (0, InteractiveUpdateRateMs); } else { timerSeconds.Change (Timeout.Infinite, 0); } } }
- 我还添加了环境模式,以防
watch
处于非活动状态。它将简单地禁用所有动画以节省电池寿命,一旦唤醒,它将从当前时间重新开始。public override void OnAmbientModeChanged (bool inAmbientMode) { base.OnAmbientModeChanged (inAmbientMode); if (Log.IsLoggable (Tag, LogPriority.Debug)) { Log.Debug (Tag, "OnAmbientMode"); } if (lowBitAmbient) { bool antiAlias = !inAmbientMode; hourPaint.AntiAlias = antiAlias; minutePaint.AntiAlias = antiAlias; secondPaint.AntiAlias = antiAlias; tickPaint.AntiAlias = antiAlias; } Invalidate (); UpdateTimer (); }
- 在完成本文之前,提供有关广播接收器的信息,该接收器处理用户可能正在旅行并更改时区的情况。此接收器将简单地清除保存的时区并重置显示时间。
public class TimeZoneReceiver: BroadcastReceiver { public Action<Intent> Receive { get; set; } public override void OnReceive (Context context, Intent intent) { if (Receive != null) { Receive (intent); } } }
输出
如果您在模拟器中运行附带的代码,它应该会像这样运行:
关注点
- 尽管这是一个非常基础的创建手表服务应用程序,但通过为您的球队制作一个手表来支持他们是一件很有趣的事情。您可以制作自己的球队支持手表。我已指明您需要更改 UI 的位置。
- 为了简单起见,我没有提供任何设置,但如果用户可以选择多个体育团队进行支持,这将是一个很棒的功能。我将撰写一篇关于可穿戴设备配置设置的文章,这将是另一个有趣和学习的过程。
- 您也可以在实现
draw
方法的区域自定义背景颜色。这些 UI 更改的自定义完全取决于您。 - 诸如位图缩放和加载、动画、抗锯齿以及持续信息更新等昂贵操作必须非常小心。如果它们可以提前准备好,尽量只执行一次或在非常需要时执行。当某些东西在非交互模式下是不必要时,请确保将其停止或置于休眠模式。Google 的 Android Wear 开发者页面有一些建议,您应该仔细研究并作为指南。
- Go Hawks。请喜欢并评价此应用程序,评论和建议随时欢迎。
结论
表盘是最受欢迎也是最重要的用户功能。当然,对于智能手表来说,仅显示当前时间只是非常基本的功能。表盘上还可以显示各种信息或数据。在下一个教程中,我们可以学习如何以交互方式为用户提供一些配置选项,以便他们可以根据自己的喜好设置手表上要显示的内容。作为开发人员,我们还需要了解如何以相当有效的方式检索数据,以便整体性能和电池寿命仅受到最小影响。
历史
- 版本 1.0