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

为BlackBerry PlayBook创建自定义列表皮肤

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 4 月 18 日

CPOL

5分钟阅读

viewsIcon

12460

皮肤化列表与皮肤化简单的按钮或文本输入框有很大的不同

免责声明:我相当确定这不是最佳实践。ListCellRenderer 中有很多我不太理解的东西。我希望有一天能和 QNX 团队坐下来,弄清楚如何正确地完成这件事,但目前,我找到了一个可行的办法,所以想与所有遇到困难的人分享。

List 设置皮肤与为 ButtonTextInput 框(我在这里的文章中讨论过)这类简单的组件设置皮肤有很大的不同。自定义列表外观和感觉的正确方法似乎是创建一个 CellRenderer,它允许您访问列表的标签,然后通过创建专门为该 CellRenderer 设计的皮肤来替换图形。但是,我的特定代码不依赖于标签字段,所以我跳过了第一步,只创建了一个扩展 UISkin 并实现 ICellRendererCellRendererSkin。通过实现 ICellRenderer,我可以访问数据方法,但正如您将看到的,这产生了一些问题。

首先,这是我想创建的列表。它有一个图像和几行文字。您可以查看正常状态(白色)和选中状态(灰色)的外观。我想要更自定义的外观,所以在每个项目之间留了一些空间,并在内容周围绘制了一个带边框的圆角矩形。

Custom-List-Skin/pintley_list.png

要以这种方式为列表设置皮肤,最重要的仍然是 initializeStates() 方法。必须像我们为任何其他组件设置皮肤一样重写该方法,并且在那里我们调用 setSkinState 来将图形与状态关联起来。列表基本上有 8 种不同的 SkinStatesSkinStates.UPSkinStates.UP_ODDSkinStates.DOWNSkinStates.FOCUSSkinStates.DISABLEDSkinStates.SELECTEDSkinStates.DOWN_SELECTEDSkinStates.DISABLED_SELECTED。我不知道 FOCUS 是做什么的,我没有交替行,所以不关心 ODD,并且我决定我的 DOWNSELECTEDDOWN_SELECTED 保持相同。所以这是我的 initializeStates() 方法的代码。

override protected function initializeStates():void
{
     _upSkin = new Sprite();    
     setSkinState(SkinStates.UP,_upSkin);
 
     _downSkin = new Sprite();    
     setSkinState(SkinStates.DOWN,_downSkin);
     setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
     setSkinState(SkinStates.SELECTED,_downSkin);
 
     SkinStates              
     showSkin(_upSkin);
}

事情在这里变得有点棘手。在普通的皮肤设置中,我可以直接将图形添加到我的 sprite 中,然后相应地设置皮肤状态。但我的发现是,当调用 initializeStates() 方法时,组件的宽度和高度尚未设置。所以当我尝试绘制一个使用组件宽度/高度的矩形时,它会看起来被挤压。如果您知道确切的尺寸,可以硬编码这些值。但我想在不同大小的列表上使用它,所以我想使用这些动态值。

我发现,如果我重写 setState(state:String) 方法,我就可以在那里获取宽度/高度的值,然后根据正在设置的每个单元格的高度/宽度对应的状态绘制正确的图形。

override protected function setState(state:String):void
{
     super.setState(state);
 
     var matrix:Matrix = new Matrix();
     matrix.createGradientBox(width,height,90/180*Math.PI);
 
     if(state == SkinStates.UP)
     {                   
          _background.graphics.clear();
          _background.graphics.beginGradientFill(GradientType.LINEAR,
                    [0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
          _background.graphics.lineStyle(2,0x221206);
          _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
          _background.graphics.endFill();
     }
     if(state == SkinStates.DOWN ||
          state == SkinStates.DOWN_SELECTED ||
          state == SkinStates.SELECTED)
     {                        
          _background.graphics.clear();
          _background.graphics.beginGradientFill(GradientType.LINEAR,
                    [0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
          _background.graphics.lineStyle(2,0x221206);
          _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
          _background.graphics.endFill();
     }              
}

在这里,我遇到了另一个奇怪的问题。请注意,我不是将图形添加到状态,而是将它们添加到 _background Sprite。我发现,当我尝试直接在皮肤 sprite 上绘制图形时,当状态更改时,它要么会覆盖我的其他内容而我无法恢复,要么根本不会显示图形。我仍然不完全确定这两种情况为什么会发生,而且我经历了太多次迭代,以至于我不记得导致这种情况的代码了。但我的发现是,如果我创建一个 background Sprite 并先将其添加到显示列表中,我可以根据状态修改它,它就会正确绘制。这也是为什么我有一个 _background.graphics.clear() 调用,因为 _background Sprite 存在于每种状态中,只需在状态更改时重新绘制它。

下一步是添加所有内容到 cell renderer,然后在 cell renderer 消失时进行清理。所有 QNX 组件都有一个 onAdded()onRemoved() 方法,当对象被添加到舞台或从舞台移除时会被调用。所以我只是重写了这些方法并添加了我的内容。

override protected function onAdded():void
{
     super.onAdded();
     addChild(_background);
     addChild(_image);
     addChild(_name);
     addChild(_brewery);
     addChild(_beerType);
     addChild(_ratingText);
     addChild(_avgRating);
}
 
override protected function onRemoved():void
{
     super.onRemoved();
     removeChild(_background);
     removeChild(_image);
     removeChild(_name);
     removeChild(_brewery);
     removeChild(_beerType);
     removeChild(_ratingText);
     removeChild(_avgRating);
}

还有一个最后的步骤。正如您将在下面的代码中看到的,我在构造函数中设置了标签和图像的大部分属性。但我没有设置任何动态数据。这是因为我很难找到何时可以访问 cell renderer 的 data 属性。在 init()onAdded()initializeStates() 中没有数据,所以在这些方法中尝试设置动态数据会引发错误。我**可以**在 setState() 方法中访问它,但我发现当我在那里设置时,列表不会正确显示值。我最终发现这是因为列表是虚拟化的。setState() 方法在您首次滚动列表时不会被调用,因为状态尚未更改,只有数据更改了。所以当我滚动列表时,我看到了重复的值,直到我点击它并强制调用 setState(),我才看到正确的值。

解决方案只是拥抱我正在实现 ICellRenderer 的事实,并在数据 setter 方法中设置所有数据。这为每个单元格正确设置了数据,并且根本不依赖于状态。我还必须在那里设置 Label 对象的宽度,以便文本不会被截断。

public function set data(data:Object):void
{
     _beer = data;
 
     _image.setImage(data.thumb);
 
     _name.text = data.beerName;
     _name.width = width-150;
 
     _brewery.text = data.brewerName;
     _brewery.width = width-150;
 
     _beerType.text = data.styleName;
     _beerType.width = width-150;
 
     if(data.avgRating > 3)
     {
          _avgRatingFormat.color = 0x4c9d17;
     } else if (data.avgRating < 1)
     {
          _avgRatingFormat.color = 0x9d1717;
     }
 
     _avgRating.text = data.avgRating;
     _avgRating.format = _avgRatingFormat;
}

再次强调,我认为这可能不是理想的实现方式。**特别是**如果您有一个非常简单的标签,您可以只扩展 CellRenderer 并重写 draw()drawLabel() 方法来实现您想要的功能。或者,如我上面所说,为该 CellRenderer 应用一个特殊的皮肤,该皮肤正确处理列表的所有状态。但这确实是我深入研究的“兔子洞”,我发现它很有用,因为我用一个类解决了我所有的难题。即使它是一个丑陋的类。这是完整的代码。

package com.pintley.components.listClasses
{
     import flash.display.GradientType;
     import flash.display.Sprite;
     import flash.filters.DropShadowFilter;
     import flash.geom.Matrix;
     import flash.text.TextFormat;
 
     import qnx.ui.display.Image;
     import qnx.ui.listClasses.ICellRenderer;
     import qnx.ui.skins.SkinStates;
     import qnx.ui.skins.UISkin;
     import qnx.ui.text.Label;
 
     public class BeerCellRendererSkin extends UISkin implements ICellRenderer
     {
          protected var _beer:Object;
          private var _row:int;
          private var _column:int;
          private var _section:int;
          private var _index:int;    
 
          private var _yOffset:int = 12;
          private var _xOffset:int = 35;
 
          /**
           * Skins
           **/
          protected var _upSkin:Sprite;
          protected var _selectedSkin:Sprite;
          protected var _downSkin:Sprite;
          protected var _disabledSkin:Sprite;
 
          /**
           * Cell Renderer content
           **/
          // I use a background sprite because I want to
          // make sure it's the lowest layer. Then I can just
          // modify the lowest layer without overwriting the text.
          protected var _background:Sprite;
          protected var _name:Label;
          protected var _brewery:Label;
          protected var _beerType:Label;
          protected var _ratingText:Label;
          protected var _avgRating:Label;
          protected var _image:Image;
 
          /**
           * TextFormats
           **/
          protected var _nameFormat:TextFormat;
          protected var _breweryFormat:TextFormat;
          protected var _beerTypeFormat:TextFormat;
          protected var _ratingTextFormat:TextFormat;
          protected var _avgRatingFormat:TextFormat;
 
          public function BeerCellRendererSkin()
          {
               super();
 
               /**
                * TextFormats
                **/
               _nameFormat = new TextFormat();
               _nameFormat.color = 0xbd5251;
               _nameFormat.size = 16;
               _nameFormat.bold = true;
 
               _breweryFormat = new TextFormat();
               _breweryFormat.color = 0x525252;
               _breweryFormat.size = 14;
               _breweryFormat.bold = true;
 
               _beerTypeFormat = new TextFormat();
               _beerTypeFormat.color = 0x525252;
               _beerTypeFormat.size = 14;
 
               _ratingTextFormat = new TextFormat();
               _ratingTextFormat.color = 0x79523e;
               _ratingTextFormat.size = 12;
               _ratingTextFormat.bold = true;
 
               _avgRatingFormat = new TextFormat();
               _avgRatingFormat.color = 0x000000;
               _avgRatingFormat.size = 14;
               _avgRatingFormat.bold = true;
 
               /**
                * CellRenderer Content
                **/
               _image = new Image();
               _image.x = _xOffset;
               _image.y = 20;
               _image.filters = [new DropShadowFilter(3,45,0x000000,.5,4,4,.5)];
 
               _name = new Label();
               _name.x = _xOffset+80;
               _name.y = _yOffset;
               _name.format = _nameFormat;
 
               _brewery = new Label();
               _brewery.x = _xOffset+80;
               _brewery.y = _yOffset+20;
               _brewery.format = _breweryFormat;
 
               _beerType = new Label();
               _beerType.x = _xOffset+80;
               _beerType.y = _yOffset+35;
               _beerType.format = _beerTypeFormat;
 
               _ratingText = new Label();
               _ratingText.x = _xOffset+80;
               _ratingText.y = _yOffset+55;
               _ratingText.format = _ratingTextFormat;
               _ratingText.text = "AVG Rating";
 
               _avgRating = new Label();
               _avgRating.x = _xOffset + 150;
               _avgRating.y = _yOffset+54;
 
               _background = new Sprite();
          } 
 
          /**
           * Getters/Setters
           **/
 
          public function get data():Object
          {
               return _beer;
          }
 
          public function set data(data:Object):void
          {
               _beer = data;
 
               // Set the text and images for the
               // label after we get data from the list.
               _image.setImage(data.thumb);
 
               _name.text = data.beerName;
               _name.width = width-150;
 
               _brewery.text = data.brewerName;
               _brewery.width = width-150;
 
               _beerType.text = data.styleName;
               _beerType.width = width-150;
 
               if(data.avgRating > 3)
               {
                    _avgRatingFormat.color = 0x4c9d17;
               } else if (data.avgRating < 1)
               {
                    _avgRatingFormat.color = 0x9d1717;
               }
 
               _avgRating.text = data.avgRating;
               _avgRating.format = _avgRatingFormat;
          }
 
          public function get index():int
          {
               return _index;
          }
 
          public function set index(value:int):void
          {
               _index = value;
          }
 
          public function get row():int
          {
               return _row;
          }
 
          public function set row(value:int):void
          {
               _row = value;
          }
 
          public function get column():int
          {
               return _column;
          }
 
          public function set column(value:int):void
          {
               _column = value;
          }
 
          public function get section():int
          {
               return _section;
          }
 
          public function set section(section:int):void
          {
               _section = section;
          }
 
          public function get isHeader():Boolean
          {
               return false;
          }
 
          /**
           * Overriden Functions
           */
 
          override protected function initializeStates():void
          {
               // Set up the skin states
               _upSkin = new Sprite();    
               setSkinState(SkinStates.UP,_upSkin);
 
               _downSkin = new Sprite();    
               setSkinState(SkinStates.DOWN,_downSkin);
               setSkinState(SkinStates.DOWN_SELECTED,_downSkin);
               setSkinState(SkinStates.SELECTED,_downSkin);
 
               showSkin(_upSkin);
          }
 
          override protected function setState(state:String):void
          {
               super.setState(state);
 
               var matrix:Matrix = new Matrix();
               matrix.createGradientBox(width,height,90/180*Math.PI);
 
               // Check to see what state is being set and then draw
               // the graphics on the background Sprite accordingly.
               if(state == SkinStates.UP)
               {                   
                    _background.graphics.clear();
                    _background.graphics.beginGradientFill(GradientType.LINEAR,
                              [0xffffff,0xf2f2f2,0xffffff],[1,1,1],[0,127,255],matrix);
                    _background.graphics.lineStyle(2,0x221206);
                    _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
                    _background.graphics.endFill();
               }
               if(state == SkinStates.DOWN ||
                    state == SkinStates.DOWN_SELECTED ||
                    state == SkinStates.SELECTED)
               {                        
                    _background.graphics.clear();
                    _background.graphics.beginGradientFill(GradientType.LINEAR,
                              [0xaaaaaa,0xcfcfcf,0xaaaaaa],[1,1,1],[0,127,255],matrix);
                    _background.graphics.lineStyle(2,0x221206);
                    _background.graphics.drawRoundRect(20,10,width-35,height-20,7,7);
                    _background.graphics.endFill();
               }              
          }
 
          override protected function onAdded():void
          {
               super.onAdded();
               addChild(_background);
               addChild(_image);
               addChild(_name);
               addChild(_brewery);
               addChild(_beerType);
               addChild(_ratingText);
               addChild(_avgRating);
          }
 
          override protected function onRemoved():void
          {
               super.onRemoved();
               removeChild(_background);
               removeChild(_image);
               removeChild(_name);
               removeChild(_brewery);
               removeChild(_beerType);
               removeChild(_ratingText);
               removeChild(_avgRating);
          }
     }
}

相关文章

  1. 在 PlayBook 列表中设置自定义标签
  2. PlayBook 组件的皮肤定制
  3. 使用容器类布局 PlayBook 应用程序
  4. 开始使用 BlackBerry PlayBook 和 Adobe AIR
  5. AIR for Android 的摄像头 API 和地理位置 Exif 数据

历史

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