内容简介
现在移动开发是个很热的话题,特别是当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开发者中心,寻找移动开发相关的文章和资源,加深这一领域的知识技能。
本文探讨如何基于ActionScript3开发移动项目滚动容器,包括ScrollableContainer与ScrollableList组件的设计与实现,以及如何与触摸事件交互。通过具体代码示例,详细介绍了组件的属性设置、事件监听与滚动逻辑,旨在提升移动应用的用户体验。
3371

被折叠的 条评论
为什么被折叠?



