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
run()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 
scheduleNext(long delay)90         private void scheduleNext(long delay) {
91             removeCallbacks(mFlipper);
92             postDelayed(mFlipper, delay);
93         }
94     }
95 
PhotoCarousel(Context context, AttributeSet as)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 
lockTo180(float a)122     private float lockTo180(float a) {
123         return 180f * (float) Math.floor(a / 180f);
124     }
125 
wrap360(float a)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 
PhotoLoadTask()133         public PhotoLoadTask () {
134             mOptions = new BitmapFactory.Options();
135             mOptions.inTempStorage = new byte[32768];
136         }
137 
138         @Override
doInBackground(Void... unused)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
onPostExecute(Bitmap photo)149         public void onPostExecute(Bitmap photo) {
150             if (photo != null) {
151                 mBitmapQueue.offer(photo);
152             }
153             mFlipper.run();
154         }
155     };
156 
maybeLoadMore()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 
getBackface()175     private ImageView getBackface() {
176         return (ImageView) ((mPanel[0].getAlpha() < 0.5f) ? mPanel[0] : mPanel[1]);
177     }
178 
canFlip()179     private boolean canFlip() {
180         return mBitmapStore.containsKey(getBackface());
181     }
182 
changePhoto()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 
setScaleType(View photo)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 
flip(float sgn)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
onAttachedToWindow()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
onLayout(boolean changed, int left, int top, int right, int bottom)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
onTouchEvent(MotionEvent event)295     public boolean onTouchEvent(MotionEvent event) {
296         mGestureDetector.onTouchEvent(event);
297         return true;
298     }
299 
log(String message)300     private void log(String message) {
301         if (DEBUG) {
302             Log.i(TAG, message);
303         }
304     }
305 }
306