Feathers:item-renderers
原文地址:http://wiki.starling-framework.org/feathers/item-renderers
目录 |
为Feathers创建列表控制的项目渲染器
DefaultListItemRenderer类提供了丰富的自定义选项来支持列表项目的布局和外观的渲染器。然而,有时它不能提供自定义列表的能力或者布局选项,并且你需要创建自定义的项目渲染器类。
布局虚拟化
默认情况下,所有的内置布局类在Feathers中的布局虚拟化默认打开。该特性被列表组件用来限制项目渲染器实例在任何给定的时间内被创建时的数量。这意味着有一千项的列表几乎可以和一个项的列表一样快,因为只有可见的项目渲染器才会被创建。列表还会用新数据做为列表滚轴重用项目渲染器。 对你来说意味着什么?作为一名开发人员要创建这样一个自定义项渲染器,你的渲染器做到在项目渲染器接收到完全不同的项目时能够显示出来。在以下一个示例中,我们可以看到它是如何做到的。
一个微型项目渲染器
为了方便起见,让一个基本的项目渲染器类写在下面。它只是简单地在'Label'类组件中显示一些文本。它可以是一个很好的起点让您自己定制项目渲染器。 在后面的部分,更多的一些功能将被添加到这个项目渲染器。
package { import feathers.controls.Label; import feathers.controls.List; import feathers.controls.renderers.IListItemRenderer; import feathers.core.FeathersControl; import starling.events.Event; public class BasicItemRenderer extends FeathersControl implements IListItemRenderer { public function BasicItemRenderer() { } protected var itemLabel:Label; protected var _index:int = -1; public function get index():int { return this._index; } public function set index(value:int):void { if(this._index == value) { return; } this._index = value; this.invalidate(INVALIDATION_FLAG_DATA); } protected var _owner:List; public function get owner():List { return List(this._owner); } public function set owner(value:List):void { if(this._owner == value) { return; } this._owner = value; this.invalidate(INVALIDATION_FLAG_DATA); } protected var _data:Object; public function get data():Object { return this._data; } public function set data(value:Object):void { if(this._data == value) { return; } this._data = value; this.invalidate(INVALIDATION_FLAG_DATA); } protected var _isSelected:Boolean; public function get isSelected():Boolean { return this._isSelected; } public function set isSelected(value:Boolean):void { if(this._isSelected == value) { return; } this._isSelected = value; this.invalidate(INVALIDATION_FLAG_SELECTED); this.dispatchEventWith(Event.CHANGE); } override protected function initialize():void { if(!this.itemLabel) { this.itemLabel = new Label(); this.addChild(this.itemLabel); } } override protected function draw():void { const dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA); const selectionInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SELECTED); var sizeInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SIZE); if(dataInvalid) { this.commitData(); } sizeInvalid = this.autoSizeIfNeeded() || sizeInvalid; if(dataInvalid || sizeInvalid) { this.layout(); } } protected function autoSizeIfNeeded():Boolean { const needsWidth:Boolean = isNaN(this.explicitWidth); const needsHeight:Boolean = isNaN(this.explicitHeight); if(!needsWidth && !needsHeight) { return false; } this.itemLabel.width = NaN; this.itemLabel.height = NaN; this.itemLabel.validate(); var newWidth:Number = this.explicitWidth; if(needsWidth) { newWidth = this.itemLabel.width; } var newHeight:Number = this.explicitHeight; if(needsHeight) { newHeight = this.itemLabel.height; } return this.setSizeInternal(newWidth, newHeight, false); } protected function commitData():void { if(this._data) { this.itemLabel.text = this._data.toString(); } else { this.itemLabel.text = ""; } } protected function layout():void { this.itemLabel.width = this.actualWidth; this.itemLabel.height = this.actualHeight; } } }
几点注意:
- 当数据属性变化,项目渲染器是无效的。这使得在Starling下次渲染之前,项目渲染器就进行重绘。当用isInvalid()函数检测到'INVALIDATION_FLAG_DATA'时,commitData()函数被调用来更新内部'Label'组件上的文本。
- 为方便起见,我们在这里使用'Label'。如果您正在使用一个主题,它将自动使用默认的文本渲染器更换皮肤。你也可以用FeathersControl.defaultTextRendererFactory()直接创建一个文本渲染器。这也和默认渲染器一样。您也可以通过文本渲染器添加一个某种文本格式的属性到你的项目渲染器类,。
- 当'isSelected'发生变化时,别忘了发出'Event.CHANGE'事件。列表需要侦听该事件以便正确地改变被选择的项目。当你不希望这样变化时,你可以不这样做,然而在被你选择的列表中,你可能会得到多个项目渲染器。
用触摸进行选择
除非你在列表上设定selectedIndex或设置selectedItem,不然在上述类中没有提供任何可以改变的选择列表项目。如果我们希望用户能够触摸一个项目就选择它,我们需要自己添加。也许有其他的方式来选择在一个列表上的项目,比如触发一个按钮子组件或选择一个复选框,从而Feathers List组件在这里没有任何问题。这全取决于项目渲染器是否正确地执行工作。
首先,让我们在构造函数中添加一个侦听器TouchEvent:
public function BasicItemRenderer() { this.addEventListener(TouchEvent.TOUCH, touchHandler); }
现在,触摸事件有了侦听器:
protected function touchHandler(event:TouchEvent):void { const touches:Vector.<Touch> = event.getTouches(this); if(touches.length == 0) { //hover has ended return; } if(this.touchPointID >= 0) { var touch:Touch; for each(var currentTouch:Touch in touches) { if(currentTouch.id == this.touchPointID) { touch = currentTouch; break; } } if(!touch) { return; } if(touch.phase == TouchPhase.ENDED) { this.touchPointID = -1; touch.getLocation(this, HELPER_POINT); //check if the touch is still over the target //also, only change it if we're not selected. we're not a toggle. if(this.hitTest(HELPER_POINT, true) != null && !this._isSelected) { this.isSelected = true; } return; } } else { for each(touch in touches) { if(touch.phase == TouchPhase.BEGAN) { this.touchPointID = touch.id; return; } } } }
这有点复杂,因为在多点触控的环境下,我们需要确保我们始终跟踪同一触摸它的ID。
上面的侦听器引用了一个名为“touchPointID”的变量。它应该被定义为一个成员变量:
protected var touchPointID:int = -1;
这就是跟踪我们的触摸的ID。它是用TouchPhase.BEGAN设置启用,用TouchPhase.ENDED清除。
还有引用一个命名为'HELPER_POINT'的静态常数:
private static const HELPER_POINT:Point = new Point();
由于性能的原因,我们使用这个以避免每次我们用'touch.getLocation()'获取一个触摸事件的位置时创建一个新的'Point'对象。这有助于避免在运行时中有太多的垃圾回收。
最后,在构造函数中添加一个'Event.REMOVED_FROM_STAGE'的事件侦听器:
this.addEventListener(Event.REMOVED_FROM_STAGE, removedFromStageHandler);
在'TouchPhase.ENDED'时,侦听器函数将设置'touchPointID'为1:
protected function removedFromStageHandler(event:Event):void { this.touchPointID = -1; }
这是一个各种触摸事件在Starling中很好的实践。如果一个显示对象从舞台中移除,同时用户正在触摸它,它可能永远不会侦听到TouchPhase.ENDED事件。然后,当它再一次被添加到舞台,它的'touchPointID'可能对应一个不复存在的旧触摸事件。这就是为什么我们在移除时要清除它的标记。
取消滚动时的相互作用
当列表被滚动时,如果你想停止被选择项'TouchPhase.ENDED'时获得的渲染器,有几种方法能实现。两种情况下,您都需要在'List'类型的owner赋值器上侦听'Event.SCROLL'事件。让我们略微改变owner赋值器:
public function set owner(value:List):void { if(this._owner == value) { return; } if(this._owner) { this._owner.removeEventListener(Event.SCROLL, owner_scrollHandler); } this._owner = value; if(this._owner) { this._owner.addEventListener(Event.SCROLL, owner_scrollHandler); } this.invalidate(INVALIDATION_FLAG_DATA); }
我们花点时间看看'owner_scrollHandler()'的实现。这有点不同的是取决于我们想如何去实现这个方法。
方法1:清除触摸ID
方法1:清除触摸ID 最容易的方法就是当列表滚动简单地设置'touchPointID'为-1。当这样设置后,我们'TouchEvent.TOUCH'事件的侦听器将忽略任何其他触摸事件,直到下一次'TouchPhase.BEGAN'重新出现时。
protected function owner_scrollHandler(event:Event):void { this.touchPointID = -1; }
如果在不同接触状态下你的背景皮肤有变化,当列表滚动时你也应该让它回到向上的状态。因为我们已经清理了保存的触摸ID,我们不能在'TouchPhase.ENDED'或当悬停结束时对它进行比较。
方法2:保持一个布尔类型标志
如果我们需要知道什么时候有TouchPhase.ENDED,但是我们仍然想要追踪是否滚动了列表,我们可以使用一个布尔类型标志来跟踪状态的滚动。首先,让我们添加一个标志作为我们的类中的一个成员变量:
protected var hasScrolled:Boolean = false;
当TouchPhase.BEGAN出现时,我们要设置标志为false。它可能已经被设置为false,但也没关系。
if(touch.phase == TouchPhase.BEGAN) { this.touchPointID = touch.id; this.hasScrolled = false; return; }
在我们的Event.SCROLL侦听器中,我们设置了标志为true。
protected function owner_scrollHandler(event:Event):void { this.hasScrolled = true; }
最后,在TouchPhase.ENDED出现时,我们检查我们的标志的值:
if(touch.phase == TouchPhase.ENDED) { this.touchPointID = -1; if(!this.hasScrolled) { touch.getLocation(this, HELPER_POINT); //check if the touch is still over the target //also, only change it if we're not selected. we're not a toggle. if(this.hitTest(HELPER_POINT, true) != null && !this._isSelected) { this.isSelected = true; } } return; }
如果标志的值仍然是false,我们可以改变选项。如果是true,则因为列表滚动,我们跳过这部分。
子组件的交互
如果在你的项目渲染器中有子组件,如复选框,切换开关、滑动条,和其他的,这技术可以扩展到防止这些组件相互影响。换个说法,如果你与其子组件交互时防止滚动。例如,如果你开始拖动一个滑块,您可能不希望在同一时间内列表进行滚动。
如何实现这个功能取决于子组件的类型。最简单的方法是侦听子组件的TouchEvent.TOUCH事件,然后停止此事件的传播,这样列表就没有收到这个触摸事件的通知。
subComponent.addEventListener( TouchEvent.TOUCH, subComponent_touchHandler );
下面是这个事件的侦听器:
protected function subComponent_touchHandler( event:TouchEvent ):void { event.stopPropagation(); }
根据您的需要和子组件的类型,您可能宁愿使用类似Event.CHANGE、FeathersEventType.BEGIN_INTERACTION和“FeathersEventType.END_INTERACTION的事件。
相关链接
更多教程,返回到Feathers文档.
翻译者:吴金鸿