65.9K
CodeProject 正在变化。 阅读更多。
Home

扩展QNX TileList:Liquid Tile List

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011 年 4 月 18 日

CPOL

6分钟阅读

viewsIcon

18231

downloadIcon

17

扩展TileList和AlternatingCellRenderer QNX

引言

在我尝试了用于 PlayBook 开发的 QNX UI 库中可用的列表组件后,我的第一个问题是:如何自定义这些组件的外观和功能?

默认情况下,TileList 看起来是这样的

Extending-QNX/list5.jpg

您使用 dataProvider 属性来设置数据,其默认的渲染器(CellRenderer)期望数据是一个具有名为 labelString 属性的对象。这个属性将用于显示每个图块的文本。您的控件可以通过 columnCount 属性控制图块的数量,并且可以通过 columnWidthrowHeight 属性设置图块的宽度和高度。您可以使用 cellPadding 属性来控制图块之间的间距。

这很棒。但如果您想显示更复杂的数据呢?比如说,您想包含一张图片、一个标签和一些图形?显然,您需要扩展默认的渲染器来实现这一点。这是我的想法:

Extending-QNX/tileList1.jpg

其次,我希望 TileList 能够根据三个输入数据动态计算布局的列数:TileList 的宽度、cellPadding 值以及图块的大小。因此,在创建组件时,我没有硬编码 columnCount 属性,而是设置了图块的大小(columnWidthrowHeight)以及 TileListwidth,然后基于这些,TileList 将计算出给定宽度可以容纳多少列,并相应地设置 columnCount 属性。这里有一个例子(在第一张图中,图块大小设置为 130 像素,在第二张图中设置为 250 像素)

Extending-QNX/tilelist2.jpg

Extending-QNX/tilelist3.jpg

这意味着我需要扩展 TileList 组件来添加此行为。但是,第一步是创建一个自定义的 CellRenderer,以便我能够显示图片、标签和圆角灰色方块。幸运的是,我的同事、传播大使 Renaun Erickson 创建了一个示例,它非常接近我需要的东西。

为 TileList 创建自定义 CellRenderer

Renaun 的示例非常接近我想达到的目标。我需要修改它的原因是我想要不同的组件排列方式,更重要的是,我想将图块的宽度和高度从 TileList 推送过来——我不想在 CellRenderer 类中硬编码这些值。

现在,让我们退一步,谈谈 CellRenderer 组件的生命周期。列表组件使用虚拟化;这意味着列表将创建有限数量的 CellRenderer 组件,并在您上下滚动列表时重复使用它们。每次重用 CellRenderer 实例时,TileList 组件都会通过 CellRenderer 的 data 属性推送需要由该单元格显示的新数据。

接下来,每次更改 columnWidth/rowHeight 属性时,TileList 将遍历所有 CellRender 实例并调用 setSize() 方法来推送新值。

了解了这一点,就很清楚我需要做什么来创建我的自定义 CellRenderer:覆盖 data()setSize() 方法,并添加我需要的部分(imagerectanglelabel)。这是部分代码(完整代码请访问下载部分并获取项目源文件)

public class PictureCellRenderer extends AlternatingCellRenderer {
 
    /**
     * Skin parts used to render the data
     */
    protected var img:Image;
    protected var lbl:Label;
    protected var bg:Sprite;
    protected var format:TextFormat;
 
    public function PictureCellRenderer() {
        super();
        // hide the built in label
        label.visible = false;
        createUI();
    }
    /**
     * setSize() is called everytime the tiles size are changed
     * this is where we add our method layout() to reposition/redraw
     * various parts of the cell renderer.
     */
    override public function setSize(w:Number, h:Number):void {
        super.setSize(w, h);
        //we want to draw the skin parts only when the
        //actual width/height are set
        //this method is called first for the default sizes and then for
        //for the user preferred sizes
        if (stage) {
            layout();
        }
    }
 /**
     * Updates the text and image everytime a new data is set
     * for this renderer instance.
     */
    override public function set data(value:Object):void {
        super.data = value;
        //Update the image source and text if there is valid data.
        if (value) {
            img.setImage(value.img);
            lbl.text = value.label;
        }
    }
 /**
     * Create all the cell renderer components just once
     */
    private function createUI():void {
        img = new Image();
        bg = new Sprite();
        lbl = new Label();
 
        //...
        //code to position the img, Sprite, and label
        //...
 
        addChild(img);
        addChild(bg);
        addChild(lbl);
    }
 /**
     *     Draws the rectangle used as background
     *  for the label
     */
    private function drawBg():void {
        //code to redraw the gray rounded rectangular
    }
    /**
     * Reposition/redraw the renderer parts
     * everytime the tile size is changed
     */
    private function layout():void {
        lbl.y = height - 20;
        lbl.width = width - 10;
        drawBg();
        onComplete(new Event(Event.COMPLETE));
    }
    /**
     * Resize the image once the bits were loaded
     */
    private function onComplete(e:Event):void {
        img.setSize(width - 20, height - 20);
    }
}

关于上面代码的一些说明

  • 用于显示数据的组件(ImageSpriteLabel)在构造函数中调用的 createUI() 方法中创建——它们只创建一次。
  • 加载图像时,您必须监听 Event.COMPLETE 事件,如果您想将宽度和高度设置为与图像大小不同的值。
  • 每次调用 setSize() 方法时,都会执行 layout() 函数;因此,ImageSpriteLabel 组件会被重新定位或调整大小,以使用新的可用宽度和高度。
  • 在构造函数中,我将父类的内置标签组件设为不可见。我试图重用它而不是创建一个新的标签,但我找不到改变其位置或格式的方法。我不确定这是一个 bug,还是我的方法有问题。
  • 我扩展了 AlternatingCellRenderer 而不是 CellRender,只是为了让相邻单元格具有不同的背景。如果您扩展后者,代码也应该可以工作。
  • 您传递给此渲染器的数据必须是一个对象,该对象具有用于文本的 label 属性和用于 Image URL 的 img 属性(两者都必须是 String)。
  • 此渲染器使用 ImageCache 来缓存图像。根据您的应用程序,您必须考虑缓存多少图像,以免占用过多系统内存。

扩展 TileList 组件以支持 Liquid Layout

有了自定义 CellRenderer,一半以上的工作就完成了——至少我一开始是这么认为的 :)。那么剩下的就是实现 TileList 能够自动决定给定宽度能容纳多少列,并且每次组件的宽度或图块的宽度发生变化时,重新计算列数并更新其布局。将此行为集成到我的自定义 TileList 中意味着:

  • 当显示方向改变时,我无需执行任何操作——TileList 将自动更改列数以适应新的宽度和高度。
  • 我可以在运行时控制图块的大小;例如,用户可以使用滑块放大或缩小。

我有两个选项可以实现此行为:

  • 创建一个执行所有数学计算以确定新的 columnCount 的方法。
  • 或者扩展 TileList 组件,并将此行为构建到组件本身中。

我选择了第二个选项,在解释我如何做之前,这是 LiquidTileList 组件的代码。

public class LiquidTileList extends TileList {
	/**
	 * if true, when changing the size of the tiles
	 * scrolls the list so the first tile from the previous
	 * state is still visible
	 */
	public var keepVisibleItem:Boolean;
 
	public function LiquidTileList() {
		super();
		setSkin(PictureCellRenderer);
	}
	/**
	 * Overriding the draw method to inject 
	 * our method of calculating the number of
	 * tiles that fit the screen
	 * This method is called every time the width/height
	 * is changed
	 */
	override protected function draw():void {
		var i:int;
		if (keepVisibleItem && firstVisibleItem)
			i = firstVisibleItem.index;
		scrollIndexVisible(0, 0);
		calculateColumns();
		super.draw();
		if (keepVisibleItem && i)
			scrollIndexVisible(i, 0);
	}
	/**
	 * Calculates the number of tiles that fit
	 * the width
	 */
 private function calculateColumns():void {
	var columnNumber:int = Math.floor( (width - (cellPadding * columnCount -1) )
                                                      / columnWidth);
	if (columnNumber != columnCount)
		columnCount = columnNumber;
	}
}

在构造函数中,我将 PictureCellRenderer 设置为默认皮肤。接下来,我覆盖了 draw() 方法。此方法在每次更改列表的宽度和高度,或更改图块大小(columnWidthrowHeight)时调用;首先,我使用新的尺寸(通过调用 calculateColumns() 方法)计算列数,然后调用父类的 draw() 方法,使用新的 columnCount 值重新进行布局。

如果您想知道 scrollIndexVisible() 调用有什么作用,那么这就是难点。我认为 TileList 的虚拟化引擎存在一个 bug,因为当我更改图块大小时,有时会出现丢失项或无法滚动。所以这是我找到的解决此问题的唯一方法。其次,我希望将第一个可见元素保留在视图中。例如,假设您有一个大列表,您滚动了一点,然后决定将图块的大小增加三倍。我认为用户会期望看到之前视图中的第一个项目。所以这就是我添加一个名为 keepVisibleItempublic 属性的原因。

在我的测试中,应用 scrollIndexVisible 修复后,组件工作正常。但是,如果您想使用滑块更改图块大小,并且实时应用新值(通过监听 SliderEvent.MOVE 事件应用新值),那么最好将 keepVisibleItem 设置为 false

这是如何使用此自定义组件的代码片段。

peopleList = new LiquidTileList();
peopleList.keepVisibleItem = true;
peopleList.dataProvider = getPeople();
peopleList.scrollDirection = ScrollDirection.VERTICAL;
peopleList.size = 100;
peopleList.sizeUnit = SizeUnit.PERCENT;
peopleList.sizeMode = SizeMode.BOTH;
//provide a default number of columns;
//this can be override by LiquidTileList so it fill all
//the available width
peopleList.columnCount = 5;
peopleList.columnWidth = 150;
peopleList.rowHeight = 150;
peopleList.cellPadding = 5;
peopleList.selectionMode = ListSelectionMode.SINGLE;
 
slider = new Slider();
slider.minimum = 130; //min value
slider.maximum = 250; //max value
slider.value = 150;   //default value
slider.addEventListener(SliderEvent.END, onSliderMove);
slider.addEventListener(SliderEvent.MOVE, onSliderMove);
...
/**
 * Slider event changing the size of the tiles
 */
private function onSliderMove(e:SliderEvent):void {
	peopleList.columnWidth = e.value;
	peopleList.rowHeight = e.value;
}

下载代码

您可以从. 下载整个项目。仔细查看,如果您发现 bug 或有更好的想法,请发表评论。 :)

历史

  • 2011 年 4 月 18 日:初始版本
© . All rights reserved.