    /**
     * @author Ed Spencer (http://sencha.com)
     * Transition plugin for DataViews
     */
    Ext.define('Ext.ux.DataView.Animated', {

        /**
         * @property defaults
         * @type Object
         * Default configuration options for all DataViewTransition instances
         */
        defaults: {
            duration  : 750,
            idProperty: 'id'
        },

        /**
         * Creates the plugin instance, applies defaults
         * @constructor
         * @param {Object} config Optional config object
         */
        constructor: function(config) {
            Ext.apply(this, config || {}, this.defaults);
        },

        /**
         * Initializes the transition plugin. Overrides the dataview's default refresh function
         * @param {Ext.view.View} dataview The dataview
         */
        init: function(dataview) {
            /**
             * @property dataview
             * @type Ext.view.View
             * Reference to the DataView this instance is bound to
             */
            this.dataview = dataview;

            var store = dataview.store;

            dataview.blockRefresh = true;
            dataview.updateIndexes = Ext.Function.createSequence(dataview.updateIndexes, function() {
                this.getTargetEl().select(this.itemSelector).each(function(element, composite, index) {
                    element.id = element.dom.id = Ext.util.Format.format("{0}-{1}", dataview.id, store.getAt(index).internalId);
                }, this);
            }, dataview);

            /**
             * @property dataviewID
             * @type String
             * The string ID of the DataView component. This is used internally when animating child objects
             */
            this.dataviewID = dataview.id;

            /**
             * @property cachedStoreData
             * @type Object
             * A cache of existing store data, keyed by id. This is used to determine
             * whether any items were added or removed from the store on data change
             */
            this.cachedStoreData = {};

            //catch the store data with the snapshot immediately
            this.cacheStoreData(store.data || store.snapshot);

            dataview.on('resize', function() {
                var store = dataview.store;
                if (store.getCount() > 0) {
                    // reDraw.call(this, store);
                }
            }, this);

            dataview.store.on('datachanged', reDraw, this);

            function reDraw(store) {
                var parentEl = dataview.getTargetEl(),
                    calcItem = store.getAt(0),
                    added    = this.getAdded(store),
                    removed  = this.getRemoved(store),
                    previous = this.getRemaining(store);

                // Not yet rendered
                if (!parentEl) {
                    return;
                }

                //make sure the correct styles are applied to the parent element
                if (Ext.isIEQuirks) {
                    parentEl.applyStyles({
                        zoom: 1,
                        display : 'block',
                        position: 'relative'
                    });
                }

                //hide old items
                Ext.each(removed, function(item) {
                    var id = this.dataviewID + '-' + item.internalId;
                    Ext.fly(id).animate({
                        remove  : false,
                        duration: duration,
                        opacity : 0,
                        useDisplay: true,
                        callback: function() {
                            Ext.fly(id).setDisplayed(false);
                        }
                    });
                }, this);

                //store is empty
                if (calcItem == undefined) {
                    this.cacheStoreData(store);
                    return;
                }

                this.cacheStoreData(store);

                var el = Ext.get(this.dataviewID + "-" + calcItem.internalId);

                //if there is nothing rendered, force a refresh and return. This happens when loading asynchronously (was not
                //covered correctly in previous versions, which only accepted local data)
                if (!el) {
                    dataview.refresh();
                    return true;
                }

                //calculate the number of rows and columns we have
                var itemWidth   = el.getMargin('lr') + el.getWidth(),
                    itemHeight  = el.getMargin('bt') + el.getHeight(),
                    dvWidth     = parentEl.dom.clientWidth,
                    columns     = Math.floor(dvWidth / itemWidth),
                    rtl = this.dataview.getHierarchyState().rtl,
                    styleSide = rtl ? 'right' : 'left',
                    newStyle;

                //stores the current top and left values for each element (discovered below)
                var oldPositions = {},
                    newPositions = {},
                    elCache      = {};

                //find current positions of each element and save a reference in the elCache
                Ext.iterate(previous, function(id, item) {
                    var id = item.internalId,
                        el = elCache[id] = Ext.get(this.dataviewID + '-' + id);

                    oldPositions[id] = {
                        top : el.getY()  - parentEl.getY()  - el.getMargin('t') - parentEl.getPadding('t')
                    };
                    oldPositions[id][styleSide] = this.getItemX(el);
                }, this);

                //set absolute positioning on all DataView items. We need to set position, left and
                //top at the same time to avoid any flickering
                Ext.iterate(previous, function(id, item) {
                    var oldPos = oldPositions[id],
                        el     = elCache[id];

                    if (el.getStyle('position') != 'absolute') {
                        newStyle = {
                            position: 'absolute',
                            top     : oldPos.top + "px"
                        };
                        newStyle[styleSide] = oldPos[styleSide] + "px";
                        elCache[id].applyStyles(newStyle);
                    }
                });

                //get new positions
                var index = 0;
                Ext.iterate(store.data.items, function(item) {
                    var id = item.internalId,
                        column = index % columns,
                        row    = Math.floor(index / columns),
                        top    = row    * itemHeight,
                        left   = column * itemWidth;

                    newPositions[id] = {
                        top : top
                    };
                    newPositions[id][styleSide] = left;

                    index ++;
                }, this);

                //do the movements
                var startTime  = new Date(),
                    duration   = this.duration,
                    dataviewID = this.dataviewID;

                var doAnimate = function() {
                    var elapsed  = new Date() - startTime,
                        fraction = elapsed / duration,
                        id;

                    if (fraction >= 1) {
                        for (id in newPositions) {
                            newStyle = {
                                top : newPositions[id].top + "px"
                            };
                            newStyle[styleSide] = newPositions[id][styleSide] + "px";

                            Ext.fly(dataviewID + '-' + id).applyStyles(newStyle);
                        }

                        Ext.TaskManager.stop(task);
                    } else {
                        //move each item
                        for (id in newPositions) {
                            if (!previous[id]) {
                                continue;
                            }

                            var oldPos  = oldPositions[id],
                                newPos  = newPositions[id],
                                oldTop  = oldPos.top,
                                newTop  = newPos.top,
                                oldLeft = oldPos[styleSide],
                                newLeft = newPos[styleSide],
                                diffTop = fraction * Math.abs(oldTop  - newTop),
                                diffLeft= fraction * Math.abs(oldLeft - newLeft),
                                midTop  = oldTop  > newTop  ? oldTop  - diffTop  : oldTop  + diffTop,
                                midLeft = oldLeft > newLeft ? oldLeft - diffLeft : oldLeft + diffLeft;

                            newStyle = {
                                top : midTop + "px"
                            };
                            newStyle[styleSide] = midLeft + "px";
                            Ext.fly(dataviewID + '-' + id).applyStyles(newStyle).setDisplayed(true);
                        }
                    }
                };

                var task = {
                    run     : doAnimate,
                    interval: 20,
                    scope   : this
                };

                Ext.TaskManager.start(task);

                //show new items
                Ext.iterate(added, function(id, item) {
                    newStyle = {
                        top    : newPositions[item.internalId].top + "px"
                    };
                    newStyle[styleSide] = newPositions[item.internalId][styleSide] + "px";
                    Ext.fly(this.dataviewID + '-' + item.internalId).applyStyles(newStyle).setDisplayed(true);

                    Ext.fly(this.dataviewID + '-' + item.internalId).animate({
                        remove  : false,
                        duration: duration,
                        opacity : 1
                    });
                }, this);

                this.cacheStoreData(store);
            }
        },

        getItemX: function(el) {
            var rtl = this.dataview.getHierarchyState().rtl,
                parentEl = el.up('');

            if (rtl) {
                return parentEl.getViewRegion().right - el.getRegion().right + el.getMargin('r');
            } else {
                return el.getX() - parentEl.getX() - el.getMargin('l') - parentEl.getPadding('l');
            }
        },

        /**
         * Caches the records from a store locally for comparison later
         * @param {Ext.data.Store} store The store to cache data from
         */
        cacheStoreData: function(store) {
            this.cachedStoreData = {};

            store.each(function(record) {
                 this.cachedStoreData[record.internalId] = record;
            }, this);
        },

        /**
         * Returns all records that were already in the DataView
         * @return {Object} All existing records
         */
        getExisting: function() {
            return this.cachedStoreData;
        },

        /**
         * Returns the total number of items that are currently visible in the DataView
         * @return {Number} The number of existing items
         */
        getExistingCount: function() {
            var count = 0,
                items = this.getExisting();

            for (var k in items) {
                count++;
            }

            return count;
        },

        /**
         * Returns all records in the given store that were not already present
         * @param {Ext.data.Store} store The updated store instance
         * @return {Object} Object of records not already present in the dataview in format {id: record}
         */
        getAdded: function(store) {
            var added = {};

            store.each(function(record) {
                if (this.cachedStoreData[record.internalId] == undefined) {
                    added[record.internalId] = record;
                }
            }, this);

            return added;
        },

        /**
         * Returns all records that are present in the DataView but not the new store
         * @param {Ext.data.Store} store The updated store instance
         * @return {Array} Array of records that used to be present
         */
        getRemoved: function(store) {
            var removed = [],
                id;

            for (id in this.cachedStoreData) {
                if (store.findBy(function(record) {return record.internalId == id;}) == -1) {
                    removed.push(this.cachedStoreData[id]);
                }
            }

            return removed;
        },

        /**
         * Returns all records that are already present and are still present in the new store
         * @param {Ext.data.Store} store The updated store instance
         * @return {Object} Object of records that are still present from last time in format {id: record}
         */
        getRemaining: function(store) {
            var remaining = {};

            store.each(function(record) {
                if (this.cachedStoreData[record.internalId] != undefined) {
                    remaining[record.internalId] = record;
                }
            }, this);

            return remaining;
        }
    });