Brion VIBBER has submitted this change and it was merged. Change subject: Volley images ......................................................................
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 --- M commons/pom.xml M commons/res/layout/fragment_media_detail.xml M commons/res/layout/layout_contribution.xml M commons/src/main/java/org/wikimedia/commons/CommonsApplication.java A commons/src/main/java/org/wikimedia/commons/MediaWikiImageView.java M commons/src/main/java/org/wikimedia/commons/contributions/ContributionsListFragment.java M commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java M pom.xml 8 files changed, 298 insertions(+), 37 deletions(-) Approvals: Brion VIBBER: Verified; Looks good to me, approved diff --git a/commons/pom.xml b/commons/pom.xml index 7adf9e8..c66d489 100644 --- a/commons/pom.xml +++ b/commons/pom.xml @@ -53,6 +53,12 @@ <version>4.4.0</version> </dependency> <dependency> + <groupId>com.android</groupId> + <artifactId>volley</artifactId> + <version>1.0</version> + </dependency> + + <dependency> <groupId>de.keyboardsurfer.android.widget</groupId> <artifactId>crouton</artifactId> <version>1.7</version> diff --git a/commons/res/layout/fragment_media_detail.xml b/commons/res/layout/fragment_media_detail.xml index d56a4c6..a8112d9 100644 --- a/commons/res/layout/fragment_media_detail.xml +++ b/commons/res/layout/fragment_media_detail.xml @@ -22,11 +22,10 @@ android:visibility="gone" /> - <ImageView android:id="@+id/mediaDetailImage" + <org.wikimedia.commons.MediaWikiImageView android:id="@+id/mediaDetailImage" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="fitCenter" - android:visibility="gone" /> <RelativeLayout diff --git a/commons/res/layout/layout_contribution.xml b/commons/res/layout/layout_contribution.xml index f436e96..1c7dcf0 100644 --- a/commons/res/layout/layout_contribution.xml +++ b/commons/res/layout/layout_contribution.xml @@ -16,7 +16,7 @@ android:typeface="serif" android:layout_gravity="end|bottom" /> - <ImageView android:id="@+id/contributionImage" + <org.wikimedia.commons.MediaWikiImageView android:id="@+id/contributionImage" android:layout_width="fill_parent" android:layout_height="240dp" android:scaleType="centerCrop" diff --git a/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java b/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java index 6e1fc47..e88d05b 100644 --- a/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java +++ b/commons/src/main/java/org/wikimedia/commons/CommonsApplication.java @@ -14,6 +14,8 @@ import android.net.Uri; import android.os.Build; +import android.support.v4.util.LruCache; +import com.android.volley.RequestQueue; import com.nostra13.universalimageloader.cache.disc.impl.TotalSizeLimitedDiscCache; import com.nostra13.universalimageloader.cache.memory.impl.LimitedAgeMemoryCache; import com.nostra13.universalimageloader.core.ImageLoader; @@ -28,6 +30,8 @@ import org.apache.http.impl.client.*; import org.apache.http.params.CoreProtocolPNames; import org.wikimedia.commons.data.*; + +import com.android.volley.toolbox.*; // TODO: Use ProGuard to rip out reporting when publishing @ReportsCrashes(formKey = "", @@ -62,6 +66,7 @@ public static final String FEEDBACK_EMAIL = "mobile-feedbac...@lists.wikimedia.org"; public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App (%s) Feedback"; + public RequestQueue volleyQueue; public static AbstractHttpClient createHttpClient() { DefaultHttpClient client = new DefaultHttpClient(); @@ -106,6 +111,35 @@ // Initialize EventLogging EventLog.setApp(this); + + volleyQueue = Volley.newRequestQueue(this); + } + + private com.android.volley.toolbox.ImageLoader imageLoader; + // based off https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html + // Cache for 1/8th of available VM memory + private LruCache<String, Bitmap> imageCache = new LruCache<String, Bitmap>((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 0000000..37fa04d --- /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 685b55c..e8b076d 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.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 @@ 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 43bb24f..39343d8 100644 --- a/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java +++ b/commons/src/main/java/org/wikimedia/commons/media/MediaDetailFragment.java @@ -3,13 +3,16 @@ 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.*; @@ -78,29 +81,38 @@ } 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 a2ad105..42c177b 100644 --- a/pom.xml +++ b/pom.xml @@ -205,10 +205,11 @@ </profiles> <repositories> + <repository> <id>yuvi.in</id> - <name>Yuvi's Maven Repo</name> - <url>http://yuvi.in/blog/maven</url> + <name>Yuvi's Newer Maven Repo</name> + <url>http://yuvi.in/maven</url> </repository> </repositories> -- To view, visit https://gerrit.wikimedia.org/r/68282 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: Ia21a7b19fefa552d5a0b013085d0f5f1f80dc5ff Gerrit-PatchSet: 2 Gerrit-Project: apps/android/commons Gerrit-Branch: master Gerrit-Owner: SuchABot <yuvipanda+sucha...@gmail.com> Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org> Gerrit-Reviewer: Yuvipanda <yuvipa...@gmail.com> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits