summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris Wren <cwren@android.com>2012-08-30 09:54:06 -0400
committerChris Wren <cwren@android.com>2012-08-30 16:38:49 -0400
commit135f525b62eb20c31c593e09f8bdb92215d538a4 (patch)
treeeed018777afcdd993646a37ff135054c9257ef5e
parent23f9b01ba3f51a33a2ba92bca7d7a53f25b1b146 (diff)
downloadandroid_packages_screensavers_PhotoTable-135f525b62eb20c31c593e09f8bdb92215d538a4.tar.gz
android_packages_screensavers_PhotoTable-135f525b62eb20c31c593e09f8bdb92215d538a4.tar.bz2
android_packages_screensavers_PhotoTable-135f525b62eb20c31c593e09f8bdb92215d538a4.zip
fix and enable flicking, also refactor
Change-Id: Iec0033370fd040cb6d650c3746ee397195c64416
-rw-r--r--res/values-sw800dp/config.xml4
-rw-r--r--res/values/config.xml11
-rw-r--r--src/com/android/dreams/phototable/PhotoTable.java762
-rw-r--r--src/com/android/dreams/phototable/PhotoTouchListener.java260
-rw-r--r--src/com/android/dreams/phototable/StockSource.java96
-rw-r--r--src/com/android/dreams/phototable/Table.java447
6 files changed, 810 insertions, 770 deletions
diff --git a/res/values-sw800dp/config.xml b/res/values-sw800dp/config.xml
index eff4c2b..adfccc1 100644
--- a/res/values-sw800dp/config.xml
+++ b/res/values-sw800dp/config.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<resources>
- <!-- Number of photos to drop when the screensaver starts.-->
- <integer name="initial_drop">5</integer>
+ <!-- Milliseconds to wait before the next fast drop.-->
+ <integer name="fast_drop">4000</integer>
<!-- Parts per million ratio between image size and screen size. -->
<integer name="image_ratio">500000</integer>
diff --git a/res/values/config.xml b/res/values/config.xml
index c510c2e..1a2e340 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -14,12 +14,11 @@
limitations under the License.
-->
<resources>
- <!-- Milliseconds between drops, needs to be tuned to slide_duration
- to control the number of photos on the table at any given time. -->
+ <!-- Milliseconds between drops. -->
<integer name="drop_period">30000</integer>
- <!-- Number of photos to drop when the screensaver starts.-->
- <integer name="initial_drop">10</integer>
+ <!-- Milliseconds to wait before the next fast drop.-->
+ <integer name="fast_drop">2000</integer>
<!-- Maximum number of photos to leave on the table.-->
<integer name="table_capacity">10</integer>
@@ -40,12 +39,12 @@
<bool name="enable_manual_image_rotation">false</bool>
<!-- Enable flinging away photos. -->
- <bool name="enable_fling">false</bool>
+ <bool name="enable_fling">true</bool>
<!-- Honor tap on table to exit. -->
<bool name="enable_tap_to_exit">false</bool>
<!-- Parts per million damping coefficient of the table. -->
- <integer name="table_damping">500000</integer>
+ <integer name="table_damping">950000</integer>
</resources>
diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java
index 9066396..9f67254 100644
--- a/src/com/android/dreams/phototable/PhotoTable.java
+++ b/src/com/android/dreams/phototable/PhotoTable.java
@@ -15,776 +15,15 @@
*/
package com.android.dreams.phototable;
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
import android.service.dreams.Dream;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.AsyncTask;
-import android.os.PowerManager;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.GestureDetector;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewPropertyAnimator;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.FrameLayout;
-import android.widget.FrameLayout.LayoutParams;
-import android.widget.ImageView;
-
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.Random;
/**
* Example interactive screen saver.
*/
public class PhotoTable extends Dream {
private static final String TAG = "PhotoTable";
- private static final boolean DEBUG = false;
- private static final int[] PHOTOS = {R.drawable.photo_044_002,
- R.drawable.photo_039_002,
- R.drawable.photo_059_003,
- R.drawable.photo_070_004,
- R.drawable.photo_072_001,
- R.drawable.photo_077_002,
- R.drawable.photo_098_002,
- R.drawable.photo_119_003,
- R.drawable.photo_119_004,
- R.drawable.photo_126_001,
- R.drawable.photo_147_002,
- R.drawable.photo_175_004
- };
-
private Table mTable;
- static class PhotoTouchListener implements View.OnTouchListener,
- GestureDetector.OnGestureListener {
- private static final int INVALID_POINTER = -1;
- private static final int MAX_POINTER_COUNT = 10;
- private final int mTouchSlop;
- private final int mTapTimeout;
- private final Table mTable;
- private final GestureDetector mDetector;
- private final float mBeta;
- private final float mTableRatio;
- private final boolean mEnableFling;
- private View mTarget;
- private float mInitialTouchX;
- private float mInitialTouchY;
- private float mInitialTouchA;
- private long mInitialTouchTime;
- private float mInitialTargetX;
- private float mInitialTargetY;
- private float mInitialTargetA;
- private int mA = INVALID_POINTER;
- private int mB = INVALID_POINTER;
- private float[] pts = new float[MAX_POINTER_COUNT];
-
- public PhotoTouchListener(Context context, Table table) {
- mTable = table;
- mDetector = new GestureDetector(context, this);
- final ViewConfiguration configuration = ViewConfiguration.get(context);
- mTouchSlop = configuration.getScaledTouchSlop();
- mTapTimeout = configuration.getTapTimeout();
- final Resources resources = context.getResources();
- mBeta = resources.getInteger(R.integer.table_damping) / 1000000f;
- mTableRatio = resources.getInteger(R.integer.table_ratio) / 1000000f;
- mEnableFling = resources.getBoolean(R.bool.enable_fling);
- }
-
- /** Get angle defined by first two touches, in degrees */
- private float getAngle(View target, MotionEvent ev) {
- float alpha = 0f;
- int a = ev.findPointerIndex(mA);
- int b = ev.findPointerIndex(mB);
- if (a >=0 && b >=0) {
- alpha = (float) (Math.atan2(pts[2*a + 1] - pts[2*b + 1],
- pts[2*a] - pts[2*b]) *
- 180f / Math.PI);
- }
- return alpha;
- }
-
- private void resetTouch(View target) {
- mInitialTouchX = -1;
- mInitialTouchY = -1;
- mInitialTouchA = 0f;
- mInitialTargetX = (float) target.getX();
- mInitialTargetY = (float) target.getY();
- mInitialTargetA = (float) target.getRotation();
- }
-
- @Override
- public boolean onDown(MotionEvent e) {
- return false;
- }
-
- @Override
- public void onLongPress(MotionEvent e) {
- }
-
- @Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float deltaX, float deltaY) {
- return false;
- }
-
- @Override
- public void onShowPress(MotionEvent e) {
- }
-
- @Override
- public boolean onSingleTapUp(MotionEvent e) {
- return false;
- }
-
- @Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float dX, float dY) {
- if (!mEnableFling) {
- return false;
- }
- pts[0] = dX;
- pts[1] = dY;
- mTarget.getMatrix().mapVectors(pts);
- // velocity components in global coordinate frame
- dX = - pts[0];
- dY = - pts[1];
-
- if (DEBUG) {
- Log.i(TAG, "fling " + dX + ", " + dY);
- }
-
- final int idx = e2.getActionIndex();
- pts[0] = e2.getX(idx);
- pts[1] = e2.getY(idx);
- mTarget.getMatrix().mapPoints(pts);
- // starting position compionents in global corrdinate frame
- final float x0 = pts[0];
- final float y0 = pts[1];
-
- // velocity
- final float v = (float) Math.hypot(dX, dY);
- // number of steps to come to a stop
- final float n = (float) (- Math.log(v) / Math.log(mBeta));
- // distance travelled before stopping
- final float s = (float) (v * (1f - Math.pow(mBeta, n)) / (1f - mBeta));
-
- // ending posiiton after stopping
- final float x1 = x0 + s * dX / v;
- final float y1 = y0 + s * dY / v;
-
- if (DEBUG) {
- Log.i(TAG, "fling v = " + v);
- Log.i(TAG, "fling n = " + n);
- Log.i(TAG, "fling s = " + n);
- Log.i(TAG, "fling x0 = " + x0);
- Log.i(TAG, "fling y0 = " + y0);
- Log.i(TAG, "fling x1 = " + x1);
- Log.i(TAG, "fling y1 = " + y1);
- }
-
- final float photoWidth = ((Integer) mTarget.getTag(R.id.photo_width)).floatValue();
- final float photoHeight = ((Integer) mTarget.getTag(R.id.photo_height)).floatValue();
- final float tableWidth = mTable.getWidth();
- final float tableHeight = mTable.getHeight();
-
- pts[0] = 0f;
- pts[1] = 0f;
- pts[2] = photoHeight;
- pts[3] = photoWidth;
- mTarget.getMatrix().mapPoints(pts);
- pts[0] += x1;
- pts[1] += y1;
- pts[2] += x1;
- pts[3] += y1;
-
- boolean xOut = true;
- boolean yOut = true;
- for (int i = 0; i < 2; i++) {
- if(pts[2 * i] >= 0f && pts[2 * i] < tableWidth) {
- xOut = false;
- if (DEBUG) {
- Log.i(TAG, "fling x in: " + pts[2 * i]);
- }
- }
- if(pts[2 * i + 1] >= 0f && pts[2 * i + 1] < tableHeight) {
- yOut = false;
- if (DEBUG) {
- Log.i(TAG, "fling y in: " + pts[2 * i + 1]);
- }
- }
- }
- final View photo = mTarget;
- ViewPropertyAnimator animator = photo.animate()
- .withLayer()
- .x(x1)
- .y(y1)
- .setDuration((int) (100f * n));
-
- if (xOut || yOut) {
- if (DEBUG) {
- Log.i(TAG, "fling away");
- }
- animator.withEndAction(new Runnable() {
- @Override
- public void run() {
- mTable.fadeAway(photo);
- mTable.launch();
- }
- });
- }
-
- return true;
- }
-
- @Override
- public boolean onTouch(View target, MotionEvent ev) {
- mTarget = target;
- if (mDetector.onTouchEvent(ev)) {
- return true;
- }
- final int action = ev.getActionMasked();
-
- // compute raw coordinates
- for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) {
- pts[i*2] = ev.getX(i);
- pts[i*2 + 1] = ev.getY(i);
- }
- target.getMatrix().mapPoints(pts);
-
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- mTable.moveToBackOfQueue(target);
- mInitialTouchTime = ev.getEventTime();
- mA = ev.getPointerId(ev.getActionIndex());
- resetTouch(target);
- break;
-
- case MotionEvent.ACTION_POINTER_DOWN:
- if (mB == INVALID_POINTER) {
- mB = ev.getPointerId(ev.getActionIndex());
- mInitialTouchA = getAngle(target, ev);
- }
- break;
-
- case MotionEvent.ACTION_POINTER_UP:
- if (mB == ev.getPointerId(ev.getActionIndex())) {
- mB = INVALID_POINTER;
- mInitialTargetA = (float) target.getRotation();
- }
- if (mA == ev.getPointerId(ev.getActionIndex())) {
- mA = mB;
- resetTouch(target);
- mB = INVALID_POINTER;
- }
- break;
-
- case MotionEvent.ACTION_MOVE: {
- if (mA != INVALID_POINTER) {
- int idx = ev.findPointerIndex(mA);
- float x = pts[2 * idx];
- float y = pts[2 * idx + 1];
- if (mInitialTouchX == -1 && mInitialTouchY == -1) {
- mInitialTouchX = x;
- mInitialTouchY = y;
- }
- if (mTable.getSelected() != target) {
- target.animate().cancel();
-
- target.setX((int) (mInitialTargetX + x - mInitialTouchX));
- target.setY((int) (mInitialTargetY + y - mInitialTouchY));
- if (mTable.mManualImageRotation && mB != INVALID_POINTER) {
- float a = getAngle(target, ev);
- target.setRotation(
- (int) (mInitialTargetA + a - mInitialTouchA));
- }
- }
- }
- }
- break;
-
- case MotionEvent.ACTION_UP: {
- if (mA != INVALID_POINTER) {
- int idx = ev.findPointerIndex(mA);
- float x = pts[2 * idx];
- float y = pts[2 * idx + 1];
- if (mInitialTouchX == -1 && mInitialTouchY == -1) {
- mInitialTouchX = x;
- mInitialTouchY = y;
- }
- double distance = Math.hypot(x - mInitialTouchX,
- y - mInitialTouchY);
- if (mTable.getSelected() == target) {
- mTable.dropOnTable(target);
- mTable.clearSelection();
- } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout &&
- distance < mTouchSlop) {
- // tap
- target.animate().cancel();
- mTable.setSelection(target);
- }
- mA = INVALID_POINTER;
- mB = INVALID_POINTER;
- }
- }
- break;
-
- case MotionEvent.ACTION_CANCEL:
- break;
- }
- return true;
- }
- }
-
- public static class Table extends FrameLayout {
- class Launcher implements Runnable {
- private final Table mTable;
- public Launcher(Table table) {
- mTable = table;
- }
-
- @Override
- public void run() {
- mTable.launch();
- }
- }
-
- private static final long MAX_SELECTION_TIME = 10000L;
- private static Random sRNG = new Random();
-
- private final Launcher mLauncher;
- private final LinkedList<View> mOnTable;
- private final Dream mDream;
- private final int mDropPeriod;
- private final float mImageRatio;
- private final float mTableRatio;
- private final float mImageRotationLimit;
- private final boolean mManualImageRotation;
- private final boolean mTapToExit;
- private final int mTableCapacity;
- private final int mInset;
- private final LocalSource mLocalSource;
- private final Resources mResources;
- private boolean mStarted;
- private boolean mIsLandscape;
- private BitmapFactory.Options mOptions;
- private int mLongSide;
- private int mShortSide;
- private int mWidth;
- private int mHeight;
- private View mSelected;
- private long mSelectedTime;
-
- public Table(Dream dream, AttributeSet as) {
- super(dream, as);
- mDream = dream;
- mResources = getResources();
- setBackground(mResources.getDrawable(R.drawable.table));
- mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset);
- mDropPeriod = mResources.getInteger(R.integer.drop_period);
- mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f;
- mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f;
- mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation);
- mTableCapacity = mResources.getInteger(R.integer.table_capacity);
- mManualImageRotation = mResources.getBoolean(R.bool.enable_manual_image_rotation);
- mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
- mOnTable = new LinkedList<View>();
- mOptions = new BitmapFactory.Options();
- mOptions.inTempStorage = new byte[32768];
- mLocalSource = new LocalSource(getContext());
- mLauncher = new Launcher(this);
- mStarted = false;
- }
-
- public boolean hasSelection() {
- return mSelected != null;
- }
-
- public View getSelected() {
- return mSelected;
- }
-
- public void clearSelection() {
- mSelected = null;
- }
-
- public void setSelection(View selected) {
- assert(selected != null);
- if (mSelected != null) {
- dropOnTable(mSelected);
- }
- mSelected = selected;
- mSelectedTime = System.currentTimeMillis();
- bringChildToFront(selected);
- pickUp(selected);
- }
-
- static float lerp(float a, float b, float f) {
- return (b-a)*f + a;
- }
-
- static float randfrange(float a, float b) {
- return lerp(a, b, sRNG.nextFloat());
- }
-
- static PointF randFromCurve(float t, PointF[] v) {
- PointF p = new PointF();
- if (v.length == 4 && t >= 0f && t <= 1f) {
- float a = (float) Math.pow(1f-t, 3f);
- float b = (float) Math.pow(1f-t, 2f) * t;
- float c = (1f-t) * (float) Math.pow(t, 2f);
- float d = (float) Math.pow(t, 3f);
-
- p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x;
- p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y;
- }
- return p;
- }
-
- private static PointF randInCenter(float i, float j, int width, int height) {
- log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")");
- PointF p = new PointF();
- p.x = 0.5f * width + 0.15f * width * i;
- p.y = 0.5f * height + 0.15f * height * j;
- log("randInCenter returning " + p.x + "," + p.y);
- return p;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- if (hasSelection()) {
- dropOnTable(getSelected());
- clearSelection();
- } else {
- if (mTapToExit) {
- mDream.finish();
- }
- }
- return true;
- }
- return false;
- }
-
- @Override
- public void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")");
-
- mHeight = bottom - top;
- mWidth = right - left;
-
- mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight));
- mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight));
-
- boolean isLandscape = mWidth > mHeight;
- if (mIsLandscape != isLandscape) {
- for (View photo: mOnTable) {
- if (photo == getSelected()) {
- pickUp(photo);
- } else {
- dropOnTable(photo);
- }
- }
- mIsLandscape = isLandscape;
- }
- start();
- }
-
- @Override
- public boolean isOpaque() {
- return true;
- }
-
- @SuppressWarnings("deprecation")
- private void launch() {
- scheduleNext();
-
- log("launching");
- setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
- if (hasSelection() &&
- (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
- dropOnTable(getSelected());
- clearSelection();
- } else {
- log("inflate it");
- AsyncTask<Void, Void, View> task = new AsyncTask<Void, Void, View>() {
- @Override
- public View doInBackground(Void... unused) {
- log("load a new photo");
- LayoutInflater inflater = (LayoutInflater) getContext()
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- View photo = inflater.inflate(R.layout.photo, null);
- ImageView image = (ImageView) photo;
- Drawable[] layers = new Drawable[2];
- Bitmap decodedPhoto = null;
- decodedPhoto = mLocalSource.next(mOptions, mLongSide, mShortSide);
- if (decodedPhoto == null) {
- decodedPhoto = nextStockPhoto(mOptions, mLongSide, mShortSide);
- }
- int photoWidth = mOptions.outWidth;
- int photoHeight = mOptions.outHeight;
- if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) {
- photo = null;
- } else {
- layers[0] = new BitmapDrawable(mResources, decodedPhoto);
- layers[1] = mResources.getDrawable(R.drawable.frame);
- LayerDrawable layerList = new LayerDrawable(layers);
- layerList.setLayerInset(0, mInset, mInset, mInset, mInset);
- image.setImageDrawable(layerList);
-
- photo.setTag(R.id.photo_width, new Integer(photoWidth));
- photo.setTag(R.id.photo_height, new Integer(photoHeight));
- }
-
- return photo;
- }
-
- @Override
- public void onPostExecute(View photo) {
- if (photo != null) {
- addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT));
- if (hasSelection()) {
- bringChildToFront(getSelected());
- }
- int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
- int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
-
- log("drop it");
- throwOnTable(photo);
- }
- }
- };
- task.execute();
- }
- }
-
- private Bitmap nextStockPhoto(BitmapFactory.Options options, int longSide, int shortSide) {
- log("decoding a local resource to " + longSide + ", " + shortSide);
- int photo = PHOTOS[Math.abs(sRNG.nextInt() % PHOTOS.length)];
-
- options.inJustDecodeBounds = true;
- options.inSampleSize = 1;
- BitmapFactory.decodeResource(mResources, photo, options);
- int rawLongSide = Math.max(options.outWidth, options.outHeight);
- int rawShortSide = Math.max(options.outWidth, options.outHeight);
- log("I see bounds of " + rawLongSide + ", " + rawShortSide);
- float ratio = Math.min((float) longSide / (float) rawLongSide,
- (float) shortSide / (float) rawShortSide);
- while (ratio < 0.5) {
- options.inSampleSize *= 2;
- ratio *= 2;
- }
- log("decoding with inSampleSize " + options.inSampleSize);
- options.inJustDecodeBounds = false;
- Bitmap bitmap = BitmapFactory.decodeResource(mResources, photo, options);
- rawLongSide = Math.max(options.outWidth, options.outHeight);
- rawShortSide = Math.max(options.outWidth, options.outHeight);
- ratio = Math.min((float) longSide / (float) rawLongSide,
- (float) shortSide / (float) rawShortSide);
-
- if (ratio < 1.0f) {
- log("still too big, scaling down by " + ratio);
- int photoWidth = (int) (ratio * options.outWidth);
- int photoHeight = (int) (ratio * options.outHeight);
- bitmap = Bitmap.createScaledBitmap(bitmap, photoWidth, photoHeight, true);
- }
-
- log("returning bitmap sized to " + bitmap.getWidth() + ", " + bitmap.getHeight());
- return bitmap;
- }
-
- private void fadeAway(final View photo) {
- // fade out of view
- photo.animate().cancel();
- photo.animate()
- .withLayer()
- .alpha(0f)
- .setDuration(1000)
- .withEndAction(new Runnable() {
- @Override
- public void run() {
- removeView(photo);
- recycle(photo);
- }
- });
- }
-
- private void moveToBackOfQueue(View photo) {
- // make this photo the last to be removed.
- bringChildToFront(photo);
- invalidate();
- mOnTable.remove(photo);
- mOnTable.offer(photo);
- }
-
- private void throwOnTable(final View photo) {
- mOnTable.offer(photo);
- log("start offscreen");
- int width = ((Integer) photo.getTag(R.id.photo_width));
- int height = ((Integer) photo.getTag(R.id.photo_height));
- photo.setRotation(-100.0f);
- photo.setX(-mLongSide);
- photo.setY(-mLongSide);
- dropOnTable(photo);
- }
-
- private void dropOnTable(final View photo) {
- float angle = randfrange(-mImageRotationLimit, mImageRotationLimit);
- PointF p = randInCenter((float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(),
- mWidth, mHeight);
- float x = p.x;
- float y = p.y;
-
- log("drop it at " + x + ", " + y);
-
- float x0 = photo.getX();
- float y0 = photo.getY();
- float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
- float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
-
- x -= mTableRatio * mLongSide / 2f;
- y -= mTableRatio * mLongSide / 2f;
- log("fixed offset is " + x + ", " + y);
-
- float dx = x - x0;
- float dy = y - y0;
-
- float dist = (float) (Math.sqrt(dx * dx + dy * dy));
- int duration = (int) (1000f * dist / 400f);
- duration = Math.max(duration, 1000);
-
- log("animate it");
- // toss onto table
- photo.animate()
- .withLayer()
- .scaleX(mTableRatio / mImageRatio)
- .scaleY(mTableRatio / mImageRatio)
- .rotation(angle)
- .x(x)
- .y(y)
- .setDuration(duration)
- .setInterpolator(new DecelerateInterpolator())
- .withEndAction(new Runnable() {
- @Override
- public void run() {
- while (mOnTable.size() > mTableCapacity) {
- fadeAway(mOnTable.poll());
- }
- }
- });
-
- photo.setOnTouchListener(new PhotoTouchListener(getContext(), this));
- }
-
- /** wrap all orientations to the interval [-180, 180). */
- private float wrapAngle(float angle) {
- float result = angle + 180;
- result = ((result % 360) + 360) % 360; // catch negative numbers
- result -= 180;
- return result;
- }
-
- private void pickUp(final View photo) {
- float photoWidth = photo.getWidth();
- float photoHeight = photo.getHeight();
-
- float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
-
- log("target it");
- float x = (getWidth() - photoWidth) / 2f;
- float y = (getHeight() - photoHeight) / 2f;
-
- float x0 = photo.getX();
- float y0 = photo.getY();
- float dx = x - x0;
- float dy = y - y0;
-
- float dist = (float) (Math.sqrt(dx * dx + dy * dy));
- int duration = (int) (1000f * dist / 1000f);
- duration = Math.max(duration, 500);
-
- photo.setRotation(wrapAngle(photo.getRotation()));
-
- log("animate it");
- // toss onto table
- photo.animate()
- .withLayer()
- .rotation(0f)
- .scaleX(scale)
- .scaleY(scale)
- .x(x)
- .y(y)
- .setDuration(duration)
- .setInterpolator(new DecelerateInterpolator())
- .withEndAction(new Runnable() {
- @Override
- public void run() {
- log("endtimes: " + photo.getX());
- }
- });
- }
-
- private static void log(String message) {
- if (DEBUG) {
- Log.i(TAG, message);
- }
- }
-
- private void recycle(View photo) {
- ImageView image = (ImageView) photo;
- LayerDrawable layers = (LayerDrawable) image.getDrawable();
- BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0);
- bitmap.getBitmap().recycle();
- }
-
- public void recycleAll() {
- new AsyncTask<Void, Void, Void>() {
- @Override
- public void onPreExecute() {
- for (View photo: mOnTable) {
- removeView(photo);
- }
- }
-
- @Override
- public Void doInBackground(Void... unused) {
- while (!mOnTable.isEmpty()) {
- recycle(mOnTable.poll());
- }
- return null;
- }
- }.execute();
- }
-
- public void start() {
- if (!mStarted) {
- log("kick it");
- mStarted = true;
- for (int i = 0; i < mResources.getInteger(R.integer.initial_drop); i++) {
- launch();
- }
- }
- }
-
- public void scheduleNext() {
- removeCallbacks(mLauncher);
- postDelayed(mLauncher, mDropPeriod);
- }
- }
-
@Override
public void onStart() {
super.onStart();
@@ -796,7 +35,6 @@ public class PhotoTable extends Dream {
@Override
public void onDestroy() {
- mTable.recycleAll();
super.onDestroy();
}
}
diff --git a/src/com/android/dreams/phototable/PhotoTouchListener.java b/src/com/android/dreams/phototable/PhotoTouchListener.java
new file mode 100644
index 0000000..488bc13
--- /dev/null
+++ b/src/com/android/dreams/phototable/PhotoTouchListener.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dreams.phototable;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * Touch listener that implements phototable interactions.
+ */
+public class PhotoTouchListener implements View.OnTouchListener {
+ private static final String TAG = "PhotoTouchListener";
+ private static final boolean DEBUG = true;
+ private static final int INVALID_POINTER = -1;
+ private static final int MAX_POINTER_COUNT = 10;
+ private final int mTouchSlop;
+ private final int mTapTimeout;
+ private final Table mTable;
+ private final float mBeta;
+ private final float mTableRatio;
+ private final boolean mEnableFling;
+ private final boolean mManualImageRotation;
+ private long mLastEventTime;
+ private float mLastTouchX;
+ private float mLastTouchY;
+ private float mInitialTouchX;
+ private float mInitialTouchY;
+ private float mInitialTouchA;
+ private long mInitialTouchTime;
+ private float mInitialTargetX;
+ private float mInitialTargetY;
+ private float mInitialTargetA;
+ private float mDX;
+ private float mDY;
+ private int mA = INVALID_POINTER;
+ private int mB = INVALID_POINTER;
+ private float[] pts = new float[MAX_POINTER_COUNT];
+ private float[] tmp = new float[MAX_POINTER_COUNT];
+
+ public PhotoTouchListener(Context context, Table table) {
+ mTable = table;
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ mTouchSlop = configuration.getScaledTouchSlop();
+ mTapTimeout = configuration.getTapTimeout();
+ final Resources resources = context.getResources();
+ mBeta = resources.getInteger(R.integer.table_damping) / 1000000f;
+ mTableRatio = resources.getInteger(R.integer.table_ratio) / 1000000f;
+ mEnableFling = resources.getBoolean(R.bool.enable_fling);
+ mManualImageRotation = resources.getBoolean(R.bool.enable_manual_image_rotation);
+ }
+
+ /** Get angle defined by first two touches, in degrees */
+ private float getAngle(View target, MotionEvent ev) {
+ float alpha = 0f;
+ int a = ev.findPointerIndex(mA);
+ int b = ev.findPointerIndex(mB);
+ if (a >=0 && b >=0) {
+ alpha = (float) (Math.atan2(pts[2*a + 1] - pts[2*b + 1],
+ pts[2*a] - pts[2*b]) *
+ 180f / Math.PI);
+ }
+ return alpha;
+ }
+
+ private void resetTouch(View target) {
+ mInitialTouchX = -1;
+ mInitialTouchY = -1;
+ mInitialTouchA = 0f;
+ mInitialTargetX = (float) target.getX();
+ mInitialTargetY = (float) target.getY();
+ mInitialTargetA = (float) target.getRotation();
+ }
+
+ public void onFling(View target, float dX, float dY) {
+ if (!mEnableFling) {
+ return;
+ }
+ log("fling " + target.getId() + " " + dX + ", " + dY);
+
+ // convert to pixel per frame
+ dX /= 60f;
+ dY /= 60f;
+
+ // starting position compionents in global corrdinate frame
+ final float x0 = pts[0];
+ final float y0 = pts[1];
+
+ // velocity
+ final float v = (float) Math.hypot(dX, dY);
+
+ if (v == 0f) {
+ return;
+ }
+
+ // number of steps to come to a stop
+ final float n = (float) Math.max(1.0, (- Math.log(v) / Math.log(mBeta)));
+ // distance travelled before stopping
+ final float s = (float) Math.max(0.0, (v * (1f - Math.pow(mBeta, n)) / (1f - mBeta)));
+
+ // ending posiiton after stopping
+ final float x1 = x0 + s * dX / v;
+ final float y1 = y0 + s * dY / v;
+
+ final float photoWidth = ((Integer) target.getTag(R.id.photo_width)).floatValue();
+ final float photoHeight = ((Integer) target.getTag(R.id.photo_height)).floatValue();
+ final float tableWidth = mTable.getWidth();
+ final float tableHeight = mTable.getHeight();
+ final float halfShortSide =
+ Math.min(photoWidth * mTableRatio, photoHeight * mTableRatio) / 2f;
+ final View photo = target;
+ ViewPropertyAnimator animator = photo.animate()
+ .withLayer()
+ .xBy(x1 - x0)
+ .yBy(y1 - y0)
+ .setDuration((int) (1000f * n / 60f))
+ .setInterpolator(new DecelerateInterpolator(2f));
+
+ if (y1 + halfShortSide < 0f || y1 - halfShortSide > tableHeight ||
+ x1 + halfShortSide < 0f || x1 - halfShortSide > tableWidth) {
+ log("fling away");
+ animator.withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mTable.fadeAway(photo, true);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean onTouch(View target, MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ // compute raw coordinates
+ for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) {
+ pts[i*2] = ev.getX(i);
+ pts[i*2 + 1] = ev.getY(i);
+ }
+ target.getMatrix().mapPoints(pts);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mTable.moveToBackOfQueue(target);
+ mInitialTouchTime = ev.getEventTime();
+ mA = ev.getPointerId(ev.getActionIndex());
+ resetTouch(target);
+ break;
+
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (mB == INVALID_POINTER) {
+ mB = ev.getPointerId(ev.getActionIndex());
+ mInitialTouchA = getAngle(target, ev);
+ }
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ if (mB == ev.getPointerId(ev.getActionIndex())) {
+ mB = INVALID_POINTER;
+ mInitialTargetA = (float) target.getRotation();
+ }
+ if (mA == ev.getPointerId(ev.getActionIndex())) {
+ mA = mB;
+ resetTouch(target);
+ mB = INVALID_POINTER;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ if (mA != INVALID_POINTER) {
+ int idx = ev.findPointerIndex(mA);
+ float x = pts[2 * idx];
+ float y = pts[2 * idx + 1];
+ if (mInitialTouchX == -1 && mInitialTouchY == -1) {
+ mInitialTouchX = x;
+ mInitialTouchY = y;
+ } else {
+ float dt = (float) (ev.getEventTime() - mLastEventTime) / 1000f;
+ mDX = (x - mLastTouchX) / dt;
+ mDY = (y - mLastTouchY) / dt;
+ log("moving " + target.getId() + " with velocity: " + mDX + ", " + mDY);
+ mLastEventTime = ev.getEventTime();
+ mLastTouchX = x;
+ mLastTouchY = y;
+ }
+
+ if (mTable.getSelected() != target) {
+ target.animate().cancel();
+
+ target.setX((int) (mInitialTargetX + x - mInitialTouchX));
+ target.setY((int) (mInitialTargetY + y - mInitialTouchY));
+ if (mManualImageRotation && mB != INVALID_POINTER) {
+ float a = getAngle(target, ev);
+ target.setRotation(
+ (int) (mInitialTargetA + a - mInitialTouchA));
+ }
+ }
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ if (mA != INVALID_POINTER) {
+ int idx = ev.findPointerIndex(mA);
+ float x0 = pts[2 * idx];
+ float y0 = pts[2 * idx + 1];
+ if (mInitialTouchX == -1 && mInitialTouchY == -1) {
+ mInitialTouchX = x0;
+ mInitialTouchY = y0;
+ }
+ double distance = Math.hypot(x0 - mInitialTouchX,
+ y0 - mInitialTouchY);
+ if (mTable.getSelected() == target) {
+ mTable.dropOnTable(target);
+ mTable.clearSelection();
+ } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout &&
+ distance < mTouchSlop) {
+ // tap
+ target.animate().cancel();
+ mTable.setSelection(target);
+ } else {
+ onFling(target, mDX, mDY);
+ }
+ mA = INVALID_POINTER;
+ mB = INVALID_POINTER;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ break;
+ }
+
+ return true;
+ }
+
+ private static void log(String message) {
+ if (DEBUG) {
+ Log.i(TAG, message);
+ }
+ }
+}
diff --git a/src/com/android/dreams/phototable/StockSource.java b/src/com/android/dreams/phototable/StockSource.java
new file mode 100644
index 0000000..829ba86
--- /dev/null
+++ b/src/com/android/dreams/phototable/StockSource.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dreams.phototable;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.util.Log;
+
+import java.util.Random;
+
+/**
+ * Picks a random image from the local store.
+ */
+public class StockSource {
+ private static final String TAG = "PhotoTable.StockSource";
+ private static final boolean DEBUG = false;
+ private static final int[] PHOTOS = {R.drawable.photo_044_002,
+ R.drawable.photo_039_002,
+ R.drawable.photo_059_003,
+ R.drawable.photo_070_004,
+ R.drawable.photo_072_001,
+ R.drawable.photo_077_002,
+ R.drawable.photo_098_002,
+ R.drawable.photo_119_003,
+ R.drawable.photo_119_004,
+ R.drawable.photo_126_001,
+ R.drawable.photo_147_002,
+ R.drawable.photo_175_004
+ };
+ private static Random sRNG = new Random();
+
+ private final Context mContext;
+ private final Resources mResources;
+ public StockSource(Context context) {
+ mContext = context;
+ mResources = context.getResources();
+ }
+
+ public Bitmap next(BitmapFactory.Options options, int longSide, int shortSide) {
+ log("decoding a local resource to " + longSide + ", " + shortSide);
+ int photo = PHOTOS[Math.abs(sRNG.nextInt() % PHOTOS.length)];
+
+ options.inJustDecodeBounds = true;
+ options.inSampleSize = 1;
+ BitmapFactory.decodeResource(mResources, photo, options);
+ int rawLongSide = Math.max(options.outWidth, options.outHeight);
+ int rawShortSide = Math.max(options.outWidth, options.outHeight);
+ log("I see bounds of " + rawLongSide + ", " + rawShortSide);
+ float ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+ while (ratio < 0.5) {
+ options.inSampleSize *= 2;
+ ratio *= 2;
+ }
+ log("decoding with inSampleSize " + options.inSampleSize);
+ options.inJustDecodeBounds = false;
+ Bitmap bitmap = BitmapFactory.decodeResource(mResources, photo, options);
+ rawLongSide = Math.max(options.outWidth, options.outHeight);
+ rawShortSide = Math.max(options.outWidth, options.outHeight);
+ ratio = Math.min((float) longSide / (float) rawLongSide,
+ (float) shortSide / (float) rawShortSide);
+
+ if (ratio < 1.0f) {
+ log("still too big, scaling down by " + ratio);
+ int photoWidth = (int) (ratio * options.outWidth);
+ int photoHeight = (int) (ratio * options.outHeight);
+ bitmap = Bitmap.createScaledBitmap(bitmap, photoWidth, photoHeight, true);
+ }
+
+ log("returning bitmap sized to " + bitmap.getWidth() + ", " + bitmap.getHeight());
+ return bitmap;
+ }
+
+ private static void log(String message) {
+ if (DEBUG) {
+ Log.i(TAG, message);
+ }
+ }
+
+}
diff --git a/src/com/android/dreams/phototable/Table.java b/src/com/android/dreams/phototable/Table.java
new file mode 100644
index 0000000..0ffa7ac
--- /dev/null
+++ b/src/com/android/dreams/phototable/Table.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.dreams.phototable;
+
+import android.service.dreams.Dream;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PointF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.AsyncTask;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+
+import java.util.LinkedList;
+import java.util.Random;
+
+/**
+ * A surface where photos sit.
+ */
+public class Table extends FrameLayout {
+ private static final String TAG = "PhotoTable.Table";
+ private static final boolean DEBUG = false;
+
+ class Launcher implements Runnable {
+ private final Table mTable;
+ public Launcher(Table table) {
+ mTable = table;
+ }
+
+ @Override
+ public void run() {
+ mTable.scheduleNext(mDropPeriod);
+ mTable.launch();
+ }
+ }
+
+ private static final long MAX_SELECTION_TIME = 10000L;
+ private static Random sRNG = new Random();
+
+ private final Launcher mLauncher;
+ private final LinkedList<View> mOnTable;
+ private final Dream mDream;
+ private final int mDropPeriod;
+ private final int mFastDropPeriod;
+ private final float mImageRatio;
+ private final float mTableRatio;
+ private final float mImageRotationLimit;
+ private final boolean mTapToExit;
+ private final int mTableCapacity;
+ private final int mInset;
+ private final LocalSource mLocalSource;
+ private final StockSource mStockSource;
+ private final Resources mResources;
+ private PhotoLaunchTask mPhotoLaunchTask;
+ private boolean mStarted;
+ private boolean mIsLandscape;
+ private BitmapFactory.Options mOptions;
+ private int mLongSide;
+ private int mShortSide;
+ private int mWidth;
+ private int mHeight;
+ private View mSelected;
+ private long mSelectedTime;
+
+ public Table(Dream dream, AttributeSet as) {
+ super(dream, as);
+ mDream = dream;
+ mResources = getResources();
+ setBackground(mResources.getDrawable(R.drawable.table));
+ mInset = mResources.getDimensionPixelSize(R.dimen.photo_inset);
+ mDropPeriod = mResources.getInteger(R.integer.drop_period);
+ mFastDropPeriod = mResources.getInteger(R.integer.fast_drop);
+ mImageRatio = mResources.getInteger(R.integer.image_ratio) / 1000000f;
+ mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f;
+ mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation);
+ mTableCapacity = mResources.getInteger(R.integer.table_capacity);
+ mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
+ mOnTable = new LinkedList<View>();
+ mOptions = new BitmapFactory.Options();
+ mOptions.inTempStorage = new byte[32768];
+ mLocalSource = new LocalSource(getContext());
+ mStockSource = new StockSource(getContext());
+ mLauncher = new Launcher(this);
+ mStarted = false;
+ }
+
+ public boolean hasSelection() {
+ return mSelected != null;
+ }
+
+ public View getSelected() {
+ return mSelected;
+ }
+
+ public void clearSelection() {
+ mSelected = null;
+ }
+
+ public void setSelection(View selected) {
+ assert(selected != null);
+ if (mSelected != null) {
+ dropOnTable(mSelected);
+ }
+ mSelected = selected;
+ mSelectedTime = System.currentTimeMillis();
+ bringChildToFront(selected);
+ pickUp(selected);
+ }
+
+ static float lerp(float a, float b, float f) {
+ return (b-a)*f + a;
+ }
+
+ static float randfrange(float a, float b) {
+ return lerp(a, b, sRNG.nextFloat());
+ }
+
+ static PointF randFromCurve(float t, PointF[] v) {
+ PointF p = new PointF();
+ if (v.length == 4 && t >= 0f && t <= 1f) {
+ float a = (float) Math.pow(1f-t, 3f);
+ float b = (float) Math.pow(1f-t, 2f) * t;
+ float c = (1f-t) * (float) Math.pow(t, 2f);
+ float d = (float) Math.pow(t, 3f);
+
+ p.x = a * v[0].x + 3 * b * v[1].x + 3 * c * v[2].x + d * v[3].x;
+ p.y = a * v[0].y + 3 * b * v[1].y + 3 * c * v[2].y + d * v[3].y;
+ }
+ return p;
+ }
+
+ private static PointF randInCenter(float i, float j, int width, int height) {
+ log("randInCenter (" + i + ", " + j + ", " + width + ", " + height + ")");
+ PointF p = new PointF();
+ p.x = 0.5f * width + 0.15f * width * i;
+ p.y = 0.5f * height + 0.15f * height * j;
+ log("randInCenter returning " + p.x + "," + p.y);
+ return p;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ if (hasSelection()) {
+ dropOnTable(getSelected());
+ clearSelection();
+ } else {
+ if (mTapToExit) {
+ mDream.finish();
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ log("onLayout (" + left + ", " + top + ", " + right + ", " + bottom + ")");
+
+ mHeight = bottom - top;
+ mWidth = right - left;
+
+ mLongSide = (int) (mImageRatio * Math.max(mWidth, mHeight));
+ mShortSide = (int) (mImageRatio * Math.min(mWidth, mHeight));
+
+ boolean isLandscape = mWidth > mHeight;
+ if (mIsLandscape != isLandscape) {
+ for (View photo: mOnTable) {
+ if (photo == getSelected()) {
+ pickUp(photo);
+ } else {
+ dropOnTable(photo);
+ }
+ }
+ mIsLandscape = isLandscape;
+ }
+ start();
+ }
+
+ @Override
+ public boolean isOpaque() {
+ return true;
+ }
+
+ static class PhotoLaunchTask extends AsyncTask<Void, Void, View> {
+ private Table mTable;
+ public PhotoLaunchTask(Table table) {
+ mTable = table;
+ }
+ @Override
+ public View doInBackground(Void... unused) {
+ log("load a new photo");
+ LayoutInflater inflater = (LayoutInflater) mTable.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View photo = inflater.inflate(R.layout.photo, null);
+ ImageView image = (ImageView) photo;
+ Drawable[] layers = new Drawable[2];
+ Bitmap decodedPhoto = null;
+ decodedPhoto = mTable.mLocalSource.next(mTable.mOptions,
+ mTable.mLongSide, mTable.mShortSide);
+ if (decodedPhoto == null) {
+ decodedPhoto = mTable.mStockSource.next(mTable.mOptions,
+ mTable.mLongSide, mTable.mShortSide);
+ }
+ int photoWidth = mTable.mOptions.outWidth;
+ int photoHeight = mTable.mOptions.outHeight;
+ if (mTable.mOptions.outWidth <= 0 || mTable.mOptions.outHeight <= 0) {
+ photo = null;
+ } else {
+ layers[0] = new BitmapDrawable(mTable.mResources, decodedPhoto);
+ layers[1] = mTable.mResources.getDrawable(R.drawable.frame);
+ LayerDrawable layerList = new LayerDrawable(layers);
+ layerList.setLayerInset(0, mTable.mInset, mTable.mInset,
+ mTable.mInset, mTable.mInset);
+ image.setImageDrawable(layerList);
+
+ photo.setTag(R.id.photo_width, new Integer(photoWidth));
+ photo.setTag(R.id.photo_height, new Integer(photoHeight));
+
+ photo.setOnTouchListener(new PhotoTouchListener(mTable.getContext(), mTable));
+ }
+
+ return photo;
+ }
+
+ @Override
+ public void onPostExecute(View photo) {
+ if (photo != null) {
+ mTable.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ if (mTable.hasSelection()) {
+ mTable.bringChildToFront(mTable.getSelected());
+ }
+ int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
+ int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
+
+ log("drop it");
+ mTable.throwOnTable(photo);
+ }
+ if(mTable.mOnTable.size() < mTable.mTableCapacity) {
+ mTable.scheduleNext(mTable.mFastDropPeriod);
+ }
+ }
+ };
+
+ public void launch() {
+ log("launching");
+ setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
+ if (hasSelection() &&
+ (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
+ dropOnTable(getSelected());
+ clearSelection();
+ } else {
+ log("inflate it");
+ if (mPhotoLaunchTask == null ||
+ mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) {
+ mPhotoLaunchTask = new PhotoLaunchTask(this);
+ mPhotoLaunchTask.execute();
+ }
+ }
+ }
+ public void fadeAway(final View photo, final boolean replace) {
+ // fade out of view
+ mOnTable.remove(photo);
+ photo.animate().cancel();
+ photo.animate()
+ .withLayer()
+ .alpha(0f)
+ .setDuration(1000)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ removeView(photo);
+ recycle(photo);
+ if (replace) {
+ launch();
+ }
+ }
+ });
+ }
+
+ public void moveToBackOfQueue(View photo) {
+ // make this photo the last to be removed.
+ bringChildToFront(photo);
+ invalidate();
+ mOnTable.remove(photo);
+ mOnTable.offer(photo);
+ }
+
+ private void throwOnTable(final View photo) {
+ mOnTable.offer(photo);
+ log("start offscreen");
+ int width = ((Integer) photo.getTag(R.id.photo_width));
+ int height = ((Integer) photo.getTag(R.id.photo_height));
+ photo.setRotation(-100.0f);
+ photo.setX(-mLongSide);
+ photo.setY(-mLongSide);
+ dropOnTable(photo);
+ }
+
+ public void dropOnTable(final View photo) {
+ float angle = randfrange(-mImageRotationLimit, mImageRotationLimit);
+ PointF p = randInCenter((float) sRNG.nextGaussian(), (float) sRNG.nextGaussian(),
+ mWidth, mHeight);
+ float x = p.x;
+ float y = p.y;
+
+ log("drop it at " + x + ", " + y);
+
+ float x0 = photo.getX();
+ float y0 = photo.getY();
+ float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
+ float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
+
+ x -= mTableRatio * mLongSide / 2f;
+ y -= mTableRatio * mLongSide / 2f;
+ log("fixed offset is " + x + ", " + y);
+
+ float dx = x - x0;
+ float dy = y - y0;
+
+ float dist = (float) (Math.sqrt(dx * dx + dy * dy));
+ int duration = (int) (1000f * dist / 400f);
+ duration = Math.max(duration, 1000);
+
+ log("animate it");
+ // toss onto table
+ photo.animate()
+ .withLayer()
+ .scaleX(mTableRatio / mImageRatio)
+ .scaleY(mTableRatio / mImageRatio)
+ .rotation(angle)
+ .x(x)
+ .y(y)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator(3f))
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ while (mOnTable.size() > mTableCapacity) {
+ fadeAway(mOnTable.poll(), false);
+ }
+ }
+ });
+ }
+
+ /** wrap all orientations to the interval [-180, 180). */
+ private float wrapAngle(float angle) {
+ float result = angle + 180;
+ result = ((result % 360) + 360) % 360; // catch negative numbers
+ result -= 180;
+ return result;
+ }
+
+ private void pickUp(final View photo) {
+ float photoWidth = photo.getWidth();
+ float photoHeight = photo.getHeight();
+
+ float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
+
+ log("target it");
+ float x = (getWidth() - photoWidth) / 2f;
+ float y = (getHeight() - photoHeight) / 2f;
+
+ float x0 = photo.getX();
+ float y0 = photo.getY();
+ float dx = x - x0;
+ float dy = y - y0;
+
+ float dist = (float) (Math.sqrt(dx * dx + dy * dy));
+ int duration = (int) (1000f * dist / 1000f);
+ duration = Math.max(duration, 500);
+
+ photo.setRotation(wrapAngle(photo.getRotation()));
+
+ log("animate it");
+ // toss onto table
+ photo.animate()
+ .withLayer()
+ .rotation(0f)
+ .scaleX(scale)
+ .scaleY(scale)
+ .x(x)
+ .y(y)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator(2f))
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ log("endtimes: " + photo.getX());
+ }
+ });
+ }
+
+ private void recycle(View photo) {
+ ImageView image = (ImageView) photo;
+ LayerDrawable layers = (LayerDrawable) image.getDrawable();
+ BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0);
+ bitmap.getBitmap().recycle();
+ }
+
+ public void start() {
+ if (!mStarted) {
+ log("kick it");
+ mStarted = true;
+ scheduleNext(mDropPeriod);
+ launch();
+ }
+ }
+
+ public void scheduleNext(int delay) {
+ removeCallbacks(mLauncher);
+ postDelayed(mLauncher, delay);
+ }
+
+ private static void log(String message) {
+ if (DEBUG) {
+ Log.i(TAG, message);
+ }
+ }
+}