new allocation-minimizing bitmap decoder
Use a new image decoding and caching framework. This one maximizes
bitmap reuse by decoding into fixed-sized Bitmaps and relying on
BitmapRegionDecoder to only decode a desired region. The ultimate goal
is to allocate memory less often, as each large allocation triggers
GC jank.
The overall request/response abstraction and threading model are now
cleaner, and caching is no longer a required fixture. Multi-core
decoding is now supported, with a currently fixed thread pool size of 4.
Attachment images are the only client for now, but contact photos will
switch to this framework next.
Bug: 9566006
Change-Id: I7c437941fd984cc0038da8f0ffd1df1a9ced4dd3
diff --git a/proguard.flags b/proguard.flags
index 0485533..6bcae61 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -60,9 +60,6 @@
-keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
*** setAnimatedHeightFraction(...);
- *** setAnimatedProgressFraction(...);
- *** setAnimatedFadeFraction0(...);
- *** setAnimatedFadeFraction1(...);
*** setPhotoFlipFraction(...);
}
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 291048d..b47d8b3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -33,7 +33,7 @@
<color name="message_info_text_color">@color/gray_text_color</color>
<color name="subject_text_color">#333333</color>
<color name="ap_background_color">#fff1f1f2</color>
- <color name="ap_overflow_badge_color">#eeeeeeee</color>
+ <color name="ap_overflow_badge_color">#cceeeeee</color>
<color name="ap_overflow_text_color">#ff4f4c4c</color>
<!-- a 'checked' item is in the conversation selection set. also the 'pressed' color. -->
<!-- this is holo_blue_light @ 20% opacity -->
diff --git a/res/values/constants.xml b/res/values/constants.xml
index f0c15b5..68f9286 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -120,7 +120,7 @@
<!-- Duration of fade in/out animation for attachment previews -->
<integer name="ap_fade_animation_duration">500</integer>
<!-- Duration of placeholder pulse animation for attachment previews -->
- <integer name="ap_placeholder_animation_duration">2000</integer>
+ <integer name="ap_placeholder_animation_duration">1000</integer>
<!-- Delay before showing progress bar animations for attachment previews that are loading -->
<integer name="ap_progress_animation_delay">2000</integer>
<!-- Max overflow count to show for attachment previews -->
diff --git a/src/com/android/bitmap/AltBitmapCache.java b/src/com/android/bitmap/AltBitmapCache.java
new file mode 100644
index 0000000..c5770f2
--- /dev/null
+++ b/src/com/android/bitmap/AltBitmapCache.java
@@ -0,0 +1,15 @@
+package com.android.bitmap;
+
+public class AltBitmapCache extends AltPooledCache<DecodeTask.Request, ReusableBitmap>
+ implements BitmapCache {
+
+ public AltBitmapCache(int targetSizeBytes) {
+ super(targetSizeBytes);
+ }
+
+ @Override
+ protected int sizeOf(ReusableBitmap value) {
+ return value.getByteCount();
+ }
+
+}
diff --git a/src/com/android/bitmap/AltPooledCache.java b/src/com/android/bitmap/AltPooledCache.java
new file mode 100644
index 0000000..1f0d050
--- /dev/null
+++ b/src/com/android/bitmap/AltPooledCache.java
@@ -0,0 +1,131 @@
+package com.android.bitmap;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.LinkedBlockingQueue;
+
+
+/**
+ * An alternative implementation of a pool+cache. This implementation only counts
+ * unreferenced objects in its size calculation. Internally, it never evicts from
+ * its cache, and instead {@link #poll()} is allowed to return unreferenced cache
+ * entries.
+ * <p>
+ * You would only use this kind of cache if your objects are interchangeable and
+ * have significant allocation cost, and if your memory footprint is somewhat
+ * flexible.
+ * <p>
+ * Because this class only counts unreferenced objects toward targetSize,
+ * it will have a total memory footprint of:
+ * <code>(targetSize) + (# of threads concurrently writing to cache) +
+ * (total size of still-referenced entries)</code>
+ *
+ */
+public class AltPooledCache<K, V extends RefCountable> implements PooledCache<K, V> {
+
+ private final LinkedHashMap<K, V> mCache;
+ private final LinkedBlockingQueue<V> mPool;
+ private final int mTargetSize;
+
+ /**
+ * @param targetSize not exactly a max size in practice
+ */
+ public AltPooledCache(int targetSize) {
+ mCache = new LinkedHashMap<K, V>(0, 0.75f, true);
+ mPool = new LinkedBlockingQueue<V>();
+ mTargetSize = targetSize;
+ }
+
+ @Override
+ public V get(K key) {
+ synchronized (mCache) {
+ return mCache.get(key);
+ }
+ }
+
+ @Override
+ public V put(K key, V value) {
+ synchronized (mCache) {
+ return mCache.put(key, value);
+ }
+ }
+
+ @Override
+ public void offer(V value) {
+ if (value.getRefCount() != 0) {
+ throw new IllegalArgumentException("unexpected offer of a referenced object: " + value);
+ }
+ mPool.offer(value);
+ }
+
+ @Override
+ public V poll() {
+ final V pooled = mPool.poll();
+ if (pooled != null) {
+ return pooled;
+ }
+
+ synchronized (mCache) {
+ int unrefSize = 0;
+ Map.Entry<K, V> eldestUnref = null;
+ for (Map.Entry<K, V> entry : mCache.entrySet()) {
+ final V value = entry.getValue();
+ if (value.getRefCount() > 0) {
+ continue;
+ }
+ if (eldestUnref == null) {
+ eldestUnref = entry;
+ }
+ unrefSize += sizeOf(value);
+ if (unrefSize > mTargetSize) {
+ break;
+ }
+ }
+ // only return a scavenged cache entry if the cache has enough
+ // eligible (unreferenced) items
+ if (unrefSize <= mTargetSize) {
+ return null;
+ } else {
+ mCache.remove(eldestUnref.getKey());
+ return eldestUnref.getValue();
+ }
+ }
+ }
+
+ protected int sizeOf(V value) {
+ return 1;
+ }
+
+ @Override
+ public String toDebugString() {
+ final StringBuilder sb = new StringBuilder("[");
+ sb.append(super.toString());
+ int size = 0;
+ synchronized (mCache) {
+ sb.append(" poolCount=");
+ sb.append(mPool.size());
+ sb.append(" cacheSize=");
+ sb.append(mCache.size());
+ sb.append("\n---------------------");
+ for (V val : mPool) {
+ size += sizeOf(val);
+ sb.append("\n\tpool item: ");
+ sb.append(val);
+ }
+ sb.append("\n---------------------");
+ for (Map.Entry<K, V> item : mCache.entrySet()) {
+ final V val = item.getValue();
+ sb.append("\n\tcache key=");
+ sb.append(item.getKey());
+ sb.append(" val=");
+ sb.append(val);
+ size += sizeOf(val);
+ }
+ sb.append("\n---------------------");
+ sb.append("\nTOTAL SIZE=" + size);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/src/com/android/bitmap/BitmapCache.java b/src/com/android/bitmap/BitmapCache.java
new file mode 100644
index 0000000..38279bb
--- /dev/null
+++ b/src/com/android/bitmap/BitmapCache.java
@@ -0,0 +1,5 @@
+package com.android.bitmap;
+
+public interface BitmapCache extends PooledCache<DecodeTask.Request, ReusableBitmap> {
+
+}
diff --git a/src/com/android/bitmap/BitmapUtils.java b/src/com/android/bitmap/BitmapUtils.java
new file mode 100644
index 0000000..72ef233
--- /dev/null
+++ b/src/com/android/bitmap/BitmapUtils.java
@@ -0,0 +1,62 @@
+package com.android.bitmap;
+
+import android.graphics.Rect;
+
+public abstract class BitmapUtils {
+
+ public static void calculateCroppedSrcRect(int srcW, int srcH, int dstW, int dstH,
+ int dstSliceH, float verticalSliceFraction, Rect outRect) {
+ calculateCroppedSrcRect(srcW, srcH, dstW, dstH, dstSliceH, Integer.MAX_VALUE,
+ verticalSliceFraction, outRect);
+ }
+
+ public static void calculateCroppedSrcRect(int srcW, int srcH, int dstW, int dstH,
+ int sampleSize, Rect outRect) {
+ calculateCroppedSrcRect(srcW, srcH, dstW, dstH, dstH, sampleSize, 0.5f, outRect);
+ }
+
+ /**
+ * Calculate a center-crop rectangle for the given input and output
+ * parameters. The output rectangle to use is written in the given outRect.
+ *
+ * @param srcW the source width
+ * @param srcH the source height
+ * @param dstW the destination width
+ * @param dstH the destination height
+ * @param dstSliceH the height extent (in destination coordinates) to
+ * exclude when cropping. You would typically pass dstH, unless
+ * you are trying to normalize different items to the same
+ * vertical crop range.
+ * @param sampleSize a scaling factor that rect calculation will only use if
+ * it's more aggressive than regular scaling
+ * @param verticalSliceFraction the vertical center point for the crop rect,
+ * from [0.0, 1.0]. To perform a vertically centered crop, use
+ * 0.5.
+ * @param outRect a Rect to write the resulting crop coordinates into
+ */
+ public static void calculateCroppedSrcRect(int srcW, int srcH, int dstW, int dstH,
+ int dstSliceH, int sampleSize, float verticalSliceFraction, Rect outRect) {
+ if (sampleSize < 1) {
+ sampleSize = 1;
+ }
+ final float regularScale = Math.min(
+ (float) srcW / dstW,
+ (float) srcH / dstH);
+
+ final float scale = Math.min(sampleSize, regularScale);
+
+ final int srcCroppedW = Math.round(dstW * scale);
+ final int srcCroppedH = Math.round(dstH * scale);
+ final int srcCroppedSliceH = Math.round(dstSliceH * scale);
+
+ outRect.left = (srcW - srcCroppedW) / 2;
+ outRect.right = outRect.left + srcCroppedW;
+
+ final int centerV = Math.round(
+ (srcH - srcCroppedSliceH) * verticalSliceFraction + srcCroppedH / 2);
+
+ outRect.top = centerV - srcCroppedH / 2;
+ outRect.bottom = outRect.top + srcCroppedH;
+ }
+
+}
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
new file mode 100644
index 0000000..b679945
--- /dev/null
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -0,0 +1,321 @@
+package com.android.bitmap;
+
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Rect;
+import android.os.AsyncTask;
+
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Decodes an image from either a file descriptor or input stream on a worker thread. After the
+ * decode is complete, even if the task is cancelled, the result is placed in the given cache.
+ * A {@link BitmapView} client may be notified on decode begin and completion.
+ * <p>
+ * This class uses {@link BitmapRegionDecoder} when possible to minimize unnecessary decoding
+ * and allow bitmap reuse on Jellybean 4.1 and later.
+ * <p>
+ * FIXME: add GIF support when {@link BitmapRegionDecoder} fails to decode it. Because BitmapFactory
+ * is typically too picky to support bitmap reuse, we must also mark decoded result as not eligible
+ * for pooling as it would be sized to be an exact fit for that GIF.
+ *
+ */
+public class DecodeTask extends AsyncTask<Void, Void, ReusableBitmap> {
+
+ private final Request mKey;
+ private final int mDestW;
+ private final int mDestH;
+ private final BitmapView mView;
+ private final BitmapCache mCache;
+ private final BitmapFactory.Options mOpts = new BitmapFactory.Options();
+
+ private ReusableBitmap mInBitmap = null;
+
+ private static final boolean CROP_DURING_DECODE = true;
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * The decode task uses this class to get input to decode. You must implement at least one of
+ * {@link #createFd()} or {@link #createInputStream()}. {@link DecodeTask} will prioritize
+ * {@link #createFd()} before falling back to {@link #createInputStream()}.
+ * <p>
+ * When {@link DecodeTask} is used in conjunction with a {@link BitmapCache}, objects of this
+ * type will also serve as cache keys to fetch cached data.
+ */
+ public interface Request {
+ AssetFileDescriptor createFd() throws IOException;
+ InputStream createInputStream() throws IOException;
+ }
+
+ /**
+ * Callback interface for clients to be notified of decode state changes and completion.
+ */
+ public interface BitmapView {
+ /**
+ * Notifies that the async task's work is about to begin. Up until this point, the task
+ * may have been preempted by the scheduler or queued up by a bottlenecked executor.
+ * <p>
+ * N.B. this method runs on the UI thread.
+ *
+ * @param key
+ */
+ void onDecodeBegin(Request key);
+ void onDecodeComplete(Request key, ReusableBitmap result);
+ }
+
+ public DecodeTask(Request key, int w, int h, BitmapView view, BitmapCache cache) {
+ mKey = key;
+ mDestW = w;
+ mDestH = h;
+ mView = view;
+ mCache = cache;
+ }
+
+ @Override
+ protected ReusableBitmap doInBackground(Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+
+ // enqueue the 'onDecodeBegin' signal on the main thread
+ publishProgress();
+
+ Trace.beginSection("decodeBounds");
+ ReusableBitmap result = null;
+ AssetFileDescriptor fd = null;
+ InputStream in = null;
+ try {
+ fd = mKey.createFd();
+ if (fd == null) {
+ in = mKey.createInputStream();
+ if (in != null && !in.markSupported()) {
+ throw new IllegalArgumentException("input stream must support reset()");
+ }
+ }
+
+ mOpts.inJustDecodeBounds = true;
+ if (fd != null) {
+ BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
+ } else {
+ BitmapFactory.decodeStream(in, null, mOpts);
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+ Trace.endSection();
+ final int srcW = mOpts.outWidth;
+ final int srcH = mOpts.outHeight;
+
+ mOpts.inJustDecodeBounds = false;
+ mOpts.inMutable = true;
+ mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH);
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ mInBitmap = mCache.poll();
+ if (mInBitmap == null) {
+ if (DEBUG) System.err.println(
+ "decode thread wants a bitmap. cache dump:\n" + mCache.toDebugString());
+ mInBitmap = new ReusableBitmap(
+ Bitmap.createBitmap(mDestW, mDestH, Bitmap.Config.ARGB_8888));
+ if (DEBUG) System.err.println("*** allocated new bitmap in decode thread: "
+ + mInBitmap + " key=" + mKey);
+ } else {
+ if (DEBUG) System.out.println("*** reusing existing bitmap in decode thread: "
+ + mInBitmap + " key=" + mKey);
+
+ }
+ mOpts.inBitmap = mInBitmap.bmp;
+ }
+
+ if (isCancelled()) {
+ return null;
+ }
+
+ Bitmap decodeResult = null;
+
+ if (in != null) {
+ in.reset();
+ }
+ final Rect srcRect = new Rect();
+ if (CROP_DURING_DECODE) {
+ try {
+ Trace.beginSection("decodeCropped" + mOpts.inSampleSize);
+ decodeResult = decodeCropped(fd, in, srcRect);
+ } catch (IOException e) {
+ // fall through to below and try again with the non-cropping decoder
+ e.printStackTrace();
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
+ try {
+ Trace.beginSection("decode" + mOpts.inSampleSize);
+ decodeResult = decode(fd, in);
+ } catch (IllegalArgumentException e) {
+ System.err.println("decode failed: reason='" + e.getMessage() + "' ss="
+ + mOpts.inSampleSize);
+
+ if (mOpts.inSampleSize > 1) {
+ // try again with ss=1
+ mOpts.inSampleSize = 1;
+ decodeResult = decode(fd, in);
+ }
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ if (!isCancelled() && decodeResult != null) {
+ if (mInBitmap != null) {
+ result = mInBitmap;
+ // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
+ if (!srcRect.isEmpty()) {
+ result.setLogicalWidth((srcRect.right - srcRect.left) / mOpts.inSampleSize);
+ result.setLogicalHeight(
+ (srcRect.bottom - srcRect.top) / mOpts.inSampleSize);
+ } else {
+ result.setLogicalWidth(mOpts.outWidth);
+ result.setLogicalHeight(mOpts.outHeight);
+ }
+ } else {
+ // no mInBitmap means no pooling
+ result = new ReusableBitmap(decodeResult);
+ result.setLogicalWidth(decodeResult.getWidth());
+ result.setLogicalHeight(decodeResult.getHeight());
+ }
+ // System.out.println("*** async task decoded fd=" + mUri +
+ // " to" +
+ // " sz=" + (decodeResult.getByteCount() >> 10) + "KB dstW/H=" +
+ // result.getLogicalWidth() +
+ // "/" + result.getLogicalHeight() + " srcW/H=" + srcW + "/" +
+ // srcH + " ss=" +
+ // mOpts.inSampleSize + " mutable=" + decodeResult.isMutable() +
+ // " matchesInBitmap=" + (mOpts.inBitmap == decodeResult));
+ } else {
+ // System.out.println("*** async task cancelled decode of fd=" +
+ // mUri);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (fd != null) {
+ try {
+ fd.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (result != null) {
+ result.acquireReference();
+ mCache.put(mKey, result);
+ if (DEBUG) System.out.println("placed result in cache: key=" + mKey + " bmp="
+ + result + " cancelled=" + isCancelled());
+ } else if (isCancelled() && mInBitmap != null) {
+ if (DEBUG) System.out.println("placing cancelled bitmap in pool: key=" + mKey
+ + " bmp=" + mInBitmap);
+ mCache.offer(mInBitmap);
+ }
+ }
+ return result;
+ }
+
+ private Bitmap decodeCropped(AssetFileDescriptor fd, InputStream in, Rect outSrcRect)
+ throws IOException {
+ final BitmapRegionDecoder brd;
+ if (fd != null) {
+ brd = BitmapRegionDecoder.newInstance(fd.getFileDescriptor(),
+ true /* shareable */);
+ } else {
+ brd = BitmapRegionDecoder.newInstance(in,
+ true /* shareable */);
+ }
+ if (isCancelled()) {
+ brd.recycle();
+ return null;
+ }
+
+ final int srcW = mOpts.outWidth;
+ final int srcH = mOpts.outHeight;
+
+ // Trace.beginSection("DecodeRegionGetDimens");
+ // final int tmpw = brd.getWidth();
+ // final int tmph = brd.getHeight();
+ // Trace.endSection();
+
+ BitmapUtils.calculateCroppedSrcRect(srcW, srcH, mDestW, mDestH, mOpts.inSampleSize,
+ outSrcRect);
+ // System.out.println("rect for uri=" + uri + " is: " + outSrcRect);
+ final Bitmap result = brd.decodeRegion(outSrcRect, mOpts);
+ brd.recycle();
+ return result;
+ }
+
+ private Bitmap decode(AssetFileDescriptor fd, InputStream in) {
+ final Bitmap result;
+ if (fd != null) {
+ result = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor(), null, mOpts);
+ } else {
+ result = BitmapFactory.decodeStream(in, null, mOpts);
+ }
+ return result;
+ }
+
+ private int calculateSampleSize(int srcW, int srcH, int destW, int destH) {
+ int result;
+
+ final float sz = Math.min((float) srcW / destW, (float) srcH / destH);
+
+ // round to the nearest power of two, or just truncate
+ final boolean stricter = true;
+
+ if (stricter) {
+ result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
+ } else {
+ result = (int) sz;
+ }
+ return Math.max(1, result);
+ }
+
+ public void cancel() {
+ cancel(true);
+ mOpts.requestCancelDecode();
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ mView.onDecodeBegin(mKey);
+ }
+
+ @Override
+ public void onPostExecute(ReusableBitmap result) {
+ mView.onDecodeComplete(mKey, result);
+ }
+
+ @Override
+ protected void onCancelled(ReusableBitmap result) {
+ if (result == null) {
+ return;
+ }
+
+ result.releaseReference();
+ if (mInBitmap == null) {
+ // not reusing bitmaps: can recycle immediately
+ result.bmp.recycle();
+ }
+ }
+
+}
diff --git a/src/com/android/bitmap/PooledCache.java b/src/com/android/bitmap/PooledCache.java
new file mode 100644
index 0000000..d1fd409
--- /dev/null
+++ b/src/com/android/bitmap/PooledCache.java
@@ -0,0 +1,11 @@
+package com.android.bitmap;
+
+public interface PooledCache<K, V> {
+
+ V get(K key);
+ V put(K key, V value);
+ void offer(V scrapValue);
+ V poll();
+ String toDebugString();
+
+}
diff --git a/src/com/android/bitmap/RefCountable.java b/src/com/android/bitmap/RefCountable.java
new file mode 100644
index 0000000..97b0711
--- /dev/null
+++ b/src/com/android/bitmap/RefCountable.java
@@ -0,0 +1,7 @@
+package com.android.bitmap;
+
+public interface RefCountable {
+ void acquireReference();
+ void releaseReference();
+ int getRefCount();
+}
diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java
new file mode 100644
index 0000000..dff42f3
--- /dev/null
+++ b/src/com/android/bitmap/ReusableBitmap.java
@@ -0,0 +1,79 @@
+package com.android.bitmap;
+
+import android.graphics.Bitmap;
+
+/**
+ * A simple bitmap wrapper. Currently supports reference counting and logical width/height
+ * (which may differ from a bitmap's reported width/height due to bitmap reuse).
+ */
+public class ReusableBitmap implements RefCountable {
+
+ public final Bitmap bmp;
+ private int mWidth;
+ private int mHeight;
+ private int mRefCount = 0;
+
+ public ReusableBitmap(Bitmap bitmap) {
+ bmp = bitmap;
+ }
+
+ public void setLogicalWidth(int w) {
+ mWidth = w;
+ }
+
+ public void setLogicalHeight(int h) {
+ mHeight = h;
+ }
+
+ public int getLogicalWidth() {
+ return mWidth;
+ }
+
+ public int getLogicalHeight() {
+ return mHeight;
+ }
+
+ public int getByteCount() {
+ return bmp.getByteCount();
+ }
+
+ @Override
+ public void acquireReference() {
+ mRefCount++;
+ }
+
+ @Override
+ public void releaseReference() {
+ if (mRefCount == 0) {
+ throw new IllegalStateException();
+ }
+ mRefCount--;
+ }
+
+ @Override
+ public int getRefCount() {
+ return mRefCount;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("[");
+ sb.append(super.toString());
+ sb.append(" refCount=");
+ sb.append(mRefCount);
+ sb.append(" bmp=");
+ sb.append(bmp);
+ sb.append(" logicalW/H=");
+ sb.append(mWidth);
+ sb.append("/");
+ sb.append(mHeight);
+ if (bmp != null) {
+ sb.append(" sz=");
+ sb.append(bmp.getByteCount() >> 10);
+ sb.append("KB");
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/src/com/android/bitmap/Trace.java b/src/com/android/bitmap/Trace.java
new file mode 100644
index 0000000..8bfe6b8
--- /dev/null
+++ b/src/com/android/bitmap/Trace.java
@@ -0,0 +1,35 @@
+package com.android.bitmap;
+
+import android.os.Build;
+
+/**
+ * Stand-in for {@link android.os.Trace}.
+ */
+public abstract class Trace {
+
+ /**
+ * Begins systrace tracing for a given tag. No-op on unsupported platform versions.
+ *
+ * @param tag systrace tag to use
+ *
+ * @see android.os.Trace#beginSection(String)
+ */
+ public static void beginSection(String tag) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ android.os.Trace.beginSection(tag);
+ }
+ }
+
+ /**
+ * Ends systrace tracing for the most recently begun section. No-op on unsupported platform
+ * versions.
+ *
+ * @see android.os.Trace#endSection()
+ */
+ public static void endSection() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ android.os.Trace.endSection();
+ }
+ }
+
+}
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
new file mode 100644
index 0000000..bf2e1df
--- /dev/null
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -0,0 +1,399 @@
+package com.android.mail.bitmap;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.view.animation.LinearInterpolator;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.BitmapUtils;
+import com.android.bitmap.DecodeTask;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap;
+import com.android.mail.R;
+import com.android.mail.browse.ConversationItemViewCoordinates;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class encapsulates all functionality needed to display a single image attachment thumbnail,
+ * including request creation/cancelling, data unbinding and re-binding, and fancy animations
+ * to draw upon state changes.
+ * <p>
+ * The actual bitmap decode work is handled by {@link DecodeTask}.
+ */
+public class AttachmentDrawable extends Drawable implements DecodeTask.BitmapView,
+ Drawable.Callback, Runnable, Parallaxable {
+
+ private DecodeTask.Request mCurrKey;
+ private ReusableBitmap mBitmap;
+ private final BitmapCache mCache;
+ private DecodeTask mTask;
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+ private int mLoadState = LOAD_STATE_UNINITIALIZED;
+ private float mParallaxFraction = 0.5f;
+
+ // each attachment gets its own placeholder and progress indicator, to be shown, hidden,
+ // and animated based on Drawable#setVisible() changes, which are in turn driven by
+ // #setLoadState().
+ private Placeholder mPlaceholder;
+ private Progress mProgress;
+
+ private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4,
+ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+
+ private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
+
+ private static final boolean LIMIT_BITMAP_DENSITY = true;
+
+ private static final int MAX_BITMAP_DENSITY = DisplayMetrics.DENSITY_HIGH;
+
+ private static final int LOAD_STATE_UNINITIALIZED = 0;
+ private static final int LOAD_STATE_NOT_LOADED = 1;
+ private static final int LOAD_STATE_LOADING = 2;
+ private static final int LOAD_STATE_LOADED = 3;
+
+ private final ConversationItemViewCoordinates mCoordinates;
+ private final float mDensity;
+ private final int mProgressDelayMs;
+ private final Paint mPaint = new Paint();
+ private final Rect mSrcRect = new Rect();
+ private final Handler mHandler = new Handler();
+
+ public AttachmentDrawable(Resources res, BitmapCache cache,
+ ConversationItemViewCoordinates coordinates, Drawable placeholder, Drawable progress) {
+ mCoordinates = coordinates;
+ mDensity = res.getDisplayMetrics().density;
+ mCache = cache;
+ mPaint.setFilterBitmap(true);
+
+ final int fadeOutDurationMs = res.getInteger(R.integer.ap_fade_animation_duration);
+ final int tileColor = res.getColor(R.color.ap_background_color);
+ mProgressDelayMs = res.getInteger(R.integer.ap_progress_animation_delay);
+
+ mPlaceholder = new Placeholder(placeholder.getConstantState().newDrawable(res), res,
+ coordinates, fadeOutDurationMs, tileColor);
+ mPlaceholder.setCallback(this);
+
+ mProgress = new Progress(progress.getConstantState().newDrawable(res), res,
+ coordinates, fadeOutDurationMs, tileColor);
+ mProgress.setCallback(this);
+ }
+
+ public DecodeTask.Request getKey() {
+ return mCurrKey;
+ }
+
+ public void setDecodeWidth(int width) {
+ mDecodeWidth = width;
+ }
+
+ public void setDecodeHeight(int height) {
+ mDecodeHeight = height;
+ }
+
+ public void setImage(DecodeTask.Request key) {
+ if (mCurrKey != null && mCurrKey.equals(key)) {
+ return;
+ }
+ if (mBitmap != null) {
+ mBitmap.releaseReference();
+// System.out.println("view.bind() decremented ref to old bitmap: " + mBitmap);
+ mBitmap = null;
+ }
+ mCurrKey = key;
+
+ if (mTask != null) {
+ mTask.cancel();
+ mTask = null;
+ }
+
+ mHandler.removeCallbacks(this);
+ setLoadState(LOAD_STATE_UNINITIALIZED);
+
+ if (key == null) {
+ return;
+ }
+
+ // find cached entry here and skip decode if found.
+ final ReusableBitmap cached = mCache.get(key);
+ if (cached != null) {
+ cached.acquireReference();
+ setBitmap(cached);
+ setLoadState(LOAD_STATE_LOADED);
+ } else {
+ decode();
+ }
+ }
+
+ @Override
+ public void setParallaxFraction(float fraction) {
+ mParallaxFraction = fraction;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (bounds.isEmpty()) {
+ return;
+ }
+
+ if (mBitmap != null) {
+ BitmapUtils.calculateCroppedSrcRect(mBitmap.getLogicalWidth(),
+ mBitmap.getLogicalHeight(), bounds.width(), bounds.height(),
+ mCoordinates.attachmentPreviewsDecodeHeight, mParallaxFraction, mSrcRect);
+ canvas.drawBitmap(mBitmap.bmp, mSrcRect, bounds, mPaint);
+ }
+
+ // Draw the two possible overlay layers in reverse-priority order.
+ // (each layer will no-op the draw when appropriate)
+ // This ordering means cross-fade transitions are just fade-outs of each layer.
+ mProgress.draw(canvas);
+ mPlaceholder.draw(canvas);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ mPlaceholder.setAlpha(alpha);
+ mProgress.setAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ mPlaceholder.setColorFilter(cf);
+ mProgress.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getOpacity() {
+ return (mBitmap != null && (mBitmap.bmp.hasAlpha() || mPaint.getAlpha() < 255)) ?
+ PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ mPlaceholder.setBounds(bounds);
+ mProgress.setBounds(bounds);
+ }
+
+ @Override
+ public void onDecodeBegin(Request key) {
+ if (!key.equals(mCurrKey)) {
+ return;
+ }
+ // normally, we'd transition to the LOADING state now, but we want to delay that a bit
+ // to minimize excess occurrences of the rotating spinner
+ mHandler.postDelayed(this, mProgressDelayMs);
+ }
+
+ @Override
+ public void run() {
+ if (mLoadState == LOAD_STATE_NOT_LOADED) {
+ setLoadState(LOAD_STATE_LOADING);
+ }
+ }
+
+ @Override
+ public void onDecodeComplete(Request key, ReusableBitmap result) {
+ if (key.equals(mCurrKey)) {
+ setBitmap(result);
+ } else {
+ // if the requests don't match (i.e. this request is stale), decrement the
+ // ref count to allow the bitmap to be pooled
+ if (result != null) {
+ result.releaseReference();
+ }
+ }
+ }
+
+ private void setBitmap(ReusableBitmap bmp) {
+ if (mBitmap != null && mBitmap != bmp) {
+ mBitmap.releaseReference();
+ }
+ mBitmap = bmp;
+ setLoadState(LOAD_STATE_LOADED);
+ invalidateSelf();
+ }
+
+ private void decode() {
+ final int w;
+ final int h;
+
+ if (LIMIT_BITMAP_DENSITY) {
+ final float scale =
+ Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
+ / mDensity);
+ w = (int) (mDecodeWidth * scale);
+ h = (int) (mDecodeHeight * scale);
+ } else {
+ w = mDecodeWidth;
+ h = mDecodeHeight;
+ }
+
+ if (w == 0 || h == 0 || mCurrKey == null) {
+ return;
+ }
+// System.out.println("ITEM " + this + " w=" + w + " h=" + h);
+ if (mTask != null) {
+ mTask.cancel();
+ }
+ setLoadState(LOAD_STATE_NOT_LOADED);
+ mTask = new DecodeTask(mCurrKey, w, h, this, mCache);
+ mTask.executeOnExecutor(EXECUTOR);
+ }
+
+ private void setLoadState(int loadState) {
+ if (mLoadState == loadState) {
+ return;
+ }
+
+ switch (loadState) {
+ case LOAD_STATE_UNINITIALIZED:
+ mPlaceholder.reset();
+ mProgress.reset();
+ break;
+ case LOAD_STATE_NOT_LOADED:
+ mPlaceholder.setVisible(true);
+ mProgress.setVisible(false);
+ break;
+ case LOAD_STATE_LOADING:
+ mPlaceholder.setVisible(false);
+ mProgress.setVisible(true);
+ break;
+ case LOAD_STATE_LOADED:
+ mPlaceholder.setVisible(false);
+ mProgress.setVisible(false);
+ break;
+ }
+
+ mLoadState = loadState;
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ unscheduleSelf(what);
+ }
+
+ private static class Placeholder extends TileDrawable {
+
+ private final ValueAnimator mPulseAnimator;
+
+ public Placeholder(Drawable placeholder, Resources res,
+ ConversationItemViewCoordinates coordinates, int fadeOutDurationMs,
+ int tileColor) {
+ super(placeholder, coordinates.placeholderWidth, coordinates.placeholderHeight,
+ tileColor, fadeOutDurationMs);
+ mPulseAnimator = ValueAnimator.ofInt(55, 255)
+ .setDuration(res.getInteger(R.integer.ap_placeholder_animation_duration));
+ mPulseAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ mPulseAnimator.setRepeatMode(ValueAnimator.REVERSE);
+ mPulseAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setInnerAlpha((Integer) animation.getAnimatedValue());
+ }
+ });
+ }
+
+ @Override
+ public boolean setVisible(boolean visible) {
+ final boolean changed = super.setVisible(visible);
+ if (changed) {
+ if (isVisible()) {
+ // start
+ if (mPulseAnimator != null) {
+ mPulseAnimator.start();
+ }
+ } else {
+ // stop
+ if (mPulseAnimator != null) {
+ mPulseAnimator.cancel();
+ }
+ }
+ }
+ return changed;
+ }
+
+ }
+
+ private static class Progress extends TileDrawable {
+
+ private final ValueAnimator mRotateAnimator;
+
+ public Progress(Drawable progress, Resources res,
+ ConversationItemViewCoordinates coordinates, int fadeOutDurationMs,
+ int tileColor) {
+ super(progress, coordinates.progressBarWidth, coordinates.progressBarHeight,
+ tileColor, fadeOutDurationMs);
+
+ mRotateAnimator = ValueAnimator.ofInt(0, 10000)
+ .setDuration(res.getInteger(R.integer.ap_progress_animation_duration));
+ mRotateAnimator.setInterpolator(new LinearInterpolator());
+ mRotateAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ mRotateAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setLevel((Integer) animation.getAnimatedValue());
+ }
+ });
+ mFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mRotateAnimator != null) {
+ mRotateAnimator.cancel();
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean setVisible(boolean visible) {
+ final boolean changed = super.setVisible(visible);
+ if (changed) {
+ if (isVisible()) {
+ if (mRotateAnimator != null) {
+ mRotateAnimator.start();
+ }
+ } else {
+ // can't cancel the rotate yet-- wait for the fade-out animation to end
+ }
+ }
+ return changed;
+ }
+
+ }
+
+}
diff --git a/src/com/android/mail/bitmap/AttachmentGridDrawable.java b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
new file mode 100644
index 0000000..fc58472
--- /dev/null
+++ b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
@@ -0,0 +1,118 @@
+package com.android.mail.bitmap;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R;
+import com.android.mail.browse.ConversationItemViewCoordinates;
+
+
+/**
+ * A 2x1 grid of attachment drawables. Supports showing a small "+N" badge in the corner.
+ */
+public class AttachmentGridDrawable extends CompositeDrawable<AttachmentDrawable>
+ implements Parallaxable {
+
+ public static final int MAX_VISIBLE_ATTACHMENT_COUNT = 2;
+
+ private BitmapCache mCache;
+ private int mDecodeWidth;
+ private int mDecodeHeight;
+ private String mOverflowText;
+ private ConversationItemViewCoordinates mCoordinates;
+ private float mParallaxFraction = 0.5f;
+
+ private final Resources mResources;
+ private final Drawable mPlaceholder;
+ private final Drawable mProgress;
+ private final int mOverflowTextColor;
+ private final int mOverflowBadgeColor;
+ private final Paint mPaint = new Paint();
+ private final Rect mRect = new Rect();
+
+ public AttachmentGridDrawable(Resources res, Drawable placeholder, Drawable progress) {
+ super(MAX_VISIBLE_ATTACHMENT_COUNT);
+ mResources = res;
+ mPlaceholder = placeholder;
+ mProgress = progress;
+ mOverflowTextColor = res.getColor(R.color.ap_overflow_text_color);
+ mOverflowBadgeColor = res.getColor(R.color.ap_overflow_badge_color);
+
+ mPaint.setAntiAlias(true);
+ }
+
+ @Override
+ protected AttachmentDrawable createDivisionDrawable() {
+ final AttachmentDrawable result = new AttachmentDrawable(mResources, mCache, mCoordinates,
+ mPlaceholder, mProgress);
+ result.setDecodeWidth(mDecodeWidth);
+ result.setDecodeHeight(mDecodeHeight);
+ return result;
+ }
+
+ public void setBitmapCache(BitmapCache cache) {
+ mCache = cache;
+ }
+
+ public void setDecodeWidth(int width) {
+ mDecodeWidth = width;
+ }
+
+ public void setDecodeHeight(int height) {
+ mDecodeHeight = height;
+ }
+
+ public void setOverflowText(String text) {
+ mOverflowText = text;
+ layoutOverflowBadge();
+ }
+
+ public void setCoordinates(ConversationItemViewCoordinates coordinates) {
+ mCoordinates = coordinates;
+ layoutOverflowBadge();
+ }
+
+ private void layoutOverflowBadge() {
+ if (mCoordinates == null || mOverflowText == null) {
+ return;
+ }
+ mPaint.setTextSize(mCoordinates.overflowFontSize);
+ mPaint.setTypeface(mCoordinates.overflowTypeface);
+ mPaint.setTextAlign(Align.CENTER);
+ mPaint.getTextBounds(mOverflowText, 0, mOverflowText.length(), mRect);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ for (int i = 0; i < mCount; i++) {
+ mDrawables.get(i).setParallaxFraction(mParallaxFraction);
+ }
+
+ super.draw(canvas);
+
+ // Overflow badge and count
+ if (mOverflowText != null && mCoordinates != null) {
+ final float radius = mCoordinates.overflowDiameter / 2f;
+ // transform item-level coordinates into local drawable coordinate space
+ final float x = mCoordinates.overflowXEnd - mCoordinates.attachmentPreviewsX - radius;
+ final float y = mCoordinates.overflowYEnd - mCoordinates.attachmentPreviewsY - radius;
+
+ mPaint.setColor(mOverflowBadgeColor);
+ canvas.drawCircle(x, y, radius, mPaint);
+
+ mPaint.setColor(mOverflowTextColor);
+ canvas.drawText(mOverflowText, x, y + (mRect.height() / 2f), mPaint);
+ }
+ }
+
+ @Override
+ public void setParallaxFraction(float fraction) {
+ mParallaxFraction = fraction;
+ }
+
+}
diff --git a/src/com/android/mail/bitmap/CompositeDrawable.java b/src/com/android/mail/bitmap/CompositeDrawable.java
new file mode 100644
index 0000000..cca1103
--- /dev/null
+++ b/src/com/android/mail/bitmap/CompositeDrawable.java
@@ -0,0 +1,143 @@
+package com.android.mail.bitmap;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A drawable that contains up to 4 other smaller drawables in a regular grid. This class attempts
+ * to reuse inner drawables when feasible to promote object reuse. This design goal makes the API
+ * to reuse an instance a little awkward: you must first zero out the count, then set it to a new
+ * value, then populate the entries with {@link #getOrCreateDrawable(int)} and a drawable subclass
+ * bind() method of your design.
+ */
+public abstract class CompositeDrawable<T extends Drawable> extends Drawable
+ implements Drawable.Callback {
+
+ protected final List<T> mDrawables;
+ protected int mCount;
+
+ public CompositeDrawable(int maxDivisions) {
+ if (maxDivisions >= 4) {
+ throw new IllegalArgumentException("CompositeDrawable only supports 4 divisions");
+ }
+ mDrawables = new ArrayList<T>(maxDivisions);
+ for (int i = 0; i < maxDivisions; i++) {
+ mDrawables.add(i, null);
+ }
+ mCount = 0;
+ }
+
+ protected abstract T createDivisionDrawable();
+
+ public void setCount(int count) {
+ // zero out the composite bounds, which will propagate to the division drawables
+ // this invalidates any old division bounds, which may change with the count
+ setBounds(0, 0, 0, 0);
+ mCount = count;
+ }
+
+ public int getCount() {
+ return mCount;
+ }
+
+ public T getOrCreateDrawable(int i) {
+ if (i >= mCount) {
+ throw new IllegalArgumentException("bad index: " + i);
+ }
+
+ T result = mDrawables.get(i);
+ if (result == null) {
+ result = createDivisionDrawable();
+ mDrawables.set(i, result);
+ result.setCallback(this);
+ }
+ return result;
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ final int w = bounds.width();
+ final int h = bounds.height();
+ final int mw = w / 2;
+ final int mh = h / 2;
+ switch (mCount) {
+ case 1:
+ // 1 bitmap: passthrough
+ mDrawables.get(0).setBounds(bounds);
+ break;
+ case 2:
+ // 2 bitmaps split vertically
+ mDrawables.get(0).setBounds(0, 0, mw, h);
+ mDrawables.get(1).setBounds(mw, 0, w, h);
+ break;
+ case 3:
+ // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+ mDrawables.get(0).setBounds(0, 0, mw, h);
+ mDrawables.get(1).setBounds(mw, 0, w, mh);
+ mDrawables.get(2).setBounds(mw, mh, w, h);
+ break;
+ case 4:
+ // 4 bitmaps in a 2x2 grid
+ mDrawables.get(0).setBounds(0, 0, mw, mh);
+ mDrawables.get(1).setBounds(mw, 0, w, mh);
+ mDrawables.get(2).setBounds(0, mh, mw, h);
+ mDrawables.get(3).setBounds(mw, mh, w, h);
+ break;
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ for (int i = 0; i < mCount; i++) {
+ mDrawables.get(i).draw(canvas);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ for (int i = 0; i < mCount; i++) {
+ mDrawables.get(i).setAlpha(alpha);
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ for (int i = 0; i < mCount; i++) {
+ mDrawables.get(i).setColorFilter(cf);
+ }
+ }
+
+ @Override
+ public int getOpacity() {
+ int opacity = PixelFormat.OPAQUE;
+ for (int i = 0; i < mCount; i++) {
+ if (mDrawables.get(i).getOpacity() != PixelFormat.OPAQUE) {
+ opacity = PixelFormat.TRANSLUCENT;
+ break;
+ }
+ }
+ return opacity;
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ unscheduleSelf(what);
+ }
+
+}
diff --git a/src/com/android/mail/bitmap/ImageAttachmentRequest.java b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
new file mode 100644
index 0000000..8645b48
--- /dev/null
+++ b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
@@ -0,0 +1,83 @@
+package com.android.mail.bitmap;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.android.bitmap.DecodeTask;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A request object for image attachment previews.
+ * <p>
+ * All requests are the same size to promote bitmap reuse.
+ */
+public class ImageAttachmentRequest implements DecodeTask.Request {
+ private final Context mContext;
+ private final String lookupUri;
+
+ public ImageAttachmentRequest(Context context, String lookupUri) {
+ mContext = context;
+ this.lookupUri = lookupUri;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || !(o instanceof ImageAttachmentRequest)) {
+ return false;
+ }
+ final ImageAttachmentRequest other = (ImageAttachmentRequest) o;
+ return TextUtils.equals(lookupUri, other.lookupUri);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 17;
+ hash += 31 * hash + lookupUri.hashCode();
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("[");
+ sb.append(super.toString());
+ sb.append(" uri=");
+ sb.append(lookupUri);
+ sb.append("]");
+ return sb.toString();
+ }
+
+ @Override
+ public AssetFileDescriptor createFd() throws IOException {
+ AssetFileDescriptor result = null;
+ Cursor cursor = null;
+ final ContentResolver cr = mContext.getContentResolver();
+ try {
+ cursor = cr.query(Uri.parse(lookupUri), UIProvider.ATTACHMENT_PROJECTION, null, null,
+ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Attachment a = new Attachment(cursor);
+ // TODO: rendition support
+ result = cr.openAssetFileDescriptor(a.contentUri, "r");
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public InputStream createInputStream() {
+ return null;
+ }
+
+}
diff --git a/src/com/android/mail/bitmap/Parallaxable.java b/src/com/android/mail/bitmap/Parallaxable.java
new file mode 100644
index 0000000..1e93e1a
--- /dev/null
+++ b/src/com/android/mail/bitmap/Parallaxable.java
@@ -0,0 +1,15 @@
+package com.android.mail.bitmap;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * {@link Drawable}s that support a parallax effect when drawing should
+ * implement this interface to receive the current parallax fraction to use when
+ * drawing.
+ */
+public interface Parallaxable {
+ /**
+ * @param fraction the vertical center point for the viewport, in the range [0,1]
+ */
+ void setParallaxFraction(float fraction);
+}
\ No newline at end of file
diff --git a/src/com/android/mail/bitmap/TileDrawable.java b/src/com/android/mail/bitmap/TileDrawable.java
new file mode 100644
index 0000000..af0bf05
--- /dev/null
+++ b/src/com/android/mail/bitmap/TileDrawable.java
@@ -0,0 +1,147 @@
+package com.android.mail.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A drawable that wraps another drawable and places it in the center of this space. This drawable
+ * allows a background color for the "tile", and has a fade-out transition when
+ * {@link #setVisible(boolean, boolean)} indicates that it is no longer visible.
+ */
+public class TileDrawable extends Drawable implements Drawable.Callback {
+
+ private final Paint mPaint = new Paint();
+ private final Drawable mInner;
+ private final int mInnerWidth;
+ private final int mInnerHeight;
+
+ protected final ValueAnimator mFadeOutAnimator;
+
+ public TileDrawable(Drawable inner, int innerWidth, int innerHeight,
+ int backgroundColor, int fadeOutDurationMs) {
+ mInner = inner.mutate();
+ mInnerWidth = innerWidth;
+ mInnerHeight = innerHeight;
+ mPaint.setColor(backgroundColor);
+ mInner.setCallback(this);
+
+ mFadeOutAnimator = ValueAnimator.ofInt(255, 0)
+ .setDuration(fadeOutDurationMs);
+ mFadeOutAnimator.addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setAlpha((Integer) animation.getAnimatedValue());
+ }
+ });
+
+ reset();
+ }
+
+ public void reset() {
+ setAlpha(0);
+ setVisible(false);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+
+ if (bounds.isEmpty()) {
+ mInner.setBounds(0, 0, 0, 0);
+ } else {
+ final int l = bounds.left + (bounds.width() / 2) - (mInnerWidth / 2);
+ final int t = bounds.top + (bounds.height() / 2) - (mInnerHeight / 2);
+ mInner.setBounds(l, t, l + mInnerWidth, t + mInnerHeight);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (!isVisible() && mPaint.getAlpha() == 0) {
+ return;
+ }
+ canvas.drawRect(getBounds(), mPaint);
+ mInner.draw(canvas);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int old = mPaint.getAlpha();
+ mPaint.setAlpha(alpha);
+ setInnerAlpha(alpha);
+ if (alpha != old) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ mInner.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return 0;
+ }
+
+ public boolean setVisible(boolean visible) {
+ return setVisible(visible, true /* dontcare */);
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ mInner.setVisible(visible, restart);
+ final boolean changed = super.setVisible(visible, restart);
+ if (changed) {
+ if (isVisible()) {
+ // pop in (no-op)
+ // the transition will still be smooth if the previous state's layer fades out
+ mFadeOutAnimator.cancel();
+ setAlpha(255);
+ } else {
+ // fade out
+ if (mPaint.getAlpha() == 255 && !getBounds().isEmpty()) {
+ mFadeOutAnimator.start();
+ }
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ protected boolean onLevelChange(int level) {
+ return mInner.setLevel(level);
+ }
+
+ /**
+ * Changes the alpha on just the inner wrapped drawable.
+ *
+ * TODO: maybe use another property to control the inner alpha indepedently of this wrapper's
+ * alpha.
+ */
+ public void setInnerAlpha(int alpha) {
+ mInner.setAlpha(alpha);
+ }
+
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ invalidateSelf();
+ }
+
+ @Override
+ public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ scheduleSelf(what, when);
+ }
+
+ @Override
+ public void unscheduleDrawable(Drawable who, Runnable what) {
+ unscheduleSelf(what);
+ }
+
+}
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 6f3d819..0d14180 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -37,7 +37,6 @@
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
-import android.os.SystemClock;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.SpannableString;
@@ -59,27 +58,22 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
-import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
+import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView;
import com.android.mail.R;
-import com.android.mail.R.color;
import com.android.mail.R.drawable;
import com.android.mail.R.integer;
import com.android.mail.R.string;
+import com.android.mail.bitmap.AttachmentGridDrawable;
+import com.android.mail.bitmap.ImageAttachmentRequest;
import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
import com.android.mail.perf.Timer;
-import com.android.mail.photomanager.AttachmentPreviewsManager;
-import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewsDividedImageCanvas;
-import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewsManagerCallback;
import com.android.mail.photomanager.ContactPhotoManager;
import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.AttachmentPreviewsManager.AttachmentPreviewIdentifier;
-import com.android.mail.photomanager.PhotoManager;
import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
import com.android.mail.providers.Address;
import com.android.mail.providers.Attachment;
@@ -97,7 +91,6 @@
import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
-import com.android.mail.ui.ImageCanvas;
import com.android.mail.ui.SwipeableItemView;
import com.android.mail.ui.SwipeableListView;
import com.android.mail.ui.ViewMode;
@@ -112,8 +105,8 @@
import java.util.ArrayList;
import java.util.List;
-public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem,
- InvalidateCallback, AttachmentPreviewsManagerCallback {
+public class ConversationItemView extends View
+ implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
// Timer.
private static int sLayoutCount = 0;
@@ -143,7 +136,7 @@
private static Bitmap STATE_CALENDAR_INVITE;
private static Bitmap VISIBLE_CONVERSATION_CARET;
private static Drawable RIGHT_EDGE_TABLET;
- private static Bitmap PLACEHOLDER;
+ private static Drawable PLACEHOLDER;
private static Drawable PROGRESS_BAR;
private static String sSendersSplitToken;
@@ -154,18 +147,10 @@
private static int sSendersTextColorRead;
private static int sSendersTextColorUnread;
private static int sDateTextColor;
- private static int sAttachmentPreviewsBackgroundColor;
- private static int sOverflowBadgeColor;
- private static int sOverflowTextColor;
private static int sStarTouchSlop;
private static int sSenderImageTouchSlop;
private static int sShrinkAnimationDuration;
private static int sSlideAnimationDuration;
- private static int sProgressAnimationDuration;
- private static int sFadeAnimationDuration;
- private static float sPlaceholderAnimationDurationRatio;
- private static int sProgressAnimationDelay;
- private static Interpolator sPulseAnimationInterpolator;
private static int sOverflowCountMax;
private static int sCabAnimationDuration;
@@ -174,8 +159,6 @@
private static final TextPaint sFoldersPaint = new TextPaint();
private static final Paint sCheckBackgroundPaint = new Paint();
- private static final Rect sRect = new Rect();
-
// Backgrounds for different states.
private final SparseArray<Drawable> mBackgrounds = new SparseArray<Drawable>();
@@ -187,8 +170,6 @@
private int mDateX;
private int mPaperclipX;
private int mSendersWidth;
- private int mOverflowX;
- private int mOverflowY;
/** Whether we are on a tablet device or not */
private final boolean mTabletDevice;
@@ -232,25 +213,8 @@
private static int sScrollSlop;
private static CharacterStyle sActivatedTextSpan;
- private final AttachmentPreviewsDividedImageCanvas mAttachmentPreviewsCanvas;
- private static AttachmentPreviewsManager sAttachmentPreviewsManager;
- /**
- * Animates the mAnimatedProgressFraction field to make the progress bars spin. Cancelling
- * this animator does not remove the progress bars.
- */
- private final ObjectAnimator mProgressAnimator;
- private final ObjectAnimator mFadeAnimator0;
- private final ObjectAnimator mFadeAnimator1;
- private long mProgressAnimatorCancelledTime;
- /** Range from 0.0f to 1.0f. */
- private float mAnimatedProgressFraction;
- private float mAnimatedFadeFraction0;
- private float mAnimatedFadeFraction1;
- private int[] mImageLoadStatuses = new int[0];
- private boolean mShowProgressBar;
- private final Runnable mCancelProgressAnimatorRunnable;
- private final Runnable mSetShowProgressBarRunnable0;
- private final Runnable mSetShowProgressBarRunnable1;
+ private final AttachmentGridDrawable mAttachmentsView;
+
private static final boolean CONVLIST_ATTACHMENT_PREVIEWS_ENABLED = true;
private final Matrix mPhotoFlipMatrix = new Matrix();
@@ -278,11 +242,7 @@
final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
- if (scrolling) {
- sAttachmentPreviewsManager.pause();
- } else {
- sAttachmentPreviewsManager.resume();
- }
+ // TODO: add support for yielding attachment bitmap allocation if/when needed
if (flinging) {
sContactPhotoManager.pause();
@@ -462,7 +422,7 @@
VISIBLE_CONVERSATION_CARET = BitmapFactory.decodeResource(res,
R.drawable.ic_carrot_holo);
RIGHT_EDGE_TABLET = res.getDrawable(R.drawable.list_edge_tablet);
- PLACEHOLDER = BitmapFactory.decodeResource(res, drawable.ic_attachment_load);
+ PLACEHOLDER = res.getDrawable(drawable.ic_attachment_load);
PROGRESS_BAR = res.getDrawable(drawable.progress_holo);
// Initialize colors.
@@ -479,9 +439,6 @@
sSnippetTextReadSpan =
new ForegroundColorSpan(res.getColor(R.color.snippet_text_color_read));
sDateTextColor = res.getColor(R.color.date_text_color);
- sAttachmentPreviewsBackgroundColor = res.getColor(color.ap_background_color);
- sOverflowBadgeColor = res.getColor(color.ap_overflow_badge_color);
- sOverflowTextColor = res.getColor(color.ap_overflow_text_color);
sStarTouchSlop = res.getDimensionPixelSize(R.dimen.star_touch_slop);
sSenderImageTouchSlop = res.getDimensionPixelSize(R.dimen.sender_image_touch_slop);
sShrinkAnimationDuration = res.getInteger(R.integer.shrink_animation_duration);
@@ -493,15 +450,6 @@
sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
- sAttachmentPreviewsManager = new AttachmentPreviewsManager(context);
- sProgressAnimationDuration = res.getInteger(integer.ap_progress_animation_duration);
- sFadeAnimationDuration = res.getInteger(integer.ap_fade_animation_duration);
- final int placeholderAnimationDuration = res
- .getInteger(integer.ap_placeholder_animation_duration);
- sPlaceholderAnimationDurationRatio = sProgressAnimationDuration
- / placeholderAnimationDuration;
- sProgressAnimationDelay = res.getInteger(integer.ap_progress_animation_delay);
- sPulseAnimationInterpolator = new AccelerateDecelerateInterpolator();
sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
sCabAnimationDuration =
res.getInteger(R.integer.conv_item_view_cab_anim_duration);
@@ -538,64 +486,10 @@
mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
}
});
- mAttachmentPreviewsCanvas = new AttachmentPreviewsDividedImageCanvas(context,
- new InvalidateCallback() {
- @Override
- public void invalidate() {
- if (mCoordinates == null) {
- return;
- }
- ConversationItemView.this.invalidate(
- mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
- mCoordinates.attachmentPreviewsX
- + mCoordinates.attachmentPreviewsWidth,
- mCoordinates.attachmentPreviewsY
- + mCoordinates.attachmentPreviewsHeight);
- }
- });
- mProgressAnimator = createProgressAnimator();
- mFadeAnimator0 = createFadeAnimator(0);
- mFadeAnimator1 = createFadeAnimator(1);
- mCancelProgressAnimatorRunnable = new Runnable() {
- @Override
- public void run() {
- if (mProgressAnimator.isStarted() && areAllImagesLoaded()) {
- LogUtils.v(LOG_TAG, "progress animator: << stopped");
- mProgressAnimator.cancel();
- }
- }
- };
- mSetShowProgressBarRunnable0 = new Runnable() {
- @Override
- public void run() {
- if (mImageLoadStatuses.length <= 0
- || mImageLoadStatuses[0] != PhotoManager.STATUS_LOADING) {
- return;
- }
- LogUtils.v(LOG_TAG, "progress bar 0: >>> set to true");
- mShowProgressBar = true;
- if (mFadeAnimator0.isStarted()) {
- mFadeAnimator0.cancel();
- }
- mFadeAnimator0.start();
- }
- };
- mSetShowProgressBarRunnable1 = new Runnable() {
- @Override
- public void run() {
- if (mImageLoadStatuses.length <= 1
- || mImageLoadStatuses[1] != PhotoManager.STATUS_LOADING) {
- return;
- }
- LogUtils.v(LOG_TAG, "progress bar 1: >>> set to true");
- mShowProgressBar = true;
- if (mFadeAnimator1.isStarted()) {
- mFadeAnimator1.cancel();
- }
- mFadeAnimator1.start();
- }
- };
+ mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
+ mAttachmentsView.setCallback(this);
+
Utils.traceEndSection();
}
@@ -614,7 +508,6 @@
final ConversationListListener conversationListListener,
ConversationSelectionSet set, Folder folder, int checkboxOrSenderImage,
boolean swipeEnabled, boolean priorityArrowEnabled, AnimatedAdapter adapter) {
- boolean attachmentPreviewsChanged = false;
if (mHeader != null) {
// If this was previously bound to a different conversation, remove any contact photo
// manager requests.
@@ -638,21 +531,14 @@
!= mHeader.conversation.attachmentPreviewsCount
|| !header.conversation.getAttachmentPreviewUris()
.equals(mHeader.conversation.getAttachmentPreviewUris())) {
- attachmentPreviewsChanged = true;
- ArrayList<String> divisionIds = mAttachmentPreviewsCanvas.getDivisionIds();
- if (divisionIds != null) {
- mAttachmentPreviewsCanvas.reset();
- for (int pos = 0; pos < divisionIds.size(); pos++) {
- String uri = divisionIds.get(pos);
- for (int rendition : AttachmentRendition.PREFERRED_RENDITIONS) {
- AttachmentPreviewIdentifier id = new AttachmentPreviewIdentifier(uri,
- rendition, 0, 0);
- sAttachmentPreviewsManager
- .removePhoto(AttachmentPreviewsManager.generateHash(
- mAttachmentPreviewsCanvas, id.getKey()));
- }
- }
+
+ // unbind the attachments view (releasing bitmap references)
+ // (this also cancels all async tasks)
+ for (int i = 0, len = mAttachmentsView.getCount(); i < len; i++) {
+ mAttachmentsView.getOrCreateDrawable(i).setImage(null);
}
+ // reset the grid, as the newly bound item may have a different attachment count
+ mAttachmentsView.setCount(0);
}
}
mCoordinates = null;
@@ -664,10 +550,7 @@
mStarEnabled = folder != null && !folder.isTrash();
mSwipeEnabled = swipeEnabled;
mAdapter = adapter;
- final int attachmentPreviewsSize = mHeader.conversation.getAttachmentPreviewUris().size();
- if (attachmentPreviewsChanged || mImageLoadStatuses.length != attachmentPreviewsSize) {
- mImageLoadStatuses = new int[attachmentPreviewsSize];
- }
+ mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
@@ -729,13 +612,32 @@
mConfig.showPersonalIndicator();
}
- int overflowCount = Math.min(getOverflowCount(), sOverflowCountMax);
- mHeader.overflowText = String.format(sOverflowCountFormat, overflowCount);
+ final int overflowCount = Math.min(getOverflowCount(), sOverflowCountMax);
+ mHeader.overflowText = (overflowCount > 0) ?
+ String.format(sOverflowCountFormat, overflowCount) : null;
+
+ mAttachmentsView.setOverflowText(mHeader.overflowText);
setContentDescription();
requestLayout();
}
+ @Override
+ public void invalidateDrawable(Drawable who) {
+ boolean handled = false;
+ if (mCoordinates != null) {
+ if (mAttachmentsView.equals(who)) {
+ final Rect r = new Rect(who.getBounds());
+ r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
+ ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
+ handled = true;
+ }
+ }
+ if (!handled) {
+ super.invalidateDrawable(who);
+ }
+ }
+
/**
* Get the Conversation object associated with this view.
*/
@@ -916,10 +818,6 @@
&& !mHeader.conversation.getAttachmentPreviewUris().isEmpty();
}
- private boolean getOverflowCountVisible() {
- return isAttachmentPreviewsEnabled() && getOverflowCount() > 0;
- }
-
private int getOverflowCount() {
return mHeader.conversation.attachmentPreviewsCount - mHeader.conversation
.getAttachmentPreviewUris().size();
@@ -969,9 +867,6 @@
}
private void loadAttachmentPreviews() {
- if (!isAttachmentPreviewsEnabled()) {
- return;
- }
if (mCoordinates.attachmentPreviewsWidth <= 0
|| mCoordinates.attachmentPreviewsHeight <= 0) {
LogUtils.w(LOG_TAG,
@@ -994,11 +889,22 @@
final int displayCount = Math.min(attachmentUris.size(), DividedImageCanvas.MAX_DIVISIONS);
Utils.traceEndSection();
- final List<AttachmentPreviewIdentifier> ids = Lists.newArrayListWithCapacity(displayCount);
- final List<Object> keys = Lists.newArrayListWithCapacity(displayCount);
- // First pass: Create and set the rendition on each load request
+ mAttachmentsView.setCoordinates(mCoordinates);
+ mAttachmentsView.setCount(displayCount);
+ mAttachmentsView.setDecodeWidth(mCoordinates.attachmentPreviewsWidth);
+ final int decodeHeight;
+ // if parallax is enabled, increase the desired vertical size of attachment bitmaps
+ // so we have extra pixels to scroll within
+ if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
+ decodeHeight = Math.round(mCoordinates.attachmentPreviewsDecodeHeight
+ * SwipeableListView.ATTACHMENT_PARALLAX_MULTIPLIER);
+ } else {
+ decodeHeight = mCoordinates.attachmentPreviewsDecodeHeight;
+ }
+ mAttachmentsView.setDecodeHeight(decodeHeight);
+
for (int i = 0; i < displayCount; i++) {
- Utils.traceBeginSection("finding rendition of attachment preview");
+ Utils.traceBeginSection("setup single attachment preview");
final String uri = attachmentUris.get(i);
// Find the rendition to load based on availability.
@@ -1015,164 +921,21 @@
}
}
- final AttachmentPreviewIdentifier photoIdentifier = new AttachmentPreviewIdentifier(uri,
- bestAvailableRendition, mHeader.conversation.id, i);
- ids.add(photoIdentifier);
- keys.add(photoIdentifier.getKey());
+ // TODO: support renditions
+ LogUtils.d(LOG_TAG, "creating/setting drawable region in CIV=%s canvas=%s", this,
+ mAttachmentsView);
+ mAttachmentsView.getOrCreateDrawable(i)
+ .setImage(new ImageAttachmentRequest(getContext(), uri));
+
Utils.traceEndSection();
}
- Utils.traceBeginSection("preparing divided image canvas");
- // Prepare the canvas.
- mAttachmentPreviewsCanvas.setDimensions(mCoordinates.attachmentPreviewsWidth,
+ mAttachmentsView.setBounds(0, 0, mCoordinates.attachmentPreviewsWidth,
mCoordinates.attachmentPreviewsHeight);
- mAttachmentPreviewsCanvas.setDivisionIds(keys);
- Utils.traceEndSection();
-
- // Second pass: Find the dimensions to load and start the load request
- final ImageCanvas.Dimensions canvasDimens = new ImageCanvas.Dimensions();
- for (int i = 0; i < displayCount; i++) {
- Utils.traceBeginSection("finding dimensions");
- final PhotoIdentifier photoIdentifier = ids.get(i);
- final Object key = keys.get(i);
- mAttachmentPreviewsCanvas.getDesiredDimensions(key, canvasDimens);
- Utils.traceEndSection();
-
- Utils.traceBeginSection("start animator");
- if (!mProgressAnimator.isStarted()) {
- LogUtils.v(LOG_TAG, "progress animator: >> started");
- // Reduce progress bar stutter caused by reset()/bind() being called multiple
- // times.
- final long time = SystemClock.uptimeMillis();
- final long dt = time - mProgressAnimatorCancelledTime;
- float passedFraction = 0;
- if (mProgressAnimatorCancelledTime != 0 && dt > 0) {
- mProgressAnimatorCancelledTime = 0;
- passedFraction = (float) dt / sProgressAnimationDuration % 1.0f;
- LogUtils.v(LOG_TAG, "progress animator: correction for dt %d, fraction %f",
- dt, passedFraction);
- }
- removeCallbacks(mCancelProgressAnimatorRunnable);
- mProgressAnimator.start();
- // Wow.. this must be called after start().
- mProgressAnimator.setCurrentPlayTime((long) (sProgressAnimationDuration * (
- (mAnimatedProgressFraction + passedFraction) % 1.0f)));
- }
- Utils.traceEndSection();
-
- Utils.traceBeginSection("start load");
- LogUtils.d(LOG_TAG, "loadAttachmentPreviews: start loading %s", photoIdentifier);
- sAttachmentPreviewsManager
- .loadThumbnail(photoIdentifier, mAttachmentPreviewsCanvas, canvasDimens, this);
- Utils.traceEndSection();
- }
Utils.traceEndSection();
}
- @Override
- public void onImageDrawn(final Object key, final boolean success) {
- if (mHeader == null || mHeader.conversation == null) {
- return;
- }
- Utils.traceBeginSection("on image drawn");
- final String uri = AttachmentPreviewsManager.transformKeyToUri(key);
- final int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri);
-
- LogUtils.v(LOG_TAG,
- "loadAttachmentPreviews: <= onImageDrawn callback [%b] on index %d for %s", success,
- index, key);
- // We want to hide the spinning progress bar when we draw something.
- onImageLoadStatusChanged(index,
- success ? PhotoManager.STATUS_LOADED : PhotoManager.STATUS_NOT_LOADED);
-
- if (mProgressAnimator.isStarted() && areAllImagesLoaded()) {
- removeCallbacks(mCancelProgressAnimatorRunnable);
- postDelayed(mCancelProgressAnimatorRunnable, sFadeAnimationDuration);
- }
- Utils.traceEndSection();
- }
-
- @Override
- public void onImageLoadStarted(final Object key) {
- if (mHeader == null || mHeader.conversation == null) {
- return;
- }
- final String uri = AttachmentPreviewsManager.transformKeyToUri(key);
- final int index = mHeader.conversation.getAttachmentPreviewUris().indexOf(uri);
-
- LogUtils.v(LOG_TAG,
- "loadAttachmentPreviews: <= onImageLoadStarted callback on index %d for %s", index,
- key);
- onImageLoadStatusChanged(index, PhotoManager.STATUS_LOADING);
- }
-
- private boolean areAllImagesLoaded() {
- for (int i = 0; i < mImageLoadStatuses.length; i++) {
- if (mImageLoadStatuses[i] != PhotoManager.STATUS_LOADED) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Update the #mImageLoadStatuses state array with special logic.
- * @param index Which attachment preview's state to update.
- * @param status What the new state is.
- */
- private void onImageLoadStatusChanged(final int index, final int status) {
- if (index < 0 || index >= mImageLoadStatuses.length) {
- return;
- }
- final int prevStatus = mImageLoadStatuses[index];
- if (prevStatus == status) {
- return;
- }
-
- boolean changed = false;
- switch (status) {
- case PhotoManager.STATUS_NOT_LOADED:
- LogUtils.v(LOG_TAG, "progress bar: <<< set to false");
- mShowProgressBar = false;
- // Cannot transition directly from LOADING to NOT_LOADED.
- if (prevStatus != PhotoManager.STATUS_LOADING) {
- mImageLoadStatuses[index] = status;
- changed = true;
- }
- break;
- case PhotoManager.STATUS_LOADING:
- // All other statuses must be set to not loading.
- for (int i = 0; i < mImageLoadStatuses.length; i++) {
- if (i != index && mImageLoadStatuses[i] == PhotoManager.STATUS_LOADING) {
- mImageLoadStatuses[i] = PhotoManager.STATUS_NOT_LOADED;
- }
- }
- mImageLoadStatuses[index] = status;
-
- // Progress bar should only be shown after a delay
- LogUtils.v(LOG_TAG, "progress bar: <<< set to false");
- mShowProgressBar = false;
- LogUtils.v(LOG_TAG, "progress bar: === start delay");
- final Runnable setShowProgressBarRunnable = index == 0
- ? mSetShowProgressBarRunnable0 : mSetShowProgressBarRunnable1;
- removeCallbacks(setShowProgressBarRunnable);
- postDelayed(setShowProgressBarRunnable, sProgressAnimationDelay);
- changed = true;
- break;
- case PhotoManager.STATUS_LOADED:
- mImageLoadStatuses[index] = status;
- changed = true;
- break;
- }
- if (changed) {
- final ObjectAnimator fadeAnimator = index == 0 ? mFadeAnimator0 : mFadeAnimator1;
- if (!fadeAnimator.isStarted()) {
- fadeAnimator.start();
- }
- }
- }
-
private static int makeExactSpecForSize(int size) {
return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
@@ -1315,19 +1078,6 @@
mSendersWidth = 0;
}
- String overflowText = mHeader.overflowText != null ? mHeader.overflowText : "";
- sPaint.setTextSize(mCoordinates.overflowFontSize);
- sPaint.setTypeface(mCoordinates.overflowTypeface);
-
- sPaint.getTextBounds(overflowText, 0, overflowText.length(), sRect);
-
- final int overflowWidth = (int) sPaint.measureText(overflowText);
- final int overflowHeight = sRect.height();
- mOverflowX = mCoordinates.overflowXEnd - mCoordinates.overflowDiameter / 2
- - overflowWidth / 2;
- mOverflowY = mCoordinates.overflowYEnd - mCoordinates.overflowDiameter / 2
- + overflowHeight / 2;
-
pauseTimer(PERF_TAG_CALCULATE_COORDINATES);
}
@@ -1515,6 +1265,25 @@
}
@Override
+ public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
+ final View listItemView = unwrap();
+ if (mHeader == null || mCoordinates == null || !isAttachmentPreviewsEnabled()) {
+ return;
+ }
+
+ invalidate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY,
+ mCoordinates.attachmentPreviewsX + mCoordinates.attachmentPreviewsWidth,
+ mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight);
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
protected void onDraw(Canvas canvas) {
Utils.traceBeginSection("CIVC.draw");
@@ -1606,45 +1375,6 @@
canvas.save();
drawAttachmentPreviews(canvas);
canvas.restore();
-
- // Overflow badge and count
- if (getOverflowCountVisible() && areAllImagesLoaded()) {
- final float radius = mCoordinates.overflowDiameter / 2;
- sPaint.setColor(sOverflowBadgeColor);
- canvas.drawCircle(mCoordinates.overflowXEnd - radius,
- mCoordinates.overflowYEnd - radius, radius, sPaint);
-
- sPaint.setTextSize(mCoordinates.overflowFontSize);
- sPaint.setTypeface(mCoordinates.overflowTypeface);
- sPaint.setColor(sOverflowTextColor);
- drawText(canvas, mHeader.overflowText, mOverflowX, mOverflowY, sPaint);
- }
-
- // Placeholders and progress bars
-
- // Fade from 55 -> 255 -> 55. Each cycle lasts for #sProgressAnimationDuration secs.
- final int maxAlpha = 255, minAlpha = 55;
- final int range = maxAlpha - minAlpha;
- // We want the placeholder to pulse at a different rate from the progressbar to
- // spin.
- final float placeholderAnimFraction = mAnimatedProgressFraction
- * sPlaceholderAnimationDurationRatio;
- // During the time that placeholderAnimFraction takes to go from 0 to 1, we
- // want to go all the way to #maxAlpha and back down to #minAlpha. So from 0 to 0.5,
- // we increase #modifiedProgress from 0 to 1, while from 0.5 to 1 we decrease
- // accordingly from 1 to 0. Math.
- final float modifiedProgress = -2 * Math.abs(placeholderAnimFraction - 0.5f) + 1;
- // Make it feel like a heart beat.
- final float interpolatedProgress = sPulseAnimationInterpolator
- .getInterpolation(modifiedProgress);
- // More math.
- final int pulseAlpha = (int) (interpolatedProgress * range + minAlpha);
-
- final int count = mImageLoadStatuses.length;
- for (int i = 0; i < count; i++) {
- drawPlaceholder(canvas, i, count, pulseAlpha);
- drawProgressBar(canvas, i, count);
- }
}
// right-side edge effect when in tablet conversation mode and the list is not collapsed
@@ -1766,7 +1496,18 @@
private void drawAttachmentPreviews(Canvas canvas) {
canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
- mAttachmentPreviewsCanvas.draw(canvas);
+ final float fraction;
+ if (SwipeableListView.ENABLE_ATTACHMENT_PARALLAX) {
+ final View listView = getListView();
+ final View listItemView = unwrap();
+ fraction = 1 - (float) listItemView.getBottom()
+ / (listView.getHeight() + listItemView.getHeight());
+ } else {
+ // center the preview crop around the 1/3 point from the top
+ fraction = 0.33f;
+ }
+ mAttachmentsView.setParallaxFraction(fraction);
+ mAttachmentsView.draw(canvas);
}
private void drawSubject(Canvas canvas) {
@@ -1779,168 +1520,6 @@
mSendersTextView.draw(canvas);
}
- /**
- * Draws the specified placeholder on the canvas.
- *
- * @param canvas The canvas to draw on.
- * @param index If drawing multiple placeholders, this determines which one we are drawing.
- * @param total Whether we are drawing multiple placeholders.
- * @param pulseAlpha The alpha to draw this at.
- */
- private void drawPlaceholder(final Canvas canvas, final int index, final int total,
- final int pulseAlpha) {
- final int placeholderX = getAttachmentPreviewXCenter(index, total)
- - mCoordinates.placeholderWidth / 2;
- if (placeholderX == -1) {
- return;
- }
-
- // Set alpha for crossfading effect.
- final ObjectAnimator fadeAnimator = index == 0 ? mFadeAnimator0 : mFadeAnimator1;
- final float animatedFadeFraction = index == 0 ? mAnimatedFadeFraction0
- : mAnimatedFadeFraction1;
- final boolean notLoaded = mImageLoadStatuses[index] == PhotoManager.STATUS_NOT_LOADED;
- final boolean loading = mImageLoadStatuses[index] == PhotoManager.STATUS_LOADING;
- final boolean loaded = mImageLoadStatuses[index] == PhotoManager.STATUS_LOADED;
- final float fadeAlphaFraction;
- if (notLoaded) {
- fadeAlphaFraction = 1.0f;
- } else if (loading && !mShowProgressBar) {
- fadeAlphaFraction = 1.0f;
- } else if (fadeAnimator.isStarted() && (loading && mShowProgressBar
- || loaded && !mShowProgressBar)) {
- // Fade to progress bar or fade to image from placeholder
- fadeAlphaFraction = 1.0f - animatedFadeFraction;
- } else {
- fadeAlphaFraction = 0.0f;
- }
-
- if (fadeAlphaFraction == 0.0f) {
- return;
- }
- // Draw background
- drawAttachmentPreviewBackground(canvas, index, total, (int) (255 * fadeAlphaFraction));
-
- sPaint.setAlpha((int) (pulseAlpha * fadeAlphaFraction));
- canvas.drawBitmap(PLACEHOLDER, placeholderX, mCoordinates.placeholderY, sPaint);
- }
-
- /**
- * Draws the specified progress bar on the canvas.
- * @param canvas The canvas to draw on.
- * @param index If drawing multiple progress bars, this determines which one we are drawing.
- * @param total Whether we are drawing multiple progress bars.
- */
- private void drawProgressBar(final Canvas canvas, final int index, final int total) {
- final int progressBarX = getAttachmentPreviewXCenter(index, total)
- - mCoordinates.progressBarWidth / 2;
- if (progressBarX == -1) {
- return;
- }
-
- // Set alpha for crossfading effect.
- final ObjectAnimator fadeAnimator = index == 0 ? mFadeAnimator0 : mFadeAnimator1;
- final float animatedFadeFraction = index == 0 ? mAnimatedFadeFraction0
- : mAnimatedFadeFraction1;
- final boolean loading = mImageLoadStatuses[index] == PhotoManager.STATUS_LOADING;
- final boolean loaded = mImageLoadStatuses[index] == PhotoManager.STATUS_LOADED;
- final int fadeAlpha;
- if (loading && mShowProgressBar) {
- fadeAlpha = (int) (255 * animatedFadeFraction);
- } else if (fadeAnimator.isStarted() && (loaded && mShowProgressBar)) {
- // Fade to image from progress bar
- fadeAlpha = (int) (255 * (1.0f - animatedFadeFraction));
- } else {
- fadeAlpha = 0;
- }
-
- if (fadeAlpha == 0) {
- return;
- }
-
- // Draw background
- drawAttachmentPreviewBackground(canvas, index, total, fadeAlpha);
-
- // Set the level from 0 to 10000 to animate the Drawable.
- PROGRESS_BAR.setLevel((int) (mAnimatedProgressFraction * 10000));
- // canvas.translate() for Bitmaps, setBounds() for Drawables.
- PROGRESS_BAR.setBounds(progressBarX, mCoordinates.progressBarY,
- progressBarX + mCoordinates.progressBarWidth,
- mCoordinates.progressBarY + mCoordinates.progressBarHeight);
- PROGRESS_BAR.setAlpha(fadeAlpha);
- PROGRESS_BAR.draw(canvas);
- }
- /**
- * Draws the specified attachment previews background on the canvas.
- * @param canvas The canvas to draw on.
- * @param index If drawing for multiple attachment previews, this determines for which one's
- * background we are drawing.
- * @param total Whether we are drawing for multiple attachment previews.
- * @param fadeAlpha The alpha to draw this at.
- */
- private void drawAttachmentPreviewBackground(final Canvas canvas, final int index,
- final int total, final int fadeAlpha) {
- if (total == 0) {
- return;
- }
- final int sectionWidth = mCoordinates.attachmentPreviewsWidth / total;
- final int sectionX = getAttachmentPreviewX(index, total);
- sPaint.setColor(sAttachmentPreviewsBackgroundColor);
- sPaint.setAlpha(fadeAlpha);
- canvas.drawRect(sectionX, mCoordinates.attachmentPreviewsY, sectionX + sectionWidth,
- mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight, sPaint);
- }
-
- private void invalidateAttachmentPreviews() {
- final int total = mImageLoadStatuses.length;
- for (int index = 0; index < total; index++) {
- invalidateAttachmentPreview(index, total);
- }
- }
-
- private void invalidatePlaceholdersAndProgressBars() {
- final int total = mImageLoadStatuses.length;
- for (int index = 0; index < total; index++) {
- invalidatePlaceholderAndProgressBar(index, total);
- }
- }
-
- private void invalidateAttachmentPreview(final int index, final int total) {
- invalidate(getAttachmentPreviewX(index, total), mCoordinates.attachmentPreviewsY,
- getAttachmentPreviewX(index + 1, total),
- mCoordinates.attachmentPreviewsY + mCoordinates.attachmentPreviewsHeight);
- }
-
- private void invalidatePlaceholderAndProgressBar(final int index, final int total) {
- final int width = Math.max(mCoordinates.placeholderWidth, mCoordinates.progressBarWidth);
- final int height = Math.max(mCoordinates.placeholderHeight, mCoordinates.progressBarHeight);
- final int x = getAttachmentPreviewXCenter(index, total) - width / 2;
- final int xEnd = getAttachmentPreviewXCenter(index, total) + width / 2;
- final int yCenter = mCoordinates.attachmentPreviewsY
- + mCoordinates.attachmentPreviewsHeight / 2;
- final int y = yCenter - height / 2;
- final int yEnd = yCenter + height / 2;
-
- invalidate(x, y, xEnd, yEnd);
- }
-
- private int getAttachmentPreviewX(final int index, final int total) {
- if (mCoordinates == null || total == 0) {
- return -1;
- }
- final int sectionWidth = mCoordinates.attachmentPreviewsWidth / total;
- final int sectionOffset = index * sectionWidth;
- return mCoordinates.attachmentPreviewsX + sectionOffset;
- }
-
- private int getAttachmentPreviewXCenter(final int index, final int total) {
- if (total == 0) {
- return -1;
- }
- final int sectionWidth = mCoordinates.attachmentPreviewsWidth / total;
- return getAttachmentPreviewX(index, total) + sectionWidth / 2;
- }
-
private Bitmap getStarBitmap() {
return mHeader.conversation.starred ? STAR_ON : STAR_OFF;
}
@@ -2157,9 +1736,20 @@
return handled;
}
+ private SwipeableConversationItemView unwrap() {
+ final ViewParent vp = getParent();
+ if (vp == null || !(vp instanceof SwipeableConversationItemView)) {
+ return null;
+ }
+ return (SwipeableConversationItemView) vp;
+ }
+
private SwipeableListView getListView() {
- SwipeableListView v = (SwipeableListView) ((SwipeableConversationItemView) getParent())
- .getListView();
+ SwipeableListView v = null;
+ final SwipeableConversationItemView wrapper = unwrap();
+ if (wrapper != null) {
+ v = (SwipeableListView) wrapper.getListView();
+ }
if (v == null) {
v = mAdapter.getListView();
}
@@ -2175,10 +1765,6 @@
setAlpha(1f);
setTranslationX(0f);
mAnimatedHeightFraction = 1.0f;
- if (mProgressAnimator.isStarted()) {
- removeCallbacks(mCancelProgressAnimatorRunnable);
- postDelayed(mCancelProgressAnimatorRunnable, sFadeAnimationDuration);
- }
Utils.traceEndSection();
}
@@ -2187,17 +1773,16 @@
public void setTranslationX(float translationX) {
super.setTranslationX(translationX);
- final ViewParent vp = getParent();
- if (vp == null || !(vp instanceof SwipeableConversationItemView)) {
- LogUtils.w(LOG_TAG,
- "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s",
- vp, translationX);
- }
-
// When a list item is being swiped or animated, ensure that the hosting view has a
// background color set. We only enable the background during the X-translation effect to
// reduce overdraw during normal list scrolling.
- final SwipeableConversationItemView parent = (SwipeableConversationItemView) vp;
+ final SwipeableConversationItemView parent = unwrap();
+ if (parent == null) {
+ LogUtils.w(LOG_TAG,
+ "CIV.setTranslationX unexpected ConversationItemView parent: %s x=%s",
+ getParent(), translationX);
+ }
+
if (translationX != 0f) {
parent.setBackgroundResource(R.color.swiped_bg_color);
} else {
@@ -2272,74 +1857,6 @@
requestLayout();
}
- private ObjectAnimator createProgressAnimator() {
- final ObjectAnimator animator = ObjectAnimator
- .ofFloat(this, "animatedProgressFraction", 0.0f, 1.0f).setDuration(
- sProgressAnimationDuration);
- animator.setInterpolator(new LinearInterpolator());
- animator.setRepeatCount(ObjectAnimator.INFINITE);
- animator.setRepeatMode(ObjectAnimator.RESTART);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator animation) {
- invalidateAll();
- }
-
- @Override
- public void onAnimationCancel(final Animator animation) {
- invalidateAll();
- mProgressAnimatorCancelledTime = SystemClock.uptimeMillis();
- }
-
- private void invalidateAll() {
- invalidatePlaceholdersAndProgressBars();
- }
- });
- return animator;
- }
-
- private ObjectAnimator createFadeAnimator(final int index) {
- final ObjectAnimator animator = ObjectAnimator
- .ofFloat(this, "animatedFadeFraction" + index, 0.0f, 1.0f).setDuration(
- sFadeAnimationDuration);
- animator.setInterpolator(new DecelerateInterpolator());
- animator.setRepeatCount(0);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(final Animator animation) {
- invalidateAttachmentPreview(index, mImageLoadStatuses.length);
- }
-
- @Override
- public void onAnimationCancel(final Animator animation) {
- invalidateAttachmentPreview(index, mImageLoadStatuses.length);
- }
- });
- return animator;
- }
-
- // Used by animator
- public void setAnimatedProgressFraction(final float fraction) {
- // ObjectAnimator.cancel() sets the field to 0.0f.
- if (fraction == 0.0f) {
- return;
- }
- mAnimatedProgressFraction = fraction;
- invalidatePlaceholdersAndProgressBars();
- }
-
- // Used by animator
- public void setAnimatedFadeFraction0(final float fraction) {
- mAnimatedFadeFraction0 = fraction;
- invalidateAttachmentPreview(0, mImageLoadStatuses.length);
- }
-
- // Used by animator
- public void setAnimatedFadeFraction1(final float fraction) {
- mAnimatedFadeFraction1 = fraction;
- invalidateAttachmentPreview(1, mImageLoadStatuses.length);
- }
-
@Override
public SwipeableView getSwipeableView() {
return SwipeableView.from(this);
diff --git a/src/com/android/mail/browse/ConversationItemViewCoordinates.java b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
index 6425bfc..5d496b4 100644
--- a/src/com/android/mail/browse/ConversationItemViewCoordinates.java
+++ b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
@@ -230,26 +230,27 @@
final int contactImagesY;
// Attachment previews
- final int attachmentPreviewsX;
- final int attachmentPreviewsY;
+ public final int attachmentPreviewsX;
+ public final int attachmentPreviewsY;
final int attachmentPreviewsWidth;
final int attachmentPreviewsHeight;
+ public final int attachmentPreviewsDecodeHeight;
// Attachment previews overflow badge and count
- final int overflowXEnd;
- final int overflowYEnd;
- final int overflowDiameter;
- final float overflowFontSize;
- final Typeface overflowTypeface;
+ public final int overflowXEnd;
+ public final int overflowYEnd;
+ public final int overflowDiameter;
+ public final float overflowFontSize;
+ public final Typeface overflowTypeface;
// Attachment previews placeholder
final int placeholderY;
- final int placeholderWidth;
- final int placeholderHeight;
+ public final int placeholderWidth;
+ public final int placeholderHeight;
// Attachment previews progress bar
final int progressBarY;
- final int progressBarWidth;
- final int progressBarHeight;
+ public final int progressBarWidth;
+ public final int progressBarHeight;
/**
* The smallest item width for which we use the "wide" layout.
@@ -297,6 +298,8 @@
params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode());
attachmentPreviews.setLayoutParams(params);
}
+ attachmentPreviewsDecodeHeight = getAttachmentPreviewsHeight(context,
+ ATTACHMENT_PREVIEW_UNREAD);
final TextView folders = (TextView) view.findViewById(R.id.folders);
folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index b9f2718..7396e7b 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -20,6 +20,8 @@
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.content.Context;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AbsListView;
import android.widget.FrameLayout;
import android.widget.ListView;
@@ -30,7 +32,8 @@
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
-public class SwipeableConversationItemView extends FrameLayout implements ToggleableItem {
+public class SwipeableConversationItemView extends FrameLayout
+ implements ToggleableItem, OnScrollListener {
private final ConversationItemView mConversationItemView;
@@ -87,4 +90,18 @@
mConversationItemView.toggleSelectedState();
}
}
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ int totalItemCount) {
+ if (mConversationItemView != null) {
+ mConversationItemView.onScroll(view, firstVisibleItem, visibleItemCount,
+ totalItemCount);
+ }
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
}
diff --git a/src/com/android/mail/photomanager/AttachmentPreviewsManager.java b/src/com/android/mail/photomanager/AttachmentPreviewsManager.java
deleted file mode 100644
index c3835b3..0000000
--- a/src/com/android/mail/photomanager/AttachmentPreviewsManager.java
+++ /dev/null
@@ -1,365 +0,0 @@
-package com.android.mail.photomanager;
-
-import com.android.mail.photomanager.BitmapUtil.InputStreamFactory;
-import com.android.mail.providers.Attachment;
-import com.android.mail.providers.UIProvider;
-import com.android.mail.providers.UIProvider.AttachmentRendition;
-import com.android.mail.ui.DividedImageCanvas;
-import com.android.mail.ui.ImageCanvas.Dimensions;
-import com.android.mail.utils.Utils;
-import com.google.common.base.Objects;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.util.Pair;
-
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.google.common.primitives.Longs;
-
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Asynchronously loads attachment image previews and maintains a cache of
- * photos.
- */
-public class AttachmentPreviewsManager extends PhotoManager {
-
- private static final DefaultImageProvider sDefaultImageProvider
- = new AttachmentPreviewsDefaultProvider();
- private final Map<Object, AttachmentPreviewsManagerCallback> mCallbacks;
-
- public static int generateHash(ImageCanvas view, Object key) {
- return Objects.hashCode(view, key);
- }
-
- public static String transformKeyToUri(Object key) {
- return (String) ((Pair)key).second;
- }
-
- public AttachmentPreviewsManager(Context context) {
- super(context);
- mCallbacks = new HashMap<Object, AttachmentPreviewsManagerCallback>();
- }
-
- public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view,
- final Dimensions dimensions, final AttachmentPreviewsManagerCallback callback) {
- mCallbacks.put(id.getKey(), callback);
- super.loadThumbnail(id, view, dimensions);
- }
-
- @Override
- protected DefaultImageProvider getDefaultImageProvider() {
- return sDefaultImageProvider;
- }
-
- @Override
- protected int getHash(PhotoIdentifier id, ImageCanvas view) {
- return generateHash(view, id.getKey());
- }
-
- @Override
- protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
- return new AttachmentPreviewsLoaderThread(contentResolver);
- }
-
- @Override
- protected void onImageDrawn(Request request, boolean success) {
- Object key = request.getKey();
- if (mCallbacks.containsKey(key)) {
- AttachmentPreviewsManagerCallback callback = mCallbacks.get(key);
- callback.onImageDrawn(request.getKey(), success);
-
- if (success) {
- mCallbacks.remove(key);
- }
- }
- }
-
- @Override
- protected void onImageLoadStarted(final Request request) {
- if (request == null) {
- return;
- }
- final Object key = request.getKey();
- if (mCallbacks.containsKey(key)) {
- AttachmentPreviewsManagerCallback callback = mCallbacks.get(key);
- callback.onImageLoadStarted(request.getKey());
- }
- }
-
- @Override
- protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
- float ratio = (float) newWidth / prevWidth;
- boolean previousRequestSmaller = newWidth > prevWidth
- || newWidth > prevWidth * ratio
- || newHeight > prevHeight * ratio;
- return !previousRequestSmaller;
- }
-
- public static class AttachmentPreviewIdentifier extends PhotoIdentifier {
- public final String uri;
- public final int rendition;
- // conversationId and index used for sorting requests
- long conversationId;
- public int index;
-
- /**
- * <RENDITION, URI>
- */
- private Pair<Integer, String> mKey;
-
- public AttachmentPreviewIdentifier(String uri, int rendition, long conversationId,
- int index) {
- this.uri = uri;
- this.rendition = rendition;
- this.conversationId = conversationId;
- this.index = index;
- mKey = new Pair<Integer, String>(rendition, uri) {
- @Override
- public String toString() {
- return "<" + first + ", " + second + ">";
- }
- };
- }
-
- @Override
- public boolean isValid() {
- return !TextUtils.isEmpty(uri) && rendition >= AttachmentRendition.SIMPLE;
- }
-
- @Override
- public Object getKey() {
- return mKey;
- }
-
- @Override
- public Object getKeyToShowInsteadOfDefault() {
- return new AttachmentPreviewIdentifier(uri, rendition - 1, conversationId, index)
- .getKey();
- }
-
- @Override
- public int hashCode() {
- int hash = 17;
- hash = 31 * hash + (uri != null ? uri.hashCode() : 0);
- hash = 31 * hash + rendition;
- hash = 31 * hash + Longs.hashCode(conversationId);
- hash = 31 * hash + index;
- return hash;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- AttachmentPreviewIdentifier that = (AttachmentPreviewIdentifier) o;
-
- if (rendition != that.rendition) {
- return false;
- }
- if (uri != null ? !uri.equals(that.uri) : that.uri != null) {
- return false;
- }
- if (conversationId != that.conversationId) {
- return false;
- }
- if (index != that.index) {
- return false;
- }
-
- return true;
- }
-
- @Override
- public String toString() {
- return mKey.toString();
- }
-
- @Override
- public int compareTo(PhotoIdentifier another) {
- if (another instanceof AttachmentPreviewIdentifier) {
- AttachmentPreviewIdentifier anotherId = (AttachmentPreviewIdentifier) another;
- // We want to load SIMPLE images first because they are super fast
- if (rendition - anotherId.rendition != 0) {
- return rendition - anotherId.rendition;
- }
-
- // Load images from later messages first (later messages appear on top of the list)
- if (anotherId.conversationId - conversationId != 0) {
- return (anotherId.conversationId - conversationId) > 0 ? 1 : -1;
- }
-
- // Load images from left to right
- if (index - anotherId.index != 0) {
- return index - anotherId.index;
- }
-
- return 0;
- } else {
- return -1;
- }
- }
- }
-
- protected class AttachmentPreviewsLoaderThread extends PhotoLoaderThread {
-
- public AttachmentPreviewsLoaderThread(ContentResolver resolver) {
- super(resolver);
- }
-
- @Override
- protected int getMaxBatchCount() {
- return 1;
- }
-
- @Override
- protected Map<String, BitmapHolder> loadPhotos(final Collection<Request> requests) {
- final Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(
- requests.size());
-
- LogUtils.d(TAG, "AttachmentPreviewsManager: starting batch load. Count: %d",
- requests.size());
- for (final Request request : requests) {
- Utils.traceBeginSection("Setup load photo");
- final AttachmentPreviewIdentifier id = (AttachmentPreviewIdentifier) request
- .getPhotoIdentifier();
- final Uri uri = Uri.parse(id.uri);
- // Get the attachment for this preview
- final Cursor cursor = getResolver()
- .query(uri, UIProvider.ATTACHMENT_PROJECTION, null, null, null);
- if (cursor == null) {
- Utils.traceEndSection();
- continue;
- }
- Attachment attachment = null;
- try {
- LogUtils.v(TAG, "AttachmentPreviewsManager: found %d attachments for uri %s",
- cursor.getCount(), uri);
- if (cursor.moveToFirst()) {
- attachment = new Attachment(cursor);
- }
- } finally {
- cursor.close();
- }
-
- if (attachment == null) {
- LogUtils.w(TAG, "AttachmentPreviewsManager: attachment not found for uri %s",
- uri);
- Utils.traceEndSection();
- continue;
- }
-
- // Determine whether we load the SIMPLE or BEST image for this preview
- final Uri contentUri;
- if (id.rendition == UIProvider.AttachmentRendition.BEST) {
- contentUri = attachment.contentUri;
- } else if (id.rendition == AttachmentRendition.SIMPLE) {
- contentUri = attachment.thumbnailUri;
- } else {
- LogUtils.w(TAG,
- "AttachmentPreviewsManager: Cannot load rendition %d for uri %s",
- id.rendition, uri);
- Utils.traceEndSection();
- continue;
- }
-
- LogUtils.v(TAG, "AttachmentPreviewsManager: attachments has contentUri %s",
- contentUri);
- final InputStreamFactory factory = new InputStreamFactory() {
- @Override
- public InputStream newInputStream() {
- try {
- return getResolver().openInputStream(contentUri);
- } catch (FileNotFoundException e) {
- LogUtils.e(TAG,
- "AttachmentPreviewsManager: file not found for attachment %s."
- + " This may be due to the attachment not being "
- + "downloaded yet. But this shouldn't happen because "
- + "we check the state of the attachment downloads "
- + "before attempting to load it.",
- contentUri);
- return null;
- }
- }
- };
- Utils.traceEndSection();
-
- Utils.traceBeginSection("Decode stream and crop");
- // todo:markwei read EXIF data for orientation
- // Crop it. I've seen that in real-world situations, a 5.5MB image will be
- // cropped down to about a 200KB image, so this is definitely worth it.
- final Bitmap bitmap = BitmapUtil
- .decodeStreamWithCrop(factory, request.bitmapKey.w, request.bitmapKey.h,
- 0.5f, 1.0f / 3);
- Utils.traceEndSection();
-
- if (bitmap == null) {
- LogUtils.w(TAG, "Unable to decode bitmap for contentUri %s", contentUri);
- continue;
- }
- cacheBitmap(request.bitmapKey, bitmap);
- LogUtils.d(TAG,
- "AttachmentPreviewsManager: finished loading attachment cropped size %db",
- bitmap.getByteCount());
- }
-
- return photos;
- }
- }
-
- public static class AttachmentPreviewsDividedImageCanvas extends DividedImageCanvas {
- public AttachmentPreviewsDividedImageCanvas(Context context, InvalidateCallback callback) {
- super(context, callback);
- }
-
- @Override
- protected void drawVerticalDivider(int width, int height) {
- return; // do not draw vertical dividers
- }
-
- @Override
- protected boolean isPartialBitmapComplete() {
- return true; // images may not be loaded at the same time
- }
-
- @Override
- protected String transformKeyToDivisionId(Object key) {
- return transformKeyToUri(key);
- }
- }
-
- public static class AttachmentPreviewsDefaultProvider implements DefaultImageProvider {
-
- /**
- * All we need to do is clear the section. The ConversationItemView will draw the
- * progress bar.
- */
- @Override
- public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) {
- AttachmentPreviewsDividedImageCanvas dividedImageCanvas
- = (AttachmentPreviewsDividedImageCanvas) view;
- dividedImageCanvas.clearDivisionImage(id.getKey());
- }
- }
-
- public interface AttachmentPreviewsManagerCallback {
-
- public void onImageDrawn(Object key, boolean success);
-
- public void onImageLoadStarted(Object key);
- }
-}
diff --git a/src/com/android/mail/photomanager/BitmapUtil.java b/src/com/android/mail/photomanager/BitmapUtil.java
index f0e02ad..9c2ab2b 100644
--- a/src/com/android/mail/photomanager/BitmapUtil.java
+++ b/src/com/android/mail/photomanager/BitmapUtil.java
@@ -21,8 +21,6 @@
import com.android.mail.utils.LogUtils;
-import java.io.InputStream;
-
/**
* Provides static functions to decode bitmaps at the optimal size
*/
@@ -34,71 +32,6 @@
}
/**
- * Returns Width or Height of the picture, depending on which size is
- * smaller. Doesn't actually decode the picture, so it is pretty efficient
- * to run.
- */
- public static int getSmallerExtentFromBytes(byte[] bytes) {
- final BitmapFactory.Options options = new BitmapFactory.Options();
-
- // don't actually decode the picture, just return its bounds
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
-
- // test what the best sample size is
- return Math.min(options.outWidth, options.outHeight);
- }
-
- /**
- * Finds the optimal sampleSize for loading the picture
- *
- * @param originalSmallerExtent Width or height of the picture, whichever is
- * smaller
- * @param targetExtent Width or height of the target view, whichever is
- * bigger. If either one of the parameters is 0 or smaller, no
- * sampling is applied
- */
- public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) {
- // If we don't know sizes, we can't do sampling.
- if (targetExtent < 1)
- return 1;
- if (originalSmallerExtent < 1)
- return 1;
-
- // Test what the best sample size is. To do that, we find the sample
- // size that gives us
- // the best trade-off between resulting image size and memory
- // requirement. We allow
- // the down-sampled image to be 20% smaller than the target size. That
- // way we can get around
- // unfortunate cases where e.g. a 720 picture is requested for 362 and
- // not down-sampled at
- // all. Why 20%? Why not. Prove me wrong.
- int extent = originalSmallerExtent;
- int sampleSize = 1;
- while ((extent >> 1) >= targetExtent * 0.8f) {
- sampleSize <<= 1;
- extent >>= 1;
- }
-
- return sampleSize;
- }
-
- /**
- * Decodes the bitmap with the given sample size
- */
- public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
- final BitmapFactory.Options options;
- if (sampleSize <= 1) {
- options = null;
- } else {
- options = new BitmapFactory.Options();
- options.inSampleSize = sampleSize;
- }
- return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
- }
-
- /**
* Decode an image into a Bitmap, using sub-sampling if the hinted dimensions call for it.
* Does not crop to fit the hinted dimensions.
*
@@ -124,43 +57,6 @@
return null;
}
}
- /**
- * Decode an input stream into a Bitmap, using sub-sampling if the hinted dimensions call for
- * it. Does not crop to fit the hinted dimensions.
- *
- * @param factory a factory to retrieve fresh input streams from.
- * @param w hint width in px
- * @param h hint height in px
- * @return a decoded Bitmap that is not exactly sized to the hinted dimensions.
- */
- public static Bitmap decodeStream(InputStreamFactory factory, int w, int h) {
- try {
- // calculate sample size based on w/h
- final BitmapFactory.Options opts = new BitmapFactory.Options();
- opts.inJustDecodeBounds = true;
- InputStream src = factory.newInputStream();
- BitmapFactory.decodeStream(src, null, opts);
- if (src != null) {
- src.close();
- }
-
- if (opts.mCancel || opts.outWidth == -1 || opts.outHeight == -1) {
- return null;
- }
-
- opts.inSampleSize = Math.min(opts.outWidth / w, opts.outHeight / h);
- opts.inJustDecodeBounds = false;
- src = factory.newInputStream();
- Bitmap bitmap = BitmapFactory.decodeStream(src, null, opts);
- if (src != null) {
- src.close();
- }
- return bitmap;
- } catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
- return null;
- }
- }
/**
* Decode an image into a Bitmap, using sub-sampling if the desired dimensions call for it.
@@ -183,43 +79,6 @@
}
/**
- * Decode an input stream into a Bitmap, using sub-sampling if the desired dimensions call
- * for it. Also applies a center-crop a la {@link android.widget.ImageView
- * .ScaleType#CENTER_CROP}.
- *
- * @param factory a factory to retrieve fresh input streams from.
- * @param w desired width in px
- * @param h desired height in px
- * @param horizontalCenterPercent determines which part of the src to crop from. Range from 0
- * .0f to 1.0f. The value determines which part of the src
- * maps to the horizontal center of the resulting bitmap.
- * @param verticalCenterPercent determines which part of the src to crop from. Range from 0
- * .0f to 1.0f. The value determines which part of the src maps
- * to the vertical center of the resulting bitmap.
- * @return an exactly-sized decoded Bitmap that is center-cropped.
- */
- public static Bitmap decodeStreamWithCrop(final InputStreamFactory factory, final int w,
- final int h, final float horizontalCenterPercent, final float verticalCenterPercent) {
- final Bitmap decoded;
- try {
- decoded = decodeStream(factory, w, h);
- } catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
- return null;
- }
- try {
- final Bitmap cropped = crop(decoded, w, h, horizontalCenterPercent,
- verticalCenterPercent);
- LogUtils.d(PhotoManager.TAG, "Full decoded bitmap size %d bytes, cropped size %d bytes",
- decoded.getByteCount(), cropped.getByteCount());
- return cropped;
- } catch (Throwable t) {
- LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
- return null;
- }
- }
-
- /**
* Returns a new Bitmap copy with a center-crop effect a la
* {@link android.widget.ImageView.ScaleType#CENTER_CROP}. May return the input bitmap if no
* scaling is necessary.
@@ -314,7 +173,4 @@
return cropped;
}
- public interface InputStreamFactory {
- InputStream newInputStream();
- }
}
diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java
index 0b937cb..299f7af 100644
--- a/src/com/android/mail/photomanager/PhotoManager.java
+++ b/src/com/android/mail/photomanager/PhotoManager.java
@@ -47,9 +47,6 @@
* Asynchronously loads photos and maintains a cache of photos
*/
public abstract class PhotoManager implements ComponentCallbacks2, Callback {
- public static final int STATUS_NOT_LOADED = 0;
- public static final int STATUS_LOADING = 1;
- public static final int STATUS_LOADED = 2;
/**
* Get the default image provider that draws while the photo is being
* loaded.
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 7a9a900..3d0de01 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -33,6 +33,8 @@
import android.view.ViewGroup;
import android.widget.SimpleCursorAdapter;
+import com.android.bitmap.AltBitmapCache;
+import com.android.bitmap.BitmapCache;
import com.android.mail.R;
import com.android.mail.browse.ConversationCursor;
import com.android.mail.browse.ConversationItemView;
@@ -87,6 +89,8 @@
private final Handler mHandler;
protected long mLastLeaveBehind = -1;
+ private final BitmapCache mBitmapCache;
+
public interface ConversationListListener {
/**
* @return <code>true</code> if the list is just exiting selection mode (so animations may
@@ -215,6 +219,8 @@
private static final String LOG_TAG = LogTag.getLogTag();
private static final int INCREASE_WAIT_COUNT = 2;
+ private static final int BITMAP_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+
public AnimatedAdapter(Context context, ConversationCursor cursor,
ConversationSelectionSet batch, ControllableActivity activity,
final ConversationListListener conversationListListener, SwipeableListView listView,
@@ -230,6 +236,8 @@
mListView = listView;
mFolderViews = getNestedFolders(childFolders);
+ mBitmapCache = new AltBitmapCache(BITMAP_CACHE_TARGET_SIZE_BYTES);
+
mHandler = new Handler();
if (sDismissAllShortDelay == -1) {
final Resources r = context.getResources();
@@ -1030,6 +1038,10 @@
return oldCursor;
}
+ public BitmapCache getBitmapCache() {
+ return mBitmapCache;
+ }
+
/**
* Gets the offset for the given position in the underlying cursor, based on any special views
* that may be above it.
@@ -1068,4 +1080,5 @@
specialView.onCabModeEntered();
}
}
+
}
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index e184e41..291f64d 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -58,6 +58,19 @@
*/
private final static boolean SCROLL_PAUSE_ENABLE = true;
+ /**
+ * Set to true to enable parallax effect for attachment previews as the scroll position varies.
+ * This effect triggers invalidations on scroll (!) and requires more memory for attachment
+ * preview bitmaps.
+ */
+ public static final boolean ENABLE_ATTACHMENT_PARALLAX = true;
+
+ /**
+ * The amount of extra vertical space to decode in attachment previews so we have image data to
+ * pan within. 1.0 implies no parallax effect.
+ */
+ public static final float ATTACHMENT_PARALLAX_MULTIPLIER = 1.5f;
+
private ConversationSelectionSet mConvSelectionSet;
private int mSwipeAction;
private Account mAccount;
@@ -366,8 +379,17 @@
}
@Override
- public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+ public final void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
+ if (ENABLE_ATTACHMENT_PARALLAX) {
+ for (int i = 0, len = getChildCount(); i < len; i++) {
+ final View child = getChildAt(i);
+ if (child instanceof OnScrollListener) {
+ ((OnScrollListener) child).onScroll(view, firstVisibleItem, visibleItemCount,
+ totalItemCount);
+ }
+ }
+ }
}
@Override