summaryrefslogtreecommitdiffstats
path: root/src/com/android/dreams/phototable/PhotoTable.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dreams/phototable/PhotoTable.java')
-rw-r--r--src/com/android/dreams/phototable/PhotoTable.java782
1 files changed, 643 insertions, 139 deletions
diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java
index 50212c9..7e7f92e 100644
--- a/src/com/android/dreams/phototable/PhotoTable.java
+++ b/src/com/android/dreams/phototable/PhotoTable.java
@@ -15,30 +15,38 @@
*/
package com.android.dreams.phototable;
-import android.service.dreams.DreamService;
import android.content.Context;
-import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.AsyncTask;
+import android.service.dreams.DreamService;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
-import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.HashSet;
import java.util.LinkedList;
+import java.util.List;
import java.util.Random;
+import java.util.Set;
/**
* A surface where photos sit.
@@ -48,22 +56,34 @@ public class PhotoTable extends FrameLayout {
private static final boolean DEBUG = false;
class Launcher implements Runnable {
- private final PhotoTable mTable;
- public Launcher(PhotoTable table) {
- mTable = table;
+ @Override
+ public void run() {
+ PhotoTable.this.scheduleNext(mDropPeriod);
+ PhotoTable.this.launch();
}
+ }
+ class FocusReaper implements Runnable {
@Override
public void run() {
- mTable.scheduleNext(mDropPeriod);
- mTable.launch();
+ PhotoTable.this.clearFocus();
}
}
- private static final long MAX_SELECTION_TIME = 10000L;
+ class SelectionReaper implements Runnable {
+ @Override
+ public void run() {
+ PhotoTable.this.clearSelection();
+ }
+ }
+
+ private static final int NEXT = 1;
+ private static final int PREV = 0;
private static Random sRNG = new Random();
private final Launcher mLauncher;
+ private final FocusReaper mFocusReaper;
+ private final SelectionReaper mSelectionReaper;
private final LinkedList<View> mOnTable;
private final int mDropPeriod;
private final int mFastDropPeriod;
@@ -77,20 +97,34 @@ public class PhotoTable extends FrameLayout {
private final int mTableCapacity;
private final int mRedealCount;
private final int mInset;
- private final PhotoSourcePlexor mPhotoSource;
+ private final PhotoSource mPhotoSource;
private final Resources mResources;
private final Interpolator mThrowInterpolator;
private final Interpolator mDropInterpolator;
+ private final DragGestureDetector mDragGestureDetector;
+ private final EdgeSwipeDetector mEdgeSwipeDetector;
+ private final KeyboardInterpreter mKeyboardInterpreter;
+ private final boolean mStoryModeEnabled;
+ private final long mPickUpDuration;
+ private final int mMaxSelectionTime;
+ private final int mMaxFocusTime;
+ private final List<View> mAnimating;
+
private DreamService mDream;
private PhotoLaunchTask mPhotoLaunchTask;
+ private LoadNaturalSiblingTask mLoadOnDeckTasks[];
private boolean mStarted;
private boolean mIsLandscape;
private int mLongSide;
private int mShortSide;
private int mWidth;
private int mHeight;
- private View mSelected;
- private long mSelectedTime;
+ private View mSelection;
+ private View mOnDeck[];
+ private View mFocus;
+ private int mHighlightColor;
+ private ViewGroup mBackground;
+ private ViewGroup mStageLeft;
public PhotoTable(Context context, AttributeSet as) {
super(context, as);
@@ -103,10 +137,15 @@ public class PhotoTable extends FrameLayout {
mTableRatio = mResources.getInteger(R.integer.table_ratio) / 1000000f;
mImageRotationLimit = (float) mResources.getInteger(R.integer.max_image_rotation);
mThrowSpeed = mResources.getDimension(R.dimen.image_throw_speed);
+ mPickUpDuration = mResources.getInteger(R.integer.photo_pickup_duration);
mThrowRotation = (float) mResources.getInteger(R.integer.image_throw_rotatioan);
mTableCapacity = mResources.getInteger(R.integer.table_capacity);
mRedealCount = mResources.getInteger(R.integer.redeal_count);
mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
+ mStoryModeEnabled = mResources.getBoolean(R.bool.enable_story_mode);
+ mHighlightColor = mResources.getColor(R.color.highlight_color);
+ mMaxSelectionTime = mResources.getInteger(R.integer.max_selection_time);
+ mMaxFocusTime = mResources.getInteger(R.integer.max_focus_time);
mThrowInterpolator = new SoftLandingInterpolator(
mResources.getInteger(R.integer.soft_landing_time) / 1000000f,
mResources.getInteger(R.integer.soft_landing_distance) / 1000000f);
@@ -115,36 +154,145 @@ public class PhotoTable extends FrameLayout {
mOnTable = new LinkedList<View>();
mPhotoSource = new PhotoSourcePlexor(getContext(),
getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0));
- mLauncher = new Launcher(this);
+ mAnimating = new ArrayList<View>();
+ mLauncher = new Launcher();
+ mFocusReaper = new FocusReaper();
+ mSelectionReaper = new SelectionReaper();
+ mDragGestureDetector = new DragGestureDetector(context, this);
+ mEdgeSwipeDetector = new EdgeSwipeDetector(context, this);
+ mKeyboardInterpreter = new KeyboardInterpreter(this);
+ mLoadOnDeckTasks = new LoadNaturalSiblingTask[2];
+ mOnDeck = new View[2];
mStarted = false;
}
-
+ @Override
+ public void onFinishInflate() {
+ mBackground = (ViewGroup) findViewById(R.id.background);
+ mStageLeft = (ViewGroup) findViewById(R.id.stageleft);
+ }
+
public void setDream(DreamService dream) {
mDream = dream;
}
public boolean hasSelection() {
- return mSelected != null;
+ return mSelection != null;
}
- public View getSelected() {
- return mSelected;
+ public View getSelection() {
+ return mSelection;
}
public void clearSelection() {
- mSelected = null;
+ if (hasSelection()) {
+ dropOnTable(mSelection);
+ mPhotoSource.donePaging(getBitmap(mSelection));
+ if (mStoryModeEnabled) {
+ fadeInBackground(mSelection);
+ }
+ mSelection = null;
+ }
+ for (int slot = 0; slot < mOnDeck.length; slot++) {
+ if (mOnDeck[slot] != null) {
+ fadeAway(mOnDeck[slot], false);
+ mOnDeck[slot] = null;
+ }
+ if (mLoadOnDeckTasks[slot] != null &&
+ mLoadOnDeckTasks[slot].getStatus() != AsyncTask.Status.FINISHED) {
+ mLoadOnDeckTasks[slot].cancel(true);
+ mLoadOnDeckTasks[slot] = null;
+ }
+ }
}
public void setSelection(View selected) {
- assert(selected != null);
- if (mSelected != null) {
- dropOnTable(mSelected);
+ if (selected != null) {
+ clearSelection();
+ mSelection = selected;
+ promoteSelection();
+ if (mStoryModeEnabled) {
+ fadeOutBackground(mSelection);
+ }
}
- mSelected = selected;
- mSelectedTime = System.currentTimeMillis();
- bringChildToFront(selected);
- pickUp(selected);
+ }
+
+ public void selectNext() {
+ if (mStoryModeEnabled) {
+ log("selectNext");
+ if (hasSelection() && mOnDeck[NEXT] != null) {
+ placeOnDeck(mSelection, PREV);
+ mSelection = mOnDeck[NEXT];
+ mOnDeck[NEXT] = null;
+ promoteSelection();
+ }
+ } else {
+ clearSelection();
+ }
+ }
+
+ public void selectPrevious() {
+ if (mStoryModeEnabled) {
+ log("selectPrevious");
+ if (hasSelection() && mOnDeck[PREV] != null) {
+ placeOnDeck(mSelection, NEXT);
+ mSelection = mOnDeck[PREV];
+ mOnDeck[PREV] = null;
+ promoteSelection();
+ }
+ } else {
+ clearSelection();
+ }
+ }
+
+ private void promoteSelection() {
+ if (hasSelection()) {
+ scheduleSelectionReaper(mMaxSelectionTime);
+ mSelection.animate().cancel();
+ mSelection.setAlpha(1f);
+ moveToTopOfPile(mSelection);
+ pickUp(mSelection);
+ if (mStoryModeEnabled) {
+ for (int slot = 0; slot < mOnDeck.length; slot++) {
+ if (mLoadOnDeckTasks[slot] != null &&
+ mLoadOnDeckTasks[slot].getStatus() != AsyncTask.Status.FINISHED) {
+ mLoadOnDeckTasks[slot].cancel(true);
+ }
+ if (mOnDeck[slot] == null) {
+ mLoadOnDeckTasks[slot] = new LoadNaturalSiblingTask(slot);
+ mLoadOnDeckTasks[slot].execute(mSelection);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean hasFocus() {
+ return mFocus != null;
+ }
+
+ public View getFocus() {
+ return mFocus;
+ }
+
+ public void clearFocus() {
+ if (hasFocus()) {
+ setHighlight(getFocus(), false);
+ }
+ mFocus = null;
+ }
+
+ public void setDefaultFocus() {
+ setFocus(mOnTable.getLast());
+ }
+
+ public void setFocus(View focus) {
+ assert(focus != null);
+ clearFocus();
+ mFocus = focus;
+ moveToTopOfPile(focus);
+ setHighlight(focus, true);
+ scheduleFocusReaper(mMaxFocusTime);
}
static float lerp(float a, float b, float f) {
@@ -169,17 +317,8 @@ public class PhotoTable extends FrameLayout {
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;
- }
-
private static PointF randMultiDrop(int n, float i, float j, int width, int height) {
- log("randMultiDrop (" + n + "," + i + ", " + j + ", " + width + ", " + height + ")");
+ log("randMultiDrop (%d, %f, %f, %d, %d)", n, i, j, width, height);
final float[] cx = {0.3f, 0.3f, 0.5f, 0.7f, 0.7f};
final float[] cy = {0.3f, 0.7f, 0.5f, 0.3f, 0.7f};
n = Math.abs(n);
@@ -188,15 +327,82 @@ public class PhotoTable extends FrameLayout {
PointF p = new PointF();
p.x = x * width + 0.05f * width * i;
p.y = y * height + 0.05f * height * j;
- log("randInCenter returning " + p.x + "," + p.y);
+ log("randInCenter returning %f, %f", p.x, p.y);
return p;
}
+ private double cross(double[] a, double[] b) {
+ return a[0] * b[1] - a[1] * b[0];
+ }
+
+ private double norm(double[] a) {
+ return Math.hypot(a[0], a[1]);
+ }
+
+ private double[] getCenter(View photo) {
+ float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
+ float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
+ double[] center = { photo.getX() + width / 2f,
+ - (photo.getY() + height / 2f) };
+ return center;
+ }
+
+ public View moveFocus(View focus, float direction) {
+ return moveFocus(focus, direction, 90f);
+ }
+
+ public View moveFocus(View focus, float direction, float angle) {
+ if (focus == null) {
+ setFocus(mOnTable.getLast());
+ } else {
+ final double alpha = Math.toRadians(direction);
+ final double beta = Math.toRadians(Math.min(angle, 180f) / 2f);
+ final double[] left = { Math.sin(alpha - beta),
+ Math.cos(alpha - beta) };
+ final double[] right = { Math.sin(alpha + beta),
+ Math.cos(alpha + beta) };
+ final double[] a = getCenter(focus);
+ View bestFocus = null;
+ double bestDistance = Double.MAX_VALUE;
+ for (View candidate: mOnTable) {
+ if (candidate != focus) {
+ final double[] b = getCenter(candidate);
+ final double[] delta = { b[0] - a[0],
+ b[1] - a[1] };
+ if (cross(delta, left) > 0.0 && cross(delta, right) < 0.0) {
+ final double distance = norm(delta);
+ if (bestDistance > distance) {
+ bestDistance = distance;
+ bestFocus = candidate;
+ }
+ }
+ }
+ }
+ if (bestFocus == null) {
+ if (angle < 180f) {
+ return moveFocus(focus, direction, 180f);
+ }
+ } else {
+ setFocus(bestFocus);
+ }
+ }
+ return getFocus();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mKeyboardInterpreter.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onGenericMotionEvent(MotionEvent event) {
+ return mEdgeSwipeDetector.onTouchEvent(event) || mDragGestureDetector.onTouchEvent(event);
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (hasSelection()) {
- dropOnTable(getSelected());
clearSelection();
} else {
if (mTapToExit && mDream != null) {
@@ -211,7 +417,7 @@ public class PhotoTable extends FrameLayout {
@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 + ")");
+ log("onLayout (%d, %d, %d, %d)", left, top, right, bottom);
mHeight = bottom - top;
mWidth = right - left;
@@ -222,12 +428,18 @@ public class PhotoTable extends FrameLayout {
boolean isLandscape = mWidth > mHeight;
if (mIsLandscape != isLandscape) {
for (View photo: mOnTable) {
- if (photo == getSelected()) {
- pickUp(photo);
- } else {
+ if (photo != getSelection()) {
dropOnTable(photo);
}
}
+ if (hasSelection()) {
+ pickUp(getSelection());
+ for (int slot = 0; slot < mOnDeck.length; slot++) {
+ if (mOnDeck[slot] != null) {
+ placeOnDeck(mOnDeck[slot], slot);
+ }
+ }
+ }
mIsLandscape = isLandscape;
}
start();
@@ -238,47 +450,108 @@ public class PhotoTable extends FrameLayout {
return true;
}
- private class PhotoLaunchTask extends AsyncTask<Void, Void, View> {
+ /** Put a nice border on the bitmap. */
+ private static View applyFrame(final PhotoTable table, final BitmapFactory.Options options,
+ Bitmap decodedPhoto) {
+ LayoutInflater inflater = (LayoutInflater) table.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View photo = inflater.inflate(R.layout.photo, null);
+ ImageView image = (ImageView) photo;
+ Drawable[] layers = new Drawable[2];
+ int photoWidth = options.outWidth;
+ int photoHeight = options.outHeight;
+ if (decodedPhoto == null || options.outWidth <= 0 || options.outHeight <= 0) {
+ photo = null;
+ } else {
+ decodedPhoto.setHasMipMap(true);
+ layers[0] = new BitmapDrawable(table.mResources, decodedPhoto);
+ layers[1] = table.mResources.getDrawable(R.drawable.frame);
+ LayerDrawable layerList = new LayerDrawable(layers);
+ layerList.setLayerInset(0, table.mInset, table.mInset,
+ table.mInset, table.mInset);
+ image.setImageDrawable(layerList);
+
+ photo.setTag(R.id.photo_width, Integer.valueOf(photoWidth));
+ photo.setTag(R.id.photo_height, Integer.valueOf(photoHeight));
+
+ photo.setOnTouchListener(new PhotoTouchListener(table.getContext(),
+ table));
+ }
+ return photo;
+ }
+
+ private class LoadNaturalSiblingTask extends AsyncTask<View, Void, View> {
private final BitmapFactory.Options mOptions;
+ private final int mSlot;
+ private View mParent;
- public PhotoLaunchTask () {
+ public LoadNaturalSiblingTask (int slot) {
mOptions = new BitmapFactory.Options();
mOptions.inTempStorage = new byte[32768];
+ mSlot = slot;
}
@Override
- public View doInBackground(Void... unused) {
- log("load a new photo");
+ public View doInBackground(View... views) {
+ log("load natural %s", (mSlot == NEXT ? "next" : "previous"));
final PhotoTable table = PhotoTable.this;
+ mParent = views[0];
+ final Bitmap current = getBitmap(mParent);
+ Bitmap decodedPhoto;
+ if (mSlot == NEXT) {
+ decodedPhoto = table.mPhotoSource.naturalNext(current,
+ mOptions, table.mLongSide, table.mShortSide);
+ } else {
+ decodedPhoto = table.mPhotoSource.naturalPrevious(current,
+ mOptions, table.mLongSide, table.mShortSide);
+ }
+ return applyFrame(PhotoTable.this, mOptions, decodedPhoto);
+ }
- LayoutInflater inflater = (LayoutInflater) table.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 = table.mPhotoSource.next(mOptions,
- table.mLongSide, table.mShortSide);
- int photoWidth = mOptions.outWidth;
- int photoHeight = mOptions.outHeight;
- if (mOptions.outWidth <= 0 || mOptions.outHeight <= 0) {
- photo = null;
+ @Override
+ public void onPostExecute(View photo) {
+ if (photo != null) {
+ if (hasSelection() && getSelection() == mParent) {
+ log("natural %s being rendered", (mSlot == NEXT ? "next" : "previous"));
+ PhotoTable.this.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ PhotoTable.this.mOnDeck[mSlot] = photo;
+ float width = (float) ((Integer) photo.getTag(R.id.photo_width)).intValue();
+ float height = (float) ((Integer) photo.getTag(R.id.photo_height)).intValue();
+ photo.setX(mSlot == PREV ? -2 * width : mWidth + 2 * width);
+ photo.setY((mHeight - height) / 2);
+ photo.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ PhotoTable.this.placeOnDeck(v, mSlot);
+ v.removeOnLayoutChangeListener(this);
+ }
+ });
+ } else {
+ recycle(photo);
+ }
} else {
- decodedPhoto.setHasMipMap(true);
- layers[0] = new BitmapDrawable(table.mResources, decodedPhoto);
- layers[1] = table.mResources.getDrawable(R.drawable.frame);
- LayerDrawable layerList = new LayerDrawable(layers);
- layerList.setLayerInset(0, table.mInset, table.mInset,
- table.mInset, table.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(table.getContext(),
- table));
+ log("natural, %s was null!", (mSlot == NEXT ? "next" : "previous"));
}
+ }
+ };
+
+ private class PhotoLaunchTask extends AsyncTask<Void, Void, View> {
+ private final BitmapFactory.Options mOptions;
+
+ public PhotoLaunchTask () {
+ mOptions = new BitmapFactory.Options();
+ mOptions.inTempStorage = new byte[32768];
+ }
- return photo;
+ @Override
+ public View doInBackground(Void... unused) {
+ log("load a new photo");
+ final PhotoTable table = PhotoTable.this;
+ return applyFrame(PhotoTable.this, mOptions,
+ table.mPhotoSource.next(mOptions,
+ table.mLongSide, table.mShortSide));
}
@Override
@@ -287,16 +560,26 @@ public class PhotoTable extends FrameLayout {
final PhotoTable table = PhotoTable.this;
table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
- LayoutParams.WRAP_CONTENT));
+ LayoutParams.WRAP_CONTENT));
if (table.hasSelection()) {
- table.bringChildToFront(table.getSelected());
+ for (int slot = 0; slot < mOnDeck.length; slot++) {
+ if (mOnDeck[slot] != null) {
+ table.moveToTopOfPile(mOnDeck[slot]);
+ }
+ }
+ table.moveToTopOfPile(table.getSelection());
}
- int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
- int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
log("drop it");
table.throwOnTable(photo);
+ if (mOnTable.size() > mTableCapacity) {
+ int targetSize = Math.max(0, mOnTable.size() - mRedealCount);
+ while (mOnTable.size() > targetSize) {
+ fadeAway(mOnTable.poll(), false);
+ }
+ }
+
if(table.mOnTable.size() < table.mTableCapacity) {
table.scheduleNext(table.mFastDropPeriod);
}
@@ -304,14 +587,11 @@ public class PhotoTable extends FrameLayout {
}
};
+ /** Bring a new photo onto the table. */
public void launch() {
log("launching");
- setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
- if (hasSelection() &&
- (System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
- dropOnTable(getSelected());
- clearSelection();
- } else {
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ if (!hasSelection()) {
log("inflate it");
if (mPhotoLaunchTask == null ||
mPhotoLaunchTask.getStatus() == AsyncTask.Status.FINISHED) {
@@ -320,18 +600,52 @@ public class PhotoTable extends FrameLayout {
}
}
}
+
+ /** De-emphasize the other photos on the table. */
+ public void fadeOutBackground(final View photo) {
+ mBackground.animate()
+ .withLayer()
+ .setDuration(mPickUpDuration)
+ .alpha(0f);
+ }
+
+
+ /** Return the other photos to foreground status. */
+ public void fadeInBackground(final View photo) {
+ mAnimating.add(photo);
+ mBackground.animate()
+ .withLayer()
+ .setDuration(mPickUpDuration)
+ .alpha(1f)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mAnimating.remove(photo);
+ if (!mAnimating.contains(photo)) {
+ moveToBackground(photo);
+ }
+ }
+ });
+ }
+
+ /** Dispose of the photo gracefully, in case we can see some of it. */
public void fadeAway(final View photo, final boolean replace) {
// fade out of view
mOnTable.remove(photo);
+ exitStageLeft(photo);
+ photo.setOnTouchListener(null);
photo.animate().cancel();
photo.animate()
.withLayer()
.alpha(0f)
- .setDuration(1000)
+ .setDuration(mPickUpDuration)
.withEndAction(new Runnable() {
@Override
public void run() {
- removeView(photo);
+ if (photo == getFocus()) {
+ clearFocus();
+ }
+ mStageLeft.removeView(photo);
recycle(photo);
if (replace) {
scheduleNext(mNowDropDelay);
@@ -340,19 +654,62 @@ public class PhotoTable extends FrameLayout {
});
}
- public void moveToBackOfQueue(View photo) {
+ /** Visually on top, and also freshest, for the purposes of timeouts. */
+ public void moveToTopOfPile(View photo) {
// make this photo the last to be removed.
- bringChildToFront(photo);
+ if (isInBackground(photo)) {
+ mBackground.bringChildToFront(photo);
+ } else {
+ bringChildToFront(photo);
+ }
invalidate();
mOnTable.remove(photo);
mOnTable.offer(photo);
}
+ /** On deck is to the left or right of the selected photo. */
+ private void placeOnDeck(final View photo, final int slot ) {
+ if (slot < mOnDeck.length) {
+ if (mOnDeck[slot] != null && mOnDeck[slot] != photo) {
+ fadeAway(mOnDeck[slot], false);
+ }
+ mOnDeck[slot] = photo;
+ float photoWidth = photo.getWidth();
+ float photoHeight = photo.getHeight();
+ float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
+
+ float x = (getWidth() - photoWidth) / 2f;
+ float y = (getHeight() - photoHeight) / 2f;
+
+ float offset = (((float) mWidth + scale * (photoWidth - 2f * mInset)) / 2f);
+ x += (slot == NEXT? 1f : -1f) * offset;
+
+ photo.animate()
+ .withLayer()
+ .rotation(0f)
+ .rotationY(0f)
+ .scaleX(scale)
+ .scaleY(scale)
+ .x(x)
+ .y(y)
+ .setDuration(mPickUpDuration)
+ .setInterpolator(new DecelerateInterpolator(2f));
+ }
+ }
+
+ /** Move in response to touch. */
+ public void move(final View photo, float x, float y, float a) {
+ photo.animate().cancel();
+ photo.setAlpha(1f);
+ photo.setX((int) x);
+ photo.setY((int) y);
+ photo.setRotation((int) a);
+ }
+
+ /** Wind up off screen, so we can animate in. */
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(mThrowRotation);
photo.setX(-mLongSide);
photo.setY(-mLongSide);
@@ -360,10 +717,87 @@ public class PhotoTable extends FrameLayout {
dropOnTable(photo, mThrowInterpolator);
}
+ public void move(final View photo, float dx, float dy, boolean drop) {
+ if (photo != null) {
+ final float x = photo.getX() + dx;
+ final float y = photo.getY() + dy;
+ photo.setX(x);
+ photo.setY(y);
+ Log.d(TAG, "[" + photo.getX() + ", " + photo.getY() + "] + (" + dx + "," + dy + ")");
+ if (drop && photoOffTable(photo)) {
+ fadeAway(photo, true);
+ }
+ }
+ }
+
+ /** Fling with no touch hints, then land off screen. */
+ public void fling(final View photo) {
+ final float[] o = { mWidth + mLongSide / 2f,
+ mHeight + mLongSide / 2f };
+ final float[] a = { photo.getX(), photo.getY() };
+ final float[] b = { o[0], a[1] + o[0] - a[0] };
+ final float[] c = { a[0] + o[1] - a[1], o[1] };
+ float[] delta = { 0f, 0f };
+ if (Math.hypot(b[0] - a[0], b[1] - a[1]) < Math.hypot(c[0] - a[0], c[1] - a[1])) {
+ delta[0] = b[0] - a[0];
+ delta[1] = b[1] - a[1];
+ } else {
+ delta[0] = c[0] - a[0];
+ delta[1] = c[1] - a[1];
+ }
+
+ final float dist = (float) Math.hypot(delta[0], delta[1]);
+ final int duration = (int) (1000f * dist / mThrowSpeed);
+ fling(photo, delta[0], delta[1], duration, true);
+ }
+
+ /** Continue dynamically after a fling gesture, possibly off the screen. */
+ public void fling(final View photo, float dx, float dy, int duration, boolean spin) {
+ if (photo == getFocus()) {
+ if (moveFocus(photo, 0f) == null) {
+ moveFocus(photo, 180f);
+ }
+ }
+ moveToForeground(photo);
+ ViewPropertyAnimator animator = photo.animate()
+ .withLayer()
+ .xBy(dx)
+ .yBy(dy)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator(2f));
+
+ if (spin) {
+ animator.rotation(mThrowRotation);
+ }
+
+ if (photoOffTable(photo, (int) dx, (int) dy)) {
+ log("fling away");
+ animator.withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ fadeAway(photo, true);
+ }
+ });
+ }
+ }
+ public boolean photoOffTable(final View photo) {
+ return photoOffTable(photo, 0, 0);
+ }
+
+ public boolean photoOffTable(final View photo, final int dx, final int dy) {
+ Rect hit = new Rect();
+ photo.getHitRect(hit);
+ hit.offset(dx, dy);
+ return (hit.bottom < 0f || hit.top > getHeight() ||
+ hit.right < 0f || hit.left > getWidth());
+ }
+
+ /** Animate to a random place and orientation, down on the table (visually small). */
public void dropOnTable(final View photo) {
dropOnTable(photo, mDropInterpolator);
}
+ /** Animate to a random place and orientation, down on the table (visually small). */
public void dropOnTable(final View photo, final Interpolator interpolator) {
float angle = randfrange(-mImageRotationLimit, mImageRotationLimit);
PointF p = randMultiDrop(sRNG.nextInt(),
@@ -372,46 +806,73 @@ public class PhotoTable extends FrameLayout {
float x = p.x;
float y = p.y;
- log("drop it at " + x + ", " + y);
+ log("drop it at %f, %f", 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 -= mLongSide / 2f;
y -= mShortSide / 2f;
- log("fixed offset is " + x + ", " + y);
+ log("fixed offset is %f, %f ", x, y);
float dx = x - x0;
float dy = y - y0;
- float dist = (float) (Math.sqrt(dx * dx + dy * dy));
+ float dist = (float) Math.hypot(dx, dy);
int duration = (int) (1000f * dist / mThrowSpeed);
duration = Math.max(duration, 1000);
log("animate it");
// toss onto table
+ mAnimating.add(photo);
photo.animate()
- .scaleX(mTableRatio / mImageRatio)
- .scaleY(mTableRatio / mImageRatio)
- .rotation(angle)
- .x(x)
- .y(y)
- .setDuration(duration)
- .setInterpolator(interpolator)
- .withEndAction(new Runnable() {
- @Override
- public void run() {
- if (mOnTable.size() > mTableCapacity) {
- while (mOnTable.size() > (mTableCapacity - mRedealCount)) {
- fadeAway(mOnTable.poll(), false);
- }
- // zero delay because we already waited duration ms
- scheduleNext(0);
- }
- }
- });
+ .withLayer()
+ .scaleX(mTableRatio / mImageRatio)
+ .scaleY(mTableRatio / mImageRatio)
+ .rotation(angle)
+ .x(x)
+ .y(y)
+ .setDuration(duration)
+ .setInterpolator(interpolator)
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ mAnimating.remove(photo);
+ if (!mAnimating.contains(photo)) {
+ moveToBackground(photo);
+ }
+ }
+ });
+ }
+
+ private void moveToBackground(View photo) {
+ if (!isInBackground(photo)) {
+ removeView(photo);
+ mBackground.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ }
+ }
+
+ private void exitStageLeft(View photo) {
+ if (isInBackground(photo)) {
+ mBackground.removeView(photo);
+ } else {
+ removeView(photo);
+ }
+ mStageLeft.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ }
+
+ private void moveToForeground(View photo) {
+ if (isInBackground(photo)) {
+ mBackground.removeView(photo);
+ addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT));
+ }
+ }
+
+ private boolean isInBackground(View photo) {
+ return mBackground.indexOfChild(photo) != -1;
}
/** wrap all orientations to the interval [-180, 180). */
@@ -422,69 +883,112 @@ public class PhotoTable extends FrameLayout {
return result;
}
+ /** Animate the selected photo to the foregound: zooming in to bring it foreward. */
private void pickUp(final View photo) {
float photoWidth = photo.getWidth();
float photoHeight = photo.getHeight();
float scale = Math.min(getHeight() / photoHeight, getWidth() / photoWidth);
+ log("scale is %f", scale);
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 / 600f);
- duration = Math.max(duration, 500);
-
photo.setRotation(wrapAngle(photo.getRotation()));
log("animate it");
- // toss onto table
+ // lift up to the glass for a good look
+ moveToForeground(photo);
photo.animate()
- .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());
- }
- });
+ .withLayer()
+ .rotation(0f)
+ .rotationY(0f)
+ .alpha(1f)
+ .scaleX(scale)
+ .scaleY(scale)
+ .x(x)
+ .y(y)
+ .setDuration(mPickUpDuration)
+ .setInterpolator(new DecelerateInterpolator(2f))
+ .withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ log("endtimes: %f", photo.getX());
+ }
+ });
}
- private void recycle(View photo) {
+ private Bitmap getBitmap(View photo) {
+ if (photo == null) {
+ return null;
+ }
ImageView image = (ImageView) photo;
LayerDrawable layers = (LayerDrawable) image.getDrawable();
+ if (layers == null) {
+ return null;
+ }
BitmapDrawable bitmap = (BitmapDrawable) layers.getDrawable(0);
- bitmap.getBitmap().recycle();
+ if (bitmap == null) {
+ return null;
+ }
+ return bitmap.getBitmap();
+ }
+
+ private void recycle(View photo) {
+ if (photo != null) {
+ removeView(photo);
+ mPhotoSource.recycle(getBitmap(photo));
+ }
}
+ public void setHighlight(View photo, boolean highlighted) {
+ ImageView image = (ImageView) photo;
+ LayerDrawable layers = (LayerDrawable) image.getDrawable();
+ if (highlighted) {
+ layers.getDrawable(1).setColorFilter(mHighlightColor, PorterDuff.Mode.SRC_IN);
+ } else {
+ layers.getDrawable(1).clearColorFilter();
+ }
+ }
+
+ /** Schedule the first launch. Idempotent. */
public void start() {
if (!mStarted) {
log("kick it");
mStarted = true;
- scheduleNext(mDropPeriod);
- launch();
+ scheduleNext(0);
}
}
+ public void refreshSelection() {
+ scheduleSelectionReaper(mMaxFocusTime);
+ }
+
+ public void scheduleSelectionReaper(int delay) {
+ removeCallbacks(mSelectionReaper);
+ postDelayed(mSelectionReaper, delay);
+ }
+
+ public void refreshFocus() {
+ scheduleFocusReaper(mMaxFocusTime);
+ }
+
+ public void scheduleFocusReaper(int delay) {
+ removeCallbacks(mFocusReaper);
+ postDelayed(mFocusReaper, delay);
+ }
+
public void scheduleNext(int delay) {
removeCallbacks(mLauncher);
postDelayed(mLauncher, delay);
}
- private static void log(String message) {
+ private static void log(String message, Object... args) {
if (DEBUG) {
- Log.i(TAG, message);
+ Formatter formatter = new Formatter();
+ formatter.format(message, args);
+ Log.i(TAG, formatter.toString());
}
}
}