refactor TileLayer animation, fix #1140, #1437, #52, #1442

Refactored TileLayer animation so that it happens for each tile layer
independently instead of animating the parent of all tile layers. Moved
TileLayer animation code into a separate file (TileLayer.Anim.js).
Fixes loads of bugs and makes the code easier to understand.
This commit is contained in:
Vladimir Agafonkin 2013-02-20 18:40:00 +02:00
parent 0b14d71d7a
commit 40a824fc97
6 changed files with 185 additions and 151 deletions

View File

@ -245,7 +245,7 @@ var deps = {
}, },
AnimationZoom: { AnimationZoom: {
src: ['map/anim/Map.ZoomAnimation.js'], src: ['map/anim/Map.ZoomAnimation.js', 'layer/tile/TileLayer.Anim.js'],
deps: ['AnimationPan'], deps: ['AnimationPan'],
desc: 'Smooth zooming animation. Works only on browsers that support CSS3 Transitions.' desc: 'Smooth zooming animation. Works only on browsers that support CSS3 Transitions.'
}, },

View File

@ -0,0 +1,117 @@
/*
Zoom animation logic for L.TileLayer.
*/
L.TileLayer.include({
_animateZoom: function (e) {
var firstFrame = false;
if (!this._animating) {
this._animating = true;
firstFrame = true;
}
if (firstFrame) {
this._prepareBgBuffer();
}
var transform = L.DomUtil.TRANSFORM,
bg = this._bgBuffer;
if (firstFrame) {
//prevent bg buffer from clearing right after zoom
clearTimeout(this._clearBgBufferTimer);
// hack to make sure transform is updated before running animation
L.Util.falseFn(bg.offsetWidth);
}
var scaleStr = L.DomUtil.getScaleString(e.scale, e.origin),
oldTransform = bg.style[transform];
bg.style[transform] = e.backwards ?
(e.delta ? L.DomUtil.getTranslateString(e.delta) : oldTransform) + ' ' + scaleStr :
scaleStr + ' ' + oldTransform;
},
_endZoomAnim: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
front.style.visibility = '';
front.style.zIndex = 2;
bg.style.zIndex = 1;
// force reflow
L.Util.falseFn(bg.offsetWidth);
this._animating = false;
},
_clearBgBuffer: function () {
var map = this._map;
if (!map._animatingZoom && !map.touchZoom._zooming) {
this._bgBuffer.innerHTML = '';
this._bgBuffer.style[L.DomUtil.TRANSFORM] = '';
}
},
_prepareBgBuffer: function () {
var front = this._tileContainer,
bg = this._bgBuffer;
// if foreground layer doesn't have many tiles but bg layer does,
// keep the existing bg layer and just zoom it some more
if (bg && this._getLoadedTilesPercentage(bg) > 0.5 &&
this._getLoadedTilesPercentage(front) < 0.5) {
front.style.visibility = 'hidden';
this._stopLoadingImages(front);
return;
}
// prepare the buffer to become the front tile pane
bg.style.visibility = 'hidden';
bg.style[L.DomUtil.TRANSFORM] = '';
// switch out the current layer to be the new bg layer (and vice-versa)
this._tileContainer = bg;
bg = this._bgBuffer = front;
this._stopLoadingImages(bg);
},
_getLoadedTilesPercentage: function (container) {
var tiles = container.getElementsByTagName('img'),
i, len, count = 0;
for (i = 0, len = tiles.length; i < len; i++) {
if (tiles[i].complete) {
count++;
}
}
return count / len;
},
// stops loading all tiles in the background layer
_stopLoadingImages: function (container) {
var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
i, len, tile;
for (i = 0, len = tiles.length; i < len; i++) {
tile = tiles[i];
if (!tile.complete) {
tile.onload = L.Util.falseFn;
tile.onerror = L.Util.falseFn;
tile.src = L.Util.emptyImageUrl;
tile.parentNode.removeChild(tile);
}
}
}
});

View File

@ -53,6 +53,7 @@ L.TileLayer = L.Class.extend({
onAdd: function (map) { onAdd: function (map) {
this._map = map; this._map = map;
this._animated = map.options.zoomAnimation && L.Browser.any3d;
// create a container div for tiles // create a container div for tiles
this._initContainer(); this._initContainer();
@ -62,10 +63,17 @@ L.TileLayer = L.Class.extend({
// set up events // set up events
map.on({ map.on({
'viewreset': this._resetCallback, 'viewreset': this._reset,
'moveend': this._update 'moveend': this._update
}, this); }, this);
if (this._animated) {
map.on({
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
}
if (!this.options.updateWhenIdle) { if (!this.options.updateWhenIdle) {
this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this); this._limitedUpdate = L.Util.limitExecByInterval(this._update, 150, this);
map.on('move', this._limitedUpdate, this); map.on('move', this._limitedUpdate, this);
@ -84,10 +92,17 @@ L.TileLayer = L.Class.extend({
this._container.parentNode.removeChild(this._container); this._container.parentNode.removeChild(this._container);
map.off({ map.off({
'viewreset': this._resetCallback, 'viewreset': this._reset,
'moveend': this._update 'moveend': this._update
}, this); }, this);
if (this._animated) {
map.off({
'zoomanim': this._animateZoom,
'zoomend': this._endZoomAnim
}, this);
}
if (!this.options.updateWhenIdle) { if (!this.options.updateWhenIdle) {
map.off('move', this._limitedUpdate, this); map.off('move', this._limitedUpdate, this);
} }
@ -151,8 +166,7 @@ L.TileLayer = L.Class.extend({
redraw: function () { redraw: function () {
if (this._map) { if (this._map) {
this._map._panes.tilePane.empty = false; this._reset({hard: true});
this._reset(true);
this._update(); this._update();
} }
return this; return this;
@ -212,11 +226,20 @@ L.TileLayer = L.Class.extend({
_initContainer: function () { _initContainer: function () {
var tilePane = this._map._panes.tilePane; var tilePane = this._map._panes.tilePane;
if (!this._container || tilePane.empty) { if (!this._container) {
this._container = L.DomUtil.create('div', 'leaflet-layer'); this._container = L.DomUtil.create('div', 'leaflet-layer');
this._updateZIndex(); this._updateZIndex();
if (this._animated) {
var className = 'leaflet-tile-container leaflet-zoom-animated';
this._bgBuffer = L.DomUtil.create('div', className, this._container);
this._tileContainer = L.DomUtil.create('div', className, this._container);
} else {
this._tileContainer = this._container;
}
tilePane.appendChild(this._container); tilePane.appendChild(this._container);
if (this.options.opacity < 1) { if (this.options.opacity < 1) {
@ -225,11 +248,7 @@ L.TileLayer = L.Class.extend({
} }
}, },
_resetCallback: function (e) { _reset: function (e) {
this._reset(e.hard);
},
_reset: function (clearOldContainer) {
var tiles = this._tiles; var tiles = this._tiles;
for (var key in tiles) { for (var key in tiles) {
@ -245,8 +264,10 @@ L.TileLayer = L.Class.extend({
this._unusedTiles = []; this._unusedTiles = [];
} }
if (clearOldContainer && this._container) { this._tileContainer.innerHTML = "";
this._container.innerHTML = "";
if (this._animated && e && e.hard) {
this._clearBgBuffer();
} }
this._initContainer(); this._initContainer();
@ -319,7 +340,7 @@ L.TileLayer = L.Class.extend({
this._addTile(queue[i], fragment); this._addTile(queue[i], fragment);
} }
this._container.appendChild(fragment); this._tileContainer.appendChild(fragment);
}, },
_tileShouldBeLoaded: function (tilePoint) { _tileShouldBeLoaded: function (tilePoint) {
@ -365,8 +386,8 @@ L.TileLayer = L.Class.extend({
L.DomUtil.removeClass(tile, 'leaflet-tile-loaded'); L.DomUtil.removeClass(tile, 'leaflet-tile-loaded');
this._unusedTiles.push(tile); this._unusedTiles.push(tile);
} else if (tile.parentNode === this._container) { } else if (tile.parentNode === this._tileContainer) {
this._container.removeChild(tile); this._tileContainer.removeChild(tile);
} }
// for https://github.com/CloudMade/Leaflet/issues/137 // for https://github.com/CloudMade/Leaflet/issues/137
@ -386,7 +407,6 @@ L.TileLayer = L.Class.extend({
/* /*
Chrome 20 layouts much faster with top/left (verify with timeline, frames) Chrome 20 layouts much faster with top/left (verify with timeline, frames)
Android 4 browser has display issues with top/left and requires transform instead Android 4 browser has display issues with top/left and requires transform instead
Android 3 browser not tested
Android 2 browser requires top/left or tiles disappear on load or first drag Android 2 browser requires top/left or tiles disappear on load or first drag
(reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866 (reappear after zoom) https://github.com/CloudMade/Leaflet/issues/866
(other browsers don't currently care) - see debug/hacks/jitter.html for an example (other browsers don't currently care) - see debug/hacks/jitter.html for an example
@ -397,7 +417,7 @@ L.TileLayer = L.Class.extend({
this._loadTile(tile, tilePoint); this._loadTile(tile, tilePoint);
if (tile.parentNode !== this._container) { if (tile.parentNode !== this._tileContainer) {
container.appendChild(tile); container.appendChild(tile);
} }
}, },
@ -499,6 +519,10 @@ L.TileLayer = L.Class.extend({
this._tilesToLoad--; this._tilesToLoad--;
if (!this._tilesToLoad) { if (!this._tilesToLoad) {
this.fire('load'); this.fire('load');
// clear scaled tiles after all new tiles are loaded (for performance)
clearTimeout(this._clearBgBufferTimer);
this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 500);
} }
}, },

View File

@ -628,12 +628,9 @@ L.Map = L.Class.extend({
}, },
_onTileLayerLoad: function () { _onTileLayerLoad: function () {
// TODO super-ugly, refactor!!!
// clear scaled tiles after all new tiles are loaded (for performance)
this._tileLayersToLoad--; this._tileLayersToLoad--;
if (this._tileLayersNum && !this._tileLayersToLoad && this._tileBg) { if (this._tileLayersNum && !this._tileLayersToLoad) {
clearTimeout(this._clearTileBgTimer); this.fire('tilelayersload');
this._clearTileBgTimer = setTimeout(L.bind(this._clearTileBg, this), 500);
} }
}, },

View File

@ -26,32 +26,21 @@ L.Map.include(!L.DomUtil.TRANSITION ? {} : {
// if offset does not exceed half of the view // if offset does not exceed half of the view
if (!this._offsetIsWithinView(offset, 1)) { return false; } if (!this._offsetIsWithinView(offset, 1)) { return false; }
L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
this this
.fire('movestart') .fire('movestart')
.fire('zoomstart'); .fire('zoomstart');
this.fire('zoomanim', {
center: center,
zoom: zoom
});
var origin = this._getCenterLayerPoint().add(offset); var origin = this._getCenterLayerPoint().add(offset);
this._prepareTileBg(); this._animateZoom(center, zoom, origin, scale);
this._runAnimation(center, zoom, scale, origin);
return true; return true;
}, },
_catchTransitionEnd: function () { _animateZoom: function (center, zoom, origin, scale, delta, backwards) {
if (this._animatingZoom) {
this._onZoomTransitionEnd(); L.DomUtil.addClass(this._mapPane, 'leaflet-zoom-anim');
}
},
_runAnimation: function (center, zoom, scale, origin, backwardsTransform) {
this._animateToCenter = center; this._animateToCenter = center;
this._animateToZoom = zoom; this._animateToZoom = zoom;
this._animatingZoom = true; this._animatingZoom = true;
@ -60,110 +49,31 @@ L.Map.include(!L.DomUtil.TRANSITION ? {} : {
L.Draggable._disabled = true; L.Draggable._disabled = true;
} }
var transform = L.DomUtil.TRANSFORM, this.fire('zoomanim', {
tileBg = this._tileBg; center: center,
zoom: zoom,
clearTimeout(this._clearTileBgTimer); origin: origin,
scale: scale,
L.Util.falseFn(tileBg.offsetWidth); //hack to make sure transform is updated before running animation delta: delta,
backwards: backwards
var scaleStr = L.DomUtil.getScaleString(scale, origin), });
oldTransform = tileBg.style[transform];
tileBg.style[transform] = backwardsTransform ?
oldTransform + ' ' + scaleStr :
scaleStr + ' ' + oldTransform;
}, },
_prepareTileBg: function () { _catchTransitionEnd: function () {
var tilePane = this._tilePane, if (this._animatingZoom) {
tileBg = this._tileBg; this._onZoomTransitionEnd();
// If foreground layer doesn't have many tiles but bg layer does, keep the existing bg layer and just zoom it some more
if (tileBg && this._getLoadedTilesPercentage(tileBg) > 0.5 &&
this._getLoadedTilesPercentage(tilePane) < 0.5) {
tilePane.style.visibility = 'hidden';
tilePane.empty = true;
this._stopLoadingImages(tilePane);
return;
}
if (!tileBg) {
tileBg = this._tileBg = this._createPane('leaflet-tile-pane', this._mapPane);
tileBg.style.zIndex = 1;
}
// prepare the background pane to become the main tile pane
tileBg.style[L.DomUtil.TRANSFORM] = '';
tileBg.style.visibility = 'hidden';
// tells tile layers to reinitialize their containers
tileBg.empty = true; //new FG
tilePane.empty = false; //new BG
//Switch out the current layer to be the new bg layer (And vice-versa)
this._tilePane = this._panes.tilePane = tileBg;
var newTileBg = this._tileBg = tilePane;
L.DomUtil.addClass(newTileBg, 'leaflet-zoom-animated');
this._stopLoadingImages(newTileBg);
},
_getLoadedTilesPercentage: function (container) {
var tiles = container.getElementsByTagName('img'),
i, len, count = 0;
for (i = 0, len = tiles.length; i < len; i++) {
if (tiles[i].complete) {
count++;
}
}
return count / len;
},
// stops loading all tiles in the background layer
_stopLoadingImages: function (container) {
var tiles = Array.prototype.slice.call(container.getElementsByTagName('img')),
i, len, tile;
for (i = 0, len = tiles.length; i < len; i++) {
tile = tiles[i];
if (!tile.complete) {
tile.onload = L.Util.falseFn;
tile.onerror = L.Util.falseFn;
tile.src = L.Util.emptyImageUrl;
tile.parentNode.removeChild(tile);
}
} }
}, },
_onZoomTransitionEnd: function () { _onZoomTransitionEnd: function () {
this._restoreTileFront();
L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim'); L.DomUtil.removeClass(this._mapPane, 'leaflet-zoom-anim');
L.Util.falseFn(this._tileBg.offsetWidth); // force reflow
this._animatingZoom = false; this._animatingZoom = false;
this._resetView(this._animateToCenter, this._animateToZoom, true, true); this._resetView(this._animateToCenter, this._animateToZoom, true, true);
if (L.Draggable) { if (L.Draggable) {
L.Draggable._disabled = false; L.Draggable._disabled = false;
} }
},
_restoreTileFront: function () {
this._tilePane.innerHTML = '';
this._tilePane.style.visibility = '';
this._tilePane.style.zIndex = 2;
this._tileBg.style.zIndex = 1;
},
_clearTileBg: function () {
if (!this._animatingZoom && !this.touchZoom._zooming) {
this._tileBg.innerHTML = '';
}
} }
}); });

View File

@ -57,12 +57,11 @@ L.Map.TouchZoom = L.Handler.extend({
if (this._scale === 1) { return; } if (this._scale === 1) { return; }
if (!this._moved) { if (!this._moved) {
L.DomUtil.addClass(map._mapPane, 'leaflet-zoom-anim leaflet-touching'); L.DomUtil.addClass(map._mapPane, 'leaflet-touching');
map map
.fire('movestart') .fire('movestart')
.fire('zoomstart') .fire('zoomstart');
._prepareTileBg();
this._moved = true; this._moved = true;
} }
@ -77,19 +76,10 @@ L.Map.TouchZoom = L.Handler.extend({
_updateOnMove: function () { _updateOnMove: function () {
var map = this._map, var map = this._map,
origin = this._getScaleOrigin(), origin = this._getScaleOrigin(),
center = map.layerPointToLatLng(origin); center = map.layerPointToLatLng(origin),
zoom = map.getScaleZoom(this._scale);
map.fire('zoomanim', { map._animateZoom(center, zoom, this._startCenter, this._scale, this._delta, true);
center: center,
zoom: map.getScaleZoom(this._scale)
});
// Used 2 translates instead of transform-origin because of a very strange bug -
// it didn't count the origin on the first touch-zoom but worked correctly afterwards
map._tileBg.style[L.DomUtil.TRANSFORM] =
L.DomUtil.getTranslateString(this._delta) + ' ' +
L.DomUtil.getScaleString(this._scale, this._startCenter);
}, },
_onTouchEnd: function () { _onTouchEnd: function () {
@ -112,14 +102,10 @@ L.Map.TouchZoom = L.Handler.extend({
roundZoomDelta = (floatZoomDelta > 0 ? roundZoomDelta = (floatZoomDelta > 0 ?
Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)), Math.ceil(floatZoomDelta) : Math.floor(floatZoomDelta)),
zoom = map._limitZoom(oldZoom + roundZoomDelta); zoom = map._limitZoom(oldZoom + roundZoomDelta),
scale = map.getZoomScale(zoom) / this._scale;
map.fire('zoomanim', { map._animateZoom(center, zoom, origin, scale, null, true);
center: center,
zoom: zoom
});
map._runAnimation(center, zoom, map.getZoomScale(zoom) / this._scale, origin, true);
}, },
_getScaleOrigin: function () { _getScaleOrigin: function () {