From 57888260ecae61517c3993238f55c0f7e5f53a7f Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 12 Jun 2013 20:44:57 +0000 Subject: [PATCH] Volley images Add support for using Volley to load remote images instead of UIL 1. Gives us Caching 2. Load images at full resolution, rather than hardcode them. This is done by trying to fetch an image at highest width possible for the particular view. If it 500s, we assume that the image is smaller than the requested width and just request the full size image 3. Created a MediaWikiImageView, to which you can pass a Media object and it will display it. Takes care of sizing, etc. Optionally you can also specify a view to use as the 'loading' view. TODO: Loading from content:// URIs still use UIL. Need to write a Volley HTTP Stack that can fake responses for content:// URIs. GitHub: https://github.com/wikimedia/apps-android-commons/pull/1 Change-Id: Ia21a7b19fefa552d5a0b013085d0f5f1f80dc5ff --- commons/pom.xml | 6 + commons/res/layout/fragment_media_detail.xml | 3 +- commons/res/layout/layout_contribution.xml | 2 +- .../wikimedia/commons/CommonsApplication.java | 34 +++ .../wikimedia/commons/MediaWikiImageView.java | 201 ++++++++++++++++++ .../ContributionsListFragment.java | 28 ++- .../commons/media/MediaDetailFragment.java | 56 +++-- pom.xml | 5 +- 8 files changed, 298 insertions(+), 37 deletions(-) create mode 100644 commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java diff --git a/commons/pom.xml b/commons/pom.xml index 7adf9e8c5..c66d489c3 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -52,6 +52,12 @@ acra 4.4.0 + + com.android + volley + 1.0 + + de.keyboardsurfer.android.widget crouton diff --git a/commons/res/layout/fragment_media_detail.xml b/commons/res/layout/fragment_media_detail.xml index d56a4c63f..a8112d913 100644 --- a/commons/res/layout/fragment_media_detail.xml +++ b/commons/res/layout/fragment_media_detail.xml @@ -22,11 +22,10 @@ android:visibility="gone" /> - - imageCache = new LruCache((int) (Runtime.getRuntime().maxMemory() / (1024 * 8))) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + // The cache size will be measured in kilobytes rather than + // number of items. + return bitmap.getByteCount() / 1024; + } + }; + + public com.android.volley.toolbox.ImageLoader getImageLoader() { + if(imageLoader == null) { + imageLoader = new com.android.volley.toolbox.ImageLoader(volleyQueue, new com.android.volley.toolbox.ImageLoader.ImageCache() { + public Bitmap getBitmap(String key) { + return imageCache.get(key); + } + + public void putBitmap(String key, Bitmap bitmap) { + imageCache.put(key, bitmap); + } + }); + } + return imageLoader; } public MWApi getApi() { diff --git a/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java b/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java new file mode 100644 index 000000000..37fa04de1 --- /dev/null +++ b/commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java @@ -0,0 +1,201 @@ +/** + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.wikimedia.commons; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.ImageLoader.ImageContainer; +import com.android.volley.toolbox.ImageLoader.ImageListener; + + +public class MediaWikiImageView extends ImageView { + + private Media mMedia; + + private ImageLoader mImageLoader; + + private ImageContainer mImageContainer; + + private View loadingView; + + public MediaWikiImageView(Context context) { + this(context, null); + } + + public MediaWikiImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public MediaWikiImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setMedia(Media media, ImageLoader imageLoader) { + this.mMedia = media; + mImageLoader = imageLoader; + loadImageIfNecessary(false); + } + + public void setLoadingView(View loadingView) { + this.loadingView = loadingView; + } + + public View getLoadingView() { + return loadingView; + } + + private void loadImageIfNecessary(final boolean isInLayoutPass) { + loadImageIfNecessary(isInLayoutPass, false); + } + + private void loadImageIfNecessary(final boolean isInLayoutPass, final boolean tryOriginal) { + int width = getWidth(); + int height = getHeight(); + + // if the view's bounds aren't known yet, hold off on loading the image. + if (width == 0 && height == 0) { + return; + } + + final String mUrl; + if(tryOriginal) { + mUrl = mMedia.getImageUrl(); + } else { + // Round it down to the nearest 320 + // Possible a similar size image has already been generated. + // Reduces Server cache fragmentation, also increases chance of cache hit + // If width is less than 320, we just use that directly, to avoid a case of the Maths + mUrl = mMedia.getThumbnailUrl(width <= 320 ? width : (width / 320) * 320); + } + + // if the URL to be loaded in this view is empty, cancel any old requests and clear the + // currently loaded image. + if (TextUtils.isEmpty(mUrl)) { + if (mImageContainer != null) { + mImageContainer.cancelRequest(); + mImageContainer = null; + } + setImageBitmap(null); + return; + } + + // Don't repeat work. Prevents onLayout cascades + // We ignore it if the image request was for either the current URL of for the full URL + // Since the full URL is always the second, and + if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { + Log.d("Commons", "Older one is " + mImageContainer.getRequestUrl() + " new one is " + mUrl); + if (mImageContainer.getRequestUrl().equals(mMedia.getImageUrl()) || mImageContainer.getRequestUrl().equals(mUrl)) { + return; + } else { + // if there is a pre-existing request, cancel it if it's fetching a different URL. + mImageContainer.cancelRequest(); + BitmapDrawable actualDrawable = (BitmapDrawable)getDrawable(); + if(actualDrawable != null && actualDrawable.getBitmap() != null) { + setImageBitmap(null); + if(loadingView != null) { + loadingView.setVisibility(View.VISIBLE); + } + } + } + } + + // The pre-existing content of this view didn't match the current URL. Load the new image + // from the network. + ImageContainer newContainer = mImageLoader.get(mUrl, + new ImageListener() { + @Override + public void onErrorResponse(final VolleyError error) { + Log.d("Commons", "Error: or Url " + mUrl + " value is " + tryOriginal); + if(!tryOriginal) { + post(new Runnable() { + public void run() { + loadImageIfNecessary(false, true); + } + }); + } + + } + + @Override + public void onResponse(final ImageContainer response, boolean isImmediate) { + // If this was an immediate response that was delivered inside of a layout + // pass do not set the image immediately as it will trigger a requestLayout + // inside of a layout. Instead, defer setting the image by posting back to + // the main thread. + if (isImmediate && isInLayoutPass) { + post(new Runnable() { + @Override + public void run() { + onResponse(response, false); + } + }); + return; + } + + + Log.d("Commons", "No-Error: For Url " + mUrl + " value is " + tryOriginal); + + if (response.getBitmap() != null) { + setImageBitmap(response.getBitmap()); + if(loadingView != null) { + loadingView.setVisibility(View.GONE); + } + } else { + Log.d("Commons", "Whelp, fully can not load an image at all!"); + // We got nothing back, figure out some sort of a solution? + } + } + }); + + // update the ImageContainer to be the new bitmap container. + mImageContainer = newContainer; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + Log.d("Commons", "Called via onLayout"); + loadImageIfNecessary(true); + // Called via onLayout + } + + @Override + protected void onDetachedFromWindow() { + if (mImageContainer != null) { + // If the view was bound to an image request, cancel it and clear + // out the image from the view. + mImageContainer.cancelRequest(); + setImageBitmap(null); + // also clear out the container so we can reload the image if necessary. + mImageContainer = null; + } + super.onDetachedFromWindow(); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + invalidate(); + } +} diff --git a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsListFragment.java b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsListFragment.java index 685b55cd4..e8b076d98 100644 --- a/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsListFragment.java +++ b/commons/src/main/java/org/wikimedia/commons/contributions/ContributionsListFragment.java @@ -22,14 +22,16 @@ import com.actionbarsherlock.app.SherlockFragment; import com.actionbarsherlock.view.Menu; import com.actionbarsherlock.view.MenuInflater; import com.actionbarsherlock.view.MenuItem; -import com.nostra13.universalimageloader.core.*; -import com.nostra13.universalimageloader.core.assist.*; + +import com.nostra13.universalimageloader.core.DisplayImageOptions; import java.io.*; import java.util.*; +import com.nostra13.universalimageloader.core.assist.SimpleImageLoadingListener; import org.wikimedia.commons.*; +import org.wikimedia.commons.R; public class ContributionsListFragment extends SherlockFragment { @@ -85,17 +87,23 @@ public class ContributionsListFragment extends SherlockFragment { String actualUrl = TextUtils.isEmpty(contribution.getImageUrl()) ? contribution.getLocalUri().toString() : contribution.getThumbnailUrl(320); if(views.url == null || !views.url.equals(actualUrl)) { - ImageLoader.getInstance().displayImage(actualUrl, views.imageView, contributionDisplayOptions, new SimpleImageLoadingListener() { + if(actualUrl.startsWith("http")) { + MediaWikiImageView mwImageView = (MediaWikiImageView)views.imageView; + mwImageView.setMedia(contribution, ((CommonsApplication) getActivity().getApplicationContext()).getImageLoader()); + // FIXME: For transparent images + } else { + com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, views.imageView, contributionDisplayOptions, new SimpleImageLoadingListener() { - @Override - public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { - if(loadedImage.hasAlpha()) { - views.imageView.setBackgroundResource(android.R.color.white); + @Override + public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { + if(loadedImage.hasAlpha()) { + views.imageView.setBackgroundResource(android.R.color.white); + } + views.seqNumView.setVisibility(View.GONE); } - views.seqNumView.setVisibility(View.GONE); - } - }); + }); + } views.url = actualUrl; } diff --git a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java index 43bb24f8e..39343d825 100644 --- a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java +++ b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java @@ -3,14 +3,17 @@ package org.wikimedia.commons.media; import android.graphics.*; import android.os.*; import android.text.*; +import android.util.Log; import android.view.*; import android.widget.*; import com.actionbarsherlock.app.SherlockFragment; +import com.android.volley.toolbox.NetworkImageView; import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.assist.ImageLoadingListener; +import com.android.volley.toolbox.*; + import org.wikimedia.commons.*; public class MediaDetailFragment extends SherlockFragment { @@ -78,29 +81,38 @@ public class MediaDetailFragment extends SherlockFragment { } String actualUrl = TextUtils.isEmpty(media.getImageUrl()) ? media.getLocalUri().toString() : media.getThumbnailUrl(640); - ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() { - public void onLoadingStarted(String s, View view) { - loadingProgress.setVisibility(View.VISIBLE); - } - - public void onLoadingFailed(String s, View view, FailReason failReason) { - loadingProgress.setVisibility(View.GONE); - loadingFailed.setVisibility(View.VISIBLE); - } - - public void onLoadingComplete(String s, View view, Bitmap bitmap) { - loadingProgress.setVisibility(View.GONE); - loadingFailed.setVisibility(View.GONE); - image.setVisibility(View.VISIBLE); - if(bitmap.hasAlpha()) { - image.setBackgroundResource(android.R.color.white); + if(actualUrl.startsWith("http")) { + ImageLoader loader = ((CommonsApplication)getActivity().getApplicationContext()).getImageLoader(); + MediaWikiImageView mwImage = (MediaWikiImageView)image; + mwImage.setLoadingView(loadingProgress); //FIXME: Set this as an attribute + mwImage.setMedia(media, loader); + Log.d("Volley", actualUrl); + // FIXME: For transparent images + } else { + com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(actualUrl, image, displayOptions, new ImageLoadingListener() { + public void onLoadingStarted(String s, View view) { + loadingProgress.setVisibility(View.VISIBLE); } - } - public void onLoadingCancelled(String s, View view) { - throw new RuntimeException("Image loading cancelled. But why?"); - } - }); + public void onLoadingFailed(String s, View view, FailReason failReason) { + loadingProgress.setVisibility(View.GONE); + loadingFailed.setVisibility(View.VISIBLE); + } + + public void onLoadingComplete(String s, View view, Bitmap bitmap) { + loadingProgress.setVisibility(View.GONE); + loadingFailed.setVisibility(View.GONE); + image.setVisibility(View.VISIBLE); + if(bitmap.hasAlpha()) { + image.setBackgroundResource(android.R.color.white); + } + } + + public void onLoadingCancelled(String s, View view) { + throw new RuntimeException("Image loading cancelled. But why?"); + } + }); + } title.setText(media.getDisplayTitle()); title.addTextChangedListener(new TextWatcher() { diff --git a/pom.xml b/pom.xml index a2ad105c1..42c177ba0 100644 --- a/pom.xml +++ b/pom.xml @@ -205,10 +205,11 @@ + yuvi.in - Yuvi's Maven Repo - http://yuvi.in/blog/maven + Yuvi's Newer Maven Repo + http://yuvi.in/maven