From eb70217554d14807d73cae033b4f756c9b80fe3b Mon Sep 17 00:00:00 2001 From: Chris Wren Date: Thu, 14 Mar 2013 13:34:06 -0400 Subject: add keyboard navigation to photo table daydream arrows: move focus enter: select/deselect x/del: throw away Bug: 8387448 Change-Id: I45d9b2273051abd18aaa82a7e6201196b06f7ce0 --- src/com/android/dreams/phototable/PhotoTable.java | 267 +++++++++++++++++++-- .../dreams/phototable/PhotoTouchListener.java | 22 +- 2 files changed, 254 insertions(+), 35 deletions(-) (limited to 'src') 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.SharedPreferences; 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 @@ 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; + 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 mOnTable; private final int mDropPeriod; private final int mFastDropPeriod; @@ -91,6 +98,9 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { mOnTable = new LinkedList(); 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 class PhotoTable extends FrameLayout { } 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 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { 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 class PhotoTable extends FrameLayout { 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 class PhotoTable extends FrameLayout { }); } - 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 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { 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 @@ public class PhotoTable extends FrameLayout { 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 class PhotoTable extends FrameLayout { } } + 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.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. @@ -127,22 +125,10 @@ public class PhotoTouchListener implements View.OnTouchListener { 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 @@ public class PhotoTouchListener implements View.OnTouchListener { switch (action) { case MotionEvent.ACTION_DOWN: - mTable.moveToBackOfQueue(target); + mTable.moveToTopOfPile(target); mInitialTouchTime = ev.getEventTime(); mA = ev.getPointerId(ev.getActionIndex()); resetTouch(target); -- cgit v1.2.3