内容简介
现在移动开发是个很热的话题,特别是当Adobe从工具到平台都针对移动应用做了很多增强之后,身为ActionScript开发者的我们,是不是已经跃跃欲试了?基于Adobe Flash平台,我们可以开发两种类型的移动项目:ActionScript移动项目和Flex移动项目。基于Flex框架的移动项目,可以使用Flex框架的核心内容以及专为移动设备优化的移动组件和外观,比如按钮,列表等,如果内容的尺寸超出了屏幕的显示范围,还可以用Scroller容器包含该内容,用户可以在移动设备的触摸屏上,用手指和界面交互,控制超出部分内容的显示。如图1,展示了一个Scroller容器包含的地图组件:
图1:Flex项目中可以用Scroller容器处理超出的内容
显然基于Flex框架开发移动应用可以降低我们的开发成本。但是,在一些性能敏感的应用中,Flex框架还是显的太"重"了,所以还会有相当数量的移动项目,我们会基于纯ActionScript进行开发。对于纯ActionScript项目来说,显而易见的好处是,我们有更大的灵活性,更容易优化和控制文件的体积。但是问题也随之而来,对于一些基本的组件和容器等需求,我们也不得不“自己动手,丰衣足食”。虽然网络上已经有一些针对ActionScript项目类型的轻量级UI库,但似乎很难找到对移动设备有完善支持的。
如果有这么一个开源库,那么我们期待它可以包含哪些组件?除了按钮,复选框等这样常见的控件类型,还有我们刚才所说的,对于大尺寸的超出屏幕显示区域的显示对象,应该实现一个类似于Flex中的Scroller这样的容器,来处理超出部分的显示。再联想一下,列表这样的控件跟Scroller的行为非常类似,也是我们非常需要的一个组件。
在这篇文章和下一篇文章中,我们将研讨如何创建两个都具备滚动处理机制的组件:一个是ScrollableContainer,这是一个容器,类似于Sprite,但它可以处理超出内容的显示;一个是ScrollableList,它和Flex中的List组件性质类似,都是根据数据源,展现一个可视和可交互的数据列表。
这些源码已经放在瑞研社区(RIADEV)的开源项目RIASamples中,大家可以用SVN检出所有的源码。
SVN地址:
http://svn.riadev.com/riasamples/trunk/source/riadev-mobile-lib/
前置知识
您需要具备基础的ActionScript 3编程经验,并对于移动项目的特点(屏幕尺寸,交互方式,触碰事件)有一定的了解。
所需软件
您需要下载和安装下面的软件,以便运行这篇文章的相关源码:
- Flash Builder 4.5 | 下载试用
实现过程
下面我们就来详细了解如何实现ScrollableContainer这个容器。先来看一个SWF展示,了解它的基本特性(您可以拖动鼠标和它交互):
它的基本特性:
- 继承自Sprite,是显示对象容器,可以用addChild等方法添加或删除显示列表中的元件
- 内建遮罩,超出尺寸的部分不予显示,但可以通过鼠标点击或Touch控制内容滚动
- 内容滚动时,显示一个滚动条(不可交互),指示当前滚动的位置
ScrollableContainer类的实现
首先,它是一个容器,所以继承自Sprite。
然后,我们考虑应该给它添加哪些属性。对于一个可滚动的容器,我们应该可以设置按水平方向滚动,或按垂直方向滚动,或者两者兼顾。所以增加两个属性,并且是getter和setter形式的:
-
/**@private*/
-
private var _horizontalScrollEnabled:Boolean= true ;
-
/**@private*/
-
private var _verticalScrollEnabled:Boolean= true ;
-
/**水平可滚动*/
-
public function get horizontalScrollEnabled ( ):Boolean
-
{
-
return _horizontalScrollEnabled ;
-
}
-
public function set horizontalScrollEnabled (value:Boolean ): void
-
{
-
_horizontalScrollEnabled = value ;
-
}
-
/**垂直可滚动*/
-
public function get verticalScrollEnabled ( ):Boolean
-
{
-
return _verticalScrollEnabled ;
-
}
-
public function set verticalScrollEnabled (value:Boolean ): void
-
{
-
_verticalScrollEnabled = value ;
-
}
然后我们要添加一个容器:contentGroup,类型也是Sprite。注意这个容器非常重要,之后我们调用ScrollableContainer的addChild等方法时,实际上是对contentGroup进行操作。我们之所以取消默认的显示列表添加机制,将实际内容放在这样一个子容器里,是为了方便去实现滚动交互,这样我们只控制contentGroup这个容器就可以,而不必循环去让每一个子显示对象都移动位置。
然后需要加一个显示对象,但这个显示对象不是用于显示的,而是用做遮罩,超出这个遮罩范围的内容将不会显示出来。如果您之前使用过Flash Professional软件进行动画创作,应该会遮罩这个概念非常熟悉,这在Flash里确实是个很神奇而且很实用的东东;如果您没有Flash Professional的使用体验,对于遮罩可能会产生一些疑惑,这个机制从某些方面来看确实也很怪异,既然只是确定一个显示区域,那应该用Rectangular就可以了,何必多此一举,还要用显示对象呢?个人感觉,这应该是为了兼容在Flash Professional中进行动画创作的流程,在这个流程中遮罩确实是显示对象。关于遮罩的定义和用途,建议您浏览这篇文章,加深理解:
http://help.adobe.com/zh_CN/flash/cs/using/WS9388626D-B940-43f3-87BB-7C3159F5EDE4.html
然后我们需要4个变量,来分别存储:容器的约束尺寸(即用户为组件设置的尺寸),和contengGroup的实际尺寸,注意两者是不一样的,通常情况下contentGroup的实际尺寸要比容器的约束尺寸要大,所以才会有用滚动显示超出内容的需求。
容器支持简单的样式设置,所以需要一个对象来存储样式的值,在后面的setStyle方法中,会对这个对象进行操作:
-
/**样式对象*/
-
private var style:Object ;
这里我们还需要增加一个needUpdate的机制,这个机制利用了延迟渲染显示列表的机制(当尺寸变更,样式变更等因素导致需要重新渲染显示列表时,我们只是在代码中加以标记,防止一些重复性的操作导致性能问题(比如我们代码先设置组件宽度,再设置组件高度,那么渲染两次是没有必要的,应该合并为一次渲染)。关于这个机制的解释和详细说明,参见这篇文章。)
然后创建一个createChildren方法,并在构造方法中调用。这个方法需要创建容器必须的对象。
然后我们需要创建一个measure方法,顾名思义,这个方法的作用就是度量contentGroup的实际尺寸。这是一个性能方面的优化,大家可以看到,在必要的时候会对contentGroup进行度量,然后后面很多的计算方法中将直接使用度量好的值,这将节省一些非必须的性能消耗。类似于这样的方式,在我们尝试对应用的执行性能进行优化的时候,非常有效。
这里我们需要思考一个问题,我们创建了一个contentGroup容器,并且希望用户将内容添加到contentGroup容器,而不是ScrollableContainer容器。那么应该提供给用户什么样的接口?ScrollableContainer.contentGroup.addChild() ? 这样会产生一些问题,首先暴露了内部逻辑给用户,给用户造成迷惑,而且用户不一定按照您预想的方式去调用“正确”的方法,他很可能习惯性的仍然使用ScrollableContainer.addChild()。为了避免这些问题,我们决定对用户隐藏细节,用户仍然可以将ScrollableContainer视为一个普通的容器进行操作,但我们内部要override一些方法,以取消默认的行为。
-
/**下面override的这些方法,都是为了更正contentGroup可能导致的错误行为*/
-
-
/**@private*/
-
override public function addChild (child:DisplayObject ):DisplayObject
-
{
-
contentGroup.addChild (child ) ;
-
measure ( ) ;
-
return child ;
-
}
-
-
/**@private*/
-
override public function addChildAt (child:DisplayObject, index: int ):DisplayObject
-
{
-
contentGroup.addChildAt (child, index ) ;
-
measure ( ) ;
-
return child ;
-
}
-
-
/**@private*/
-
override public function removeChild (child:DisplayObject ):DisplayObject
-
{
-
contentGroup.removeChild (child ) ;
-
measure ( ) ;
-
return child ;
-
}
-
-
/**@private*/
-
override public function removeChildAt (index: int ):DisplayObject
-
{
-
var child:DisplayObject=contentGroup.removeChildAt (index ) ;
-
measure ( ) ;
-
return child ;
-
}
-
-
/**@private*/
-
override public function setChildIndex (child:DisplayObject, index: int ): void
-
{
-
contentGroup.setChildIndex (child, index ) ;
-
}
-
-
/**@private*/
-
override public function getChildAt (index: int ):DisplayObject
-
{
-
return contentGroup.getChildAt (index ) ;
-
}
-
-
/**@private*/
-
override public function getChildByName (name:String ):DisplayObject
-
{
-
return contentGroup.getChildByName (name ) ;
-
}
-
-
/**@private*/
-
override public function getChildIndex (child:DisplayObject ): int
-
{
-
return contentGroup.getChildIndex (child ) ;
-
}
-
-
/**@private*/
-
override public function get numChildren ( ): int
-
{
-
return contentGroup.numChildren ;
-
}
同样我们会重写关于尺寸的定义,我们仍然让用户通过设置width和height来控制容器的尺寸,但这个行为会被我们修改,我们不会直接将这些值设置到容器的尺寸上,而是保存到内部的变量上,并将needUpdate设置为true,在下一次渲染的时候,根据约束尺寸,控制内部容器的显示。如果不更改这个行为,用户对尺寸的定义会让容器产生错误的后果(试想一个100*100的图片,被调整为200*200,是什么效果?显然是整体被放大了)。
-
/**下面对于尺寸的定义,会被容器更改默认行为*/
-
-
/**@private*/
-
override public function get width ( ):Number
-
{
-
return containerWidth ;
-
}
-
-
/**@private*/
-
override public function set width (value:Number ): void
-
{
-
if (containerWidth == value )
-
return ;
-
containerWidth=value ;
-
needUpdate= true ;
-
}
-
-
/**@private*/
-
override public function get height ( ):Number
-
{
-
return containerHeight ;
-
}
-
-
/**@private*/
-
override public function set height (value:Number ): void
-
{
-
if (containerHeight == value )
-
return ;
-
containerHeight=value ;
-
needUpdate= true ;
-
}
我们需要创建一个updateDisplayList方法,当needUpdate设置为true,那么延迟到下一帧,就会调用这个方法对显示列表进行更新:
我们还需要考虑到一些意外情况,比如一个loader,加载内容前和加载内容后的尺寸是不一样的,这可能会导致容器的计算错误,所以需要增加一个强制更新的方法,必要的时候调用这个方法强制刷新显示列表:
我们允许用户设置简单的样式,并且样式的更改应该触发显示列表的更新:
目前的样式支持,只是绘制边框和背景,所以有个绘制背景的方法:
做到这一步,您已经可以进行简单的测试了,比如在您的主类里,创建一个ScrollableContaienr的实例,添加一个尺寸较大的图片进去,然后改变浏览器的尺寸,去观察这个容器的行为。类似于下面的语句:
运行结果:
图2:图片已经应用了遮罩
注意虽然超出的内容已经被隐藏了,但现在还无法对它进行交互。我们把交互单独设计为一个控制类ScrollControler来实现,它不需要是显示对象,来看它的实现过程:
ScrollControler类的实现
类定义:
有一些值实际上是从ScrollableContainer拷贝过来,便于计算,重新定义一次:
这里需要传递两个容器,一个是contengGroup,一个是ScrollableContainer本身,比较这两者的尺寸,才能计算出滚动所需的值。
-
/**包含内容的容器*/
-
private var contentGroup:Sprite ;
-
/**容器的父级,即ScrollableContainer*/
-
private var parent:Sprite ;
滚动时,需要知道滚动的方向,和滚动的速度,存在下面的属性中:
-
/**X轴速度*/
-
private var speedX: int= 0 ;
-
/**Y轴速度*/
-
private var speedY: int= 0 ;
-
/**手指的水平方向,即内容的水平滚动方向*/
-
private var directionX:String= "left" ; //or right,手指方向
-
/**手指的垂直方向,即内容的垂直滚动方向*/
-
private var directionY:String= "up" ; //or down,手指方向
启动滚动时的初始速度,是根据光标位置的迁移量,和停顿时间综合计算的,这些值需要保存,以供计算:
-
/**光标坐标点距离起点的位移X*/
-
private var currentMouseOffsetX:Number= 0 ;
-
/**光标坐标点距离起点的位移Y*/
-
private var currentMouseOffsetY:Number= 0 ;
-
/**启动拖放时的坐标点X*/
-
private var startX:Number ;
-
/**启动拖放时的坐标点Y*/
-
private var startY:Number ;
-
/**结束拖放时的坐标点X*/
-
private var endX:Number ;
-
/**结束拖放时的坐标点Y*/
-
private var endY:Number ;
-
/**启动拖放时的时间*/
-
private var startTime:Number ;
-
/**结束拖放时的时间*/
-
private var endTime:Number ;
在构造方法中,将所依赖的两个容器传入。
-
public function ScrollControler (contentGroup:Sprite, parent:Sprite )
-
{
-
this.contentGroup=contentGroup ;
-
this.parent=parent ;
-
initContainer ( ) ;
-
}
-
/**
-
* 什么也不做,等待容器添加到显示列表
-
*/
-
private function initContainer ( ): void
-
{
-
container.addEventListener (Event.ADDED_TO_STAGE,addListeners ) ;
-
container.addEventListener (Event.REMOVED_FROM_STAGE,clear ) ;
-
}
-
/**
-
* 当容器添加到显示列表,则注册事件侦听
-
* @param event
-
*/
-
protected function addListeners ( event:Event ): void
-
{
-
container.addEventListener (MouseEvent.MOUSE_DOWN, containerMouseHandler ) ;
-
container.stage.addEventListener (Event.MOUSE_LEAVE, mouseLeaveHandler ) ;
-
}
在containerMouseHandler这个方法里面,集中了对于鼠标事件(单点Touch事件会自动进行映射)的判断和处理。实现的过程是:首先捕获到MouseDown事件,这个时候启动拖动,让contentGroup容器跟随用户的光标进行移动,当捕获到MouseUp事件,则根据移动的坐标值和停留时间综合计算,得出X和Y方向上的起始速度,然后通过EnterFrame事件触发的方法执行,去实现动画。
下图展示了这个过程:
图3:触发滚动的过程
-
private function containerMouseHandler ( event:Event ): void
-
{
-
if ( event.type == MouseEvent.MOUSE_DOWN )
-
{
-
startX=parent.mouseX ;
-
startY=parent.mouseY ;
-
startTime=getTimer ( ) ;
-
currentMouseOffsetX=parent.mouseX - container.x ;
-
currentMouseOffsetY=parent.mouseY - container.y ;
-
fllowMouse= true ;
-
container.stage.addEventListener (MouseEvent.MOUSE_UP, containerMouseHandler ) ;
-
container.addEventListener (Event.ENTER_FRAME, enterFrameHandler ) ;
-
}
-
else if ( event.type == MouseEvent.MOUSE_UP )
-
{
-
endX=parent.mouseX ;
-
endY=parent.mouseY ;
-
endTime=getTimer ( ) ;
-
var timeOffset:Number=endTime - startTime ;
-
directionX= (endX <= startX ) ? "left" : "right" ;
-
directionY= (endY <= startY ) ? "up" : "down" ;
-
speedX= (endX - startX ) / (timeOffset / 20 ) ;
-
speedY= (endY - startY ) / (timeOffset / 20 ) ;
-
currentMouseOffsetX= 0 ;
-
currentMouseOffsetY= 0 ;
-
fllowMouse= false ;
-
checkXRange ( ) ;
-
checkYRange ( ) ;
-
container.stage.removeEventListener (MouseEvent.MOUSE_UP, containerMouseHandler ) ;
-
}
-
}
动画的实现,实际上是在enterFrameHandler这个方法里完成的。当起始速度被设置,意味着动画开始,然后速度值进行递增或递减,实现缓动效果,滚动会逐渐减速,最终停止。
-
private function enterFrameHandler ( event:Event ): void
-
{
-
if (fllowMouse )
-
{
-
if (horizontalScrollEnabled )
-
container.x=parent.mouseX - currentMouseOffsetX ;
-
if (verticalScrollEnabled )
-
container.y=parent.mouseY - currentMouseOffsetY ;
-
scrollBarRef.update (container.x,container.y ) ;
-
return ;
-
}
-
if (Math.abs (speedX ) < 4 )
-
speedX= 0 ;
-
if (Math.abs (speedY ) < 4 )
-
speedY= 0 ;
-
if (speedX == 0 && speedY == 0 )
-
{
-
scrollBarRef.clear ( ) ;
-
container.removeEventListener (Event.ENTER_FRAME, enterFrameHandler ) ;
-
return ;
-
}
-
container.x+=speedX ;
-
container.y+=speedY ;
-
checkXRange ( ) ;
-
checkYRange ( ) ;
-
if (directionX == "left" )
-
speedX+= 1 ;
-
else
-
speedX-= 1 ;
-
if (directionY == "up" )
-
speedY+= 1 ;
-
else
-
speedY-= 1 ;
-
scrollBarRef.update (container.x,container.y ) ;
-
}
在滚动的过程中,需要判断坐标值出界的情况,一旦出界,则停止动画。
-
/**检查X值,确定不超出允许范围*/
-
private function checkXRange ( ): void
-
{
-
if (contentWidth <= parentWidth )
-
{
-
speedX= 0 ;
-
container.x= 0 ;
-
return ;
-
}
-
if (container.x + contentWidth <= parentWidth )
-
{
-
speedX= 0 ;
-
container.x=parentWidth - contentWidth ;
-
return ;
-
}
-
if (container.x >= 0 )
-
{
-
speedX= 0 ;
-
container.x= 0 ;
-
return ;
-
}
-
}
-
-
/**检查Y值,确定不超出允许范围*/
-
private function checkYRange ( ): void
-
{
-
if (contentHeight <= parentHeight )
-
{
-
speedY= 0 ;
-
container.y= 0 ;
-
return ;
-
}
-
if (container.y + contentHeight <= parentHeight )
-
{
-
speedY= 0 ;
-
container.y=parentHeight - contentHeight ;
-
return ;
-
}
-
if (container.y >= 0 )
-
{
-
speedY= 0 ;
-
container.y= 0 ;
-
return ;
-
}
-
}
至此这个滚动控制类实现完毕,下面需要和我们刚才创建的ScrollableContainer容器进行整合。
为ScrollableContainer增加滚动交互控制
回到ScrollableContainer类,首先声明变量:
修改createChildren方法,增加实例化的代码:
修改updateDisplayList方法,将尺寸传递给控制类:
OK,现在再运行刚才的测试代码,您可以看到容器已经可交互了,图片已经可以随着您的鼠标拖动,而以缓冲的效果进行滚动。现在美中不足的是,没有一个滚动条的显示来明确指出当前的滚动位置,我们可以再创建一个ScrollBar类来实现这个功能。这里不再对这个类的实现进行详细描述,代码中已经对主体部分做了注释:
ScrollBar.as-
package com.riadev.mobile.ui.support
-
{
-
import com.greensock.TweenLite ;
-
-
import flash.display.Graphics ;
-
import flash.display.Shape ;
-
-
/**
-
* 只用于显示滚动位置,不可交互
-
* @author NeoGuo
-
*
-
*/
-
public class ScrollBar extends Shape
-
{
-
/**水平可滚动*/
-
public var horizontalScrollEnabled:Boolean= true ;
-
/**垂直可滚动*/
-
public var verticalScrollEnabled:Boolean= true ;
-
-
/**实际内容宽度*/
-
public var contentWidth:Number = 0 ;
-
/**实际内容高度*/
-
public var contentHeight:Number = 0 ;
-
/**组件可用宽度*/
-
public var parentWidth:Number = 0 ;
-
/**组件可用高度*/
-
public var parentHeight:Number = 0 ;
-
-
/**线段与边界的距离*/
-
private var paddingValue:Number = 10 ;
-
/**线段的粗细程度*/
-
private var lineStroke:Number = 10 ;
-
/**
-
* 构造方法
-
*/
-
public function ScrollBar ( )
-
{
-
super ( ) ;
-
}
-
/**
-
* 更新当前的显示图形
-
* @param scrollX contentGroup的x坐标
-
* @param scrollY contentGroup的y坐标
-
*/
-
public function update (scrollX:Number,scrollY:Number ): void
-
{
-
var g:Graphics = this.graphics ;
-
g.clear ( ) ;
-
//draw horizontal graphics
-
if (horizontalScrollEnabled && contentWidth > parentWidth )
-
{
-
g.lineStyle (lineStroke+ 2, 0x000000, 0 .5 ) ;
-
var endY:Number = parentHeight-paddingValue ;
-
g.moveTo (paddingValue,endY ) ;
-
g.lineTo (parentWidth-paddingValue,endY ) ;
-
g.lineStyle (lineStroke, 0xFFFFFF, 1 ) ;
-
var lineWidth:Number = (parentWidth-2*paddingValue )* (parentWidth/contentWidth ) ;
-
var lineStartX:Number = -scrollX/ (contentWidth-parentWidth )* (parentWidth-2*paddingValue-lineWidth ) + paddingValue ;
-
if (lineStartX<paddingValue )
-
{
-
lineWidth -= (paddingValue-lineStartX ) ;
-
lineStartX = paddingValue ;
-
}
-
if (lineStartX+lineWidth > parentWidth-paddingValue )
-
{
-
lineWidth -= lineStartX+lineWidth- (parentWidth-paddingValue ) ;
-
}
-
g.moveTo (lineStartX,endY ) ;
-
g.lineTo (lineStartX+lineWidth,endY ) ;
-
}
-
//draw vertical graphics
-
if (verticalScrollEnabled && contentHeight > parentHeight )
-
{
-
g.lineStyle (lineStroke+ 2, 0x000000, 0 .5 ) ;
-
var endX:Number = parentWidth-paddingValue ;
-
g.moveTo (endX,paddingValue ) ;
-
g.lineTo (endX,parentHeight-paddingValue ) ;
-
g.lineStyle (lineStroke, 0xFFFFFF, 1 ) ;
-
var lineHeight:Number = (parentHeight-2*paddingValue )* (parentHeight/contentHeight ) ;
-
var lineStartY:Number = -scrollY/ (contentHeight-parentHeight )* (parentHeight-2*paddingValue-lineHeight ) + paddingValue ;
-
if (lineStartY<paddingValue )
-
{
-
lineHeight -= (paddingValue-lineStartY ) ;
-
lineStartY = paddingValue ;
-
}
-
if (lineStartY+lineHeight > parentHeight-paddingValue )
-
{
-
lineHeight -= lineStartY+lineHeight- (parentHeight-paddingValue ) ;
-
}
-
g.moveTo (endX,lineStartY ) ;
-
g.lineTo (endX,lineStartY+lineHeight ) ;
-
}
-
}
-
/**
-
* 清理
-
*/
-
public function clear ( ): void
-
{
-
TweenLite.to ( this, 0 .5,{alpha: 0,onComplete:reset} ) ;
-
}
-
/**
-
* 重置
-
*/
-
private function reset ( ): void
-
{
-
var g:Graphics = this.graphics ;
-
g.clear ( ) ;
-
alpha = 1 ;
-
}
-
}
-
}
现在尝试重新运行测试代码,拖动鼠标,可以看到实时的图形显示:
图4:滚动条效果
在下一篇文章里,我们将继续封装可滚动的List组件。
后续工作
您可以尝试使用这个容器,体验它的特性,当然它的代码刚刚编写完毕,还没有经过大量实践的验证,可能存在一些缺陷和Bug,欢迎您批评和指正。然后您可以继续浏览Adobe开发者中心,寻找移动开发相关的文章和资源,加深这一领域的知识技能。