I'm specifically looking at doing this in a CardScrollAdapter
, but it would be also good to know in the LiveCard situation when publishing a card to the timeline. Is there a way to use volley or any other networking library to load images using the generic Card
class?
4 Answers
"There's the additional complexity that the current XE12 GDK only supports card.addImage(Uri) with local resource and file URIs. So in addition to the caveat above, you have to download the image to a file before being able to add it as a card image."
Looks like XE16+ now supports card.addImage(bitmap) so using a file as an temporary cache may no longer be necessary.
While you can't use Volley's NetworkImageView
class directly with a Card
(because the Card
manages its own layout), you should be able to use other features in Volley to handle delay-loading your images.
At a high-level, you could use an approach like this:
- When you create your
Card
, have it initially use a placeholder image if the desired image isn't already available (cached somewhere). Keep track of the instance of theCard
somewhere (in the adapter, for example). - Enqueue a request to load the image.
- Once the image has been retrieved, write the image out to your application's cache directory and add it to the card using a
file:
URL. - Call
Card.toView()
again to regenerate the card's views and update your UI as needed.
"UPDATE: I get slowdowns loading lists with 10+ images. I'm not sure if it's due to calling updateViews() after each image load or what."
Make sure that your CardScrollAdapter getView() method does not attempt to load the image on every invocation.
for example:
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if ( getCards() == null ) return null;
MyCard card = getCards().get(position);
if (! card.isImageLoaded()) {
CardImageLoader.loadCardImage(...);
}
return card.toView();
}
Okay there is a way to do it, but it's a bit convoluted. The trick is that you can't dispatch an asynchronous image load task in the getView()
of the adapter, the way your normally would in android. If you try, you'll find that the ImageView
returned by the card.toView()
will be GC'd by the time you're ready to load the image. Seems to be a bug in the GDK, or maybe it's the way it's designed, I'm not sure.
There's the additional complexity that the current XE12 GDK only supports card.addImage(Uri)
with local resource and file URIs. So in addition to the caveat above, you have to download the image to a file before being able to add it as a card image.
I'll be using the Android Universal Image Loader to assist in the image download task. Here's class that does it all:
public class CardImageLoader {
private static final String TAG = CardImageLoader.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int MAX_IMAGE_LOAD_RETRIES = 3;
private static Map<String, Integer> mLoadFailures = new ConcurrentHashMap<String, Integer>();
public static void init(Context context) {
File cacheDir = StorageUtils.getCacheDirectory(context);
DisplayImageOptions options = new DisplayImageOptions.Builder()
.cacheOnDisc(true)
.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.discCache(new UnlimitedDiscCache(cacheDir))
.defaultDisplayImageOptions(options)
.build();
ImageLoader.getInstance().init(config);
}
public static void loadCardImage(final Card card, final String imageUri, final CardScrollView cardScrollView) {
card.setImageLayout(Card.ImageLayout.FULL);
int failures = mLoadFailures.containsKey(imageUri) ? mLoadFailures.get(imageUri) : 0;
if (failures > MAX_IMAGE_LOAD_RETRIES) {
if (DEBUG) Log.i(TAG, "Exceeded max retries on imageUri=" + imageUri);
return;
}
File file = ImageLoader.getInstance().getDiscCache().get(imageUri);
if (file != null && file.exists() && file.length() > 0) {
Uri uri = Uri.fromFile(file);
card.addImage(uri);
}
else {
ImageLoader.getInstance().loadImage(imageUri, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
if (DEBUG) Log.i(TAG, "onLoadingStarted");
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
if (DEBUG) Log.i(TAG, "onLoadingFailed");
int failures = mLoadFailures.containsKey(imageUri) ? mLoadFailures.get(imageUri) : 0;
mLoadFailures.put(imageUri, ++failures);
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
File file = ImageLoader.getInstance().getDiscCache().get(imageUri);
if (DEBUG) Log.i(TAG, "onLoadingComplete uri=" + imageUri + " file=" + file
+ " len=" + (file == null ? 0 : file.length()));
if (file != null && file.exists() && file.length() > 0) {
if (DEBUG) Log.i(TAG, "onLoadingComplete scheduling update of scroll views");
if (cardScrollView != null)
cardScrollView.updateViews(true);
}
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
if (DEBUG) Log.i(TAG, "onLoadingCancelled");
}
});
}
}
}
Here's how it works. In the getView()
of the adapter, we find the URL we want to load. We ask the image loader if it's already got the image cached. If it has, we find the local file Uri and call card.addImage(Uri)
, and we're done. If we don't have the image cached, we can't use the normal image loader displayImage()
call for the reasons described heretofore. Instead, we download the file with loadImage()' and if successful, ask the
CardScrollView' to redisplay itself (don't try setting the card here, even with a final
variable, or nothing will happen). This will end up causing the getView()
to be called again, only this time the file will be cached. Thus the image will be added to the card, and your card will show up properly.
There's also a bit of tracking of failed downloads to prevent a loop of the CardScrollView
redisplay on failed downloads.
How do you use it? Pretty easy. First in your onCreate()
you need to initialise the image loader with this line:
CardImageLoader.init(this);
Second, in your adapter getView()
method, when you want to load an image in a card you call this method:
CardImageLoader.loadCardImage(card, url, mCardScrollView);
You'll then have cards with the image show up. Only caveat I've found so far is, don't load more than 20 or so images at once or you can start to have performance problems. Loading 150 crashed glass, so be careful. Besides that's its been working fine.
UPDATE: I get slowdowns loading lists with 10+ images. I'm not sure if it's due to calling updateViews()
after each image load or what.