Making performant scrolling lists in hybrid apps is very difficult. There are a few decent attempts to fix this problem. IonicFramework Collection Repeat being the best we've used so far, Including our own attempt.
When you do not control the source of the images for these lists you start to get problems when scaling massive images on the main javascript thread. This can lead to crazy jankyness on older phones and newer if the images are massive.
This plugin offloads the image processing to native land to download the image and scale it to the desired size in a background thread. When everything is ready a base64 string of the image is sent back to javascript land to be injected into the img tag.
It's essentially a wrapper around SDWebImage optimised to work with Ionic's collection repeat
var image = document.getElementById('scaled-image');
var rect = image.getBoundingClientRect();
var options = {
data: src,
index: scope.id,
quality: 0,
scale: Math.round(rect.width) + 'x' + Math.round(rect.height),
downloadOptions: window.CollectionRepeatImageOptions.SDWebImageCacheMemoryOnly | window.CollectionRepeatImageOptions.SDWebImageLowPriority
};
window.CollectionRepeatImage.getImage(options, function (data) {
image.src = 'data:image/jpeg;base64,' + data;
});
cordova plugin add https://github.com/mallzee/cordova-collection-repeat-image-plugin.git
- iOS
- Android (Soon)
- window.CollectionRepeatImage.getImage(options, success, failure)
- window.CollectionRepeatImage.cancel(index)
- window.CollectionRepeatImage.cancelAll()
- url: The url for the specified image
- index: The index associated with this image. Used to cancel jobs when items have gone out of view
- quality: [0 - 1] The compression applied to the image.
- scale: The image magic format "[width]x[height]" See here for more variations
- downloadOptions: Bit mask of options to apply to the download. See Download Options
By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying. This flag disable this blacklisting.
By default, image downloads are started during UI interactions, this flags disable this feature, leading to delayed download on UIScrollView deceleration for instance.
This flag disables on-disk caching
This flag enables progressive download, the image is displayed progressively during download as a browser would do. By default, the image is only displayed once completely downloaded.
Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed. The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation. This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics. If a cached image is refreshed, the completion block is called once with the cached image and again with the final image.
Use this flag only if you can't make your URLs static with embedded cache busting parameter.
In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for extra time in background to let the request finish. If the background task expires the operation will be cancelled.
Handles cookies stored in NSHTTPCookieStore by setting NSMutableURLRequest.HTTPShouldHandleCookies = YES;
Enable to allow untrusted SSL certificates. Useful for testing purposes. Use with caution in production.
By default, image are loaded in the order they were queued. This flag move them to the front of the queue and is loaded immediately instead of waiting for the current queue to be loaded (which could take a while).
By default, placeholder images are loaded while the image is loading. This flag will delay the loading of the placeholder image until after the image has finished loading.
Example of the markup inside of a collection repeat.
<div class="product-multi" collection-repeat="product in products track by product.id" collection-item-width="'50%'" collection-item-height="'50%'">
<mlz-img src="{{product.image}}" id="{{$id}}"></mlz-img>
<div class="price"{{product.cost}}</div>
</div>
Example directive to make use of the plugin.
angular.directive('mlzImg', ['$animate', function ($animate) {
return {
restrict: 'E',
scope: {
src: '@',
id: '@'
},
template: '<img />',
link: function (scope, element) {
var image = element.children(),
hasLoader = false,
hasLoaded = false,
rect;
function getDimensions() {
if (!rect) {
rect = element[0].getBoundingClientRect();
}
return rect;
}
if (!hasLoader) {
image.on('load', function () {
ionic.requestAnimationFrame(function () {
$animate.addClass(image[0], 'fade-in-not-out');
});
hasLoaded = true;
}).on('error', function () {
// TODO: Have some default error image
});
hasLoader = true;
}
function replaceWithScaledImage(src, rect) {
if (ionic.Platform.isWebView()) {
var options = {
data: src,
index: scope.id,
quality: 1,
options: window.CollectionRepeatImageOptions.SDWebImageCacheMemoryOnly,
scale: (Math.round(rect.width) * 2) + 'x' + (Math.round(rect.height) * 2)// + '#' // Uncomment to crop and scale
};
window.CollectionRepeatImage.getImage(options, function (data) {
image[0].src = 'data:image/jpeg;base64,' + data;
}, function (error) {
// TODO: Place broken image stuff in
console.log('Resize error', error);
});
} else {
image[0].src = src;
}
}
function loadNewImage(src) {
ionic.requestAnimationFrame(function () {
image[0].classList.remove('fade-in-not-out');
replaceWithScaledImage(src, getDimensions());
});
}
scope.$watch('src', function (src, oldSrc) {
if (src && rect && src !== oldSrc || !hasLoaded) {
if (ionic.Platform.isWebView()) {
window.CollectionRepeatImage.cancel(scope.id);
loadNewImage(src);
} else {
loadNewImage(src);
}
}
});
}
};
});
Cancel all operations on a scope destroy
$scope.$on('destroy', function () {
if(ionic.Platform.isWebView()) {
cordova.plugins.CollectionRepeatImage.cancelAll()
}
});