functional progressbars for glide-loaded images in headlines buffer

This commit is contained in:
Andrew Dolgov 2017-06-04 10:34:51 +03:00
parent 7e16eb5022
commit 89c57fca2d
6 changed files with 377 additions and 10 deletions

View File

@ -28,6 +28,10 @@ android {
dependencies {
compile project(':taskerlocaleapi')
compile files('libs/dashclock-api-r1.1.jar')
compile 'com.squareup.okhttp3:okhttp:3.8.0'
compile('com.github.bumptech.glide:okhttp3-integration:1.5.0') {
exclude group: 'glide-parent'
}
compile 'org.jsoup:jsoup:1.10.2'
compile 'com.bogdwellers:pinchtozoom:0.1'
compile 'com.github.bumptech.glide:glide:3.8.0'
@ -44,8 +48,6 @@ dependencies {
compile 'com.ToxicBakery.viewpager.transforms:view-pager-transforms:1.2.32@aar'
compile 'me.relex:circleindicator:1.2.2@aar'
compile 'com.viewpagerindicator:library:2.4.1'
//compile 'com.nhaarman.listviewanimations:lib-core:3.1.0@aar'
//compile 'com.nhaarman.listviewanimations:lib-manipulation:3.1.0@aar'
compile 'com.nineoldandroids:library:2.4.0'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile files('libs/YouTubeAndroidPlayerApi.jar')

View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.fox.ttrss"
android:versionCode="436"
android:versionName="1.202" >
android:versionName="1.202">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -18,6 +19,12 @@
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<!-- <meta-data android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"
tools:node="remove" /> -->
<meta-data android:name="org.fox.ttrss.util.OkHttpProgressGlideModule"
android:value="GlideModule" />
<activity
android:name=".LaunchActivity"
android:label="@string/app_name"

View File

@ -64,6 +64,8 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.GlideDrawableImageViewTarget;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
import com.google.gson.JsonElement;
import com.shamanland.fab.FloatingActionButton;
@ -74,6 +76,7 @@ import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.util.HeaderViewRecyclerAdapter;
import org.fox.ttrss.util.HeadlinesRequest;
import org.fox.ttrss.util.ProgressTarget;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
@ -777,6 +780,7 @@ public class HeadlinesFragment extends Fragment {
public TextureView flavorVideoView;
//public int position;
public boolean flavorImageEmbedded;
public ProgressTarget<String, GlideDrawable> flavorProgressTarget;
public ArticleViewHolder(View v) {
super(v);
@ -804,14 +808,44 @@ public class HeadlinesFragment extends Fragment {
topChangedMessage = v.findViewById(R.id.headlines_row_top_changed);
flavorImageOverflow = v.findViewById(R.id.gallery_overflow);
flavorVideoView = (TextureView) v.findViewById(R.id.flavor_video);
if (flavorImageView != null && flavorImageLoadingBar != null) {
flavorProgressTarget = new FlavorProgressTarget<>(new GlideDrawableImageViewTarget(flavorImageView), flavorImageLoadingBar);
}
}
public void clearAnimation() {
view.clearAnimation();
}
}
private static class FlavorProgressTarget<Z> extends ProgressTarget<String, Z> {
private final ProgressBar progress;
public FlavorProgressTarget(Target<Z> target, ProgressBar progress) {
super(target);
this.progress = progress;
}
@Override public float getGranualityPercentage() {
return 0.1f; // this matches the format string for #text below
}
@Override protected void onConnecting() {
progress.setIndeterminate(true);
progress.setVisibility(View.VISIBLE);
}
@Override protected void onDownloading(long bytesRead, long expectedLength) {
progress.setIndeterminate(false);
progress.setProgress((int)(100 * bytesRead / expectedLength));
}
@Override protected void onDownloaded() {
progress.setIndeterminate(true);
}
@Override protected void onDelivered() {
progress.setVisibility(View.INVISIBLE);
}
}
private class ArticleListAdapter extends RecyclerView.Adapter<ArticleViewHolder> {
private ArrayList<Article> items;
@ -1072,6 +1106,7 @@ public class HeadlinesFragment extends Fragment {
/* reset to default in case of convertview */
holder.flavorImageLoadingBar.setVisibility(View.GONE);
holder.flavorImageLoadingBar.setIndeterminate(false);
holder.flavorImageView.setVisibility(View.GONE);
holder.flavorVideoKindView.setVisibility(View.GONE);
holder.flavorImageOverflow.setVisibility(View.GONE);
@ -1155,17 +1190,18 @@ public class HeadlinesFragment extends Fragment {
//Log.d(TAG, "TAG:" + holder.flavorImageOverflow.getTag());
if (!article.flavorImageUri.equals(holder.flavorImageOverflow.getTag())) {
holder.flavorImageLoadingBar.setVisibility(View.VISIBLE);
holder.flavorImageLoadingBar.setIndeterminate(true);
//holder.flavorImageLoadingBar.setVisibility(View.VISIBLE);
//holder.flavorImageLoadingBar.setIndeterminate(true);
holder.flavorImageView.setMaxHeight((int)(m_screenHeight * 0.8f));
holder.flavorProgressTarget.setModel(article.flavorImageUri);
Glide.with(HeadlinesFragment.this)
.load(article.flavorImageUri)
.dontAnimate()
.dontTransform()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
@ -1201,7 +1237,7 @@ public class HeadlinesFragment extends Fragment {
}
}
})
.into(holder.flavorImageView);
.into(holder.flavorProgressTarget);
} else {
holder.flavorImageOverflow.setVisibility(View.VISIBLE);

View File

@ -0,0 +1,160 @@
package org.fox.ttrss.util;
import java.io.*;
import java.util.*;
import android.content.Context;
import android.os.*;
import com.bumptech.glide.*;
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.module.GlideModule;
import okhttp3.*;
import okio.*;
public class OkHttpProgressGlideModule implements GlideModule {
@Override public void applyOptions(Context context, GlideBuilder builder) {
}
@Override public void registerComponents(Context context, Glide glide) {
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(createInterceptor(new DispatchingProgressListener()))
.build();
glide.register(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(client));
}
private static Interceptor createInterceptor(final ResponseProgressListener listener) {
return new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
return response.newBuilder()
.body(new OkHttpProgressResponseBody(request.url(), response.body(), listener))
.build();
}
};
}
public interface UIProgressListener {
void onProgress(long bytesRead, long expectedLength);
/**
* Control how often the listener needs an update. 0% and 100% will always be dispatched.
* @return in percentage (0.2 = call {@link #onProgress} around every 0.2 percent of progress)
*/
float getGranualityPercentage();
}
public static void forget(String url) {
DispatchingProgressListener.forget(url);
}
public static void expect(String url, UIProgressListener listener) {
DispatchingProgressListener.expect(url, listener);
}
private interface ResponseProgressListener {
void update(HttpUrl url, long bytesRead, long contentLength);
}
private static class DispatchingProgressListener implements ResponseProgressListener {
private static final Map<String, UIProgressListener> LISTENERS = new HashMap<>();
private static final Map<String, Long> PROGRESSES = new HashMap<>();
private final Handler handler;
DispatchingProgressListener() {
this.handler = new Handler(Looper.getMainLooper());
}
static void forget(String url) {
LISTENERS.remove(url);
PROGRESSES.remove(url);
}
static void expect(String url, UIProgressListener listener) {
LISTENERS.put(url, listener);
}
@Override public void update(HttpUrl url, final long bytesRead, final long contentLength) {
//System.out.printf("%s: %d/%d = %.2f%%%n", url, bytesRead, contentLength, (100f * bytesRead) / contentLength);
String key = url.toString();
final UIProgressListener listener = LISTENERS.get(key);
if (listener == null) {
return;
}
if (contentLength <= bytesRead) {
forget(key);
}
if (needsDispatch(key, bytesRead, contentLength, listener.getGranualityPercentage())) {
handler.post(new Runnable() {
@Override public void run() {
listener.onProgress(bytesRead, contentLength);
}
});
}
}
private boolean needsDispatch(String key, long current, long total, float granularity) {
if (granularity == 0 || current == 0 || total == current) {
return true;
}
float percent = 100f * current / total;
long currentProgress = (long)(percent / granularity);
Long lastProgress = PROGRESSES.get(key);
if (lastProgress == null || currentProgress != lastProgress) {
PROGRESSES.put(key, currentProgress);
return true;
} else {
return false;
}
}
}
private static class OkHttpProgressResponseBody extends ResponseBody {
private final HttpUrl url;
private final ResponseBody responseBody;
private final ResponseProgressListener progressListener;
private BufferedSource bufferedSource;
OkHttpProgressResponseBody(HttpUrl url, ResponseBody responseBody,
ResponseProgressListener progressListener) {
this.url = url;
this.responseBody = responseBody;
this.progressListener = progressListener;
}
@Override public MediaType contentType() {
return responseBody.contentType();
}
@Override public long contentLength() {
return responseBody.contentLength();
}
@Override public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) { // this source is exhausted
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
progressListener.update(url, totalBytesRead, fullLength);
return bytesRead;
}
};
}
}
}

View File

@ -0,0 +1,110 @@
package org.fox.ttrss.util;
import android.graphics.drawable.Drawable;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.Target;
public abstract class ProgressTarget<T, Z> extends WrappingTarget<Z> implements OkHttpProgressGlideModule.UIProgressListener {
private T model;
private boolean ignoreProgress = true;
public ProgressTarget(Target<Z> target) {
this(null, target);
}
public ProgressTarget(T model, Target<Z> target) {
super(target);
this.model = model;
}
public final T getModel() {
return model;
}
public final void setModel(T model) {
Glide.clear(this); // indirectly calls cleanup
this.model = model;
}
/**
* Convert a model into an Url string that is used to match up the OkHttp requests. For explicit
* {@link com.bumptech.glide.load.model.GlideUrl GlideUrl} loads this needs to return
* {@link com.bumptech.glide.load.model.GlideUrl#toStringUrl toStringUrl}. For custom models do the same as your
* {@link com.bumptech.glide.load.model.stream.BaseGlideUrlLoader BaseGlideUrlLoader} does.
* @param model return the representation of the given model, DO NOT use {@link #getModel()} inside this method.
* @return a stable Url representation of the model, otherwise the progress reporting won't work
*/
protected String toUrlString(T model) {
return String.valueOf(model);
}
@Override public float getGranualityPercentage() {
return 1.0f;
}
@Override public void onProgress(long bytesRead, long expectedLength) {
if (ignoreProgress) {
return;
}
if (expectedLength == Long.MAX_VALUE) {
onConnecting();
} else if (bytesRead == expectedLength) {
onDownloaded();
} else {
onDownloading(bytesRead, expectedLength);
}
}
/**
* Called when the Glide load has started.
* At this time it is not known if the Glide will even go and use the network to fetch the image.
*/
protected abstract void onConnecting();
/**
* Called when there's any progress on the download; not called when loading from cache.
* At this time we know how many bytes have been transferred through the wire.
*/
protected abstract void onDownloading(long bytesRead, long expectedLength);
/**
* Called when the bytes downloaded reach the length reported by the server; not called when loading from cache.
* At this time it is fairly certain, that Glide either finished reading the stream.
* This means that the image was either already decoded or saved the network stream to cache.
* In the latter case there's more work to do: decode the image from cache and transform.
* These cannot be listened to for progress so it's unsure how fast they'll be, best to show indeterminate progress.
*/
protected abstract void onDownloaded();
/**
* Called when the Glide load has finished either by successfully loading the image or failing to load or cancelled.
* In any case the best is to hide/reset any progress displays.
*/
protected abstract void onDelivered();
private void start() {
OkHttpProgressGlideModule.expect(toUrlString(model), this);
ignoreProgress = false;
onProgress(0, Long.MAX_VALUE);
}
private void cleanup() {
ignoreProgress = true;
T model = this.model; // save in case it gets modified
onDelivered();
OkHttpProgressGlideModule.forget(toUrlString(model));
this.model = null;
}
@Override public void onLoadStarted(Drawable placeholder) {
super.onLoadStarted(placeholder);
start();
}
@Override public void onResourceReady(Z resource, GlideAnimation<? super Z> animation) {
cleanup();
super.onResourceReady(resource, animation);
}
@Override public void onLoadFailed(Exception e, Drawable errorDrawable) {
cleanup();
super.onLoadFailed(e, errorDrawable);
}
@Override public void onLoadCleared(Drawable placeholder) {
cleanup();
super.onLoadCleared(placeholder);
}
}

View File

@ -0,0 +1,52 @@
package org.fox.ttrss.util;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.*;
public class WrappingTarget<Z> implements Target<Z> {
protected final @NonNull Target<? super Z> target;
public WrappingTarget(@NonNull Target<? super Z> target) {
this.target = target;
}
public @NonNull Target<? super Z> getWrappedTarget() {
return target;
}
@Override public void getSize(SizeReadyCallback cb) {
target.getSize(cb);
}
@Override public void onLoadStarted(Drawable placeholder) {
target.onLoadStarted(placeholder);
}
@Override public void onLoadFailed(Exception e, Drawable errorDrawable) {
target.onLoadFailed(e, errorDrawable);
}
@SuppressWarnings("unchecked")
@Override public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
target.onResourceReady(resource, (GlideAnimation)glideAnimation);
}
@Override public void onLoadCleared(Drawable placeholder) {
target.onLoadCleared(placeholder);
}
@Override public Request getRequest() {
return target.getRequest();
}
@Override public void setRequest(Request request) {
target.setRequest(request);
}
@Override public void onStart() {
target.onStart();
}
@Override public void onStop() {
target.onStop();
}
@Override public void onDestroy() {
target.onDestroy();
}
}