Home | History | Annotate | Download | only in phototable
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.dreams.phototable;
     17 
     18 import android.content.Context;
     19 import android.content.res.Resources;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.os.AsyncTask;
     23 import android.util.AttributeSet;
     24 import android.util.Log;
     25 import android.view.GestureDetector;
     26 import android.view.MotionEvent;
     27 import android.view.View;
     28 import android.view.ViewPropertyAnimator;
     29 import android.widget.FrameLayout;
     30 import android.widget.ImageView;
     31 
     32 import java.util.HashMap;
     33 import java.util.LinkedList;
     34 import java.util.ListIterator;
     35 
     36 /**
     37  * A FrameLayout that holds two photos, back to back.
     38  */
     39 public class PhotoCarousel extends FrameLayout {
     40     private static final String TAG = "PhotoCarousel";
     41     private static final boolean DEBUG = false;
     42 
     43     private static final int LANDSCAPE = 1;
     44     private static final int PORTRAIT = 2;
     45 
     46     private final Flipper mFlipper;
     47     private final PhotoSourcePlexor mPhotoSource;
     48     private final GestureDetector mGestureDetector;
     49     private final View[] mPanel;
     50     private final int mFlipDuration;
     51     private final int mDropPeriod;
     52     private final int mBitmapQueueLimit;
     53     private final HashMap<View, Bitmap> mBitmapStore;
     54     private final LinkedList<Bitmap> mBitmapQueue;
     55     private final LinkedList<PhotoLoadTask> mBitmapLoaders;
     56     private View mSpinner;
     57     private int mOrientation;
     58     private int mWidth;
     59     private int mHeight;
     60     private int mLongSide;
     61     private int mShortSide;
     62     private long mLastFlipTime;
     63 
     64     class Flipper implements Runnable {
     65         @Override
     66         public void run() {
     67             maybeLoadMore();
     68 
     69             if (mBitmapQueue.isEmpty()) {
     70                 mSpinner.setVisibility(View.VISIBLE);
     71             } else {
     72                 mSpinner.setVisibility(View.GONE);
     73             }
     74 
     75             long now = System.currentTimeMillis();
     76             long elapsed = now - mLastFlipTime;
     77 
     78             if (elapsed < mDropPeriod) {
     79                 scheduleNext((int) mDropPeriod - elapsed);
     80             } else {
     81                 scheduleNext(mDropPeriod);
     82                 if (changePhoto() ||
     83                         (elapsed > (5 * mDropPeriod) && canFlip())) {
     84                     flip(1f);
     85                     mLastFlipTime = now;
     86                 }
     87             }
     88         }
     89 
     90         private void scheduleNext(long delay) {
     91             removeCallbacks(mFlipper);
     92             postDelayed(mFlipper, delay);
     93         }
     94     }
     95 
     96     public PhotoCarousel(Context context, AttributeSet as) {
     97         super(context, as);
     98         final Resources resources = getResources();
     99         mDropPeriod = resources.getInteger(R.integer.carousel_drop_period);
    100         mBitmapQueueLimit = resources.getInteger(R.integer.num_images_to_preload);
    101         mFlipDuration = resources.getInteger(R.integer.flip_duration);
    102         mPhotoSource = new PhotoSourcePlexor(getContext(),
    103                 getContext().getSharedPreferences(FlipperDreamSettings.PREFS_NAME, 0));
    104         mBitmapStore = new HashMap<View, Bitmap>();
    105         mBitmapQueue = new LinkedList<Bitmap>();
    106         mBitmapLoaders = new LinkedList<PhotoLoadTask>();
    107 
    108         mPanel = new View[2];
    109         mFlipper = new Flipper();
    110         // this is dead code if the dream calls setInteractive(false)
    111         mGestureDetector = new GestureDetector(context,
    112                 new GestureDetector.SimpleOnGestureListener() {
    113                     @Override
    114                     public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
    115                         log("fling with " + vX);
    116                         flip(Math.signum(vX));
    117                         return true;
    118                     }
    119                 });
    120     }
    121 
    122     private float lockTo180(float a) {
    123         return 180f * (float) Math.floor(a / 180f);
    124     }
    125 
    126     private float wrap360(float a) {
    127         return a - 360f * (float) Math.floor(a / 360f);
    128     }
    129 
    130     private class PhotoLoadTask extends AsyncTask<Void, Void, Bitmap> {
    131         private final BitmapFactory.Options mOptions;
    132 
    133         public PhotoLoadTask () {
    134             mOptions = new BitmapFactory.Options();
    135             mOptions.inTempStorage = new byte[32768];
    136         }
    137 
    138         @Override
    139         public Bitmap doInBackground(Void... unused) {
    140             Bitmap decodedPhoto;
    141             if (mLongSide == 0 || mShortSide == 0) {
    142                 return null;
    143             }
    144             decodedPhoto = mPhotoSource.next(mOptions, mLongSide, mShortSide);
    145             return decodedPhoto;
    146         }
    147 
    148         @Override
    149         public void onPostExecute(Bitmap photo) {
    150             if (photo != null) {
    151                 mBitmapQueue.offer(photo);
    152             }
    153             mFlipper.run();
    154         }
    155     };
    156 
    157     private void maybeLoadMore() {
    158         if (!mBitmapLoaders.isEmpty()) {
    159             for(ListIterator<PhotoLoadTask> i = mBitmapLoaders.listIterator(0);
    160                 i.hasNext();) {
    161                 PhotoLoadTask loader = i.next();
    162                 if (loader.getStatus() == AsyncTask.Status.FINISHED) {
    163                     i.remove();
    164                 }
    165             }
    166         }
    167 
    168         if ((mBitmapLoaders.size() + mBitmapQueue.size()) < mBitmapQueueLimit) {
    169             PhotoLoadTask task = new PhotoLoadTask();
    170             mBitmapLoaders.offer(task);
    171             task.execute();
    172         }
    173     }
    174 
    175     private ImageView getBackface() {
    176         return (ImageView) ((mPanel[0].getAlpha() < 0.5f) ? mPanel[0] : mPanel[1]);
    177     }
    178 
    179     private boolean canFlip() {
    180         return mBitmapStore.containsKey(getBackface());
    181     }
    182 
    183     private boolean changePhoto() {
    184         Bitmap photo = mBitmapQueue.poll();
    185         if (photo != null) {
    186             ImageView destination = getBackface();
    187             int width = photo.getWidth();
    188             int height = photo.getHeight();
    189             int orientation = (width > height ? LANDSCAPE : PORTRAIT);
    190 
    191             destination.setImageBitmap(photo);
    192             destination.setTag(R.id.photo_orientation, Integer.valueOf(orientation));
    193             destination.setTag(R.id.photo_width, Integer.valueOf(width));
    194             destination.setTag(R.id.photo_height, Integer.valueOf(height));
    195             setScaleType(destination);
    196 
    197             Bitmap old = mBitmapStore.put(destination, photo);
    198             mPhotoSource.recycle(old);
    199 
    200             return true;
    201         } else {
    202             return false;
    203         }
    204     }
    205 
    206     private void setScaleType(View photo) {
    207         if (photo.getTag(R.id.photo_orientation) != null) {
    208             int orientation = ((Integer) photo.getTag(R.id.photo_orientation)).intValue();
    209             int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
    210             int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
    211 
    212             if (width < mWidth && height < mHeight) {
    213                 log("too small: FIT_CENTER");
    214                 ((ImageView) photo).setScaleType(ImageView.ScaleType.CENTER_CROP);
    215             } else if (orientation == mOrientation) {
    216                 log("orientations match: CENTER_CROP");
    217                 ((ImageView) photo).setScaleType(ImageView.ScaleType.CENTER_CROP);
    218             } else {
    219                 log("orientations do not match: CENTER_INSIDE");
    220                 ((ImageView) photo).setScaleType(ImageView.ScaleType.CENTER_INSIDE);
    221             }
    222         } else {
    223             log("no tag!");
    224         }
    225     }
    226 
    227     public void flip(float sgn) {
    228         mPanel[0].animate().cancel();
    229         mPanel[1].animate().cancel();
    230 
    231         float frontY = mPanel[0].getRotationY();
    232         float backY = mPanel[1].getRotationY();
    233         float frontA = mPanel[0].getAlpha();
    234         float backA = mPanel[1].getAlpha();
    235 
    236         frontY = wrap360(frontY);
    237         backY = wrap360(backY);
    238 
    239         mPanel[0].setRotationY(frontY);
    240         mPanel[1].setRotationY(backY);
    241 
    242         frontY = lockTo180(frontY + sgn * 180f);
    243         backY = lockTo180(backY + sgn * 180f);
    244         frontA = 1f - frontA;
    245         backA = 1f - backA;
    246 
    247         // Don't rotate
    248         frontY = backY = 0f;
    249 
    250         ViewPropertyAnimator frontAnim = mPanel[0].animate()
    251                 .rotationY(frontY)
    252                 .alpha(frontA)
    253                 .setDuration(mFlipDuration);
    254         ViewPropertyAnimator backAnim = mPanel[1].animate()
    255                 .rotationY(backY)
    256                 .alpha(backA)
    257                 .setDuration(mFlipDuration)
    258                 .withEndAction(new Runnable() {
    259                     @Override
    260                     public void run() {
    261                         maybeLoadMore();
    262                     }
    263                 });
    264 
    265         frontAnim.start();
    266         backAnim.start();
    267     }
    268 
    269     @Override
    270     public void onAttachedToWindow() {
    271         mPanel[0]= findViewById(R.id.front);
    272         mPanel[1] = findViewById(R.id.back);
    273         mSpinner = findViewById(R.id.spinner);
    274         mFlipper.run();
    275     }
    276 
    277     @Override
    278     public void onLayout(boolean changed, int left, int top, int right, int bottom) {
    279         mHeight = bottom - top;
    280         mWidth = right - left;
    281 
    282         mOrientation = (mWidth > mHeight ? LANDSCAPE : PORTRAIT);
    283 
    284         mLongSide = (int) Math.max(mWidth, mHeight);
    285         mShortSide = (int) Math.min(mWidth, mHeight);
    286 
    287         // reset scale types for new aspect ratio
    288         setScaleType(mPanel[0]);
    289         setScaleType(mPanel[1]);
    290 
    291         super.onLayout(changed, left, top, right, bottom);
    292     }
    293 
    294     @Override
    295     public boolean onTouchEvent(MotionEvent event) {
    296         mGestureDetector.onTouchEvent(event);
    297         return true;
    298     }
    299 
    300     private void log(String message) {
    301         if (DEBUG) {
    302             Log.i(TAG, message);
    303         }
    304     }
    305 }
    306