add keyboard navigation to photo table daydream
arrows: move focus
enter: select/deselect
x/del: throw away
Bug: 8387448
Change-Id: I45d9b2273051abd18aaa82a7e6201196b06f7ce0
diff --git a/res/layout/table.xml b/res/layout/table.xml
index b5663cb..b1575b7 100644
--- a/res/layout/table.xml
+++ b/res/layout/table.xml
@@ -23,6 +23,7 @@
android:id="@+id/table"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:focusable="true"
/>
<!-- View
android:background="@+drawable/vignette_br"
diff --git a/res/values/colors.xml b/res/values/colors.xml
index a6c7746..ec487e7 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -18,4 +18,5 @@
<color name="tabletop_dark">#ff222222</color>
<color name="vignette_light">#00000000</color>
<color name="vignette_dark">#bf000000</color>
+ <color name="highlight_color">@android:color/holo_blue_bright</color>
</resources>
diff --git a/src/com/android/dreams/phototable/PhotoTable.java b/src/com/android/dreams/phototable/PhotoTable.java
index b0d7a01..e5eb007 100644
--- a/src/com/android/dreams/phototable/PhotoTable.java
+++ b/src/com/android/dreams/phototable/PhotoTable.java
@@ -21,22 +21,25 @@
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Color;
import android.graphics.PointF;
+import android.graphics.PorterDuff;
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.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
+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.LinkedList;
import java.util.Random;
@@ -48,22 +51,26 @@
private static final boolean DEBUG = false;
class Launcher implements Runnable {
- private final PhotoTable mTable;
- public Launcher(PhotoTable table) {
- mTable = table;
- }
-
@Override
public void run() {
- mTable.scheduleNext(mDropPeriod);
- mTable.launch();
+ PhotoTable.this.scheduleNext(mDropPeriod);
+ PhotoTable.this.launch();
}
}
- private static final long MAX_SELECTION_TIME = 10000L;
+ class FocusReaper implements Runnable {
+ @Override
+ public void run() {
+ PhotoTable.this.clearFocus();
+ }
+ }
+
+ private static final int MAX_SELECTION_TIME = 10000;
+ private static final int MAX_FOCUS_TIME = 5000;
private static Random sRNG = new Random();
private final Launcher mLauncher;
+ private final FocusReaper mFocusReaper;
private final LinkedList<View> mOnTable;
private final int mDropPeriod;
private final int mFastDropPeriod;
@@ -91,6 +98,9 @@
private int mHeight;
private View mSelected;
private long mSelectedTime;
+ private View mFocused;
+ private long mFocusedTime;
+ private int mHighlightColor;
public PhotoTable(Context context, AttributeSet as) {
super(context, as);
@@ -107,6 +117,7 @@
mTableCapacity = mResources.getInteger(R.integer.table_capacity);
mRedealCount = mResources.getInteger(R.integer.redeal_count);
mTapToExit = mResources.getBoolean(R.bool.enable_tap_to_exit);
+ mHighlightColor = mResources.getColor(R.color.highlight_color);
mThrowInterpolator = new SoftLandingInterpolator(
mResources.getInteger(R.integer.soft_landing_time) / 1000000f,
mResources.getInteger(R.integer.soft_landing_distance) / 1000000f);
@@ -115,7 +126,8 @@
mOnTable = new LinkedList<View>();
mPhotoSource = new PhotoSourcePlexor(getContext(),
getContext().getSharedPreferences(PhotoTableDreamSettings.PREFS_NAME, 0));
- mLauncher = new Launcher(this);
+ mLauncher = new Launcher();
+ mFocusReaper = new FocusReaper();
mStarted = false;
}
@@ -133,20 +145,46 @@
}
public void clearSelection() {
+ if (hasSelection()) {
+ dropOnTable(getSelected());
+ }
mSelected = null;
}
public void setSelection(View selected) {
assert(selected != null);
- if (mSelected != null) {
- dropOnTable(mSelected);
- }
+ clearSelection();
mSelected = selected;
mSelectedTime = System.currentTimeMillis();
- bringChildToFront(selected);
+ moveToTopOfPile(selected);
pickUp(selected);
}
+ public boolean hasFocus() {
+ return mFocused != null;
+ }
+
+ public View getFocused() {
+ return mFocused;
+ }
+
+ public void clearFocus() {
+ if (hasFocus()) {
+ setHighlight(getFocused(), false);
+ }
+ mFocused = null;
+ }
+
+ public void setFocus(View focus) {
+ assert(focus != null);
+ clearFocus();
+ mFocused = focus;
+ mFocusedTime = System.currentTimeMillis();
+ moveToTopOfPile(focus);
+ setHighlight(focus, true);
+ scheduleFocusReaper(MAX_FOCUS_TIME);
+ }
+
static float lerp(float a, float b, float f) {
return (b-a)*f + a;
}
@@ -192,11 +230,141 @@
return p;
}
+ private double cross(double[] a, double[] b) {
+ return a[0] * b[1] - a[1] * b[0];
+ }
+
+ private double dot(double[] a, double[] b) {
+ return a[0] * b[0] + a[1] * b[1];
+ }
+
+ 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 {
+ clearFocus();
+ }
+ } else {
+ setFocus(bestFocus);
+ }
+ }
+ return getFocused();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ final View focus = getFocused();
+ boolean consumed = true;
+
+ if (hasSelection()) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_ESCAPE:
+ setFocus(getSelected());
+ clearSelection();
+ break;
+ default:
+ log("dropped unexpected: " + keyCode);
+ consumed = false;
+ break;
+ }
+ } else {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (hasFocus()) {
+ setSelection(getFocused());
+ clearFocus();
+ } else {
+ setFocus(mOnTable.getLast());
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DEL:
+ case KeyEvent.KEYCODE_X:
+ if (hasFocus()) {
+ fling(getFocused());
+ }
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_K:
+ moveFocus(focus, 0f);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_L:
+ moveFocus(focus, 90f);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ case KeyEvent.KEYCODE_J:
+ moveFocus(focus, 180f);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_H:
+ moveFocus(focus, 270f);
+ break;
+
+ default:
+ log("dropped unexpected: " + keyCode);
+ consumed = false;
+ break;
+ }
+ }
+
+ return consumed;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (hasSelection()) {
- dropOnTable(getSelected());
clearSelection();
} else {
if (mTapToExit && mDream != null) {
@@ -289,7 +457,7 @@
table.addView(photo, new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
if (table.hasSelection()) {
- table.bringChildToFront(table.getSelected());
+ table.moveToTopOfPile(table.getSelected());
}
int width = ((Integer) photo.getTag(R.id.photo_width)).intValue();
int height = ((Integer) photo.getTag(R.id.photo_height)).intValue();
@@ -316,7 +484,6 @@
setSystemUiVisibility(View.STATUS_BAR_HIDDEN);
if (hasSelection() &&
(System.currentTimeMillis() - mSelectedTime) > MAX_SELECTION_TIME) {
- dropOnTable(getSelected());
clearSelection();
} else {
log("inflate it");
@@ -330,6 +497,9 @@
public void fadeAway(final View photo, final boolean replace) {
// fade out of view
mOnTable.remove(photo);
+ if (photo == getFocused()) {
+ clearFocus();
+ }
photo.animate().cancel();
photo.animate()
.withLayer()
@@ -347,7 +517,7 @@
});
}
- public void moveToBackOfQueue(View photo) {
+ public void moveToTopOfPile(View photo) {
// make this photo the last to be removed.
bringChildToFront(photo);
invalidate();
@@ -367,6 +537,54 @@
dropOnTable(photo, mThrowInterpolator);
}
+ 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, true);
+ }
+
+ public void fling(final View photo, float dx, float dy, int duration,
+ boolean flingAway, boolean spin) {
+ if (photo == getFocused()) {
+ if (moveFocus(photo, 0f) == null) {
+ moveFocus(photo, 180f);
+ }
+ }
+ ViewPropertyAnimator animator = photo.animate()
+ .xBy(dx)
+ .yBy(dy)
+ .setDuration(duration)
+ .setInterpolator(new DecelerateInterpolator(2f));
+
+ if (spin) {
+ animator.rotation(mThrowRotation);
+ }
+
+ if (flingAway) {
+ log("fling away");
+ animator.withEndAction(new Runnable() {
+ @Override
+ public void run() {
+ fadeAway(photo, true);
+ }
+ });
+ }
+ }
+
public void dropOnTable(final View photo) {
dropOnTable(photo, mDropInterpolator);
}
@@ -393,7 +611,7 @@
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);
@@ -463,6 +681,16 @@
bitmap.getBitmap().recycle();
}
+ 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();
+ }
+ }
+
public void start() {
if (!mStarted) {
log("kick it");
@@ -472,6 +700,11 @@
}
}
+ public void scheduleFocusReaper(int delay) {
+ removeCallbacks(mFocusReaper);
+ postDelayed(mFocusReaper, delay);
+ }
+
public void scheduleNext(int delay) {
removeCallbacks(mLauncher);
postDelayed(mLauncher, delay);
diff --git a/src/com/android/dreams/phototable/PhotoTouchListener.java b/src/com/android/dreams/phototable/PhotoTouchListener.java
index 8076e72..fd52749 100644
--- a/src/com/android/dreams/phototable/PhotoTouchListener.java
+++ b/src/com/android/dreams/phototable/PhotoTouchListener.java
@@ -21,8 +21,6 @@
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.
@@ -127,22 +125,10 @@
final float halfShortSide =
Math.min(photoWidth * mTableRatio, photoHeight * mTableRatio) / 2f;
final View photo = target;
- ViewPropertyAnimator animator = photo.animate()
- .xBy(x1 - x0)
- .yBy(y1 - y0)
- .setDuration((int) (1000f * n / 60f))
- .setInterpolator(new DecelerateInterpolator(2f));
+ boolean flingAway = y1 + halfShortSide < 0f || y1 - halfShortSide > tableHeight ||
+ x1 + halfShortSide < 0f || x1 - halfShortSide > tableWidth;
- 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);
- }
- });
- }
+ mTable.fling(photo, x1 - x0, y1 - y0, (int) (1000f * n / 60f), flingAway, false);
}
@Override
@@ -158,7 +144,7 @@
switch (action) {
case MotionEvent.ACTION_DOWN:
- mTable.moveToBackOfQueue(target);
+ mTable.moveToTopOfPile(target);
mInitialTouchTime = ev.getEventTime();
mA = ev.getPointerId(ev.getActionIndex());
resetTouch(target);