如何添加组件返回首页

Ellen Spertus(spertus@google.com

本文档介绍如何为App Inventor设计和添加新组件。  

1.背景

1A.Android

1B。类层次结构

1C。Yail

1D,命名

1E。类型

1F。属性

Java的类型

物业编辑

遗产

访问

设计师的外观

块编辑器中的外观

发短信的例子

1克中。方法

参数

返回类型

1H。活动

2.界面设计

2A。一般原则

2B。应用发明特有的原则

让初学者轻松一点

以对用户有意义的方式组织功能

手机≮桌面

提供默认值

3.提出建议

4.实施

4A。创建一个图标

4B。模拟组件

4C。宣布班级

SimpleObject注释

DesignerComponent注释

UsesPermissions注释

4D。属性

SimpleProperty注释

DesignerProperty注释

隐藏超类中定义的属性

4E。方法

SimpleFunction注释

启动线程

4F。活动

SimpleEvent注释

4克。管理活动生命周期

4小时。包括外部库

添加到appinventor / lib

添加到build.xml文件

在组件中使用库

使用本机库

4I。测试

浏览器测试

静态方法的单元测试

实例化组件的单元测试

系统测试

5.在调色板类别中订购组件

6.国际化

7.支持传统设备

1.背景

1A.Android

假设本文档的读者知道如何使用Java SDK编写Android程序。如果不这样做,Android开发者  网站会提供有关构建您的第一个应用程序  和管理活动生命周期的一些信息

值得强调和阅读的Android的一个方面是Android UI线程,它负责响应UI交互(例如按钮按下)和更新UI。所有用户代码和大多数组件库代码都在UI线程中运行。  (稍后,我们将讨论由于用户操作而创建线程。)这为用户提供了一个简单的执行模型:App Inventor过程永远不会被事件处理程序中断,反之亦然; 也不会被另一个事件处理程序打断。这在 1中说明

图1:演示串行语义的示例程序。执行Button1.Click时,两个Ball放在相同的位置,导致事件处理程序Ball1.CollidedWith被添加到调度队列中。但是,在第一个处理程序完成之前,无论未示出的用户过程等待多长时间,它都不会被执行 。  
最终的标签显示始终是:“... Button1.Click ...... Ball1.CollidedWith ......”。

1B。类层次结构

所有组件都适合类/接口层次结构,其子集如图2所示。所有组件都实现了Component  接口,该接口主要由有用的常量组成。每个组件都扩展了VisibleComponent  或AndroidNonvisibleComponent  抽象类,还有一些组件还实现了ComponentContainer  接口。每个应用程序都有一个Form  (向用户显示为“Screen”),它包含零个或多个AndroidViewComponent实例,这是大多数可见组件的超类。例外情况包括Sprite,它只能包含在Canvas中

图2:组件类/接口层次结构的不完整子集。  
接口和抽象方法以斜体显示。  

1C。Yail

编译期间应用程序的中间表示是YAIL [Young Android Intermediate Language [1] ](通常写为“Yail”或“yail”,发音为“Yale”)。YAIL程序由s表达式组成  ,可以由Scheme编译器或解释器使用在runtime.scm中定义的相应宏和过程进行转换

1D。命名

组件的名称及其属性,方法和事件都大写为UpperCamelCase。

1E。类型

表1显示了App Inventor语言中的类型,它们与Yail编译时和Scheme运行时类型的对应关系,以及这些类型的样本属性。Java和Yail类型之间的转换发生在ComponentProcessor .javaTypeToYailType()中。

Java类型

Yail类型

(编译时间)

方案类型
(运行时)

样本财产

布尔

布尔

布尔

Button.Enabled

文本

Label.Text

INT

浮动

Label.FontSize

Pedometer.ElapsedTime

Sprite.Heading

LocationSensor.Longitude

java.util.Calendar中

InstantInTime

java.util.Calendar中

任何Java类型,包括基本类型和对象类型。

任何

任何Java类型,包括基本类型和对象类型。

零件

零件

零件

YailList
Collection
<?>

名单

yail列表

ListPicker.Elements

BluetoothClient.AddressesAndNames

表1:Java和Yail类型之间的对应关系。  
前三行中的Yail类型(布尔值,文本和数字)是内置的Scheme类型。接下来三行(Calendar,Object和Component)的运行时类型只是它们的Java类型,可以通过我们使用的kawa Scheme实现访问它们。  
Scheme名称yail-list  用于引用Java类型YailList,它是kawa类型gnu.list.Pair的子类。

全局变量是无类型的。  仅当块的输入是文字时,或者当我们确切知道相关类型时,才执行静态类型检查。  例如,块编辑器不允许文本文字作为数字加操作的输入,并且您不能将数字函数插入IF块的测试套接字中。  

1F。属性

属性对应于Java对象中的属性/字段/实例变量。  

在过去,为了记录目的,属性被分为“行为”,“外观”和“不推荐”类别。例如,Sprite.Enabled  属于“行为”类别,而Sprite.Visible  属于“外观”类别。不再使用这种区别,也不 需要在新代码中进行。

Java类型

属性可以是表1中所示的任何类型。虽然存在float 类型的属性,但double  是首选。同样,int  应该优先于short

物业编辑

除了Java类型之外,每个属性都有一个编辑器类型,用于控制可在Designer中指定的值以及如何指定它们。例如,Label.TextAlignment 的Java类型 是int ,但其编辑器类型是PROPERTY_TYPE_TEXTALIGNMENT,这使得它可以通过下拉列表设置为对应于“left”,“center”或“right”的三个整数之一菜单如图所示:

(该字符串中所定义OdeMessages.java  并且可以与翻译被容易地更换。)表2示出关于现有属性编辑器类型(其在指定的缩略信息PropertyTypeConstants和YoungAndroidPalettePanel.java单独的文档如何向组件添加属性描述了如果没有现有的PropertyEditor满足您的需求,如何创建新的PropertyEditor。 

名称

样品使用

ASSET

上传资产集(媒体文件)

Sound.Source

BLUETOOTHCLIENT

此项目中的BluetoothClient组件集

LegoMindstormsNxtBase。
BluetoothClient

布尔

{真假}

Button.Enabled

BUTTON_SHAPE

 

Button.Shape

颜色

{“黑色”,“蓝色”等}

Label.FontColor

零件

此项目中的组件集

不直接使用

浮动

浮点值

Sprite.Speed

整数

整数值

不曾用过

LEGO_NXT_SENSOR_PORT

{“1”,“2”,“3”,“4”}

NxtColorSensor.SensorPort

NON_NEGATIVE_FLOAT

非负浮点值

Label.FontSize

NON_NEGATIVE_INTEGER

非负整数值

Sound.MinimumInterval

SCREEN_ORIENTATION

{“未指定”,“肖像”,“风景”}

Form.ScreenOrientation

StringPropertyEditor

ButtonBase.String

文本

TextPropertyEditor

不曾用过

TEXTALIGNMENT

{“left”,“center”,“right”}

Label.TextAlignment

字体

{“default”,“san serif”,“serif”,“monospace”}

Label.FontTypeface

表2:不同类型的PropertyEditors。  
“名称”列中的所有条目都省略了前缀“PROPERTY_TYPE_”。  

遗产

通常,组件继承其超类的属性,但可以抑制它。例如,即使(如图1所示),Ball  也是VisibleComponent的间接子类,它具有Height  和Width 属性Ball  不具有。相反,它有Radius 如何抑制继承将在实现部分中讨论。

虽然应该可以更改编辑器类型,但是无法更改继承属性的Java类型。

访问

虽然可以在Designer和Blocks Editor中读取和写入大多数属性,但也有一些例外。表3给出了一些例子。

零件

属性

设计师

块编辑器

标签

文本

读/写

读/写

TableArrangement

读/写

-

LocationSensor

纬度

-

只读

ListPicker

ElementsFromString

只写

只写

AndroidViewComponent

行列

间接写

-

表3:Designer和Blocks Editor中属性的可见性。

如表1所示,属于Row和Column的属性(属于所有扩展AndroidViewComponent的组件,如Button和Label)只能间接设置。它们仅在将组件放置在Horizo​​ntalArrangementTableArrangementVerticalArrangement中时设置,  并指定组件的相对位置,该组件通过在Designer中拖动组件来间接设置。有关示例,请参见图3。

图3: 包含三个其他组件的Horizo​​ntalArrangement

虽然在“属性”面板中不可见,但三个内部组件具有 指示其位置的“ 列”  和“ 行” 属性。

设计师的外观

属性在Designer中按字母顺序列出,但宽度  和高度  始终显示在可见组件的“属性”窗格的底部,如图3所示。(它们显式添加到MockVisibleComponent.addWidthHeightProperties()中的属性列表中。)

块编辑器中的外观

组件的属性在块编辑器中按字母顺序显示。塞特斯是淡蓝色(#A2CDFA),而吸气剂是浅蓝色(#D9E5FF)。没有办法给属性一个不同的顺序或着色。

发短信的例子

为Texting组件定义的属性(任意示例)如表3所示。

名称

Java类型

编辑类型

电话号码

PROPERTY_TYPE_STRING

信息

PROPERTY_TYPE_STRING

ReceivingEnabled

布尔

PROPERTY_TYPE_BOOLEAN

表4:为Texting组件定义的属性。  
所有都可以在Designer和Blocks Editor中读取和写入。

1g中。方法

方法比属性更简单。它们仅在块编辑器中可见,而不在Designer中可见。它们可以被继承,虽然它们通常只为那些本身不是子类的组件定义。(一个例外是Sprite .CollidingWith(),它由ImageSprite  和Ball 继承。)

参数

方法可能具有表1中所示的任何Java类型的零个或多个参数。除了Java原语boolean,int和long之外,还有一些方法可以接受以下类型的参数:

返回类型

类似地,返回类型可以是表1中所示的任何Java类型(或void )。以下是具有不同返回类型的方法示例:

1H。活动

与方法类似,事件仅在块编辑器中可见。它们可能具有表1中所示的任何Java类型的参数,但它们的返回类型始终为void

2. 界面设计

2A。一般原则

Josh Bloch 编写  并介绍了  一些优秀的API设计原则,包括(引用的逐字): 

2B。App Inventor特有的原则

以前未经编写的App Inventor设计原则(我们以后并不总是成功)是:

让初学者轻松一点

我们选择将列表中第一个元素的偏移量设置为1而不是0,因为1是一个天真的用户所期望的。虽然这可能会使传统编程语言的过渡变得更加困难,但我们更关心的是帮助人们发现计算的乐趣,而不是在你上几节课之前没有意义的细节。遵循最小惊讶原则如果你必须在令人惊讶的初学者或经验丰富的计算机科学家之间作出决定,那么后者就会感到震惊。

对于用户来说最简单的并不总是显而易见的。考虑Sound组件的vibrate方法的数字参数。我们选择使用毫秒,这使人们可以使用整数(500毫秒)而不是分数(.5)。另一方面,如果没有阅读本手册或者不知道几毫秒的人可能会开始尝试1的参数,这会引起难以察觉的振动。  

以对用户有意义的方式组织功能

继续我们对振动方法的讨论,物理学家可能知道在Sound组件中寻找它,但大多数用户不会。不幸的是,我没有更好的建议振动功能应该在哪里,没有创建一个名称会让人窃笑的新组件。

良好组织的一个例子是在Media类别中拥有Camera,ImagePicker和VideoPlayer。它们的实现完全不同,但是对于用户来说,它们在一起是有意义的。

手机≮桌面

移动设备不应被视为具有小型显示器的台式(或膝上型)计算机。更少关注在大屏幕上比小屏幕更好地工作的功能,并更多地关注利用移动设备的独特功能的功能,例如便携性,连接性(SMS,蓝牙,NFC等),传感器(加速度)和位置)和录像机(音频,照片,视频)。这个原则表明,利用移动设备的所有这些功能来开发数据收集组件比开发,例如,在单个屏幕上显示多个视频的能力更好,这只能做得很差。 (现有)移动技术。

提供默认值

用户不必了解组件的所有属性即可使用它。例如,除了Text(具有不言自明的初始值“Text for Label1”)之外,新标签的所有属性都有合理的默认值。这使得某人可以快速开始使用组件,并且只在不满意默认行为时查看其他属性(例如标签在他们选择的屏幕背景图像上难以阅读)。通过不要求用户在需要之前理解属性,这使得它们成为解决方案而不是问题。

同样,应为内置块提供合理的默认值。“make color”块的参数是一个列表,必须包含具有红色,绿色和蓝色组件值的元素,并且可以选择包含具有alpha级别的第四个元素,这是大多数用户永远不需要的。(为每个数字输入采用单个列表参数而不是一个参数的缺点是套接字标签意义不大。)

3.提出建议

在编写任何组件代码之前,请与其他App Inventor开发人员(以及理想情况下的用户)讨论相关属性,方法和事件应该是什么。理想情况下,您的讨论应包括天真和复杂的用户。例如,当Trevor Adams设计乐器组件时,他与Ellen Spertus以及电子音乐的先驱教授Chris Brown进行了长时间的交谈。

提案应包括:

简要说明应与参考文档中的说明类似类型信息不仅包括Java类型(例如,int),还包括对域或范围的任何限制(例如,0到255之间的整数,包括0和255)。

4. 实施

每当创建或修改新组件时,都必须创建Companion应用程序的新副本。有关如何将新Companion推送到您的设备的信息,请查看本节  中有关如何从源文档构建的文档 

4 a。创建一个图标

每个图像应在目录appengine / src / com / google / appinventor / images中以png格式显示16x16图标该文件的名称应该在lowerCamelCase中,并带有小写扩展名。例如,ImageSprite组件的图标名称是“imageSprite.png”。图像必须可自由分发而不归属。

必须在多个文件中引用该图标。对于Twitter组件,以下行添加到Images

/ **

 *设计师调色板项目:

 * /

 @Source( “COM /谷歌/ appinventor /图像/ twitter.png”)

 ImageResource twitterComponent()

以下行添加到SimpleComponentDescriptor:

  bundledImages “图像/ twitter.png”  图像twitterComponent ());

4B。模拟组件

除了在设备上运行的实际组件代码之外,每个组件都由Designer中的“mock”组件表示。例如,3显示了与用户创建的Label和Button对应的MockLabel和MockButton。模拟组件是MockComponent的间接子类,其外观应尽可能接近它们所代表的实际组件。例如,在Designer中更改Button的Image,Text或TextColor属性应该会导致MockButton中的相应更改。对于不可见的组件,这是微不足道的,它们都由MockNonVisibleComponent表示。模拟可见组件时的“问题”是确保所有浏览器(Chrome,Firefox,Internet Explorer和Safari)的外观都正确。

有关如何使模拟组件 反映其属性值的讨论,请 参见如何向组件添加属性如果需要创建一个新的MockComponent,通常应该对现有的MockComponent进行子类化(通常是对应于组件超类的mock)。 

一旦选择或创建了MockComponent来表示组件,就需要在方法SimpleComponentDescriptor.createMockComponent()中添加一个大小写

4C。宣布班级

每个组件都实现为一个类,该类应位于com.google.appinventor.component.runtime  包中。如上所述每个组件必须是AndroidNonVisibleComponent  或AndroidViewComponent的子类(可能是间接的)组件类必须使用以下注释这些注释用于为系统的其他部分生成有关组件及其属性的信息。

SimpleObject  注释

注释SimpleObject  必须位于定义或是组件超类的任何类的定义之前。

DesignerComponent  注释

DesignerComponent  注释必须先应出现在设计任何组件的定义。有七个要素:

图4显示了LocationSensor组件的完整注释,从DesignerComponent开始。

UsesPermissions  注释

UsesPermissions  注释具有单个元件,permissionNames ,用逗号分隔的字符串与从恒定许可字符串Android.manifest.Permission,如“ android.permission.CAMERA ”。更长的例子如图4所示对于不需要任何特殊权限的组件(例如Clock),可以省略这一点。

@DesignerComponent version  = YaVersion.LOCATIONSENSOR_COMPONENT_VERSION,
 
description =“<p>提供位置信息的不可见组件,”+    “包括经度,纬度,海拔高度(如果设备支持),”+    “和地址。这也可以执行\“geocoding \”,将给定的“+    ”地址(不一定是当前的地址)转换为纬度(使用“+    ”<code> LatitudeFromAddress </ code>方法)和经度(使用“+     ”<代码> LongitudeFromAddress </ code>方法)。</ p>“+    ”<p>为了运行,组件必须有“+    ”<code>启用</ code>属性设置为True,设备必须为“+”  







   “通过无线网络或GPS”+
   “卫星(如果在外面)启用位置感知。</ p>”,
   
category = ComponentCategory.SENSORS,
   
nonVisible = true,
   
iconName =“images / locationSensor.png”)

@SimpleObject @UsesPermissions (permissionNames =                 “android.permission.ACCESS_FINE_LOCATION,”+                 “android.permission.ACCESS_COARSE_LOCATION,”+                 “android.permission.ACCESS_MOCK_LOCATION,”+                 “android.permission.ACCESS_LOCATION_EXTRA_COMMANDS”)




4:LocationSensor的完整注释集。

4D。属性

每个属性都有一个具有相同名称的getter和/或setter。   5  显示了Canvas组件的PaintColor属性的声明和部分实现。有些要点需要注意:

/ **
  *将当前指定的绘制颜色返回为alpha-red-green-blue
  *整数,即{@code 0xAARRGGBB}。{@code 00}
  * 的alpha 表示完全透明,{@code FF}表示不透明。
  *
  * @return以0xAARRGGBB格式绘制颜色,包括alpha,
  *红色,绿色和蓝色组件
  * /
 @SimpleProperty(
     description =“绘制线条的颜色”,
     category = PropertyCategory.APPEARANCE)
 public int PaintColor( ){
   return paintColor;
 }

 / **
  *将颜色指定为alpha-red-green-blue整数,
  即{@code 0xAARRGGBB}。{@code 00}的alpha表示完全
  *透明,{@code FF}表示不透明。
  *
  * @param argb以0xAARRGGBB格式绘制颜色,其中包括
  * alpha,红色,绿色和蓝色组件
  * /
 @DesignerProperty(editorType = DesignerProperty.PROPERTY_TYPE_COLOR,
     defaultValue = Component.DEFAULT_VALUE_COLOR_BLACK)
 @SimpleProperty
 public void PaintColor(int argb) {
   paintColor = argb;
   changePaint(paint,argb);
 }

图5:Canvas组件的PaintColor属性的部分实现。

SimpleProperty  注释

每个属性getter和setter都应该以SimpleProperty  注释开头如图 5 所示

第一个注释的三个元素,类别,不再使用。在过去,为了记录目的,属性被分为“行为”,“外观”和“不推荐”类别。例如,Sprite.Enabled  属于“行为”类别,而Sprite.Visible  属于“外观”类别。我们已经放弃了这种区别,部分原因是并非所有属性都适合这些类别。

描述 元件中使用的参考文档  和在工具提示  的设计器(内图6)。描述是针对属性本身,而不是为getter和setter分别设置。描述可以包含在getter或setter之前的注释中,但不应该包含在两者中。(虽然这样做没有任何危害,但如果两个描述不同,可能会导致意外结果,并增加维护。)

图6:Designer中的工具提示。  
这来自
图4中的代码

userVisible  元素,默认为,指定属性是否在块编辑器访问。添加了此元素(以及将其设置为false的功能)以支持行和列属性,因此可以在Designer中间接设置它们,但不能在块编辑器中访问它们。可以在Designer中设置的一些其他属性具有   false的userVisible  值,例如Canvas.TextAlignment,以防止用户将其设置为非法值(“left”,“right”或“center”以外的任何值) 。

DesignerProperty  注释

虽然每个属性getter和setter都应该具有SimpleProperty  注释,但DesignerProperty  注释只应用于Designer中可见的属性设置器。Designer中可见的一些示例或属性是Row,Column和LocationSensor.Latitude。   

第一DesignerProperty 的两个元件是默认值,一个字符串,使该属性的初始值。(如果没有指定,则是空字符串。)对于Canvas.PaintColor(图5),该缺省值是Component.DEFAULT_VALUE_COLOR_BLACK ,在定义了许多有用的常量的一个组件 

DesignerProperty 的第二个元素是editorType ,它指定用于在Designer中输入属性值的PropertyEditor。(参见前面对属性编辑器的讨论。)   对于Canvas.PaintColor,PropertyEditor是YoungAndroidColorChoicePropertyEditor,它允许用户在一组预定义颜色中进行选择,如图6所示 (可以在Blocks Editor中以编程方式指定其他颜色。)  

单独的文档如何向组件添加属性  描述 如果没有现有的PropertyEditor 表2)满足您的需求,如何创建新的PropertyEditor

隐藏超类中定义的属性

有时,最好隐藏超类中定义的属性。例如,作为VisibleComponent 的间接子类,Ball继承了Height  和Width  属性。我们不希望将这些暴露给用户,因为更合适的抽象是Radius  属性,它保证了Ball的高度和宽度相同。要隐藏 用户的高度 和宽度,它们将在Ball.java中重写,但不会 使用SimpleProperty  和DesignerProperty进行注释如图7所示同样的技巧应该可以隐藏方法和事件。

图6:设置Canvas.BackgroundColor  与YoungAndroidColorChoicePropertyEditor

//抽象超类
// VisibleComponent 需要以下方法因为我们不想将它们暴露给
//程序员,所以我们省略了SimpleProperty和DesignerProperty编译指示。
@Override
public int Height(){
 return 2 * radius;
}

@Override
public void Height(int height){
 // ignored
}

图7:隐藏子类中的属性

/ **

 * <p>获取给定像素的颜色,忽略精灵。</ p>

 *

 * @param x x坐标

 * @param y坐标

 * @将该位置的颜色恢复为alpha-red-blue-green整数,

 *或{@link Component#COLOR_NONE}如果该点不在此
* Canvas上

 * /

@SimpleFunction(description =“获取指定点的颜色。”

    +“这包括背景和任何绘制的点,线或”

    +“圈子但不是精灵。”)

public int GetBackgroundPixelColor(int x,int y){

  return view.getBackgroundPixelColor(x,y);

}

图8:方法Canvas.GetBackgroundPixelColor()

4E。方法

图8  显示了一个方法示例:Canvas.GetBackgroundPixelColor()不允许超载。支持可变数量参数的间接方法是将List作为参数,因为List的长度不是类型规范的一部分。

SimpleFunction注释

SimpleFunction  注释具有两个元素:描述 和userVisible ,其含义平行那些SimpleProperty应始终提供用于标记和参考文档的描述。userVisible 的默认值为 true。对于已弃用的方法(例如VideoPlayer.VideoPlayerError())或内部开发下的方法,通常只会返回false

启动线程

如上所述,所有用户代码和大多数组件代码都在Android UI线程中运行例外情况是Twitter,Web和TinyWebDB等组件,这些组件执行动作(发送Web请求),这些动作不需要(因性能原因)在UI线程中运行。图9  显示了Web  组件的Get方法的部分实现,它使用我们的实用程序类AsynchUtil,它在新的非UI线程中启动Runnable(例如,获取某个Web页面的请求)。  图10  显示了在 收到响应后如何在UI线程中启动事件处理程序GotText  

/ **
*使用Url属性执行HTTP GET请求并检索  

 * response。<br>
*如果SaveResponse属性为true,则响应将保存在
*文件中,并且将触发GotFile事件。ResponseFileName
*属性可用于指定文件的名称。<br>
*如果SaveResponse属性为false,则会
触发GotText事件
* /
@SimpleFunction
public void Get(){
 //在运行之前捕获局部变量中的属性值

  //异步。
 final CapturedProperties webProps = capturePropertyValues(“Get”);

 
AsynchUtil.run异步(new Runnable(){
   @


       
Override    public void run(){      try { performRequest(webProps,null,null);
     } catch(
       FileUtil.FileException e){ form.dispatchErrorOccurredEvent(Web.this,“Get”,
           e .getErrorMessageNumber());
     } catch(Exception e){
       form.dispatchErrorOccurredEvent(Web.this,“Get”,
           ErrorMessages.ERROR_WEB_UNABLE_TO_GET,webProps.urlString);
     }
   }
 }
);
}

图9:Web组件的Get方法。它创建一个Runnable,调用方法performRequest()(图10)完成打开HTTP连接的实际工作,生成并发送请求头和cookie,并等待响应。AsynchUtil.runAsynchronously()的调用会导致这些操作在新的非UI线程中发生。

private void performRequest(最终CapturedProperties webProps,

    byte [] postData,String postFile)抛出IOException {

  HttpURLConnection connection = openConnect(webProps);

  [:]

    尝试{

      [:]
     //发送活动。
   
 activity.runOnUiThread(new Runnable(){
       @

         
Override        public void run(){ GotText(webProps.urlString,responseCode,responseType,

              responseContent);
        }
     }
;

  [:]

}

图10:Web.performRequest()的一部分,它在收到响应后启动用户GetText事件处理程序。调用activity.runOnUiThread()的方法 使事件处理程序代码在UI线程中运行。

4F。活动

图11  显示了一个事件示例:Clock.Timer()。事件是普通方法,通常通过调用EventDispatcher.dispatchEvent()来触发对用户编写的事件处理程序的调用事件调度程序确保用户编写的事件处理程序不会相互中断或其他用户代码。  

SimpleEvent注释

SimpleEvent  注解有相同的两个元件SimpleFunction 描述userVisible ,其具有真实的默认值。 

 @SimpleEvent(description =“Timer已经关闭。”)
public void Timer(){
  if(timerAlwaysFires || onScreen){
    EventDispatcher.dispatchEvent(this,“Timer”);
  }
}


图11:方法Clock.Timer(),它将一个调用排入

用户定义的事件处理程序(如果存在)。

4克。管理活动生命周期

单个App Inventor应用程序可以使多个Activity  实例保持一致,其中一些是动态的。虽然Form  是子类Activity 的唯一组件,但许多组件都会启动一个Activity ,包括:

某些组件需要考虑活动生命周期  才能正常运行或避免过多的CPU或电池使用。例如, 当关联的“ 活动”  暂停OrientationSensor会停止响应手机位置的更改,并在删除时重新启动。我们提供以下接口来响应生命周期变化。

单个操作可以导致调用多个这些方法。例如,当用户按下Home按钮时,将 调用onPause() 和onStop()方法。假设操作系统不需要内存并销毁应用程序,返回它(无论是通过手机的启动器,后退按钮还是最近的应用程序按钮)都会导致 调用onResume()方法。

作为如何使用这些方法的示例,请考虑Sound,它具有以下方法:

为了接收方法调用,组件必须使用其容器(通常是Form)注册自己。  图12  显示了Sound的一些相关代码

public class Sound扩展AndroidNonvisibleComponent

    实现Component,OnResumeListener OnStopListener

    OnDestroyListener ,可删除{

  public Sound(ComponentContainer容器){

    (集装箱$形式())超;

    soundPool = new SoundPool(MAX_STREAMS,AudioManager.STREAM_MUSIC,0);

    :

    form.registerForOnResume(本);

    form.registerForOnStop(本);

    form.registerForOnDestroy(本);

    :

  }

  // OnStopListener实现

  @覆盖

  public void onStop(){

    if(streamId!= 0){

      soundPool.pause(流ID);

    }

  }

  :

}

图12:Sound中的活动生命周期代码。的onResume() 和的onDestroy()方法
已经为空间省略。

类似的接口和方法,Deleteable.onDelete()  存在于动态删除(通常通过解释器)时需要执行某些操作的组件。这样的部件也没有 需要注册自己。如果它们被声明为实现Deleteable ,它们将被Form.deleteComponent()删除

4小时。包括外部库

某些组件(如Twitter)需要外部库(twitter4j.jar)。以下是如何添加组件源文件引用的jar文件。

添加到appinventor / lib

appinventor / lib目录包含每个外部库的一个子目录。例如,有一个子目录appinventor / lib / twitter。每个子目录都应包含以下文件:

添加到build.xml

应该将新库的路径添加到build.xml文件中,只要有对Twitter库的引用:

    <pathelement location = $ {lib.dir} /twitter/twitter4j-2.0.10-SNAPSHOT.jar“/>

在撰写本文件时,这些地点是:

如果组件中的Javadoc链接到外部库的Javadoc,则还应该 在appinventor / build.xml中javadoc 目标添加链接条目

您还需要修改/ buildserver /目录中的build.xml文件(这是appinventor / buildserver / build.xml中的文件),以便在下面的<copy>标记列表中包含jar文件的行。 “BuildServer”目标。

只需要进行一次修改即可将jar文件添加到<copy>标记声明列表中。

此<copy>条目的格式应如下所示:

<copy toFile =“$ {classes.files.dir} / simplifiedNameForJARFile.jar

          file =“$ {lib.dir} / subfolderNameFromStep1 / nameOfJARFileToAdd.jar ”/>

注意:在每个<copy>标记中,file  属性指的是将jar文件放在appinventor / lib中的路径。“$ {lib.dir}”等同于“appinventor / lib”。TOFILE  属性的状态到该文件将在Ant构建过程中被复制的路径。“$ {classes.files.dir}”指的是路径“appinventor / buildserver / build / classes / BuildServer / files”。在ant脚本完成执行BuildServer ant目标之后,您应该能够导航到appinventor / buildserver / build / classes / BuildServer / files并验证您的jar文件确实被复制了。

请注意,所有这些更改都是在命令行中使用ant构建系统所必需的。如果使用Eclipse之类的IDE,则还需要在Build Path  或项目中添加新的jar文件

在组件中使用库

您的组件应包含“@UsesLibraries”注释,例如:

@UsesLibraries(libraries =“library1.jar,”+“library2.jar,”+ ... +“libraryN.jar”)

将注释放在类定义之前。请参阅 com.google.appinventor.components.runtime包中Twitter.java  或FusiontablesControl.java组件。

使用本机库

在某些情况下,您可能希望使用Native库而不是jar包装的库。以下注释可用于此目的。

@UsesNativeLibraries(libraries =“library1.so,”+“library2.so,”+ ... +“libraryN.so”)

文件本身也需要添加到lib文件夹中,并在build.xml文件中处理。有关完整示例,请查看  此关闭拉取请求中的说明。

4I。测试

浏览器测试

如果您在添加模拟组件时做了一些非常重要的事情,那么您应该在我们支持的所有浏览器的当前和以前的主要版本中测试他们的行为:Chrome,Firefox,Internet Explorer和Safari。将Shape属性添加到按钮时,我们忽略了这一点,导致Firefox和Internet Explorer中的显示不正确。

静态方法的单元测试

我们强烈支持单元测试,并使用JUnit 4test-libraries-for-JavaPowerMock,它们都包含在我们的外部库中。

要执行的最简单的测试类型是静态方法。有关示例请参阅utilTestWebTestutils  子目录中的大多数文件 当我们放宽可见性限制以增强可测试性时,我们在正在测试的代码中使用注释@VisibleForTesting

实例化组件的单元测试

更困难但非常有价值的是实例化组件的测试。一个例子是BuletoothConnectionBaseTest,它实例化BluetoothConnectionBase的匿名子类, 覆盖某些在没有实际蓝牙连接的情况下无法测试的方法(因此不适合单元测试)。

通常,不能 使用静态,最终或私有方法完全模拟对象; 但是,有可能使用PowerMock。  SpriteTest  使用此功能来创建许多类的“漂亮” 模拟,包括android.os.Handler  和android.view.View(当调用未实现的方法时,严格的模拟抛出异常,而好的模拟带有默认方法,除了返回“空”值,如0,false或空字符串之外什么都不做。)

测试Sprite的挑战是每个实例都有一个Canvas  (传递给构造函数),它充当Sprite的容器,以及一个android.os.Handler  (在构造函数中实例化),请求是发布。为了测试Sprite, 创建了一个构造函数的替代版本,其中Canvas和Handler都是参数(允许使用模拟)。我们不希望测试后面这些类,而只是让它们提供被测试的Sprite部分所调用的方法。  图13  提供了SpriteTest.java的摘录, 显示了如何创建模拟,并给出了一个示例,用于验证它是否检测到一对Sprite之间的冲突。

使用PowerMock时需要以下注释。

@RunWith(PowerMockRunner.class)

以下注释指示将模拟哪些类。

@PrepareForTest({Canvas.class,Handler.class,Form.class,View.class})

公共类SpriteTest {

  :

    创建以下每个类的漂亮模拟:

  private final Form formMock = PowerMock.createNiceMock(Form.class);

  private final查看canvasViewMock = PowerMock.createNiceMock(View.class);

  private final Canvas canvasMock = PowerMock.createNiceMock(Canvas.class);

  private final Handler handlerMock =
     PowerMock.createNiceMock(Handler.class);

    创建Sprite的子类进行测试,覆盖某些方法(未显示),以防止它们
   调用不可用和不必要的方法。

  static class TestSprite扩展Sprite {

    :

    public static TestSprite createTestSprite(Canvas canvas,
       Handler handler){
     return new TestSprite(canvas,handler);

    }

    public static TestCreate createTestSprite(Canvas canvas,

        处理程序处理程序,int h,int w){

      TestSprite sprite = createTestSprite(canvas,handler);

      sprite.height = h;

      sprite.width = w;

      返回精灵;

  }

  @Before   ←注释适用于JUnit                           

  public void setUp()throws Exception {

      指定(一旦模拟处于播放模式),每当canvasMock.getView()时

      调用时,返回值应为canvasViewMock。

      EasyMock.expect(canvasMock.getView())。andReturn(canvasViewMock)

           .anyTimes();

     指定(一旦模拟处于播放模式),每当canvasMock。$ form()为

     调用时,返回值应为formMock。

   EasyMock.expect(canvasMock $形式())andReturn(formMock).anyTimes()。

     指定以下模拟应进入播放模式。

   EasyMock.replay(canvasViewMock,canvasMock,handlerMock);

 }

 @Test   ←Annotation适用于JUnit                             

 public void testCollidingCatMouse()throws Exception {

   TestSprite cat = TestSprite.createTestSprite(canvasMock,handlerMock,

       CAT_HEIGHT,CAT_WIDTH);

   cat.MoveTo(95,83);

   TestSprite mouse = TestSprite.createTestSprite(canvasMock,handlerMock,

       MOUSE_HEIGHT,MOUSE_WIDTH);

   mouse.MoveTo(98,86);

   assertTrue(Sprite.colliding(cat,mouse);

   assertTrue(Sprite.colliding(mouse,cat);

 }

}

图13:SpriteTest的摘录,说明了PowerMock的使用

系统测试

您还应该使用新组件至少有两个示例应用程序,并手动测试您可以使用开发服务器构建,编译和运行它们。

5.在调色板类别中订购组件

待写(安德鲁)

6.国际化

方法,事件和属性的名称需要国际化,以便它们可以以多种语言出现。不要对这些名称使用硬编码字符串。而是在OdeMessages.java中添加变量。类似的组件描述。

将每个新属性/事件/方法的条目添加到OdeMessages.java中(除非在其他组件中已经使用了相同的名称。例如,要添加“Foo”  属性,您将添加:

@DefaultMessage( “富”)

@Description(“这个美妙的Foo属性的名字”)

String FooProperties();

添加方法和事件时,FooMethods()和FooEvents()的过程相同。

不同语言的本地化字符串可以位于:appinventor / appengine / src / com / google / appinventor / client.OdeMessages <language> .properties

系统的设计使得如果没有翻译,则使用英语。如果在实现属性时无法为其他语言提供翻译,则可以在以后添加翻译。

如果您添加或编辑组件的描述,你必须 找到并更新OdeMessages.java说明。在@DesignerComponent注释中编辑到description属性是不够的。您还必须在OdeMessages.java中编辑(或添加)HelpStringComponentPallette变量。例如,这是注释中Spinner的描述:

description =“<p>一个微调器组件,显示一个包含元素列表的弹出窗口。” +

            “通过设置”+“,可以在Designer或Blocks Editor中设置这些元素

            “<code> ElementsFromString </ code>属性为字符串分隔的连接”+

            “(例如,<em>选择1,选择2,选择3 </ em>)或通过设置”+

            “<code> Elements </ code>属性到Blocks编辑器中的List。”

Ť 他下面还需要定义OdeMessages:

@DefaultMessage(“<p>一个spinner组件,显示一个带有元素列表的弹出窗口。通过将<code> ElementsFromString </ code>属性设置为字符串分隔,可以在Designer或Blocks Editor中设置这些元素连接(例如,<em>选择1,选择2,选择3 </ em>)或通过在块编辑器中将<code> Elements </ code>属性设置为List。</ p>“)

@描述(””)

String SpinnerHelpStringComponentPallette();

如果您的方法或事件具有参数,则需要在方法或事件块中添加该名称的翻译。请注意,参数名称在所有组件块之间共享,因此您的参数名称很可能已被翻译。如果确实需要添加新的参数名称,比如“specialResultName”,则应将其添加到OdeMessages.java:

  @DefaultMessage( “specialResultName”)

  @Description(“特殊结果参数的名称”)

  String specialResultNameParams();

然后进入

./appengine/src/com/google/appinventor/client/TranslationComponentParams.java

如下:

map.put(“specialResultName”,MESSAGES.specialResultNameParams());

然后将“specialResutlName”的本地化字符串添加到OdeMessages <language> .properties

7. 支持传统设备

每个人都喜欢拥有最新的手机。但是许多App Inventor用户,尤其是学校,都有较旧的设备。截至2015年夏季,约有5%的App Inventor用户将App Inventor与运行Android系统2.3(Gingerbread)或更早版本的手机配合使用。构建组件时,应该设计它们以便它们在系统2.3(API级别9)上运行。当然,这并不总是可行的:许多现代Android功能需要更高的API级别,因此您应该确保您的组件提供良好的错误消息或在旧手机上优雅地失败,并且有各种方法来执行此操作。  

使用新功能时,应查看Android开发人员文档,以查看引入它的API级别。如果它晚于9,则组件应测试其运行的设备的API级别,并采取适当的措施。您可以使用SDKLevel.getLevel()来检查设备的操作系统。

如果设备太旧而无法进行某些操作,您可能会做的一件事就是提醒最终用户该功能无法正常工作。另一种方法是创建在旧手机上运行的操作的替代版本。您可以在App Inventor源代码中找到这两种技术的示例。

无论您使用何种方法,都应该在提交系统2.3以进行审核之前测试您的系统2.3(甚至更早!)。如果您没有旧手机,则可以使用Android SDK管理器创建运行较旧Android版本的模拟器。


[1]  另一种扩展是“又一种中间语言”。

原文


原文