65.9K
CodeProject 正在变化。 阅读更多。
Home

如何让你的Android项目更方便地移植到Windows/MacOS(反之亦然)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (14投票s)

2014年3月25日

LGPL3

18分钟阅读

viewsIcon

55274

使用 ThinkAlike(一个正在开发的以 Android 和 JavaFX 为视图层的 Java MVVM 框架)来实现跨平台的炉石传说游戏卡牌参考应用等。

ThinkAlike Android ThinkAlike JavaFX

目录

引言

本文讨论了使用 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

  • MVVM in WPF MVVM in ThinkAlike

    "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 仅用于平台特定的视图层实现。那些在这两个领域都有经验的人可能知道它们在许多方面有何不同(例如,如何将图像自动适应其容器等细节)。然而,即使您不熟悉 Android/JavaFX,您仍然可以通读本文并掌握 MVVM 跨平台流程。

    Android 主题

    JavaFX 主题

环境

以下是我的开发工具清单

  • 基础:Eclipse/e(fx)clipse 4.2 Juno + JDK 1.7
  • 我强烈推荐 e(fx)clipse,它为 Eclipse IDE 提供了 JavaFX 工具,并与其他开发保持稳定的兼容性。
    JavaFX 使用 JDK 1.7。Android 项目默认会降级到 JDK 1.6 编译器兼容级别。
  • Android SDK + ADT 插件
  • JavaFX SDK(包含在 JDK 1.7 中)+ JavaFX Scene Builder

包划分

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. 节点(卡牌)选择器(基础)
  2. 节点(卡牌)详情(基础)
  3. 在中央区域显示所选卡牌(在卡牌选择器中)的详细信息。
  4. 节点(卡牌)选择器过滤器(基础,_待办_)
  5. 卡组构建器(_待办_)
  6. 基于战力计算的卡牌组合提示(_高级_)
  7. 卡组之间的自动对抗(_高级_)

第 1-3 项是通用功能,将在 ThinkAlike 项目中实现。卡组构建器将来可以分叉为独立项目。功能 #2,_显示卡牌详情_,将在本文中进行说明。

架构分析与设计

序列图

Sequence Diagram of ThinkAlike MVVM (Click to enlarge)
(点击放大)

详细信息将在_实现_部分中以纯文本形式进行说明

词汇表

为避免歧义,以下术语的用法将予以澄清

  • 平台特定:也称为“平台依赖”。指与特定技术平台(如 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 类(javafx.application),并实现了平台无关的 Platform 接口(以便向 generic 模块提供平台特定的数据/方法)
  • 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 实例(根类是 UINode)的数据更新底层组件视图。
  • L5: **_平台无关_**组件接口:例如 com.thinkalike.generic.viewmodel.control.IImageNodeView
  • 声明“自适应”组件类必须满足的要求,通常是一个 update(UINode) 接口。
  • L6: **_平台无关_**ComponentModel 类:例如 com.thinkalike.generic.viewmodel.control.UIImageNode
  • 包括与 UI 相关的数据/方法。某些 UINode 可能包含背景数据对象的只读“外观”(基本接口是 INodeRO)。
  • 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 层和(域)模型层。

1. 首先(也是最直观的)一步,是勾勒出用例(功能#2)的非逻辑 UI

UI markup file - JavaFX

  • UI 布局:_ \src\com\thinkalike\jfx\res\layout\scene_main.fxml_
  • 以上是功能#2(名为_“工作区”_)UI 区域的代码片段。它应该放在一个声明整个视图布局(**L2**)的 FXML 文件中。对于那些有 HTML/XAML/Android-XML 标记经验的人来说,FXML 的标记语言很容易学习。要快速开始 JavaFX UI 编程(Java 或标记),您可以参考_所需知识_小节

    fx:id="inv_nodeContent" 标识显示用户选择的节点(卡牌)详细信息的图像元素。请注意,ImageNodeView 不是 JavaFX 原生类,而是“自适应组件类”(**L4**)。JavaFX 允许在 UI 标记文件中直接使用自定义布局/控制类(如 Android),从而实现了非逻辑 UI/UX 开发人员和逻辑开发人员之间的彻底分工。

  • UI 样式:_ \src\com\thinkalike\jfx\res\style_dark.css_(深色样式)
  • JavaFX 大量利用层叠样式表 (CSS)。拥有 W3C CSS 知识的 Web 开发人员可以轻松掌握其 JavaFX 等效项,这些等效项以供应商前缀“-fx-”开头。(是的,对我来说比 Android 的更容易。)

2. 将平台特定的*自适应视图类(**L2**)与其对应的_平台无关_ViewModel 类(**L7**)关联起来,以处理 IProperty 更改事件。

SD_ThinkAlike_MVVM_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 是一个平台特定的实用类。资源/资产管理代码超出了本文的主题,但您可以查看源代码包以获取详细信息。

  • 平台特定的*自适应组件类实例化:@平台特定的*自适应视图类(**L2**)
  • //com.thinkalike.jfx.view.MainScene
    @FXML
    com.thinkalike.jfx.control.ImageNodeView inv_nodeContent;

    在 JavaFX 中,视图类以这种方式将它们的组件(字段,在相关的 FXML 中标记)声明为实例变量。然后,当 FXML 被解析时,inv_nodeContent 将自动分配给同名组件(fx:id="inv_nodeContent")。

  • _平台无关_ ViewModel 类的实例化(引用):@平台特定的*自适应视图类(**L2**)
  • //com.thinkalike.jfx.view.MainScene
    _vm_workarea = com.thinkalike.generic.viewmodel.WorkareaViewModel.getInstance();

    跨平台 MVVM 项目的真正核心,ViewModel 类,通常由其响应式 View 实例之一实例化为单例,并具有更长的生命周期。

  • 属性事件维护:@_平台无关_ ViewModel 类(**L7**)
    1. 事件注册(由平台特定的*自适应视图类(**L2**)调用)
    2. //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 模式会很有用。

    3. 事件取消注册:尚未启用。
    4. 有关更多实现细节,有一个 PropertyChangeListenerAdapter 管理一个 WeakReference 映射,以避免通知过时的事件监听器。您也可以故意调用 removePropertyChangeListener() 来取消事件订阅。
    5. 事件提交
    6. //com.thinkalike.generic.viewmodel.WorkareaViewModel
      this.firePropertyChange(Constant.PropertyName.Node, oldValue, _uiNode);

      当 ViewModel 准备好新的视图对象或 ComponentModel(例如 UIImageNode)时,它会将属性更改事件与该对象作为参数一起提交。

  • 属性事件处理:@平台特定的*自适应视图类(**L2**)和自适应组件类(**L4**)
    1. 事件处理
    2. //com.thinkalike.jfx.view.MainScene
      @Override
      public void onPropertyChanged(PropertyChangeEvent event) {
          if (event.getPropertyName().equals(Constant.PropertyName.Node)){
              updateWorkarea((UINode)event.getNewValue());
          }
      }

      当收到 Node 已更改的通知时,自适应视图类会将事件参数转换为内部方法参数。

    3. UI 更新
    4. 有两种方法可以将通用 ComponentModel 对象提供给代表层

      1. UINode.attachView(INodeView)
      2. 如果自适应组件实例已存在(如 NodeContent 的情况),ComponentModel 可以附加到它。
      3. UINode.createView()
      4. 否则,如果没有自适应组件,ComponentModel 可以请求动态实例化相关类型的 Component 类(如 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 事件

SD_ThinkAlike_MVVM_ICommand (Click to enlarge)

  • 要激活的 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 模式。但在简化情况下,同步方法调用也同样有效。

  • ICommand 事件处理:@_平台无关_ ViewModel 类(**L7**)
  • //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 细节、调整,并达成目标。

ThinkAlike JavaFX

至此,JavaFX 项目中的功能#2已完成。这种架构是否能经受住平台迁移的考验,可以在 Android 项目的开发中进行测试。

[Android]

开发步骤在_很大程度上_类似于之前的 JavaFX 项目,这证明了跨平台解决方案的好处。因此,我只需要替换并指定更改的部分。

0. 修正序列图中所示的类/实例

  • L0: 平台特定“自适应”应用程序类:com.thinkalike.android.ThinkAlikeApp
  • L1: 平台特定原生视图类:例如 android.support.v4.app.Fragment
  • Android 中的 Fragment 支持针对不同移动设备的自适应布局。
  • 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

毫无疑问,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**)。

  • UI 样式:不适用。相比之下,JavaFX 在这方面的学习曲线更短。

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 标记文件中所设计。

  • 平台特定的*自适应组件类实例化:@平台特定的*自适应视图类(**L2**)
  • //com.thinkalike.android.view.WorkareaFragment
    com.thinkalike.android.control.ImageNodeView _inv_nodecontent = 
            (ImageNodeView) rootView.findViewById(R.id.iv_nodecontent);
  • _平台无关_ ViewModel 类的实例化(引用):@平台特定的*自适应视图类(**L2**)
  • //com.thinkalike.android.view.WorkareaFragment
    com.thinkalike.generic.viewmodel.WorkareaViewModel _viewModel = 
            WorkareaViewModel.getInstance();
  • 属性事件维护:@_平台无关_ ViewModel 类(**L7**)
    1. 事件注册(由平台特定的*自适应视图类(**L2**)调用)
    2. //com.thinkalike.android.view.WorkareaFragment
      _listenToVM = new PropertyChangeListener(){
          @Override
          public void onPropertyChanged(PropertyChangeEvent event) {...}
      };
      _viewModel.addPropertyChangeListener(Constant.PropertyName.Node,
                                  _listenToVM);
    3. 事件取消注册:未使用。
    4. 事件提交
    5. //com.thinkalike.generic.viewmodel.WorkareaViewModel
      this.firePropertyChange(Constant.PropertyName.Node, oldValue, _uiNode);
  • 属性事件处理:@平台特定的*自适应视图类(**L2**)和自适应组件类(**L4**)
    1. 事件处理
    2. //com.thinkalike.android.view.WorkareaFragment
      @Override
      public void onPropertyChanged(PropertyChangeEvent event) {
          if (event.getPropertyName().equals(Constant.PropertyName.Node)){
              updateWorkarea((UINode)event.getNewValue());
          }
      }
    3. UI 更新
    4. 正如在 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(_viewModelNodeSelectorViewModel)的 ICommand(onNodeSelected())。

  • ICommand 事件处理:@_平台无关_ ViewModel 类(**L7**)
  • NodeSelectorViewModel 如何接受 ICommand 请求,转换参数,并将其分派给其监听器 WorkareaViewModel 的过程与 JavaFX 项目中的完全相同。
    只要 UI 逻辑保持不变,_平台无关_代码就不需要更新。

4. 调试、研究原生 API 细节、调整,并达成目标。 ThinkAlike Android

分发

具体来说,将 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:第一版

© . All rights reserved.