Spark Image - Functional and Design Specification


Glossary

loader queue - The SDKs stock image cache will allow for queuing of remote load requests so that one or more image requests can be prioritized over others already queued for execution.

Summary and Background

In Flex 4.0, developers must currently fall back to leveraging the legacy mx:Image component when they require certain functionality that the stock BitmapImage primitive does not provide (remote image support, scaling w/respect to aspect ratio etc.). While a viable solution in the general case, mx:Image is not a Spark component and is not customizable in the manner that other Spark components are. In addition mx:Image is not as light weight as BitmapImage.

Many image-heavy or thumbnail-heavy Flex applications in production make use of custom image caching in order to improve the performance (and perceived performance) of image presentation. Since this has proven to be a common pattern, both BitmapImage and Image will support an extensible caching mechanism.

The high level goals of the Flex 4.5 Image Enhancements feature are:

1. Enhance the BitmapImage primitive to support:

  • Loading remote images.
  • Loading and presentation of images from untrusted sources (w/restrictions as necessary).
  • Ability to scale w/respect to aspect ratio.
  • Support for much higher quality scaling than the native player bitmap smoothing provides (at default stage quality).
  • Optional bitmap data caching in order to improve perceived performance of image loading.

2. Provide a skinnable Spark Image component:

  • Customizable image presentation (custom borders, image frames, etc.).
  • Customizable "loading" state.
  • Customizable "error" (broken image) state.

3. Provide an extensible caching and queueing mechanism:

  • s:Image and s:BitmapImage will both support the ability to leverage a shared BitmapData cache.
  • The default caching and queueing loader will be provide a configurable cache size as well as manual cache control features (invalidation, etc.).

Usage Scenarios

A developer wishes to leverage the s:BitmapImage primitive in a list item renderer to render thumbnail data that is obtained from a remote URL.

A developer makes use of remote image thumbnails in a virtualized Flex list and wants to avoid a flicker or delay in image rendering when scrolling to pages or items that were previously in view. To improve perceived performance of the thumbnail rendering, the developer makes use of an image cache for the thumbnails in the list.

A Catalyst designer provides the developer with a non-standard image skin containing a custom preloader and custom image frame.

Detailed Description

BitmapImage Additions

The BitmapImage primitive will now support loading images from remote sources.

Cross-domain image loading is supported but with restrictions. When an cross-domain image is loaded from an untrusted source (a source not configured via a policy file allowing access to the image), the loaded image's Loader instance will be hosted directly and used as the backing BitmapImage's display object (needsDisplayObject will return true). When this occurs BitmapImage's trustedSource property becomes false and any advanced operations on the image data (such as high quality scaling, tiling, etc.) are no longer supported.

Only images can be loaded from remote source URLs. If non-image data is detected (such as a SWF) an RTE will be thrown and the load operation discontinued. LoaderInfo.contentType is leveraged to ensure the loaded content is in fact an image.

The following public properties are being added to the BitmapImage class:

preliminaryHeight, preliminaryWidth - The purpose of these attributes is to provide layout a more appropriate initial size to use when computing the measured bounds (measuredHeight and measuredWidth) of an image when the image data has yet to fully load. This improves the end user experience for image based item renderers in a virtualized list and is considered the better alternative to minHeight and minWidth for this use case. If values for these attributes are left unspecified, the measured bounds default to 0 until loading is complete.

contentLoader - These properties allow association with an IContentLoader instance. When one of the two is specified, the BitmapImage instance will make use of the provided IContentLoader to load remote image data. This mechanism facilitates association with an image cache (for instance).

scaleMode - Defines how the image is scaled when fillMode is set to BitmapFillMode.SCALE. If set to BitmapScaleMode.STRETCH the image content is stretched to fit (the default). BitmapScaleMode.LETTERBOX specifies that the image content will be displayed with the same aspect ratio of the original unscaled image. Note: The behavior BitmapScaleMode.LETTERBOX should be an improvement over that of the mx:Image component's maintainAspectRatio property in that when only one dimension (width/height) is specified, the image will size to fit properly, and report its correct bounds.

horizontalAlign, verticalAlign - These attributes are considered when the scale mode is set to BitmapScaleMode.LETTERBOX and the scaled content's aspect ratio does not match the aspect ratio of the image component itself, e.g. it determines how to account for the leftover space. The use of these attributes is illustrated below. Note that the bounding region of each image is highlighted in red for illustration purposes only (denotes the true width/height of the image bounds).

smoothingQuality - When smooth is true, specifies the quality of the image smoothing algorithm used. By default the image is scaled at the quality of the stage (which by default for a Flex application is high, one setting below best). When smoothingQuality is set to high a multi-step scale algorithm is used resulting in a much higher quality downscale, then one would obtain with the default stage quality. This option is useful for high quality thumbnail presentation.

bytesLoaded, bytesTotal - Convenience attributes that provide feedback on the load progress.

sourceWidth, sourceHeight - Provide the unscaled width and height of the original image data.

trustedSource - A read only flag denoting whether the currently loaded content is considered loaded from a source whose security policy allows for cross domain image access. When false, advanced bitmap operations such as high quality scaling, tiling, etc. are not permitted.

Note that cross-domain image loading is allowed, but when it's detected that the security policy does not allow access to the image content (and it's sandboxed accordingly) the following BitmapImage features cease to function:

  • BitmapFillMode.REPEAT
  • scaleGrid properties
  • Image smoothing when scaled (smooth, smoothingQuality)

In both cases, the content's Loader instance be added to the display list directly and the image content will be inaccessible by the user.

The following new events are now dispatched from the BitmapImage class.

Event.COMPLETE - Dispatched when a remote image source has finished loading.

IOErrorEvent.IO_ERROR - Dispatched when an IO error occurs during loading of a remote image source.

HTTPStatusEvent.HTTP_STATUS - Dispatched when an HTTP status is received for the loading image.

ProgressEvent.PROGRESS - Dispatched as image data is loaded from a remote image source.

SecurityErrorEvent.SECURITY_ERROR - Dispatched when an security related error occurs during loading of a remote image source.

Skinnable Image Component

Image - A new SkinnableComponent instance providing a customizable image control. The Image component consists of the optional skin parts: (imageDisplay:BitmapImage, progressIndicator:Range), and skin states: (uninitialized, loading, ready, invalid).

Most if not all BitmapImage properties are marshalled directly from the Image instance to the contained imageDisplay part.

All events dispatched from the contained imageDisplay part are forwarded and dispatched from the Image component as well.

The Image component will by default disable the loading state but will provide a style (enablePreload) that can be used to enable the behavior. This is because we want the stock Image skins to include a preloader progress bar, but in many cases (small to medium sized images) the preloader bar when only displayed for a very short time, is not the desired behavior.

ImageSkin - Stock Spark and Wireframe themed skins will be provided as part of this feature. Each containing a simple preloader and icon for images that fail to load (e.g. broken image icon).

Custom Content Loader Classes and Interfaces

The IContentLoader interface is a simple interface representing a custom data loader. The interface consists a load method. The load method accepts a unique key and returns a ContentRequest instance. The load method also accepts an optional contentLoaderGrouping argument, which allows the request to be grouped as part of other possibly related requests. This grouping identifier allows for the ContentLoader to act on multiple requests as a group, for example, the queuing mechanism of the image cache provided by the SDK supports request prioritization by contentLoaderGrouping.

The ContentRequest object returned from the load method is essentially just a wrapper around a (possibly shared) LoaderInfo instance or other content (such as BitmapData). The ContentRequest instance notifies of all load progress and errors. It also provides a mechanism for notification of content invalidation events from the associated content loader.

The BitmapImage class (and Image component) both support association with a content loader via their contentLoader property.

NOTE: The content loader interfaces and classes and not necessarily "image" centric but typically are. This way they can be leveraged by any data/media centric components. Some example use cases for a custom loader may be to support unique image formats that the player does not already support, or to allow a component to acquire data from some other data source or transport other than HTTP.

Please see the API section below for more details of the types described.

Caching and Queuing Content Loader

The SDK will provide a stock caching image loader as a convenience, which can be configured and associated with one or more BitmapImage or s:Image instances.

The ContentCache is an SDK provided IContentLoader instance that supports caching and queuing of remote image assets. The class provides properties and methods to manage the cache size and/or to control invalidation or storage behavior of the cache.

The ContentCache has several configuration parameters:

maxCacheEntries - Sets the limit on how many cache entries are stored at any given time. When the limit is exceeded all least recently used entries are pruned from the cache automatically.

enableQueuing - Enables queuing behavior and functionality. Defaults to false.

enableCaching - Enables caching behavior and functionality. Defaults to true.

maxActiveRequests - When queuing is enabled this value dictates how many actual load requests can be pending at once.

The caching related methods and properties include:

load() - Given a source key (typically a URL or URLRequest), the load method will return a new ContentRequest proxy representing the content requested. If complete returns false on the ContentRequest instance the client is to assume that the resource is still loading and can attach status/error listeners as appropriate. If the returned ContentRequest instance is marked complete, the resource was most likely found in the cache and returned immediately - access to the contained content is available immediately in that case.

The load method attempts to locate the source within a Dictionary based hash of cache entries. If found the data is returned immediately. Otherwise an actual request for the content is initiated and a new entry in the cache for the given source is added. At this time the Dictionary and an associated linked list of cache entries is pruned if necessary to remove the least recently used cache entries (to conform to the specified maxEntries value).

By default when the cache detects that a request has completed, and the data is not from a "trusted" location (that is, the data was received from a cross-domain source and the Loader's childAllowsParent property returns false, the request is marked in the cache as "untrusted" and future requests for that resource are not served from the cache. If an untrusted source is detected, any actively loading requests are notified via an invalidation event and their ContentRequest instances automatically re-request the the content (this time bypassing the cache).

removeAllCacheEntries(), removeCacheEntry() - removeCacheEntry() can be used to remove a cache entry that's no longer needed, and removeAllCacheEntries() can be used to remove all of the cached content.

getCacheEntry(), addCacheEntry() - These methods are used to test for existence of a pre-existing cache entry, obtain a reference to a cache entry, or add a new cache entry. addCacheEntry is only used when one manually wishes to "seed" a cache entry directly.


The queuing related methods include:

prioritize() - When queuing behavior is enabled, this method will force all pending requests of the specified contentLoaderGrouping to be bumped to the "head of the line" so to speak. Any active (true) URL requests active at the time are canceled and requeued if their contentLoaderGrouping does not match the prioritized content grouping.

removeAllQueueEntries() - removeAllQueueEntries() can be used to cancel all queued requests.

*NOTE: A queuing-only loader can be configured by simply setting enableCaching to false if desired.


See the ContentCache API description for a list of supported properties and methods on the ContentCache, and the Examples for sample usage.

API Description

BitmapImage additions:

BitmapImage.as
package spark.primitives
{
...

[Event(name="complete", type="flash.events.Event")]
[Event(name="httpStatus", type="flash.events.HTTPStatusEvent")]
[Event(name="ioError", type="flash.events.IOErrorEvent")]
[Event(name="progress", type="flash.events.ProgressEvent")]
[Event(name="securityError", type="flash.events.SecurityErrorEvent")]

public class BitmapImage extends GraphicElement
{
...

//-----------------------------------
//  Properties
//-----------------------------------

/**
* A BitmapData object representing the currently
* loaded image content (unscaled).  This property
* is null for untrusted cross domain content.
*
* @default null
*/
public function get bitmapData():BitmapData;

/**
* The number of bytes of the image already loaded.
* Only relevant for images loaded by request URL.
*
* @default NaN
*/
[Bindable("progress")]
public function get bytesLoaded():Number;

/**
* The total size of the image data in bytes.
* Only relevant for images loaded by request URL.
*
* @default NaN
*/
[Bindable("progress")]
public function get bytesTotal():Number;

/**
* Denotes whether or not to clear previous 
* image content prior to loading new content.
*
* @default true
*/
public function get clearOnLoad():Boolean;
public function set clearOnLoad(value:Boolean):void;

/**
* Provides an estimate to use for height when the "measured" bounds
* of the image is requested by layout, but the image data has
* yet to complete loading. When NaN the measured height is 0 until
* the image has finished loading.
*
* @default NaN
*/
public function set preliminaryHeight(value:Number):void;
public function get preliminaryHeight():Number;

/**
* Provides an estimate to use for width when the "measured" bounds
* of the image is requested by layout, but the image data has
* yet to complete loading. When NaN the measured width is 0 until
* the image has finished loading.
*
* @default NaN
*/
public function set preliminaryWidth(value:Number):void;
public function get preliminaryWidth():Number;

/**
*  The horizontal alignment of the content when it does not have
*  a one-to-one aspect ratio.
*
*  Can be one of HorizontalAlign.LEFT ("left"),
*  HorizontalAlign.CENTER ("center"), or
*  HorizontalAlign.RIGHT ("right").
*
*  This property is only applied when scaleMode is set to
*  to BitmapFillMode.SCALE ("scale").
*
*  @default HorizontalAlign.CENTER
*/
public function set horizontalAlign(value:String):void;
public function get horizontalAlign():String;

/**
* Determines how the image is scaled when fillMode is set to
* BitmapFillMode.SCALE.
*
* When set to BitmapScaleMode.STRETCH ("stretch"), the image
* is stretched to fit.
*
* When set to BitmapScaleMode.LETTERBOX ("letterbox"), the image
* is scaled with respect to the original unscaled image's aspect
* ratio.
*
* @default BitmapScaleMode.STRETCH
*/
public function set scaleMode(value:String):void;
public function get scaleMode():String;

/**
* Determines how the image is down-scaled.  When set to
* BitmapSmoothingQuality.BEST, the image is resampled (if data
* is from a trusted source) to achieve a higher quality result.
* If set to 'default', the default stage quality for scaled
* bitmap fills is used.
*
* @default "default"
*/
public function set smoothingQuality(value:String):void;
public function get smoothingQuality():String;

/**
* Provides the unscaled height of the original image data.
*
* @default NaN
*/
public function get sourceHeight():Number;

/**
* Provides the source width of the original image data.
*
* @default NaN
*/
public function get sourceWidth():Number;

/**
* A read-only flag denoting whether the currently loaded
* content is considered loaded form a source whose security
* policy allows for cross domain image access.  When false,
* advanced bitmap operations such as high quality scaling,
* tiling, etc. are not permitted.  This flag is set once an
* image has been fully loaded.
*
* @default true
*/
public function get trustedSource():Boolean;

/**
*  The vertical alignment of the content when it does not have
*  a one-to-one aspect ratio.
*
*  Can be one of VerticalAlign.TOP ("top"),
*  VerticalAlign.MIDDLE ("middle"), or
*  VerticalAlign.BOTTOM ("bottom").
*
*  This property is only applied when scaleMode is set to
*  to BitmapFillMode.SCALE ("scale").
*
*  @default VerticalAlign.MIDDLE
*/
public function set verticalAlign(value:String):void;
public function get verticalAlign():String;

//-----------------------------------
//  Custom Content Loading
//-----------------------------------

/**
* Optional custom image loader (e.g. image cache or queue) to
* associate with content loader client.
*/
public function set contentLoader(value:IContentLoader):void;
public function get contentLoader():IContentLoader;

/**
* Optional content grouping identifier to pass to the an
* associated IContentLoader instance's load() method.
* This property is only considered when a valid contentLoader
* is assigned.
*/
function get contentLoaderGrouping():String;
function set contentLoaderGrouping(value:String):void

}
}

Image class:

Image.as
package spark.components
{
...

[Event(name="complete", type="flash.events.Event")]
[Event(name="httpStatus", type="flash.events.HTTPStatusEvent")]
[Event(name="ioError", type="flash.events.IOErrorEvent")]
[Event(name="progress", type="flash.events.ProgressEvent")]
[Event(name="securityError", type="flash.events.SecurityErrorEvent")]

//--------------------------------------
//  SkinStates
//--------------------------------------

/**
*  Disabled state of the Image.
*/
[SkinState("disabled")]

/**
*  Uninitialized state of the Image.
*/
[SkinState("uninitialized")]

/**
*  Loading state of the Image. enablePreload must
*  be set to true to enable this component state.
*/
[SkinState("loading")]

/**
*  Ready state of the Image.
*/
[SkinState("ready")]

/**
*  Invalid state of the Image, e.g. Image could not be
*  successfully loaded.
*/
[SkinState("invalid")]

//-----------------------------------
//  Styles
//-----------------------------------

/**
* The alpha of the background for this component.
*/
[Style(name="backgroundAlpha", type="Number", inherit="no", theme="spark", minValue="0.0", maxValue="1.0")]

/**
* The background color for this component.
*/
[Style(name="backgroundColor", type="uint", format="Color", inherit="no", theme="spark")]

/**
* When true, enables the 'loading' skin state.
*/
[Style(name="enableLoadingState", type="Boolean", inherit="no")]

/**
* Style equivalent to BitmapImage's smoothingQuality property.
*/
[Style(name="smoothingQuality", type="String", inherit="no", enumeration="default,high")]

public class Image extends SkinnableComponent
{
...

//-----------------------------------
//  Skin Parts
//-----------------------------------

/**
* BitmapImage display.
*/
[SkinPart(required="false")]
public var imageDisplay:BitmapImage;

/**
* Optional range part which is automatically
* pushed the current % loaded value (0-100)
*/
[SkinPart(required="false")]
public var progressIndicator:Range;

//-----------------------------------
//  Proxied to/from BitmapImage
//-----------------------------------

/**
* @copy spark.primitives.BitmapImage#bitmapData
*/
public function get bitmapData():BitmapData;

/**
* @copy spark.primitives.BitmapImage#bytesLoaded
*/
public function get bytesLoaded():Number;

/**
* @copy spark.primitives.BitmapImage#bytesTotal
*/
public function get bytesTotal():Number;

/**
* @copy spark.primitives.BitmapImage#clearOnLoad
*/
public function get clearOnLoad():Boolean;
public function set clearOnLoad(value:Boolean):void;

/**
* @copy spark.primitives.BitmapImage#contentLoader
*/
public function set contentLoader(value:IContentLoader):void;
public function get contentLoader():IContentLoader;

/**
* @copy spark.primitives.BitmapImage#contentLoaderGrouping
*/
function get contentLoaderGrouping():String;
function set contentLoaderGrouping(value:String):void

/**
* @copy spark.primitives.BitmapImage#fillMode
*/
public function set fillMode(value:String):void;
public function get fillMode():String;

/**
* @copy spark.primitives.BitmapImage#horizontalAlign
*/
public function set horizontalAlign(value:String):void;
public function get horizontalAlign():String;

/**
* @copy spark.primitives.BitmapImage#preliminaryHeight
*/
public function set preliminaryHeight(value:Number):void;
public function get preliminaryHeight():Number;

/**
* @copy spark.primitives.BitmapImage#preliminaryWidth
*/
public function set preliminaryWidth(value:Number):void;
public function get preliminaryWidth():Number;

/**
* @copy spark.primitives.BitmapImage#scaleMode
*/
public function set scaleMode(value:Boolean):void;
public function get scaleMode():Boolean;

/**
* @copy spark.primitives.BitmapImage#smooth
*/
public function set smooth(value:Boolean):void;
public function get smooth():Boolean;

/**
* @copy spark.primitives.BitmapImage#source
*/
public function set source(value:Object):void;
public function get source():Object;

/**
* @copy spark.primitives.BitmapImage#sourceHeight
*/
public function get sourceHeight():Number;

/**
* @copy spark.primitives.BitmapImage#sourceWidth
*/
public function get sourceWidth():Number;

/**
* @copy spark.primitives.BitmapImage#trustedSource
*/
public function get trustedSource():Boolean;

/**
* @copy spark.primitives.BitmapImage#verticalAlign
*/
public function set verticalAlign(value:String):void;
public function get verticalAlign():String;

}
}


Constants used with BitmapImage's smoothingQuality property:

BitmapSmoothingQuality.as
package spark.core
{
/**
* An enum of the possible smoothing qualities for BitmapImage.
*/
public final class BitmapSmoothingQuality
{
/**
* Default smoothing algorithm is used when scaling,
* consistent with quality of the stage (stage.quality).
*/
public static const DEFAULT:String = "default";

/**
* High quality smoothing algorithm is used when scaling. Used
* when a higher quality (down-sampled) scale is preferred. Yields
* a result similar to one obtained when stage.quality is set to
* 'best', without requiring global stage quality be set to something
* other than the default.
*/
public static const HIGH:String = "high";
}
}

Constants used with BitmapImage's scaleMode property:

BitmapScaleMode.as
package mx.graphics
{
/**
* An enum of the possible scaling modes for BitmapImage.
*/
public final class BitmapScaleMode
{
/**
* Image is stretched to fit the filled region.
*/
public static const STRETCH:String = "stretch";

/**
* Image is scaled while maintaining the aspect ratio of the
* original content.
*/
public static const LETTERBOX:String = "letterbox";
}
}

Default Spark Image skin (pending XD design):

ImageSkin.mxml

<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx" alpha.disabled="0.5">
    
    <s:states>
        <s:State name="disabled" />
        <s:State name="uninitialized" />
        <s:State name="loading"/>
        <s:State name="ready" />
        <s:State name="invalid" />
    </s:states>
    
    <!-- host component -->
    <fx:Metadata>
        <![CDATA[ 
        [HostComponent("spark.components.Image")]
        ]]>
    </fx:Metadata>
    
    <s:BitmapImage id="imageDisplay" left="0" top="0" right="0" bottom="0"/>
    
    <s:Range id="progressIndicator" skinClass="skins.ImageLoadingSkin" 
         verticalCenter="0" horizontalCenter="0" includeIn="loading" />
    
    <s:BitmapImage includeIn="invalid" 
            source="@Embed(source='Assets.swf',symbol='__brokenImage')" 
            verticalCenter="0" horizontalCenter="0"/>
    
</s:Skin>


Default Image preloader skin (pending XD design):

ImagePreloaderSkin.mxml
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
    xmlns:fb="http://ns.adobe.com/flashbuilder/2009">
    
    <!-- host component -->
    <fx:Metadata>
    <![CDATA[ 
        [HostComponent("spark.components.supportClasses.Range")]
    ]]>
    </fx:Metadata>
    
    <s:Rect height="9" width="26">
        <s:stroke>
            <s:SolidColorStroke color="#000000"/>
        </s:stroke>
        <s:fill>
            <s:SolidColor color="#FFFFFF"/>
        </s:fill>
    </s:Rect>
    
    <s:Rect height="5" width="22" top="2" left="2">
        <s:stroke>
            <s:SolidColorStroke color="#000000"/>
        </s:stroke>
        <s:fill>
            <s:SolidColor color="#FFFFFF"/>
        </s:fill>
    </s:Rect>
    
    <s:Group width="21" left="3" top="3" >
        <s:Rect height="4" percentWidth="{hostComponent.value}">
            <s:fill>
                <s:SolidColor color="#B6B2A7"/>
            </s:fill>
        </s:Rect>
    </s:Group>
    
    <s:Label left="29" verticalCenter="0"  
           text="{hostComponent.value}%" />
</s:Skin>

Content Caching and Queuing Classes

IContentLoader interface:

IContentLoader.as
package spark.core
{
/**
* Interface representing a generic content loader. Currently
* BitmapImage supports association with a custom image loader
* implementing the IContentLoader interface.
*/
public interface IContentLoader extends IEventDispatcher
{
/**
* Initiates a content request for the resource identified
* by the key specified.
*
* @param source Unique key used to represent the requested content resource.
*
* @param contentLoaderGrouping - (Optional) grouping identifier for the loaded resource.
* ContentLoader instances supporting content groups generally allow for
* resources within the same named grouping to be addressed as a whole. For
* example the ContentCache's loader queue allows requests to be prioritized
* by contentLoaderGrouping.
*
* @return A ContentRequest instance representing the requested resource.
*
*/
function load(source:Object, contentLoaderGrouping:String):ContentRequest;
}
}

ContentRequest class:

ContentRequest.as
package spark.core
{
[Event(name="complete", type="flash.events.Event")]
[Event(name="httpStatus", type="flash.events.HTTPStatusEvent")]
[Event(name="ioError", type="flash.events.IOErrorEvent")]
[Event(name="progress", type="flash.events.ProgressEvent")]
[Event(name="securityError", type="flash.events.SecurityErrorEvent")]

/**
*  Represents an IContentLoader's content request instance.
*  Wraps a LoaderInfo object that can potentially be shared.
*/
public class ContentRequest extends EventDispatcher
{
/**
* Read only property access to the contained content. This
* can be (among many things), a LoaderInfo instance, BitmapData,
* or any other generic content.  When the complete event has fired
* and/or complete() returns true, the content is considered valid.
*/
public function get content():Object;

/**
* Read only flag. Returns true if content is
* considered fully loaded and accessible.
*/
public function get complete():Boolean;
}
}


ContentCache class:

ContentCache.as
package spark.core
{
/**
* SDK provided caching and queuing content loader.
*/
public class ContentCache extends EventDispatcher implements IContentLoader
{
//-----------------------------------
//  Methods
//-----------------------------------

/**
* @copy spark.core.IContentLoader#load
*/
function load(source:Object, contentLoaderGrouping:String):ContentRequest;

/**
* Adds new entry to cache (or replaces existing entry).
*/
public function addCacheEntry(source:Object, value:Object):void;

/**
* Obtain an entry for the given key if one exists.
*/
public function getCacheEntry(source:Object):Object;

/**
* Removes all cache entries.
*/
public function removeAllCacheEntries():void;

/**
* Removes single cache entry.
*/
public function removeCacheEntry(source:Object):void;

//-----------------------------------
//  Properties
//-----------------------------------

/**
* Enables caching behavior and functionality.
* @default true
*/
public function set enableCaching(value:Boolean):void;
public function get enableCaching():Boolean;


/**
* Maximum size of MRU based cache.  When numEntries exceeds
* maxCacheEntries the least recently used are pruned to fit.
* @default 100
*/
public function set maxCacheEntries(value:int):void;
public function get maxCacheEntries():int;

/**
* Count of active/in-use cache entries.
*/
public function get numCacheEntries():int;

//-----------------------------------
//  Queuing Methods and Properties
//-----------------------------------

/**
* Promotes a content grouping to the head of the loading queue.
*/
public function prioritize(contentLoaderGrouping:String):void;

/**
*  Resets the queue to initial empty state.  All requests, both active
*  and queued, are cancelled. All cache entries associated with canceled
*  requests are invalidated.
*/
public function removeAllQueueEntries():void;

/**
* Enables queuing behavior and functionality.
* @default false
*/
public function set enableQueuing(value:Boolean):void;
public function get enableQueuing():Boolean;

/**
* Maximum simultaneous active requests when queuing is enabled.
* @default 2
*/
public function set maxActiveRequests(value:int):void;
public function get maxActiveRequests():int;

}
}

B Features

Progressive Image Formats
Customers have asked for a means of rendering "progressive" image formats (and updating while the image data is received). Unfortunately the only mechanism to do so is via Loader.loadBytes which has security implications and as such we wouldn't be able to enable this support by default. It would clearly have to be an opt-in, "use at your own risk" feature.

Auto Cache Expiration / Memory Conservation
Currently when the developer wishes to free up memory resources they must do so manually by removing all cache entries from the cache when no longer required. If they fail to do so references to the bitmap data will persist indefinitely.

We would prefer that all "least recently used" items are automatically released (optional), or perhaps all items would expire over time (as configured by the user). Another possibility is to support an option where all cache entries refer to their associated image data via a weak reference. The concern here is that the player is pretty aggressive about GC'ing weakly referenced objects when there are no hard references (as contrasted with say Java soft references which are generally only released when memory constraints warrant).

BitmapFill Enhancements - Adding a scaleMode and support for BitmapScaleMode.letterbox, as well as smoothingQuality/HIGH to BitmapFill would make things a bit more consistent with BitmapImage (though not sure how often it would be required). Adding remote image loading would also be desired but it's currently unknown how feasible an asynchronous fill implementation would be.

BitmapScaleMode.ZOOM - When scaling images to fit, say a fixed size picture frame, BitmapScaleMode.LETTERBOX (with center/middle) alignment usually suffices. In some cases it's desired that the image retain it's original aspect ratio but be "zoomed" to fit the bounds. Any excess content outside the constrained image bounds would be clipped. This ensures there the image content is zoomed to fit, eliminating any excess space that may exist when the image content aspect ratio doesn't match the constrained size.

Examples and Usage

BitmapImage

Remote URL support:

<s:BitmapImage source="http://www.mysite.com/image.jpg"/>

Scaling with regard to aspect ratio:


<s:BitmapImage source="@Embed('images.png')" width="100" height="100" 
    scaleMode="letterbox" horizontalAlign="center" verticalAlign="middle" />

High(er) quality down-scale:


<s:BitmapImage source="{data.profileImageSource}" width="50" height="50" 
    smooth="true" smoothingQuality="high" />

Virtualization "friendly" list item renderer:

<s:ItemRenderer 
        xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark"
        focusEnabled="false">

    <!-- preliminaryWidth and preliminaryHeight allow the cumulative bounds
         of the item renderer to be something other than 0x0 while the 
         image is loading -->
    <s:BitmapImage preliminaryWidth="50" preliminaryHeight="50" source="{data.src}" />

</s:ItemRenderer>

Image

Use of s:Image with default Spark skin configured to display preloader and broken image icon when load fails.

<s:Image source="http://www.mysite.com/image.jpg" enablePreload="true"/>

Custom Image Skin

Custom Image skin:

<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:s="library://ns.adobe.com/flex/spark" 
        xmlns:mx="library://ns.adobe.com/flex/mx" 
        width="137" height="120">
    
    <!-- HostComponent metadata -->
    <fx:Metadata>
        <![CDATA[ 
        [HostComponent("components.Image")]
        ]]>
    </fx:Metadata>
    
    <s:states>
        <s:State name="uninitialized" />
        <s:State name="loading"/>
        <s:State name="ready" />
        <s:State name="invalid" />
        <s:State name="disabled" />
    </s:states>
    
    <!-- Our imageDisplay part is constrained to the bounds of a Group
         which in turn is constrained to the fixed bounds of our image frame
         via the hardcoded width and height on our Skin (corresponding to the
         bounds of our image frame artwork. -->
    <s:Group left="10" top="10" right="10" bottom="10">
        <s:BitmapImage id="imageDisplay" width="100%" height="100%" 
            verticalCenter="0" horizontalCenter="0"
            verticalAlign="middle" horizontalAlign="center"
            scaleMode="letterbox" smooth="true" smoothingQuality="high"/>
    </s:Group>
    
    <!-- Preloader -->
    <s:Range id="progressIndicator" skinClass="skins.ImageLoadingSkin" 
             verticalCenter="0" horizontalCenter="0" includeIn="loading"  />
    
    <!-- "Broken" image icon -->
    <s:BitmapImage includeIn="invalid" 
        source="@Embed(source='Assets.swf',symbol='__brokenImage')" 
        verticalCenter="0" horizontalCenter="0"/>
    
    <!-- Fancy image frame border overlayed on centered/scaled image. -->
    <s:BitmapImage source="@Embed('pictureFrame.png')"/>
</s:Skin>

Image Cache

Example use of image cache by an image based item renderer (assumes the existence of a previously existing ContentCache with id 'employeeCache' in the top level application scope):

<s:ItemRenderer 
        xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark"
        focusEnabled="false">
    
    <fx:Script>
    <![CDATA[
        import mx.core.FlexGlobals;
    ]]>
    </fx:Script>


    <!-- Associate this image instance with the 'employeeCache' content
         loader.  If the 'employeeCache' content loader was not previously
         declared, one will be created. -->
    <s:BitmapImage source="{data.src}" 
        contentLoaderName="{FlexGlobals.topLevelApplication.employeeCache}" />

</s:ItemRenderer>

The image cache from the above example could have been declared and configured at the
application level (or elsewhere):

<s:Application 
        xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark">

    <s:List id="employeeList">
        ...
    </s:List>

    <fx:Declarations>
        <s:ContentCache id="employeeCache" maxCacheEntries="100"/>
    </fx:Declarations>

</s:Application>


Hypothetical (contrived) queuing example below:

On each tab a list of thumbnails appears. As the user switches tabs quickly the thumbnails for the "current" active tab are prioritized over any other group (e.g. any images previously loading from the previous tab(s)).

(Obviously in a more typical use case the image lists would be actual lists with image based item renderers.)

<s:Application 
        xmlns:fx="http://ns.adobe.com/mxml/2009" 
        xmlns:mx="library://ns.adobe.com/flex/mx"
        xmlns:s="library://ns.adobe.com/flex/spark">

    <s:layout>
        <s:VerticalLayout/>
    </s:layout>

    <fx:Script>
    <![CDATA[
    private function setView(index:int):void
        {
            tn.selectedIndex = index;

            switch(index)
            {
                case 0: 
                    myCache.prioritize("Employees");
                    break;
                case 1: 
                    myCache.prioritize("Managers");
                    break;
                case 2: 
                    myCache.prioritize("Execs");
                    break;
            }
        }       
    ]]>
    </fx:Script>

    <s:HGroup>
        <s:Button label="Employees" click="setView(0);"/>
        <s:Button label="Managers" click="setView(1);"/>
        <s:Button label="Execs" click="setView(2);"/>
    </mx:HGroup>

    <mx:TabNavigator id="navigator" width="100%" height="100%">
        <s:NavigatorContent label="Employees">
            <s:VGroup>
                <s:BitmapImage source="imgs/BigImage01.jpg" 
                    contentLoaderGrouping="Employees" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage02.jpg" 
                    contentLoaderGrouping="Employees" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage03.jpg" 
                    contentLoaderGrouping="Employees" contentLoader="{myCache}"/>
                ...
            <s:/VGroup>
        </s:NavigatorContent>

        <s:NavigatorContent label="Managers">
            <s:VGroup>
                <s:BitmapImage source="imgs/BigImage06.jpg" 
                    contentLoaderGrouping="Managers" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage07.jpg" 
                    contentLoaderGrouping="Managers" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage08.jpg" 
                    contentLoaderGrouping="Managers" contentLoader="{myCache}"/>
                ...
            <s:/VGroup>
        </s:NavigatorContent>

        <s:NavigatorContent label="Execs">
            <s:VGroup>
                <s:BitmapImage source="imgs/BigImage11.jpg" 
                    contentLoaderGrouping="Execs" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage12.jpg" 
                    contentLoaderGrouping="Execs" contentLoader="{myCache}"/>
                <s:BitmapImage source="imgs/BigImage13.jpg" 
                    contentLoaderGrouping="Execs" contentLoader="{myCache}"/>
                ...
            <s:/VGroup>
        </s:NavigatorContent>
                
    </mx:TabNavigator>


    <fx:Declarations>
        <s:ContentCache name="myCache" enableQueuing="true"/>
    </fx:Declarations>

</s:Application>

Additional Implementation Details

For a better understanding of how BitmapImage interacts with a configured ContentCache the following flow is provided:

1. User associates a BitmapImage instance with a ContentCache:

<s:BitmapImage source="http://www.mysite.com/myImage.jpg" contentLoader="{cache}"/>

2. BitmapImage does the source lookup (URL request) within commitProperties. The content loader's load method is invoked with "http://www.mysite.com/myImage.jpg".

3. The ContentCache checks its cache (Dictionary hash) for an entry corresponding to the provided URL. None is found, so a new URLLoader request is made for the resource. A new cache entry is added to the hash (our # entries is now 1). The entry is also added to a linked list (ordered by most recently used). We ensure we haven't exceeded our maxCacheEntries, then a ContentRequest instance is returned to the caller.

4. The BitmapImage checks the complete() property on the returned ContentRequest instance. It's returns false, so the BitmapImage attaches listeners to the request object and waits for either completion or an error condition.

5. Meanwhile the ContentCache awaits completion as well, so that it can inspect the data once it arrives to make sure that the content can be considered "trusted" (meaning it can be shared by multiple cache clients).

6. The image data arrives and the BitmapImage acquires the content from the ContentRequest instance, and the image is displayed on the screen.

Now contrast this with 2nd request made for the same URL:

1. Another BitmapImage instance referencing the same source as above and associated with the same cache does the source lookup (URL request) within commitProperties. The content loader's load method is invoked with "http://www.mysite.com/myImage.jpg" as before.

2. The ContentCache checks its cache (Dictionary hash) for an entry corresponding to the provided URL. One is found so a ContentRequest object is returned containing a reference to the cached data, and already marked 'complete'. The cache entry is bumped to the top of our MRU list.

3. The BitmapImage checks the complete() property on the returned ContentRequest instance. It's returns true, so the BitmapImage acquires the contained LoaderInfo (via the content property) and renders the image.

Lastly, consider the case where our Image cache already contains the maximum number of entries and a new request is made:

1. A load request is made as before.

2. The ContentCache checks its cache (Dictionary hash) for an entry corresponding to the provided URL. None is found, so a new URLLoader request is made for the resource. A new cache entry is added to the hash (our # entries is now 1). The entry is also added to a linked list (ordered by most recently used). We ensure we haven't exceeded our maxCacheEntries, in this case we have...so the least recently used item is pruned from our MRU list and its associate hash entry is deleted.

Compiler Work

Not applicable.

Backwards Compatibility

Syntax changes

Not applicable.

Behavior

Currently only the BitmapImage tag is being extended as detailed in this specification.

NOTE: The BitmapImage class is considered part of the FXG markup specification. It's currently assumed unless determined otherwise that it's legal for the SDK to extend the interface of BitmapImage as long as it does not directly conflict with the definition or function of BitmapImage in the FXG specification.

Warnings/Deprecation

Not applicable.

Accessibility

Not applicable.

Performance

Given the fact that there is an additional display object in the mix, it is currently expected that the skinnable image component will be less performant (both with memory and time to instantiate) than the BitmapImage primitive, but only moderately so.

Globalization

We may possibly need to ensure that the default Image skin presents a default progress indicator appropriate for RTL languages.

Localization

Compiler Features

Not applicable.

Framework Features

InvalidContentError - This localized RTE message will be thrown when a BitmapImage attempts to load non-image content.

QE Notes

Suggested focus areas and test cases to consider (not exhaustive):

  • Test BitmapImage with a remote "trusted" image source URL (cross domain image with a valid crossdomain policy file in place).
  • Test BitmapImage with a remote "untrusted" image source URL (cross domain image with no valid crossdomain policy file in place).
  • Ensure that for untrusted images, trustedSource is false (once loaded), and needsDisplayObject returns true on the BitmapImage instance.
  • Ensure all loader events are dispatched properly from the BitmapImage as images load.
  • Test that smoothingQuality = "high" yields a better looking thumbnail in the general case than the default.
  • Create a BitmapImage based Spark list item renderer and ensure that preliminaryWidth, preliminaryHeight are honored by the list layout (using a virtualized list).
  • Test scaleMode/letterbox when width/height of the BitmapImage are set to a different aspect ratio than the actual original image.
  • Test scaleMode/letterbox with only setting either width or height (not both).
  • Test that horizontalAlign and verticalAlign positions the image content relative to the "leftover" space when scaleMode/letterbox is used.
  • Make sure the Image component pushes all BitmapImage properties correct to its imageDisplay part.
  • Ensure that the default Image component skin displays the invalid image (broken image) icon when a remote URL fails to load.
  • Test the enablePreload style on Image,and ensure that the loading state signals on the Image skin (and that the default preloader properly reflects load progress).
  • Experiment with creating a custom image skin.
  • Test BitmapImage with a string value reflecting the name of a preconfigured ContentCache instance.
  • Ensure images are loading from the cache (should be obvious by visible load time) - might have to be manual test or you can use getCacheEntry() on the cache to ensure a previous image load request was successfully cached.
  • Test that multiple images sharing the same URL only yield 1 entry in the cache.
  • Test each of the attributes of the ContentCache (zap all the cached entries and ensure new image instances must reload).
  • Test that a virtualized list containing item renderers with images (configured with a cache) render quickly as you scroll "old" items back into view.
  • Test that maxCacheEntries is enforced by the cache (e.g. that numEntries shrinks if set set maxCacheEntries after the cache has been populated).
  • Test queuing functionality by enabling queing and giving a batch of images a "contentLoaderGrouping" name... ensure that you can increase the priority of content groups correctly.
  • Test maxActiveRequests (default is 2, try setting it to 1 to ensure when queuing is enabled only one image loads at a time). If an image is already in the cache it will bypass the queue so ensure you test with empty cache.
  • Test any of the additional methods/properties of the ContentCache not covered in the other tests.
  • Test BitmapImage with a custom image loader of your own.

eXperience Design (XD)

We will require a design for the default Image preloader (either one that is range based and presents progress, or a simpler indeterminate progress representation (e.g. spinner)). Both Spark and Wireframe versions are required.

We will also require a default broken/error image icon. Hopefully something a little fresher than our existing Flex 3 broken image icon.

You must be logged in to comment.
  1. Jul 18, 2010

    marc wensauer says:

    Looks great, is this code intended to try or just an example how the code will l...

    Looks great,
    is this code intended to try or just an example how the code will look?
    Any Example flex 4 Project to try ?