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.service.dreams.DreamService; 19import android.content.Context; 20import android.content.SharedPreferences; 21import android.content.res.Resources; 22import android.graphics.Bitmap; 23import android.graphics.BitmapFactory; 24import android.graphics.PointF; 25import android.graphics.drawable.BitmapDrawable; 26import android.graphics.drawable.Drawable; 27import android.graphics.drawable.LayerDrawable; 28import android.os.AsyncTask; 29import android.util.AttributeSet; 30import android.util.Log; 31import android.view.LayoutInflater; 32import android.view.MotionEvent; 33import android.view.View; 34import android.view.animation.DecelerateInterpolator; 35import android.view.animation.Interpolator; 36import android.widget.FrameLayout; 37import android.widget.FrameLayout.LayoutParams; 38import android.widget.ImageView; 39 40import java.util.LinkedList; 41import java.util.Random; 42 43/** 44 * A surface where photos sit. 45 */ 46public class PhotoTable extends FrameLayout { 47 private static final String TAG = "PhotoTable"; 48 private static final boolean DEBUG = false; 49 50 class Launcher implements Runnable { 51 private final PhotoTable mTable; 52 public Launcher(PhotoTable table) { 53 mTable = table; 54 } 55 56 @Override 57 public void run() { 58 mTable.scheduleNext(mDropPeriod); 59 mTable.launch(); 60 } 61 } 62 63 private static final long MAX_SELECTION_TIME = 10000L; 64 private static Random sRNG = new Random(); 65 66 private final Launcher mLauncher; 67 private final LinkedList<View> mOnTable; 68 private final int mDropPeriod; 69 private final int mFastDropPeriod; 70 private final int mNowDropDelay; 71 private final float mImageRatio; 72 private final float mTableRatio; 73 private final float mImageRotationLimit; 74 private final float mThrowRotation; 75 private final float mThrowSpeed; 76 private final boolean mTapToExit; 77 private final int mTableCapacity; 78 private final int mRedealCount; 79 private final int mInset; 80 private final PhotoSourcePlexor mPhotoSource; 81 private final Resources mResources; 82 private final Interpolator mThrowInterpolator; 83 private final Interpolator mDropInterpolator; 84 private DreamService mDream; 85 private PhotoLaunchTask mPhotoLaunchTask; 86 private boolean mStarted; 87 private boolean mIsLandscape; 88 private int mLongSide; 89 private int mShortSide; 90 private int mWidth; 91 private int mHeight; 92 private View mSelected; 93 private long mSelectedTime; 94 95 public PhotoTable(Context context, AttributeSet as) { 96 super(context, as); 97 mResources = getResources(); 98 mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset); 99 mDropPeriod = mResources.getInteger(R.integer.table_drop_period); 100 mFastDropPeriod = mResources.getInteger(R.integer.fast_drop); 101 mNowDropDelay = mResources.getInteger(R.integer.now_drop); 102 mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f; 103 mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f; 104 mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation); 105 mThrowSpeed = mResources.getDimension(R.dimen.image_throw_speed); 106 mThrowRotation = (float) mResources.getInteger(R.integer.image_throw_rotatioan); 107 mTableCapacity = mResources.getInteger(R.integer.table_capacity); 108 mRedealCount = mResources.getInteger(R.integer.redeal_count); 109 mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit); 110 mThrowInterpolator = new SoftLandingInterpolator( 111 mResources.getInteger(R.integer.soft_landing_time) / 1000000f, 112 mResources.getInteger(R.integer.soft_landing_distance) / 1000000f); 113 mDropInterpolator = new DecelerateInterpolator( 114 (float) mResources.getInteger(R.integer.drop_deceleration_exponent)); 115 mOnTable = new LinkedList<View>(); 116 mPhotoSource = new PhotoSourcePlexor(getContext(), 117 getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0)); 118 mLauncher = new Launcher(this); 119 mStarted = false; 120 } 121 122 123 public void setDream(DreamService dream) { 124 mDream = dream; 125 } 126 127 public boolean hasSelection() { 128 return mSelected != null; 129 } 130 131 public View getSelected() { 132 return mSelected; 133 } 134 135 public void clearSelection() { 136 mSelected = null; 137 } 138 139 public void setSelection(View selected) { 140 assert(selected != null); 141 if (mSelected != null) { 142 dropOnTable(mSelected); 143 } 144 mSelected = selected; 145 mSelectedTime = System.currentTimeMillis(); 146 bringChildToFront(selected); 147 pickUp(selected); 148 } 149 150 static float lerp(float a, float b, float f) { 151 return (b-a)*f + a; 152 } 153 154 static float randfrange(float a, float b) { 155 return lerp(a, b, sRNG.nextFloat()); 156 } 157 158 static PointF randFromCurve(float t, PointF[] v) { 159 PointF p = new PointF(); 160 if (v.length == 4 && t >= 0f && t <= 1f) { 161 float a = (float) Math.pow(1f-t, 3f); 162 float b = (float) Math.pow(1f-t, 2f) * t; 163 float c = (1f-t) * (float) Math.pow(t, 2f); 164 float d = (float) Math.pow(t, 3f); 165 166 p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x; 167 p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y; 168 } 169 return p; 170 } 171 172 private static PointF randInCenter(float i, float j, int width, int height) { 173 log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")"); 174 PointF p = new PointF(); 175 p.x = 0.5f * width + 0.15f * width * i; 176 p.y = 0.5f * height + 0.15f * height * j; 177 log("randInCenter returning " + p.x + "," + p.y); 178 return p; 179 } 180 181 private static PointF randMultiDrop(int n, float i, float j, int width, int height) { 182 log("randMultiDrop (" + n + "," + i + ", " + j + ", " + width + ", " + height + ")"); 183 final float[] cx = {0.3f, 0.3f, 0.5f, 0.7f, 0.7f}; 184 final float[] cy = {0.3f, 0.7f, 0.5f, 0.3f, 0.7f}; 185 n = Math.abs(n); 186 float x = cx[n % cx.length]; 187 float y = cy[n % cx.length]; 188 PointF p = new PointF(); 189 p.x = x * width + 0.05f * width * i; 190 p.y = y * height + 0.05f * height * j; 191 log("randInCenter returning " + p.x + "," + p.y); 192 return p; 193 } 194 195 @Override 196 public boolean onTouchEvent(MotionEvent event) { 197 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 198 if (hasSelection()) { 199 dropOnTable(getSelected()); 200 clearSelection(); 201 } else { 202 if (mTapToExit && mDream != null) { 203 mDream.finish(); 204 } 205 } 206 return true; 207 } 208 return false; 209 } 210 211 @Override 212 public void onLayout(boolean changed, int left, int top, int right, int bottom) { 213 super.onLayout(changed, left, top, right, bottom); 214 log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")"); 215 216 mHeight = bottom - top; 217 mWidth = right - left; 218 219 mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight)); 220 mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight)); 221 222 boolean isLandscape = mWidth > mHeight; 223 if (mIsLandscape != isLandscape) { 224 for (View photo: mOnTable) { 225 if (photo == getSelected()) { 226 pickUp(photo); 227 } else { 228 dropOnTable(photo); 229 } 230 } 231 mIsLandscape = isLandscape; 232 } 233 start(); 234 } 235 236 @Override 237 public boolean isOpaque() { 238 return true; 239 } 240 241 private class PhotoLaunchTask extends AsyncTask<Void, Void, View> { 242 private final BitmapFactory.Options mOptions; 243 244 public PhotoLaunchTask () { 245 mOptions = new BitmapFactory.Options(); 246 mOptions.inTempStorage = new byte[32768]; 247 } 248 249 @Override 250 public View doInBackground(Void... unused) { 251 log("load a new photo"); 252 final PhotoTable table = PhotoTable.this; 253 254 LayoutInflater inflater = (LayoutInflater) table.getContext() 255 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 256 View photo = inflater.inflate(R.layout.photo, null); 257 ImageView image = (ImageView) photo; 258 Drawable[] layers = new Drawable[2]; 259 Bitmap decodedPhoto = table.mPhotoSource.next(mOptions, 260 table.mLongSide, table.mShortSide); 261 int photoWidth = mOptions.outWidth; 262 int photoHeight = mOptions.outHeight; 263 if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) { 264 photo = null; 265 } else { 266 decodedPhoto.setHasMipMap(true); 267 layers[0] = new BitmapDrawable(table.mResources, decodedPhoto); 268 layers[1] = table.mResources.getDrawable(R.drawable.frame); 269 LayerDrawable layerList = new LayerDrawable(layers); 270 layerList.setLayerInset(0, table.mInset, table.mInset, 271 table.mInset, table.mInset); 272 image.setImageDrawable(layerList); 273 274 photo.setTag(R.id.photo_width, new Integer(photoWidth)); 275 photo.setTag(R.id.photo_height, new Integer(photoHeight)); 276 277 photo.setOnTouchListener(new PhotoTouchListener(table.getContext(), 278 table)); 279 } 280 281 return photo; 282 } 283 284 @Override 285 public void onPostExecute(View photo) { 286 if (photo != null) { 287 final PhotoTable table = PhotoTable.this; 288 289 table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT, 290 LayoutParams.WRAP_CONTENT)); 291 if (table.hasSelection()) { 292 table.bringChildToFront(table.getSelected()); 293 } 294 int width = ((Integer) photo.getTag(R.id.photo_width)).intValue(); 295 int height = ((Integer) photo.getTag(R.id.photo_height)).intValue(); 296 297 log("drop it"); 298 table.throwOnTable(photo); 299 300 if(table.mOnTable.size() < table.mTableCapacity) { 301 table.scheduleNext(table.mFastDropPeriod); 302 } 303 } 304 } 305 }; 306 307 public void launch() { 308 log("launching"); 309 setSystemUiVisibility(View.STATUS_BAR_HIDDEN); 310 if (hasSelection() && 311 (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) { 312 dropOnTable(getSelected()); 313 clearSelection(); 314 } else { 315 log("inflate it"); 316 if (mPhotoLaunchTask == null || 317 mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) { 318 mPhotoLaunchTask = new PhotoLaunchTask(); 319 mPhotoLaunchTask.execute(); 320 } 321 } 322 } 323 public void fadeAway(final View photo, final boolean replace) { 324 // fade out of view 325 mOnTable.remove(photo); 326 photo.animate().cancel(); 327 photo.animate() 328 .withLayer() 329 .alpha(0f) 330 .setDuration(1000) 331 .withEndAction(new Runnable() { 332 @Override 333 public void run() { 334 removeView(photo); 335 recycle(photo); 336 if (replace) { 337 scheduleNext(mNowDropDelay); 338 } 339 } 340 }); 341 } 342 343 public void moveToBackOfQueue(View photo) { 344 // make this photo the last to be removed. 345 bringChildToFront(photo); 346 invalidate(); 347 mOnTable.remove(photo); 348 mOnTable.offer(photo); 349 } 350 351 private void throwOnTable(final View photo) { 352 mOnTable.offer(photo); 353 log("start offscreen"); 354 int width = ((Integer) photo.getTag(R.id.photo_width)); 355 int height = ((Integer) photo.getTag(R.id.photo_height)); 356 photo.setRotation(mThrowRotation); 357 photo.setX(-mLongSide); 358 photo.setY(-mLongSide); 359 360 dropOnTable(photo, mThrowInterpolator); 361 } 362 363 public void dropOnTable(final View photo) { 364 dropOnTable(photo, mDropInterpolator); 365 } 366 367 public void dropOnTable(final View photo, final Interpolator interpolator) { 368 float angle = randfrange(-mImageRotationLimit, mImageRotationLimit); 369 PointF p = randMultiDrop(sRNG.nextInt(), 370 (float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(), 371 mWidth, mHeight); 372 float x = p.x; 373 float y = p.y; 374 375 log("drop it at " + x + ", " + y); 376 377 float x0 = photo.getX(); 378 float y0 = photo.getY(); 379 float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue(); 380 float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue(); 381 382 x -= mLongSide / 2f; 383 y -= mShortSide / 2f; 384 log("fixed offset is " + x + ", " + y); 385 386 float dx = x - x0; 387 float dy = y - y0; 388 389 float dist = (float) (Math.sqrt(dx * dx + dy * dy)); 390 int duration = (int) (1000f * dist / mThrowSpeed); 391 duration = Math.max(duration, 1000); 392 393 log("animate it"); 394 // toss onto table 395 photo.animate() 396 .scaleX(mTableRatio / mImageRatio) 397 .scaleY(mTableRatio / mImageRatio) 398 .rotation(angle) 399 .x(x) 400 .y(y) 401 .setDuration(duration) 402 .setInterpolator(interpolator) 403 .withEndAction(new Runnable() { 404 @Override 405 public void run() { 406 if (mOnTable.size() > mTableCapacity) { 407 while (mOnTable.size() > (mTableCapacity - mRedealCount)) { 408 fadeAway(mOnTable.poll(), false); 409 } 410 // zero delay because we already waited duration ms 411 scheduleNext(0); 412 } 413 } 414 }); 415 } 416 417 /** wrap all orientations to the interval [-180, 180). */ 418 private float wrapAngle(float angle) { 419 float result = angle + 180; 420 result = ((result % 360) + 360) % 360; // catch negative numbers 421 result -= 180; 422 return result; 423 } 424 425 private void pickUp(final View photo) { 426 float photoWidth = photo.getWidth(); 427 float photoHeight = photo.getHeight(); 428 429 float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth); 430 431 log("target it"); 432 float x = (getWidth() - photoWidth) / 2f; 433 float y = (getHeight() - photoHeight) / 2f; 434 435 float x0 = photo.getX(); 436 float y0 = photo.getY(); 437 float dx = x - x0; 438 float dy = y - y0; 439 440 float dist = (float) (Math.sqrt(dx * dx + dy * dy)); 441 int duration = (int) (1000f * dist / 600f); 442 duration = Math.max(duration, 500); 443 444 photo.setRotation(wrapAngle(photo.getRotation())); 445 446 log("animate it"); 447 // toss onto table 448 photo.animate() 449 .rotation(0f) 450 .scaleX(scale) 451 .scaleY(scale) 452 .x(x) 453 .y(y) 454 .setDuration(duration) 455 .setInterpolator(new DecelerateInterpolator(2f)) 456 .withEndAction(new Runnable() { 457 @Override 458 public void run() { 459 log("endtimes: " + photo.getX()); 460 } 461 }); 462 } 463 464 private void recycle(View photo) { 465 ImageView image = (ImageView) photo; 466 LayerDrawable layers = (LayerDrawable) image.getDrawable(); 467 BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0); 468 bitmap.getBitmap().recycle(); 469 } 470 471 public void start() { 472 if (!mStarted) { 473 log("kick it"); 474 mStarted = true; 475 scheduleNext(mDropPeriod); 476 launch(); 477 } 478 } 479 480 public void scheduleNext(int delay) { 481 removeCallbacks(mLauncher); 482 postDelayed(mLauncher, delay); 483 } 484 485 private static void log(String message) { 486 if (DEBUG) { 487 Log.i(TAG, message); 488 } 489 } 490} 491