Created
July 15, 2011 07:08
-
-
Save tomgibara/1084229 to your computer and use it in GitHub Desktop.
A base class providing asynchronous rendering for Android Adapter implementations
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.tomgibara.android.util; | |
import java.util.HashSet; | |
import java.util.LinkedHashMap; | |
import java.util.Map; | |
import java.util.WeakHashMap; | |
import java.util.concurrent.Callable; | |
import java.util.concurrent.Future; | |
import java.util.concurrent.FutureTask; | |
import java.util.concurrent.PriorityBlockingQueue; | |
import java.util.concurrent.ThreadPoolExecutor; | |
import java.util.concurrent.TimeUnit; | |
import android.graphics.Bitmap; | |
import android.net.Uri; | |
import android.os.Handler; | |
import android.os.Process; | |
import android.view.View; | |
import android.widget.Adapter; | |
import android.widget.AdapterView; | |
import android.widget.GridView; | |
import android.widget.ImageView; | |
/** | |
* <p> | |
* Concrete extensions of this class are capable of rendering a multiplicity of | |
* Views on background threads. Interrupting the rendering of a View to start a | |
* new rendering is handled by this class. So too is rendering a view in | |
* multiple passes (initial passes are automatically prioritized over subsequent | |
* passes). Support for caching renderings is also available. | |
* <p> | |
* | |
* <p> | |
* This class is mainly intended to be used within | |
* {@link Adapter#getView(int, View, android.view.ViewGroup)} to handle all | |
* aspects of the asynchronous rendering of the views within an associated | |
* {@link AdapterView}. | |
* </p> | |
* | |
* <p> | |
* Example: A simple scenario would be a {@link GridView} containing | |
* {@link ImageView}s which are rendered by an instance of this class where | |
* <code>Param<code> is {@link Uri} and <code>Render</code> is {@link Bitmap} | |
* and the implementation loads bitmaps over HTTP for display in the view. The | |
* {@link GridView} would invoke the {@link ViewRenderer} via an {@link Adapter} | |
* that called the {@link #renderView(View, Object)} method on each call to its | |
* {@link Adapter#getView(int, View, android.view.ViewGroup)} method. Such an | |
* implementation might support two pass rendering, loading a low-res image on | |
* the first pass before loading a high-res image later. | |
* </p> | |
* | |
* <p> | |
* <strong>Note that this class requires a well defined equals method on the | |
* class that satisfies Param.</strong> | |
* </p> | |
* | |
* @author Tom Gibara | |
* | |
* @param <Param> | |
* the type of parameters that define renders | |
* @param <Render> | |
* the type of renders that are applied to views | |
*/ | |
public abstract class ViewRenderer<Param, Render> { | |
// statics | |
// convenience method for testing equality | |
private static final boolean equal(Object a, Object b) { | |
if (a == b) return true; | |
if (a == null) return false; | |
if (b == null) return false; | |
return a.equals(b); | |
} | |
// shared default executor, should be good enough in the common case | |
// and avoids thread proliferation in casual use | |
private static Executor sDefaultExecutor = null; | |
// ThreadPoolExecutor is broken for priority queues (see 6539720) so we need to patch it. | |
// Also newTaskFor() was only introduced in API level 9, so we can't use that either | |
// instead we patch the submit method directly | |
private static class Executor extends ThreadPoolExecutor { | |
public Executor(int threadCount) { | |
// ThreadPoolExecutor can't be configured to grow the thread count without risking queue rejections | |
// so we take the easy way out and clamp it with a fixed number of threads. | |
// TODO introduce our own executor (or equivalent) that doesn't share this weakness | |
// TODO our own blocking queue which stitched-together lists of the same priority would be much more efficient | |
super(threadCount, threadCount, 0L, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<Runnable>()); | |
} | |
@Override | |
public <T> Future<T> submit(Callable<T> task) { | |
if (task == null) throw new NullPointerException(); | |
ComparableFuture<T> future = new ComparableFuture<T>(task); | |
execute(future); | |
return future; | |
} | |
@SuppressWarnings("unchecked") | |
private static class ComparableFuture<T> extends FutureTask<T> implements Comparable<ComparableFuture<T>> { | |
private final Comparable mComparable; | |
public ComparableFuture(Callable<T> callable) { | |
super(callable); | |
mComparable = (Comparable) callable; | |
} | |
@Override | |
public int compareTo(ComparableFuture<T> that) { | |
return this.mComparable.compareTo(that.mComparable); | |
} | |
} | |
} | |
// wraps access to the default executor to allow for lazy creation | |
private static synchronized Executor getDefaultExecutor() { | |
return sDefaultExecutor == null ? sDefaultExecutor = new Executor(1) : sDefaultExecutor; | |
} | |
// fields | |
private final Executor mExecutor; | |
private final boolean mMayInterruptIfRunning; | |
private final Handler mHandler; | |
private final int mInheritedThreadPriority; | |
private final int mPasses; | |
private volatile long mNextOrder = 0L; | |
private volatile boolean mStopping = false; | |
private final Cache mCache; | |
// constructors | |
/** | |
* Constructs a new {@link ViewRenderer} object that coordinates concurrent | |
* background rendering for display to views. It is primarily designed for | |
* use with {@link AdapterView} based views. Supplying zero for the thread | |
* count will cause the {@link ViewRenderer} to use a single thread that | |
* can be shared among other instances. | |
* | |
* @param maxThreadCount | |
* the number of threads that will be created to support | |
* background rendering, may be zero. | |
* @param passes | |
* the number of rendering passes required to complete the | |
* rendering required for a view | |
* @param mayInterruptIfRunning | |
* whether background rendering tasks can be interrupted if they | |
* become redundant | |
* @param cacheCapacity | |
* the maximum number of render | |
* | |
*/ | |
public ViewRenderer(int threadCount, int passes, boolean mayInterruptIfRunning, int cacheCapacity) { | |
if (threadCount < 0) throw new IllegalArgumentException("negative thread count"); | |
if (passes <= 0) throw new IllegalArgumentException("passes not positive"); | |
if (cacheCapacity < 0) throw new IllegalArgumentException("negative cache capacity"); | |
mExecutor = threadCount == 0 ? getDefaultExecutor() : new Executor(threadCount); | |
mPasses = passes; | |
mMayInterruptIfRunning = mayInterruptIfRunning; | |
mCache = cacheCapacity == 0 ? null : new Cache(cacheCapacity); | |
mHandler = new Handler(); | |
mInheritedThreadPriority = Process.getThreadPriority(Process.myTid()); | |
} | |
// methods | |
/** | |
* Causes a view to be rendered using the supplied parameters. The nature of | |
* the rendering performed is determined by the concrete subclass on which | |
* this method is called, but in all cases, the view is initialized (by a | |
* call to {@link #prepare(View, Object, int)} and a Render object created | |
* in {@link #render(Object, int)} (on a background thread) before being | |
* displayed (in a call to {@link #update(View, Object, int)}. | |
* | |
* @param view | |
* the view that will display the rendering | |
* @param param | |
* the parameters that define the rendering | |
*/ | |
public void renderView(View view, Param param) { | |
if (mStopping) throw new IllegalStateException("stopped"); | |
//check the parameters | |
if (view == null) throw new IllegalArgumentException("null view"); | |
// see if there's a tag which will tell us about past rendering on this view | |
Task task = getTask(view); | |
if (task != null) { | |
if (equal(param, task.mParam)) { | |
// a task has already done this (or will) | |
// so there's nothing else to do | |
return; | |
} else { | |
// try to cancel the existing task | |
// this may save lots of work | |
task.removeFromView(view); | |
} | |
} | |
// try to obtain a task from the cache | |
if (mCache == null) { | |
task = null; | |
} else { | |
synchronized (mCache) { | |
task = mCache.get(param); | |
} | |
} | |
// create a task that will render and later update the view | |
if (task == null) task = new Task(param); | |
// associate the task with the view (and schedule it for rendering as necessary) | |
task.assignToView(view); | |
} | |
/** | |
* Instructs this renderer that any cached renders should be purged. This | |
* method may be useful for dealing with low memory conditions or situations | |
* where parameter equality is no longer valid. | |
*/ | |
public void clearCache() { | |
mCache.clear(); | |
} | |
/** | |
* Stops this renderer and makes it unusable for further rendering. A zero | |
* value for the timeout blocks indefinitely. A negative timeout value will | |
* cause the method to return immediately without waiting for background | |
* rendering operations to complete. Note that even if no timeout is set, or | |
* the timeout is exceeded, the renderer will not call | |
* {@link #update(View, Object, int)} or {@link #prepare(View, Object, int)} | |
* or otherwise modify a view at any time after the this method has been | |
* called. | |
* | |
* @param timeout | |
* the number of milliseconds for which to wait for rendering | |
* operations to terminate | |
*/ | |
public void stop(long timeout) throws InterruptedException { | |
if (mStopping) return; | |
mStopping = true; | |
if (mExecutor != sDefaultExecutor) { | |
mExecutor.shutdown(); | |
if (timeout == 0) { | |
timeout = Long.MAX_VALUE; | |
} | |
if (timeout >= 0) { | |
mExecutor.awaitTermination(timeout, TimeUnit.MILLISECONDS); | |
} | |
} | |
} | |
/** | |
* Stop the renderer without waiting for calls to | |
* {@link #render(Object, int)} to complete. This is a convenient way of | |
* calling {@link #stop(long)} without specifying a timeout. | |
*/ | |
public void stop() { | |
try { | |
stop(-1L); | |
} catch (InterruptedException e) { | |
throw new IllegalStateException("Impossible: interrupted without waiting"); | |
} | |
} | |
/** | |
* The thread priority assigned to the specified rendering pass. The default | |
* implementation returns {@link Process#THREAD_PRIORITY_BACKGROUND} for | |
* every pass. | |
* | |
* @param pass the rendering pass for which the priority is being requested | |
* @return a thread priority | |
* @see Process | |
*/ | |
protected int getThreadPriority(int pass) { | |
return Process.THREAD_PRIORITY_BACKGROUND; | |
} | |
/** | |
* <p> | |
* Prepares a view for display in advance of being updated with its rendered | |
* content. A non-negative immediatePassHint indicates the update method | |
* will be called immediately after this call to prepare (this may enable | |
* some optimizations within the prepare method), otherwise the view will | |
* displayed to the visitor until the background rendering completes. This | |
* method should return quickly with all time-consuming operations being | |
* perfomed in one-or-more rendering passes. | |
* </p> | |
* | |
* <p> | |
* Implementations will typically prepare the supplied view to display a | |
* blank placeholder, or a "loading" indicator. Note that a view may be | |
* prepared several times without receiving a render if it is part of an | |
* {@link AdapterView} that is recycling its views. | |
* </p> | |
* | |
* @param view | |
* a view will be displayed to the user presently | |
* @param param | |
* defines the content that the view will display | |
* @param immediatePassHint | |
* the pass to which rendering has already progressed | |
*/ | |
protected abstract void prepare(View view, Param param, int immediatePassHint); | |
/** | |
* Converts a Param into a Render for subsequent display in a view. The | |
* supplied pass parameter may be used to generate progressively more | |
* complete Renders. The first pass is zero. The number of passes is | |
* specified at construction time. | |
* | |
* @param param | |
* defines the rendering that needs to be produced | |
* @param pass | |
* which rendering pass is being performed | |
* @return a rendering of the supplied parameters | |
*/ | |
//TODO consider supplying the previous Render for multi-pass rendering | |
protected abstract Render render(Param param, int pass); | |
/** | |
* Applies previously rendered content to a view. | |
* | |
* @param view | |
* a view that needs to update with rendered content | |
* @param render | |
* the content that is to be applied to the view | |
* @param pass | |
* the rendering pass that generated the rendered content | |
*/ | |
protected abstract void update(View view, Render render, int pass); | |
/** | |
* Retrieves the last object that was associated with a view by the | |
* renderer. If no object has yet been associated with the view, null is | |
* returned. | |
* | |
* @see ViewRenderer.setTag | |
* @param view | |
* a rendered view | |
* @return the associated object or null | |
*/ | |
protected Object getTag(View view) { return view.getTag(); } | |
/** | |
* Associates an object with the view. The default implementation simply | |
* uses the {@link View.setTag(Object)} method. This may interfere with some | |
* layouts, so using the {@link View.setTag(int,Object)} method is | |
* preferable (API level 4 and above). A conservative implementation may be | |
* to use a {@link WeakHashMap}. | |
* | |
* @param view | |
* a rendered view | |
* @param tag | |
* the object with which the view is to be tagged, may be null | |
*/ | |
protected void setTag(View view, Object tag) { view.setTag(tag); } | |
// private utility methods | |
@SuppressWarnings("unchecked") | |
private Task getTask(View view) { | |
Object obj = getTag(view); | |
return obj == null || (obj instanceof ViewRenderer.Task) ? (Task) obj : null; | |
} | |
private void setTask(View view, Task task) { | |
setTag(view, task); | |
} | |
// inner classes | |
private class Task implements Runnable, Callable<Void>, Comparable<Task> { | |
// immutable fields for task | |
private final long mOrder = mNextOrder++; | |
private final Param mParam; | |
// only set/mutated on UI thread | |
// TODO look at strategy's for defraying cost of a hashset on each task | |
// note that this set also incurs the cost of repeated iterator creation | |
private final HashSet<View> mViews = new HashSet<View>(); | |
private Future<Void> mFuture; | |
// only set on render thread | |
private int mPass = 0; // from the UI thread, this is actually 'the next pass' | |
private Render mRender = null; | |
private boolean mFailed = false; | |
public Task(Param param) { | |
mParam = param; | |
} | |
void assignToView(View view) { | |
// mPass is actually 'the next pass' at this point | |
int pass = mPass - 1; | |
// prepare the view for display while rendering | |
prepare(view, mParam, pass); | |
// update the view with whatever work we have already done | |
if (pass >= 0) update(view, mRender, pass); | |
//check if we have work left to do | |
if (mPass < mPasses) { | |
// add the view to our set | |
mViews.add(view); | |
// tag the view so that we can cancel the task later if we need to | |
setTask(view, this); | |
// all set, queue us up | |
enqueue(); | |
} | |
} | |
void removeFromView(View view) { | |
// mViews can't be null because keep tags and set in sync | |
Task task = getTask(view); | |
if (task == this) setTask(view, null); | |
mViews.remove(view); | |
if (mViews.isEmpty()) { | |
cancel(); | |
} | |
} | |
void removeFromViews() { | |
for (View view : mViews) { | |
Task task = getTask(view); | |
if (task == this) setTask(view, null); | |
} | |
mViews.clear(); | |
cancel(); | |
} | |
// called to apply render to view | |
@Override | |
public void run() { | |
if (mStopping) return; | |
if (mFailed) { | |
// render didn't complete (probably got cancelled) | |
// remove from the view (but we may still get cached) | |
removeFromViews(); | |
} | |
// the views may be null/empty if we were removed from it while queued by the handler | |
if (mViews != null && !mViews.isEmpty()) { | |
try { | |
for (View view : mViews) { | |
// we are safe to update this view, | |
// if another task had been started on the view | |
// the view would have already have been removed from our set | |
update(view, mRender, mPass - 1); | |
} | |
} finally { | |
if (mPass < mPasses) { | |
// reschedule again if we need to perform more passes | |
enqueue(); | |
} else { | |
// we're done, remove us from all our views | |
// we lose the ability to avoid doing the same rendering for the same view (very rare) | |
// but this is worth it, so that we can re-use the render from the cache (much more likely) | |
removeFromViews(); | |
} | |
} | |
} | |
// try putting us into the cache | |
encache(); | |
} | |
// called to produce render | |
@Override | |
public Void call() throws Exception { | |
if (mStopping) return null; | |
Process.setThreadPriority(getThreadPriority(mPass)); | |
try { | |
Render render = render(mParam, mPass); | |
if (render == null) { | |
mFailed = true; | |
} else { | |
// record the render, we will apply to the view it cache it later | |
mRender = render; | |
// increment the pass now - since later calls aren't guaranteed to occur | |
mPass++; | |
} | |
mHandler.post(this); | |
return null; | |
} finally { | |
Process.setThreadPriority(mInheritedThreadPriority); | |
} | |
} | |
public int compareTo(Task that) { | |
if (this == that) return 0; | |
if (this.mPass != that.mPass) return this.mPass < that.mPass ? -1 : 1; | |
if (this.mOrder != that.mOrder) return this.mOrder < that.mOrder ? -1 : 1; | |
return 0; | |
} | |
private void enqueue() { | |
mFailed = false; | |
mFuture = mExecutor.submit((Callable<Void>) this); | |
} | |
private void encache() { | |
if (mPass > 0 && mCache != null) { | |
synchronized (mCache) { | |
Task that = mCache.get(mParam); | |
if (that == null || this.mPass > that.mPass) { | |
mCache.put(mParam, this); | |
} | |
} | |
} | |
} | |
// may only be called when all views have been removed | |
private void cancel() { | |
mFuture.cancel(mMayInterruptIfRunning); | |
} | |
} | |
private class Cache extends LinkedHashMap<Param, Task> { | |
// serialization boilerplate | |
private static final long serialVersionUID = -5867267874566891476L; | |
private final int mCapacity; | |
public Cache(int capacity) { | |
super(capacity, 0.75f, true); | |
mCapacity = capacity; | |
} | |
@Override | |
protected boolean removeEldestEntry(Map.Entry<Param, Task> eldest) { | |
return size() >= mCapacity; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment