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