diff options
-rw-r--r-- | AndroidManifest.xml | 2 | ||||
-rw-r--r-- | res/layout/carousel.xml | 13 | ||||
-rw-r--r-- | res/values/config.xml | 3 | ||||
-rw-r--r-- | src/com/android/dreams/phototable/PhotoCarousel.java | 181 | ||||
-rw-r--r-- | src/com/android/dreams/phototable/PhotoSource.java | 17 |
5 files changed, 140 insertions, 76 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 412c0f7..59a2236 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -10,8 +10,8 @@ <application android:label="@string/app_name" android:icon="@mipmap/ic_launcher_phototable" - android:largeHeap="true"> android:hardwareAccelerated="true" + android:largeHeap="true"> <service android:name="PhotoTableDream" android:exported="true" android:icon="@mipmap/ic_launcher_phototable" diff --git a/res/layout/carousel.xml b/res/layout/carousel.xml index 99ed269..fddc78d 100644 --- a/res/layout/carousel.xml +++ b/res/layout/carousel.xml @@ -18,17 +18,26 @@ android:id="@+id/carousel" android:layout_width="match_parent" android:layout_height="match_parent" > - <ImageView xmlns:android="http://schemas.android.com/apk/res/android" + <ImageView android:id="@+id/front" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerInside" /> - <ImageView xmlns:android="http://schemas.android.com/apk/res/android" + <ImageView android:id="@+id/back" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerInside" android:alpha="0" /> + <ProgressBar + android:id="@+id/spinner" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginRight="40dp" + android:layout_marginBottom="40dp" + android:layout_gravity="bottom|right" + /> </com.android.dreams.phototable.PhotoCarousel> diff --git a/res/values/config.xml b/res/values/config.xml index db2b65e..d8d745d 100644 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -82,5 +82,8 @@ <!-- Size of image recycling pool when on metered data. --> <integer name="recycle_image_pool_size">20</integer> + + <!-- Number of images to pre-load. --> + <integer name="num_images_to_preload">5</integer> </resources> diff --git a/src/com/android/dreams/phototable/PhotoCarousel.java b/src/com/android/dreams/phototable/PhotoCarousel.java index 0c957ab..495e73c 100644 --- a/src/com/android/dreams/phototable/PhotoCarousel.java +++ b/src/com/android/dreams/phototable/PhotoCarousel.java @@ -31,6 +31,8 @@ import android.widget.FrameLayout; import android.widget.ImageView; import java.util.HashMap; +import java.util.LinkedList; +import java.util.ListIterator; /** * A FrameLayout that holds two photos, back to back. @@ -49,18 +51,46 @@ public class PhotoCarousel extends FrameLayout { private final BitmapFactory.Options mOptions; private final int mFlipDuration; private final int mDropPeriod; - private boolean mOnce; + private final int mBitmapQueueLimit; + private final HashMap<View, Bitmap> mBitmapStore; + private final LinkedList<Bitmap> mBitmapQueue; + private final LinkedList<PhotoLoadTask> mBitmapLoaders; + private View mSpinner; private int mOrientation; private int mWidth; private int mHeight; private int mLongSide; private int mShortSide; - private final HashMap<View, Bitmap> mBitmapStore; + private long mLastFlipTime; class Flipper implements Runnable { @Override public void run() { - PhotoCarousel.this.flip(1f); + maybeLoadMore(); + + if (mBitmapQueue.isEmpty()) { + mSpinner.setVisibility(View.VISIBLE); + } else { + mSpinner.setVisibility(View.GONE); + } + + long now = System.currentTimeMillis(); + long elapsed = now - mLastFlipTime; + + if (elapsed < mDropPeriod) { + scheduleNext((int) mDropPeriod - elapsed); + } else { + scheduleNext(mDropPeriod); + if (changePhoto() || canFlip()) { + flip(1f); + mLastFlipTime = now; + } + } + } + + private void scheduleNext(long delay) { + removeCallbacks(mFlipper); + postDelayed(mFlipper, delay); } } @@ -68,12 +98,15 @@ public class PhotoCarousel extends FrameLayout { super(context, as); final Resources resources = getResources(); mDropPeriod = resources.getInteger(R.integer.carousel_drop_period); + mBitmapQueueLimit = resources.getInteger(R.integer.num_images_to_preload); mFlipDuration = resources.getInteger(R.integer.flip_duration); mOptions = new BitmapFactory.Options(); mOptions.inTempStorage = new byte[32768]; mPhotoSource = new PhotoSourcePlexor(getContext(), getContext().getSharedPreferences(FlipperDreamSettings.PREFS_NAME, 0)); mBitmapStore = new HashMap<View, Bitmap>(); + mBitmapQueue = new LinkedList<Bitmap>(); + mBitmapLoaders = new LinkedList<PhotoLoadTask>(); mPanel = new View[2]; mFlipper = new Flipper(); @@ -97,50 +130,78 @@ public class PhotoCarousel extends FrameLayout { } private class PhotoLoadTask extends AsyncTask<Void, Void, Bitmap> { - private ImageView mDestination; - - public PhotoLoadTask(View destination) { - mDestination = (ImageView) destination; - } - @Override public Bitmap doInBackground(Void... unused) { - Bitmap decodedPhoto = mPhotoSource.next(PhotoCarousel.this.mOptions, - PhotoCarousel.this.mLongSide, PhotoCarousel.this.mShortSide); + Bitmap decodedPhoto; + if (mLongSide == 0 || mShortSide == 0) { + return null; + } + decodedPhoto = mPhotoSource.next(mOptions, mLongSide, mShortSide); return decodedPhoto; } @Override public void onPostExecute(Bitmap photo) { if (photo != null) { - Bitmap old = mBitmapStore.get(mDestination); - int width = PhotoCarousel.this.mOptions.outWidth; - int height = PhotoCarousel.this.mOptions.outHeight; - int orientation = (width > height ? LANDSCAPE : PORTRAIT); - - mDestination.setImageBitmap(photo); - mDestination.setTag(R.id.photo_orientation, new Integer(orientation)); - mDestination.setTag(R.id.photo_width, new Integer(width)); - mDestination.setTag(R.id.photo_height, new Integer(height)); - PhotoCarousel.this.setScaleType(mDestination); - - mBitmapStore.put(mDestination, photo); - if (old != null) { - old.recycle(); - } - PhotoCarousel.this.requestLayout(); - } else { - postDelayed(new Runnable() { - @Override - public void run() { - new PhotoLoadTask(mDestination) - .execute(); - } - }, 100); + mBitmapQueue.offer(photo); } + mFlipper.run(); } }; + private void maybeLoadMore() { + if (!mBitmapLoaders.isEmpty()) { + for(ListIterator<PhotoLoadTask> i = mBitmapLoaders.listIterator(0); + i.hasNext();) { + PhotoLoadTask loader = i.next(); + if (loader.getStatus() == AsyncTask.Status.FINISHED) { + i.remove(); + } + } + } + + if ((mBitmapLoaders.size() + mBitmapQueue.size()) < mBitmapQueueLimit) { + PhotoLoadTask task = new PhotoLoadTask(); + mBitmapLoaders.offer(task); + task.execute(); + } + } + + private ImageView getBackface() { + return (ImageView) ((mPanel[0].getAlpha() < 0.5f) ? mPanel[0] : mPanel[1]); + } + + private boolean canFlip() { + return mBitmapStore.containsKey(getBackface()); + } + + private boolean changePhoto() { + Bitmap photo = mBitmapQueue.poll(); + if (photo != null) { + ImageView destination = getBackface(); + Bitmap old = mBitmapStore.get(destination); + int width = mOptions.outWidth; + int height = mOptions.outHeight; + int orientation = (width > height ? LANDSCAPE : PORTRAIT); + + destination.setImageBitmap(photo); + destination.setTag(R.id.photo_orientation, new Integer(orientation)); + destination.setTag(R.id.photo_width, new Integer(width)); + destination.setTag(R.id.photo_height, new Integer(height)); + setScaleType(destination); + + mBitmapStore.put(destination, photo); + + if (old != null) { + old.recycle(); + } + + return true; + } else { + return false; + } + } + private void setScaleType(View photo) { if (photo.getTag(R.id.photo_orientation) != null) { int orientation = ((Integer) photo.getTag(R.id.photo_orientation)).intValue(); @@ -192,28 +253,24 @@ public class PhotoCarousel extends FrameLayout { ViewPropertyAnimator backAnim = mPanel[1].animate() .rotationY(backY) .alpha(backA) - .setDuration(mFlipDuration); - - int replaceIdx = 1; - ViewPropertyAnimator replaceAnim = backAnim; - if (frontA == 0f) { - replaceAnim = frontAnim; - replaceIdx = 0; - } - - final View replaceView = mPanel[replaceIdx]; - replaceAnim.withEndAction(new Runnable() { - @Override - public void run() { - new PhotoLoadTask(replaceView) - .execute(); - } - }); + .setDuration(mFlipDuration) + .withEndAction(new Runnable() { + @Override + public void run() { + maybeLoadMore(); + } + }); frontAnim.start(); backAnim.start(); + } - scheduleNext(mDropPeriod); + @Override + public void onAttachedToWindow() { + mPanel[0]= findViewById(R.id.front); + mPanel[1] = findViewById(R.id.back); + mSpinner = findViewById(R.id.spinner); + mFlipper.run(); } @Override @@ -222,20 +279,11 @@ public class PhotoCarousel extends FrameLayout { mWidth = right - left; mOrientation = (mWidth > mHeight ? LANDSCAPE : PORTRAIT); + + boolean init = mLongSide == 0; mLongSide = (int) Math.max(mWidth, mHeight); mShortSide = (int) Math.min(mWidth, mHeight); - if (!mOnce) { - mOnce = true; - - mPanel[0] = findViewById(R.id.front); - mPanel[1] = findViewById(R.id.back); - - new PhotoLoadTask(mPanel[0]).execute(); - - scheduleNext(mDropPeriod); - } - // reset scale types for new aspect ratio setScaleType(mPanel[0]); setScaleType(mPanel[1]); @@ -249,11 +297,6 @@ public class PhotoCarousel extends FrameLayout { return true; } - public void scheduleNext(int delay) { - removeCallbacks(mFlipper); - postDelayed(mFlipper, delay); - } - private void log(String message) { if (DEBUG) { Log.i(TAG, message); diff --git a/src/com/android/dreams/phototable/PhotoSource.java b/src/com/android/dreams/phototable/PhotoSource.java index 62c0f4f..1fe6194 100644 --- a/src/com/android/dreams/phototable/PhotoSource.java +++ b/src/com/android/dreams/phototable/PhotoSource.java @@ -113,15 +113,19 @@ public abstract class PhotoSource { public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) { log(TAG, "decoding a picasa resource to " + longSide + ", " + shortSide); Bitmap image = null; + ImageData imageData = null; int tries = 0; while (image == null && tries < mBadImageSkipLimit) { - if (mImageQueue.isEmpty()) { - fillQueue(); - } + synchronized(mImageQueue) { + if (mImageQueue.isEmpty()) { + fillQueue(); + } + imageData = mImageQueue.poll(); + } if (!mImageQueue.isEmpty()) { - image = load(mImageQueue.poll(), options, longSide, shortSide); + image = load(imageData, options, longSide, shortSide); } tries++; @@ -211,10 +215,15 @@ public abstract class PhotoSource { log(TAG, "Stream decoding failed with no error" + (options.mCancel ? " due to cancelation." : ".")); } + } catch (OutOfMemoryError ome) { + log(TAG, "OUT OF MEMORY: " + ome); + image = null; } catch (FileNotFoundException fnf) { log(TAG, "file not found: " + fnf); + image = null; } catch (IOException ioe) { log(TAG, "i/o exception: " + ioe); + image = null; } finally { try { if (is != null) { |