我正在为平板设备构建移动应用程序。
我对具有平铺布局的 List 组件有很大的性能问题。
我在屏幕上显示大约 20-24 个项目的页面,用户可以滚动浏览页面。不幸的是,列表组件生成新页面的速度非常慢。
总的来说,我用平铺布局测试了独立的 List ,它在每种情况下的性能都非常慢(我已经在 iPad1 和 iPad2 上测试过)。
如果您知道问题的一些解决方案,请提供建议。
谢谢。
2 回答
通常,Tile 的弯曲速度非常慢。这是因为它允许每个项目渲染器的大小可变,并且 measure / updateDisplayList 需要大量时间。
在这种情况下,我建议您使用自定义(自己制作的)平铺列表并删除 flex 组件。如果仅将其用于布局,则可以手动/以编程方式计算图块中每个项目的位置并将其移动到右侧 x/y 像素。
还要注意效果!它们可能在 AIR 模拟器或桌面浏览器中运行良好,但会占用移动设备上的大量资源。
如果您仍然想使用 flex tile 组件,而不是创建自己的 tile 控件,那么我建议您对在其中显示的所有项目使用带渲染器的 fixed,并在 TileList declalation 中指定 columnWidth 和 rowHeight。这可能会提高一点性能...... :)
还提出一些示例代码并没有什么坏处。也许你做错了什么……不要:额外的失效,糟糕的渲染等等……!
时间有点久了,具体不记得了。这是我使用的代码,但请记住,我编写的组件是用于特定目的的,并且包含一些不良做法以在很短的时间内完成特定问题。不过我希望有用。
分页列表
package components.pagedList
{
import components.pagedList.vo.PageItemVo;
import mx.collections.ArrayCollection;
import mx.collections.IList;
import mx.core.IFactory;
import mx.core.UIComponentGlobals;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import spark.components.List;
import spark.layouts.HorizontalLayout;
/**
*
*/
public class PagedList extends List
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function PagedList()
{
super();
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* Dimentions for the inner items should be specifyed because
* this component needs to know how many items can be placed in
* one page, but don't have the ability to create this items.
* Default value is 1 to avoid division by zero.
*/
public var innerItemWidth:Number = 1;
public var innerItemHeight:Number = 1;
/**
* When inner item dimentions are set the component calculates how many items
* (per row and col) can be shown on one scren and pass these data to the page
* trough the dataProvider for the page.
*/
private var pageRowCount:int = 1;
private var pageColCount:int = 1;
/**
* Count of the items that can fit in one page.
* This count is used to slice the data into picies
* with the same length.
*/
private var itemsPerPage:int = 0;
/**
* Original data provider is saved here so we can re-slice it when
* size change occure and listen for data change event.
*
* TODO: implement the data chage listener
*/
private var _originalDataProvider:IList;
/**
*
*/
private var cachedUnscaledWidth:Number = 0;
private var cachedUnscaledHeight:Number = 0;
//----------------------------------
// Custom tiles data
//----------------------------------
private var _customTilesData:Array;
private var customTilesDataChanged:Boolean;
public function set customTilesData(value:Array):void
{
_customTilesData = value;
customTilesDataChanged = true;
invalidateProperties();
}
public function get customTilesData():Array
{
return _customTilesData;
}
//----------------------------------
// dataProvider
//----------------------------------
private var originalDataProviderChanged:Boolean;
[Inspectable(category="Data")]
/**
*
*/
override public function set dataProvider(value:IList):void
{
if (_originalDataProvider)
_originalDataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, originalDataProvider_collectionChangeHandler);
originalDataProviderChanged = true;
if (value)
value.addEventListener(CollectionEvent.COLLECTION_CHANGE, originalDataProvider_collectionChangeHandler, false, 0, true);
_originalDataProvider = value;
updatePagedData();
}
/**
*
*/
private function originalDataProvider_collectionChangeHandler(event:CollectionEvent):void
{
//trace('data changed:', event.kind, 'location:', event.location);
if (event.kind == CollectionEventKind.REPLACE)
{
updateReplacedItem(event.location);
}
}
/**
*
*/
private function updateReplacedItem(index:int):void
{
if (dataProvider)
{
var pageNumber:int = int(index / itemsPerPage);
var itemIndex:int = index - (pageNumber * itemsPerPage);
if (dataProvider[pageNumber])
{
var pageData:PageItemVo = PageItemVo(dataProvider[pageNumber])
pageData.data[itemIndex] = _originalDataProvider[index];
}
}
}
//----------------------------------
// innerItemRenderer
//----------------------------------
private var _innerItemRenderer:IFactory;
private var innerItemRendererChanged:Boolean;
public function set innerItemRenderer(value:IFactory):void
{
_innerItemRenderer = value;
innerItemRendererChanged = true;
invalidateProperties();
}
public function get innerItemRenderer():IFactory
{
return _innerItemRenderer;
}
//----------------------------------
// gaps
//----------------------------------
/**
*
*/
public function set verticalGap(value:Number):void
{
_verticalGap = value;
gapsChanged = true;
invalidateProperties();
}
public function get verticalGap():Number
{
return _verticalGap;
}
/**
*
*/
public function set horizontalGap(value:Number):void
{
_horizontalGap = value;
gapsChanged = true;
invalidateProperties();
}
public function get horizontalGap():Number
{
return _horizontalGap;
}
private var _verticalGap:Number;
private var _horizontalGap:Number;
private var gapsChanged:Boolean;
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
protected function updatePagedData():void
{
if (_originalDataProvider)
{
var pagedData:IList = createPagedData(_originalDataProvider);
super.dataProvider = pagedData;
invalidateProperties();
}
}
private function createPagedData(value:IList):IList
{
var nestedData:Array = [];
var dataList:Array = value.toArray();
var pageData:PageItemVo;
if (itemsPerPage)
{
var customTilesCount:int = customTilesData ? customTilesData.length : 0;
var normalItemsPerPage:int = itemsPerPage - customTilesCount;
while (dataList.length)
{
pageData = new PageItemVo();
var data:Array = dataList.splice(0, normalItemsPerPage);
for (var i:int = 0 ; i < customTilesCount ; i++)
{
data.push( customTilesData[i] );
}
pageData.data = new ArrayCollection( data );
pageData.colsCount = pageColCount;
pageData.rowsCount = pageRowCount;
pageData.itemWidth = innerItemWidth
pageData.itemHeight = innerItemHeight;
pageData.horizontalGap = horizontalGap;
pageData.verticalGap = verticalGap;
pageData.innerItemRenderer = _innerItemRenderer;
nestedData.push(pageData);
}
}
return new ArrayCollection(nestedData);
}
//----------------------------------
// Component lifecycle
//----------------------------------
override protected function commitProperties():void
{
if (gapsChanged || innerItemRendererChanged || customTilesDataChanged)
{
updatePagedData();
gapsChanged = false;
innerItemRendererChanged = false;
customTilesDataChanged = false;
}
}
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
caluclateItemsPerPage(unscaledWidth, unscaledHeight);
// We have to update the dataProvider so it can re-slice the pages
// in case of orientation rotation or some other resize
if (_originalDataProvider)
if ( cachedUnscaledWidth != unscaledWidth || cachedUnscaledHeight != unscaledHeight )
dataProvider = _originalDataProvider;
cachedUnscaledWidth = unscaledWidth;
cachedUnscaledHeight = unscaledHeight;
}
protected function caluclateItemsPerPage(unscaledWidth:Number, unscaledHeight:Number):void
{
var myLayout:HorizontalLayout = HorizontalLayout(layout);
var horizontalPaddings:Number = myLayout.paddingLeft + myLayout.paddingRight;
var verticalPaddings:Number = myLayout.paddingTop + myLayout.paddingRight;
pageRowCount = (unscaledHeight - verticalPaddings) / (innerItemHeight + verticalGap);
pageColCount = (unscaledWidth - horizontalPaddings) / (innerItemWidth + horizontalGap);
itemsPerPage = pageRowCount * pageColCount;
}
}
}
页面项目渲染器
package skins.pagedList
{
import components.pagedList.vo.PageItemVo;
import flash.display.DisplayObject;
import mx.collections.ArrayCollection;
import mx.core.IDataRenderer;
import mx.core.IFactory;
import mx.core.UIComponent;
import mx.events.FlexEvent;
import spark.components.IItemRenderer;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the <code>data</code> property changes.
*
* <p>When you use a component as an item renderer,
* the <code>data</code> property contains the data to display.
* You can listen for this event and update the component
* when the <code>data</code> property changes.</p>
*
* @eventType mx.events.FlexEvent.DATA_CHANGE
*/
[Event(name="dataChange", type="mx.events.FlexEvent")]
/**
*
* ASDoc comments for this item renderer class
*
*/
public class PageItemRenderer extends UIComponent
implements IDataRenderer, IItemRenderer
{
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function PageItemRenderer()
{
super();
cacheAsBitmap = true;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
//----------------------------------
// data
//----------------------------------
/**
* @private
*/
private var _data:PageItemVo;
private var dataChanged:Boolean;
[Bindable("dataChange")]
/**
* The implementation of the <code>data</code> property
* as defined by the IDataRenderer interface.
* When set, it stores the value and invalidates the component
* to trigger a relayout of the component.
*/
public function get data():Object
{
return _data;
}
/**
* @private
*/
public function set data(value:Object):void
{
_data = PageItemVo(value);
colCount = _data.colsCount;
rowCount = _data.rowsCount;
itemWidth = _data.itemWidth;
itemHeight = _data.itemHeight;
horizontalGap = _data.horizontalGap;
verticalGap = _data.verticalGap;
innerItemRenderer = _data.innerItemRenderer;
_tilesData = ArrayCollection(_data.data);
dataChanged = true;
if (hasEventListener(FlexEvent.DATA_CHANGE))
dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
invalidateProperties();
}
//----------------------------------
// gaps
//----------------------------------
/**
*
*/
public function set verticalGap(value:Number):void
{
_verticalGap = value;
gapsChanged = true;
invalidateProperties();
}
public function get verticalGap():Number
{
return _verticalGap;
}
/**
*
*/
public function set horizontalGap(value:Number):void
{
_horizontalGap = value;
gapsChanged = true;
invalidateProperties();
}
public function get horizontalGap():Number
{
return _horizontalGap;
}
private var _verticalGap:Number = 5;
private var _horizontalGap:Number = 5;
private var gapsChanged:Boolean;
//----------------------------------
// itemIndex
//----------------------------------
private var _itemIndex:int;
public function get itemIndex():int
{
return _itemIndex;
}
public function set itemIndex(value:int):void
{
if (value == _itemIndex)
return;
invalidateDisplayList();
_itemIndex = value;
}
//----------------------------------
// showsCaret
//----------------------------------
private var _showsCaret:Boolean = false;
public function get showsCaret():Boolean
{
return _showsCaret;
}
/**
* @private
*/
public function set showsCaret(value:Boolean):void
{
if (value == _showsCaret)
return;
_showsCaret = value;
invalidateDisplayList();
}
//----------------------------------
// selected
//----------------------------------
private var _selected:Boolean = false;
public function get selected():Boolean
{
return _selected;
}
public function set selected(value:Boolean):void
{
if (value == _selected)
return;
_selected = value;
}
//----------------------------------
// dragging
//----------------------------------
private var _dragging:Boolean = false;
public function get dragging():Boolean
{
return _dragging;
}
public function set dragging(value:Boolean):void
{
_dragging = value;
}
//----------------------------------
// label
//----------------------------------
private var _label:String;
public function get label():String
{
return _label;
}
/**
* @private
*/
public function set label(value:String):void
{
_label = value;
}
//----------------------------------
// item properties
//----------------------------------
/**
* Dimentions for the inner items should be specifyed because
* this component needs to know how many items can be placed in
* one page, but don't have the ability to create this items.
* Default value is 1 to avoid division by zero.
*/
public var itemWidth:Number = 1;
public var itemHeight:Number = 1;
/**
* When inner item dimentions are set the component calculates how many items
* (per row and col) can be shown on one scren and pass these data to the page
* trough the dataProvider for the page.
*/
public var rowCount:int = 1;
public var colCount:int = 1;
private var _tilesData:ArrayCollection;
private var sizeChanged:Boolean;
private var _tileContainer:UIComponent;
/**
*
*/
private var innerItemRenderer:IFactory;
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
//----------------------------------
// Component lifecycle
//----------------------------------
override protected function commitProperties():void
{
super.commitProperties();
if (dataChanged)
{
dataChanged = false;
createTiledContent();
}
if (gapsChanged)
{
createTiledContent();
}
}
override protected function measure():void
{
super.measure();
measuredMinHeight = measuredWidth = parent.width;
measuredMinHeight = measuredHeight = parent.height;
}
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
var tileCntWidth:Number = colCount * (itemWidth + horizontalGap);
_tileContainer.x = 0.5 * (unscaledWidth - tileCntWidth);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
protected function createTiledContent():void
{
clearChildren();
_tileContainer = new UIComponent();
var itemsCount:int = _tilesData.length;
var row:int, col:int, item:DisplayObject;
for (var i:int = 0 ; i < itemsCount ; i++ )
{
row = int( i / colCount );
col = i - row * colCount;
if (_tilesData[i].hasOwnProperty("itemFactory"))
{
item = IFactory(_tilesData[i].itemFactory).newInstance();
}
else
{
item = innerItemRenderer.newInstance();
}
Object(item).data = _tilesData[i];
item.x = col * (itemWidth + horizontalGap);
item.y = row * (itemHeight + verticalGap);
_tileContainer.addChild(item);
}
addChild(_tileContainer);
invalidateSize();
invalidateDisplayList();
}
//--------------------------------------------------------------------------
//
// Helper methods
//
//--------------------------------------------------------------------------
private function clearChildren():void
{
var numChildren:int = this.numChildren;
for (var i:int = 0 ; i < numChildren ; i++ )
{
this.removeChildAt(0);
}
}
}
}
PagedItemVo
package components.pagedList.vo
{
import flash.events.EventDispatcher;
import flash.events.IEventDispatcher;
import mx.collections.IList;
import mx.core.IFactory;
[Bindable]
public class PageItemVo extends EventDispatcher
{
public var data:IList;
public var rowsCount:int;
public var colsCount:int;
public var itemWidth:Number;
public var itemHeight:Number;
public var verticalGap:Number;
public var horizontalGap:Number;
public var innerItemRenderer:IFactory;
public function PageItemVo(target:IEventDispatcher=null)
{
super(target);
}
}
}
滚动列表皮肤
package skins.pagedList
{
import flash.display.Graphics;
import flash.display.Sprite;
import flash.events.MouseEvent;
import mx.core.DPIClassification;
import mx.core.ScrollPolicy;
import mx.events.FlexEvent;
import mx.events.TouchInteractionEvent;
import spark.events.IndexChangeEvent;
import spark.events.RendererExistenceEvent;
import spark.skins.mobile.ListSkin;
import spark.skins.mobile.supportClasses.MobileSkin;
public class ScrollingListSkin extends ListSkin
{
private var pageIndicator:Sprite;
private var indicatorSize:uint;
private var _isHorizontal:Boolean;
private var _suspendPageIndicatorShortcut:Boolean;
public function ScrollingListSkin()
{
super();
switch (applicationDPI)
{
case DPIClassification.DPI_320:
{
indicatorSize = 32;
break;
}
case DPIClassification.DPI_240:
{
indicatorSize = 24;
break;
}
default:
{
indicatorSize = 16;
break;
}
}
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
override protected function createChildren():void
{
super.createChildren();
scroller.setStyle("skinClass", PagedListScrollerSkin);
// page indicator
pageIndicator = new Sprite();
// TODO (jasonsj): extend pageIndicator hit area to use the entire
// width/height of the List as a shortcut. Currently this only works
// in the tiny area where the indicators are.
//pageIndicator.addEventListener(MouseEvent.MOUSE_DOWN, pageIndicaterMouseHandler);
//pageIndicator.addEventListener(MouseEvent.MOUSE_UP, pageIndicaterMouseHandler);
//pageIndicator.addEventListener(MouseEvent.MOUSE_MOVE, pageIndicaterMouseHandler);
addChild(pageIndicator);
// listen for changes to the list
dataGroup.addEventListener(FlexEvent.UPDATE_COMPLETE, dataGroupUpdateComplete);
scroller.addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START, touchInteractionStart);
scroller.addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_END, positionChanged);
}
override protected function commitProperties():void
{
super.commitProperties();
// isHorizontal
/*var hScrollPolicy:Boolean = getStyle("horizontalScrollPolicy") == ScrollPolicy.ON;
var vScrollPolicy:Boolean = getStyle("verticalScrollPolicy") == ScrollPolicy.ON;
_isHorizontal = hScrollPolicy && !vScrollPolicy;*/
_isHorizontal = true;
}
override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void
{
super.drawBackground(unscaledWidth, unscaledHeight);
var pos:Number = (isHorizontal) ? scroller.viewport.horizontalScrollPosition :
scroller.viewport.verticalScrollPosition;
var viewportSize:Number = (isHorizontal) ? scroller.viewport.width :
scroller.viewport.height;
var selectedIndex:int = Math.round(pos / viewportSize);
var numElements:int = dataGroup.numElements;
var g:Graphics = pageIndicator.graphics;
g.clear();
// if we have only one page there shouldn't be any scrollbar visuals
if (numElements == 1) return;
var axisPos:Number = 0;
var centerPos:Number = indicatorSize / 2;
var radius:Number = indicatorSize / 4;
//TODO: make so the color could be specifyed outisde
var selectionColor:Number = 0x000000; //getStyle("selectionColor");
//var elementsPerPage:int = Math.floor(unscaledWidth/FileIconItemRenderer.ITEM_WIDTH) * Math.floor(unscaledHeight/FileIconItemRenderer.ITEM_HEIGHT);
for (var i:uint = 0; i < numElements; i++)
{
if (i == selectedIndex)
g.beginFill(selectionColor, 1);
else
g.beginFill(0, .25);
if (isHorizontal)
g.drawCircle(axisPos + centerPos, centerPos, radius);
else
g.drawCircle(centerPos, axisPos + centerPos, radius);
g.endFill();
axisPos += indicatorSize;
}
var pageIndicatorX:Number = (isHorizontal) ? (unscaledWidth - axisPos) / 2 :
unscaledWidth - (indicatorSize * 1.5);
var pageIndicatorY:Number = (isHorizontal) ? unscaledHeight - (indicatorSize * 1.5):
(unscaledHeight - axisPos) / 2;
setElementPosition(pageIndicator, Math.floor(pageIndicatorX), Math.floor(pageIndicatorY));
}
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
var allStyles:Boolean = !styleProp || styleProp == "styleName";
if (allStyles || styleProp == "horizontalScrollPolicy" ||
styleProp == "verticalScrollPolicy")
{
invalidateProperties();
invalidateDisplayList();
}
}
private function get isHorizontal():Boolean
{
return _isHorizontal;
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
private function dataGroupUpdateComplete(event:FlexEvent):void
{
invalidateDisplayList();
}
private function touchInteractionStart(event:TouchInteractionEvent):void
{
_suspendPageIndicatorShortcut = true;
}
private function positionChanged(event:TouchInteractionEvent):void
{
invalidateDisplayList();
_suspendPageIndicatorShortcut = false;
}
private function pageIndicaterMouseHandler(event:MouseEvent):void
{
event.preventDefault();
if (_suspendPageIndicatorShortcut)
return;
// Mouse events on the pageIndicator sprite will jump to the selected page
var pos:Number = (isHorizontal) ? event.localX : event.localY;
var size:Number = (isHorizontal) ? pageIndicator.width : pageIndicator.height;
pos = Math.min(Math.max(pos, 0), size) - (indicatorSize / 2);
var viewportSize:Number = (isHorizontal) ? scroller.viewport.width : scroller.viewport.height;
viewportSize = viewportSize * dataGroup.numElements;
var viewportPosition:Number = (pos / size) * viewportSize;
if (isHorizontal)
scroller.viewport.horizontalScrollPosition = viewportPosition;
else
scroller.viewport.verticalScrollPosition = viewportPosition;
}
}
}
例子:
<pagedList:PagedList id="displayList"
width="100%" height="100%"
itemRenderer="skins.pagedList.PageItemRenderer"
innerItemRenderer="components.pagedList.tiles.FolderTileItem"
innerItemWidth="146" innerItemHeight="150"
skinClass="skins.pagedList.ScrollingListSkin"
verticalScrollPolicy="off"
horizontalScrollPolicy="on"
pageScrollingEnabled="true"
horizontalGap="15" verticalGap="20"
contentBackgroundAlpha="0"
selectionColor="0xFFFFFF">
<pagedList:layout>
<s:HorizontalLayout columnWidth="{displayList.width}"
variableColumnWidth="false"
gap="0"
paddingTop="30"/>
</pagedList:layout>
不幸的是,我无法与您共享 FolderTileItem 源,但它是一个扩展 Sprite 的简单组件。