如何让你的Android项目更方便地移植到Windows/MacOS(反之亦然)
使用 ThinkAlike(一个正在开发的以 Android 和 JavaFX 为视图层的 Java MVVM 框架)来实现跨平台的炉石传说游戏卡牌参考应用等。


目录
引言
本文讨论了使用 Java 和 MVVM 模式实现 Android-桌面跨平台应用的可行性。
我想首先介绍一下**_ThinkAlike_**代码库,我在其中实现了我的跨平台策略。
- 这是一个正在开发的 Java MVVM 框架(采用 _Android_ 和 _JavaFX_(或 _SWT_、_Swing_)作为视图层),旨在简化跨平台开发。
- 特别是,如果你想充分利用 Android 原生 SDK 同时保持跨平台可能性,这种 Java 解决方案是可行的。
- 它_不是__Mono/Xamarin_——.NET 解决方案很好。但在某些情况下,Android 的原生语言 Java 解决方案会更受期待。
- 它_不是__PhoneGap/Cordova_——混合解决方案在一定程度上仍然存在 UI/UX 方面的缺点。
- 它_还不是_一个生产力框架(正在开发中)。可以添加许多有用的功能,例如组件级别的属性绑定(是否可编辑、可关闭、验证规则、热键等)。然而,它已经通过一个小型付费项目——一个跨平台 epub 阅读器(适用于 Android 和 Windows)——验证了其可行性和可扩展性。
为了给 MVVM 框架增添一些趣味,引入了一些游戏元素(图片)。
**_炉石传说_**,暴雪娱乐最新发布的一款免费策略卡牌游戏,将作为我的主题。就像该游戏旨在跨平台市场一样,我们的目标是为 Android 和桌面平台提供一个_卡牌参考库_(截图如上所示)。
我将通过逐步实现“_显示选中节点(卡牌)的详细信息_”来阐述跨平台事务流。**有 MVVM 经验的读者可以直接跳到核心内容。**
本文在_背景_部分介绍了相关概念(MVVM、Android、JavaFX)的必要介绍。关于如何设置开发环境,请参阅_环境_部分。特别是,对于怀疑 JavaFX 应用程序是否可以分发到未安装 JRE 的平台的读者,请参阅_分发_部分。
背景
参考和实践的跨平台 MVVM 解决方案
在设计阶段,我参考了 iOS webkit、ZK、Xamarin、Android-Binding、PhoneGap 和 JavaFX。在此之前,我已经在 WPF(MVVM 的“母亲”,而 MVP 在谱系上是“父亲”)和 Android UI 生态系统方面有了项目经验。这个跨平台代码库最初是为了满足真实客户需求而开发的——不是大规模的,而是快速演进的 Android-桌面项目。
所需知识:MVVM、Android、JavaFX
MVVM
Android 和 JavaFX
- 概述:Android 开发者培训:入门
- API:Android API
- 示例:API 演示(随 Android SDK 发布)
- 意图:意图和意图过滤器
- 视图:活动
- Intent-View 映射:应用清单
- GUI::布局(Java+XML 标记):布局
- GUI::布局::片段:使用片段构建动态 UI
- 概述:JavaFX 文档
- API:JavaFX 2 的 JavaDoc
- 示例:JavaFX Ensemble(演示选项卡 + 源代码选项卡)
- GUI::布局(Java):在 JavaFX 中使用布局
- GUI::布局(标记):FXML 介绍
- GUI::样式:JavaFX CSS 参考指南
"M-V-VM" 源于 MVP 模式 (Martin Fowler),是微软为实现 WPF/Silverlight 而对其进行的专门化。WPF 的 MVVM 愿景被推荐并在上面的第一张图中展示。_除了 WPF,还有其他 MVVM 框架:Java 的 ZK 框架,以及 HTML5 的 AngularJS 和 KnockoutJS。事实上,不同层职责的划分是一个持续讨论和探索的主题。_
你可能会注意到“ViewModel”下方有红色的波浪线(好吧,我承认那是因为我忘了关闭拼写检查~)。这就是“VM”代表的含义。ViewModel 层将“逻辑”部分(面向用例、平台无关)从 View 层中提取出来,与“非逻辑”UI/UX 代码(大小、颜色等)分开。UI/UX 开发人员可以实现不同的 View 模块来与一个 ViewModel 进行通信,甚至可以使用不同的 GUI 技术(WPF、WinForm 等)。
上图第二张描绘了 ThinkAlike 的 MVVM 实现。与 WPF 相比,ThinkAlike 使用 IProperty/ICommand 事件处理接口作为 View 层和 ViewModel 层之间的绑定器。用户命令(而非中间 UI 操作)将被解释为 ICommand 事件,这些事件将从 View 分派并由 ViewModel 处理。同时,UI 更新反馈将被解释为 IProperty(Change) 事件,这些事件将从 ViewModel 触发并由 View 消费。
注意:数据绑定,正如 John Gossman(MVVM 创建者)所说,“有利有弊”。最重要的是,我们的跨平台解决方案还没有“跨平台”的 UI 标记文件,这也削弱了数据绑定的好处。如何平衡 Android 的 XML UI 标记、JavaFX 的 FXML UI 标记以及其他标记语言之间的妥协?Oracle 提倡将 JavaFX 开源用于 iOS 和 Android,也许会带来一些突破。
图中右侧的“组件模型”指代布局/控件(图像/文本/组合框等)。如果获得更多资源和鼓励,并且能够完成一个完整的平台无关组件库,将使开发人员能够从通用(平台无关)模块实例化和控制平台特定的布局/控件实例(正如 Xamarin 在 .NET 中所做的那样)。然而,由于“跨平台”标记文件的困境,无法享受在 UI 标记文件中声明布局/控件的好处。
有关 MVVM 的更多详细信息,
Android 和 JavaFX 仅用于平台特定的视图层实现。那些在这两个领域都有经验的人可能知道它们在许多方面有何不同(例如,如何将图像自动适应其容器等细节)。然而,即使您不熟悉 Android/JavaFX,您仍然可以通读本文并掌握 MVVM 跨平台流程。
Android 主题
JavaFX 主题
环境
以下是我的开发工具清单
- 基础:Eclipse/e(fx)clipse 4.2 Juno + JDK 1.7 我强烈推荐 e(fx)clipse,它为 Eclipse IDE 提供了 JavaFX 工具,并与其他开发保持稳定的兼容性。
- Android SDK + ADT 插件
- JavaFX SDK(包含在 JDK 1.7 中)+ JavaFX Scene Builder
JavaFX 使用 JDK 1.7。Android 项目默认会降级到 JDK 1.6 编译器兼容级别。
包划分
ThinkAlike 由 3 个项目组成,分别以后缀 _generic_(平台无关)、_jfx_(用于 JavaFX)和 _android_(用于 Android)命名。
资产文件,与平台特定的资源文件相对,将在通用包中进行管理。
(对于 Eclipse 用户)应将一些设置应用于平台特定项目
- Android 项目
- 源文件夹:项目属性 > 构建路径 > 链接源...,将 _ThinkAlike_generic\src_ 链接到 _ThinkAlike_android\src_generic_。
- Assets 文件夹:也使用链接的源文件夹。将 _ThinkAlike_generic\assets_ 链接到 _ThinkAlike_android\assets_。
- JavaFX 项目
- 源文件夹:项目属性 > Java 构建路径 > 项目 > 添加...,添加 _ThinkAlike_generic_。如果使用链接文件夹,e(fx)clipse 将会出现一些问题。
- 资产文件夹:在 Eclipse 中找到
com.thinkalike.jfx.assets
包文件夹,新建 > 文件夹 > 高级,创建 _ThinkAlike_generic\assets\ThinkAlike_ 的链接文件夹。构建路径 > 包含,将其包含到构建时的 JAR 中。
功能设计
完整版的_卡牌参考库_可能具有以下功能:
- 节点(卡牌)选择器(基础)
- 节点(卡牌)详情(基础) 在中央区域显示所选卡牌(在卡牌选择器中)的详细信息。
- 节点(卡牌)选择器过滤器(基础,_待办_)
- 卡组构建器(_待办_)
- 基于战力计算的卡牌组合提示(_高级_)
- 卡组之间的自动对抗(_高级_)
第 1-3 项是通用功能,将在 ThinkAlike 项目中实现。卡组构建器将来可以分叉为独立项目。功能 #2,_显示卡牌详情_,将在本文中进行说明。
架构分析与设计
序列图
词汇表
为避免歧义,以下术语的用法将予以澄清
- 平台特定:也称为“平台依赖”。指与特定技术平台(如 Android、JavaFX)相关的特性/模型。
- 平台无关:与平台特定相对。
- 通用:指平台无关。平台无关的代码/资产组装在一个_通用_项目中。
- 视图:指_窗口_级别的视图。Android 使用术语_Activity_,而 JavaFX 将其命名为_Scene_。
- 组件:指布局或控件,例如 Android 中的 _LinearLayout/TextView/ImageView_ 或 JavaFX 中的 _HBox/VBox/Label/ImageView_。
- 节点:为了避免在框架中使用领域特定术语(例如 Card),将使用通用术语_Node_。
实现(功能#2)
现在,让我们逐步完成实现功能#2——_显示节点(卡牌)详细信息_所需的步骤。
序列图和术语定义可在上一节中找到。
[JavaFX]
我倾向于先构建 JavaFX 项目,以利用其调试的快速性。当您需要确认 Android 功能时,您可以根据需要切换优先级。
0. 修正序列图中所示的类/实例,尤其是平台特定的类/实例
- L0: 平台特定“自适应”应用程序类:
com.thinkalike.jfx.ThinkAlikeApp
应用程序的入口。它继承了原生 Application 类( - L1: 平台特定原生视图类:例如
javafx.scene.layout.AnchorPane
主窗口的底层原生类。 - L2: 平台特定“自适应”视图类:例如
com.thinkalike.jfx.view.MainScene
继承 L1 用于原生 UI 功能,并通过 IProperty/ICommand 事件处理接口与 L7(ViewModel 类,如下所述)交互。 - L3: 平台特定原生组件类:例如
javafx.scene.image.ImageView
布局/控件的底层原生类,包括显示节点(卡牌)的图像视图。 - L4: 平台特定“自适应”组件类:例如
com.thinkalike.jfx.control.ImageNodeView
继承 L3 以重用原生功能,同时满足 L5(组件接口,如下所述)的要求,以便根据 ComponentModel 实例(根类是 - L5: **_平台无关_**组件接口:例如
com.thinkalike.generic.viewmodel.control.IImageNodeView
声明“自适应”组件类必须满足的要求,通常是一个 - L6: **_平台无关_**ComponentModel 类:例如
com.thinkalike.generic.viewmodel.control.UIImageNode
包括与 UI 相关的数据/方法。某些 - L7: **_平台无关_**ViewModel 类:例如
com.thinkalike.generic.viewmodel.WorkareaViewModel
通常是单例,由相关的 L2(“自适应”视图类)实例化。它包括 IProperty(包括事件注册/取消注册/提交)的实现,以及 ICommand 的事务处理(检索数据对象、封装视图对象,并将 IProperty 更改事件提交给所有活动的监听器)。 - L8: **_平台无关_**数据类:例如
com.thinkalike.generic.domain.ImageNode
包含与领域相关的数据/方法,通常将其可访问性限制在 ViewModel 层,或者更严格地限制在(领域)模型层。 - L9: **_平台无关_**DAO 类:例如
com.thinkalike.generic.viewmodel.NodeLoader
在持久化数据和数据对象之间进行转换。它的可访问性也限于 ViewModel 层和(域)模型层。
javafx.application
),并实现了平台无关的 Platform
接口(以便向 generic
模块提供平台特定的数据/方法)UINode
)的数据更新底层组件视图。update(UINode)
接口。UINode
可能包含背景数据对象的只读“外观”(基本接口是 INodeRO
)。1. 首先(也是最直观的)一步,是勾勒出用例(功能#2)的非逻辑 UI
- UI 布局:_ \src\com\thinkalike\jfx\res\layout\scene_main.fxml_ 以上是功能#2(名为_“工作区”_)UI 区域的代码片段。它应该放在一个声明整个视图布局(**L2**)的 FXML 文件中。对于那些有 HTML/XAML/Android-XML 标记经验的人来说,FXML 的标记语言很容易学习。要快速开始 JavaFX UI 编程(Java 或标记),您可以参考_所需知识_小节。
- UI 样式:_ \src\com\thinkalike\jfx\res\style_dark.css_(深色样式) JavaFX 大量利用层叠样式表 (CSS)。拥有 W3C CSS 知识的 Web 开发人员可以轻松掌握其 JavaFX 等效项,这些等效项以供应商前缀“-fx-”开头。(是的,对我来说比 Android 的更容易。)
fx:id="inv_nodeContent"
标识显示用户选择的节点(卡牌)详细信息的图像元素。请注意,ImageNodeView
不是 JavaFX 原生类,而是“自适应组件类”(**L4**)。JavaFX 允许在 UI 标记文件中直接使用自定义布局/控制类(如 Android),从而实现了非逻辑 UI/UX 开发人员和逻辑开发人员之间的彻底分工。
2. 将平台特定的*自适应视图类(**L2**)与其对应的_平台无关_ViewModel 类(**L7**)关联起来,以处理 IProperty 更改事件。
- 要处理的 IProperty 事件:工作区中间的节点(卡牌)已更改。
- 平台特定的*自适应视图类实例化:@(发生在)平台特定的*自适应应用程序类(**L0**)中
//com.thinkalike.jfx.ThinkAlikeApp
com.thinkalike.jfx.view.MainScene =
(MainScene) replacePrimarySceneContent(Res.getLayoutUrl("scene_main.fxml"));
MainScene
是宿主自适应视图(JavaFX 中的 Scene
表示 Activity
)。实例化它时需要 FXML 文件的 URL。
com.thinkalike.jfx.res.Res
是一个平台特定的实用类。资源/资产管理代码超出了本文的主题,但您可以查看源代码包以获取详细信息。
//com.thinkalike.jfx.view.MainScene
@FXML
com.thinkalike.jfx.control.ImageNodeView inv_nodeContent;
在 JavaFX 中,视图类以这种方式将它们的组件(字段,在相关的 FXML 中标记)声明为实例变量。然后,当 FXML 被解析时,inv_nodeContent
将自动分配给同名组件(fx:id="inv_nodeContent"
)。
//com.thinkalike.jfx.view.MainScene
_vm_workarea = com.thinkalike.generic.viewmodel.WorkareaViewModel.getInstance();
跨平台 MVVM 项目的真正核心,ViewModel
类,通常由其响应式 View 实例之一实例化为单例,并具有更长的生命周期。
- 事件注册(由平台特定的*自适应视图类(**L2**)调用)
- 事件取消注册:尚未启用。 有关更多实现细节,有一个
- 事件提交
//com.thinkalike.jfx.view.MainScene
_listenToVM_workarea = new PropertyChangeListener(){
@Override
public void onPropertyChanged(PropertyChangeEvent event) {...}
};
_vm_workarea.addPropertyChangeListener(Constant.PropertyName.Node,
_listenToVM_workarea);
_listenToVM_workarea
被实例化来处理一个简单的 onPropertyChanged()
回调接口。然后它订阅相关的 ViewModel(_vm_workarea
),监听特定属性的事件。Constant.PropertyName.Node
标识该属性,并且在 ViewModel 基类中没有重复。对于重量级应用程序,EventBus
模式会很有用。
PropertyChangeListenerAdapter
管理一个 WeakReference
映射,以避免通知过时的事件监听器。您也可以故意调用 removePropertyChangeListener()
来取消事件订阅。//com.thinkalike.generic.viewmodel.WorkareaViewModel
this.firePropertyChange(Constant.PropertyName.Node, oldValue, _uiNode);
当 ViewModel 准备好新的视图对象或 ComponentModel(例如 UIImageNode)时,它会将属性更改事件与该对象作为参数一起提交。
- 事件处理
- UI 更新
UINode.attachView(INodeView)
如果自适应组件实例已存在(如 UINode.createView()
否则,如果没有自适应组件,ComponentModel 可以请求动态实例化相关类型的 Component 类(如
//com.thinkalike.jfx.view.MainScene
@Override
public void onPropertyChanged(PropertyChangeEvent event) {
if (event.getPropertyName().equals(Constant.PropertyName.Node)){
updateWorkarea((UINode)event.getNewValue());
}
}
当收到 Node
已更改的通知时,自适应视图类会将事件参数转换为内部方法参数。
有两种方法可以将通用 ComponentModel 对象提供给代表层
NodeContent
的情况),ComponentModel 可以附加到它。NodeSelector
中的单元格)。Factory
类将根据需要实例化适当的平台特定的* Component 类。在任何一种情况下,自适应组件类(**L4**)的实例都将调用 update(UINode)
来更新自身。最后,底层原生组件类(**L3**)将接受来自 ComponentModel 的转换信息来执行实际渲染。在这方面,ComponentModel(属于通用模块)_应该_被设计成能够适应平台特定视图类的不同需求。例如,在 ImageView 的情况下,Android 和 JavaFX 在相对尺寸测量(JavaFX 没有 onPreDraw() 回调)和异步图像加载(JavaFX 默认支持,而 Android 不支持)方面有不同的过程。因此,开发人员必须研究每个平台的策略以找到合适的解决方案。值得庆幸的是,这样的工作只需要完成一次(对于每种组件和不同的非功能性需求)。对于 JavaFX
//com.thinkalike.jfx.control.ImageNodeView
protected static void update(UINode uiData, ImageView rawView) {
if(uiData instanceof UIImageNode){
//0.initialize according to context.
int width_limit, height_limit;
width_limit = (int)rawView.getFitWidth();
height_limit = (int)rawView.getFitHeight();
//1.set the default image:
rawView.setImage(new Image(Res.getImageUrl("default_image.gif")));
//2.Async load Image:
final String imageUrl = Util.getAbsoluteUrl(((UIImageNode)uiData).getRelativePath());
Image image = Util.decodeThumbFromFile(imageUrl, width_limit, height_limit);
rawView.setImage(image);
}
}
3. 使平台特定的*自适应视图类(**L2**)将用户命令操作转换为对应的_平台无关_ViewModel 类(**L7**)可以理解的 ICommand 事件
- 要激活的 ICommand 事件:NodeSelector 中选定的节点(卡牌)已更改。
- 用户命令操作:NodeSelector 区域(窗口左侧)中选定的节点(卡牌)已更改。
- ICommand 事件激活:@平台特定的*自适应视图类(**L2**)
//com.thinkalike.jfx.view.MainScene
this.lv_nodeList.getSelectionModel().selectedItemProperty().addListener(
new ChangeListener<UINode>() {
public void changed(ObservableValue<? extends UINode> ov,
UINode old_val, UINode new_val) {
if(_vm_nodeSelector!=null){
_vm_nodeSelector.onNodeSelected(new_val);
}
}
});
当用户在 NodeSelector 上进行交互时,上述 ChangeListener
将从原生 ListView 控件(lv_nodeList
)接收通知,然后激活相关 ViewModel(_vm_nodeSelector
: NodeSelectorViewModel
)的 ICommand(onNodeSelected()
)。如上所述,此处可以为 IProperty 和 ICommand 事件都采用 EventBus
模式。但在简化情况下,同步方法调用也同样有效。
//com.thinkalike.generic.viewmodel.NodeSelectorViewModel
public void onNodeSelected(UINode uiNode){
INodeRO oldValue = _nodeSelected_RO;
_nodeSelected_RO = uiNode.getDataRO();
this.firePropertyChange(Constant.PropertyName.Node, oldValue, _nodeSelected_RO);
}
在接受 ICommand 事件后,NodeSelectorViewModel
从 _UINode_ 检索一个“只读数据接口”(INodeRO
)。这是因为 _UINode_ 作为视图对象,有其自己的 UI 上下文,无法简单地被其他 UI 重用(例如,在这种情况下,列表单元格中的节点将比工作区中的节点具有更小的默认大小),因此必须暴露底层数据对象以进行传输。在这种情况下,INodeRO
,或者如果不考虑访问权限则简单地为 Node
,将用于提交 IProperty 更改事件。
//com.thinkalike.generic.viewmodel.WorkareaViewModel
_listenToVM_nodeSelector = new PropertyChangeListener(){
@Override
public void onPropertyChanged(PropertyChangeEvent event) {
thisInstance.setNodeRO((INodeRO)event.getNewValue());
}
};
...
private void setNodeRO(INodeRO nodeRO){
Object uiContext = Loader.getInstance().getPlatform().getUIContext();
if(nodeRO instanceof ImageNode.RO){
ImageNode node = new ImageNode(((ImageNode.RO)nodeRO).getRelativePath());
UIImageNode uiNode = new UIImageNode(uiContext, node, false);
setUINode(uiNode);
}
else if(nodeRO == null){
setUINode(null);
}
}
WorkareaViewModel
监听 NodeSelectorViewModel
,接收真实数据对象的只读接口,创建表示焦点节点(卡牌)的临时数据对象和视图对象,并回收旧对象。
4. 调试、研究原生 API 细节、调整,并达成目标。
至此,JavaFX 项目中的功能#2已完成。这种架构是否能经受住平台迁移的考验,可以在 Android 项目的开发中进行测试。
[Android]
开发步骤在_很大程度上_类似于之前的 JavaFX 项目,这证明了跨平台解决方案的好处。因此,我只需要替换并指定更改的部分。
0. 修正序列图中所示的类/实例
- L0: 平台特定“自适应”应用程序类:
com.thinkalike.android.ThinkAlikeApp
- L1: 平台特定原生视图类:例如
android.support.v4.app.Fragment
Android 中的 - L2: 平台特定“自适应”视图类:例如
com.thinkalike.android.view.WorkareaFragment
- L3: 平台特定原生组件类:例如
android.widget.ImageView
- L4: 平台特定“自适应”组件类:例如
com.thinkalike.android.control.ImageNodeView
- L5: _平台无关_组件接口:例如
com.thinkalike.generic.viewmodel.control.IImageNodeView
- L6: _平台无关_组件类:例如
com.thinkalike.generic.viewmodel.control.UIImageNode
- L7: _平台无关_ViewModel 类:例如
com.thinkalike.generic.viewmodel.WorkareaViewModel
- L8: _平台无关_数据类:例如
com.thinkalike.generic.domain.ImageNode
- L9: _平台无关_DAO 类:例如
com.thinkalike.generic.viewmodel.NodeLoader
Fragment
支持针对不同移动设备的自适应布局。毫无疑问,L5-L9这5个层级在平台迁移中将_零_成本。
1. 实现用例(功能#2)的非逻辑 UI
- UI 布局:\res\layout\workarea.xml
<LinearLayout
android:id="@+id/ll_work_overlay"
android:gravity="center_horizontal"
android:orientation="vertical">
...
<com.thinkalike.android.control.ImageNodeView
android:id="@+id/iv_nodecontent"
... />
...
</LinearLayout>
Android 在 UI 标记文件中使用 XML。android:id="@+id/iv_nodecontent"
标识与节点(卡牌)详细信息对应的图像控件。
ImageNodeView
是一个与 JavaFX 中的并行存在的自适应组件类(**L4**)。
2. 将平台特定的*自适应视图类(**L2**)与其对应的_平台无关_ViewModel 类(**L7**)关联起来,以处理 IProperty 更改事件。
- 要处理的 IProperty 事件:工作区中间的节点(卡牌)已更改。
- 平台特定的*自适应视图类实例化:@(发生在)平台特定的*自适应应用程序类(**L0**)中
//AndroidManifest.xml
<application
android:name=".android.ThinkAlikeApp">
<activity
android:name=".android.view.MainActivity"
android:screenOrientation="landscape" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
Android 开发者知道,Activity 类(与_Intent_关联)和 Application 类的实例化是基于 AndroidManifest.xml
的清单信息,这与 JavaFX 应用程序不同。
//\res\layout\activity_main_twopane.xml
<LinearLayout
android:id="@+id/ll_twopane">
<fragment
android:id="@+id/nodeselector"
android:name="com.thinkalike.android.view.NodeSelectorFragment"
... />
<fragment
android:id="@+id/workarea"
android:name="com.thinkalike.android.view.WorkareaFragment"
... />
</LinearLayout>
然而,功能 #2 中的直接父窗口是 WorkareFragment
,如 UI 标记文件中所设计。
//com.thinkalike.android.view.WorkareaFragment
com.thinkalike.android.control.ImageNodeView _inv_nodecontent =
(ImageNodeView) rootView.findViewById(R.id.iv_nodecontent);
//com.thinkalike.android.view.WorkareaFragment
com.thinkalike.generic.viewmodel.WorkareaViewModel _viewModel =
WorkareaViewModel.getInstance();
- 事件注册(由平台特定的*自适应视图类(**L2**)调用)
- 事件取消注册:未使用。
- 事件提交
//com.thinkalike.android.view.WorkareaFragment
_listenToVM = new PropertyChangeListener(){
@Override
public void onPropertyChanged(PropertyChangeEvent event) {...}
};
_viewModel.addPropertyChangeListener(Constant.PropertyName.Node,
_listenToVM);
//com.thinkalike.generic.viewmodel.WorkareaViewModel
this.firePropertyChange(Constant.PropertyName.Node, oldValue, _uiNode);
- 事件处理
- UI 更新
//com.thinkalike.android.view.WorkareaFragment
@Override
public void onPropertyChanged(PropertyChangeEvent event) {
if (event.getPropertyName().equals(Constant.PropertyName.Node)){
updateWorkarea((UINode)event.getNewValue());
}
}
正如在 JavaFX 实现中讨论的那样,自适应组件类(**L4**,例如 ImageNodeView
)如何转换 ComponentModel(UIImageNode
)并将其提供给原生组件类(**L3**,例如 ImageView
)取决于平台特定的 GUI API 接口。Android 有 OnPreDrawListener.onPreDraw()
,它可以在实际渲染 ImageView 之前用于检索父视图的尺寸。此事务被封装在自适应组件类(ImageNodeView
)中
//com.thinkalike.android.control.ImageNodeView
final ImageNodeView thisInstance = this;
...
@Override
public boolean onPreDraw() {
thisInstance.getViewTreeObserver().removeOnPreDrawListener(this);
int width_image = Util.getActualLayoutWidth(thisInstance);
int height_image = Util.getActualLayoutHeight(thisInstance);
MediaAsyncLoader.asyncLoadImageFile(imagePath, width_image, height_image, _onMediaLoadListener);
return true;
}
3. 使平台特定的*自适应视图类(**L2**)将用户命令操作转换为对应的_平台无关_ViewModel 类(**L7**)可以理解的 ICommand 事件
- 要激活的 ICommand 事件:NodeSelector 中选定的节点(卡牌)已更改。
- 用户命令操作:NodeSelector 区域(窗口左侧)中选定的节点(卡牌)已更改。
- ICommand 事件激活:@平台特定的*自适应视图类(**L2**)
//com.thinkalike.android.view.NodeSelectorFragment
...
_lv_nodeList.setOnItemClickListener(this);
...
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
assert(parent.getAdapter() instanceof NodeAdapter);
NodeAdapter adapter = (NodeAdapter)parent.getAdapter();
UINode uiNode = (UINode)adapter.getItem(position);
if(_viewModel!=null)
_viewModel.onNodeSelected(uiNode);
}
与 JavaFX 项目类似,用户命令操作将发生在原生 ListView 控件(_lv_nodeList
)上,然后激活相关 ViewModel(_viewModel
:NodeSelectorViewModel
)的 ICommand(onNodeSelected()
)。
NodeSelectorViewModel
如何接受 ICommand 请求,转换参数,并将其分派给其监听器 WorkareaViewModel
的过程与 JavaFX 项目中的完全相同。只要 UI 逻辑保持不变,_平台无关_代码就不需要更新。
4. 调试、研究原生 API 细节、调整,并达成目标。
分发
具体来说,将 JavaFX 应用程序分发到未安装 JRE/JDK 的平台是可行的。
以下是 Eclipse 用户关于 JavaFX 项目跨平台分发(适用于 Windows 和 MacOS)的必读内容
JavaFX 2 教程第七部分 - 使用 E(fx)clipse 进行部署
在 ThinkAlike 项目中,由于使用了“链接源文件夹”且 Ant(Eclipse 中的默认 Ant)无法解析,因此需要在 _build.xml_ 中进行一些手动修改
<target name="setup-staging-area">
...
<!-- Linked Source Folder. Need to copy them manually -->
<mkdir dir="project/src/com/thinkalike/jfx/assets" />
<copy todir="project/src/com/thinkalike/jfx/assets">
<fileset dir="${project.loc}\..\ThinkAlike_generic\assets-en">
<include name="/**" />
</fileset>
</copy>
...
</target>
project.loc
是一个您应该添加到 Eclipse 首选项(首选项 > Ant > 运行时 > 属性选项卡)的 Ant 属性,其值为 ${project_loc}
(Eclipse 的预定义变量)。MacOS 上的部署(炉石传说也支持 MacOS X)仅通过安装官方 JavaFX Ensemble 示例应用程序进行了测试。任何在 MacOS 上测试 ThinkAlike 的努力都将不胜感激。
结论
因此,在本文中,我讨论了使用 MVVM、Android SDK 和 JavaFX SDK 实现 Android-桌面跨平台应用程序的可能性。我们已经构建了一个_卡牌参考库_,希望您在享受炉石传说在线游戏时能够使用它。
ThinkAlike 的源代码可在以下地址获取:https://github.com/tiancheng2000/ThinkAlike
炉石传说卡牌游戏的官方网站:http://us.battle.net/hearthstone/en/
未来改进:1. 改进框架接口 2. 更多平台无关的组件类,并从通用代码实例化 3. EventBus。
卡牌游戏是免费的,框架是免费的,而创造和解决问题的乐趣是无价的:)
致谢
这是我第一次在 CodeProject 上发帖。感谢社区编辑们在技术和语言方面提供的帮助!也感谢我的妻子,她不懂 IT 语言,但支持我并“调试”我的英语语法。无论我们说什么语言,是“原生”还是非“原生”,我希望在有趣的事情上我们能“志同道合”(Think Alike)。
历史
2014-03-22:第一版