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 */
16package com.android.dreams.phototable;
17
18import android.content.Context;
19import android.content.res.Resources;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.os.AsyncTask;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.GestureDetector;
26import android.view.MotionEvent;
27import android.view.View;
28import android.view.ViewPropertyAnimator;
29import android.widget.FrameLayout;
30import android.widget.ImageView;
31
32import java.util.HashMap;
33import java.util.LinkedList;
34import java.util.ListIterator;
35
36/**
37 * A FrameLayout that holds two photos, back to back.
38 */
39public 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