Home | History | Annotate | Download | only in phototable
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.dreams.phototable;
     17 
     18 import android.content.Context;
     19 import android.content.res.Resources;
     20 import android.util.Log;
     21 import android.view.MotionEvent;
     22 import android.view.View;
     23 import android.view.ViewConfiguration;
     24 import android.view.ViewPropertyAnimator;
     25 import android.view.animation.DecelerateInterpolator;
     26 
     27 /**
     28  * Touch listener that implements phototable interactions.
     29  */
     30 public class PhotoTouchListener implements View.OnTouchListener {
     31     private static final String TAG = "PhotoTouchListener";
     32     private static final boolean DEBUG = false;
     33     private static final int INVALID_POINTER = -1;
     34     private static final int MAX_POINTER_COUNT = 10;
     35     private final int mTouchSlop;
     36     private final int mTapTimeout;
     37     private final PhotoTable mTable;
     38     private final float mBeta;
     39     private final float mTableRatio;
     40     private final boolean mEnableFling;
     41     private final boolean mManualImageRotation;
     42     private long mLastEventTime;
     43     private float mLastTouchX;
     44     private float mLastTouchY;
     45     private float mInitialTouchX;
     46     private float mInitialTouchY;
     47     private float mInitialTouchA;
     48     private long mInitialTouchTime;
     49     private float mInitialTargetX;
     50     private float mInitialTargetY;
     51     private float mInitialTargetA;
     52     private float mDX;
     53     private float mDY;
     54     private int mA = INVALID_POINTER;
     55     private int mB = INVALID_POINTER;
     56     private float[] pts = new float[MAX_POINTER_COUNT];
     57     private float[] tmp = new float[MAX_POINTER_COUNT];
     58 
     59     public PhotoTouchListener(Context context, PhotoTable table) {
     60         mTable = table;
     61         final ViewConfiguration configuration = ViewConfiguration.get(context);
     62         mTouchSlop = configuration.getScaledTouchSlop();
     63         mTapTimeout = configuration.getTapTimeout();
     64         final Resources resources = context.getResources();
     65         mBeta = resources.getInteger(R.integer.table_damping) / 1000000f;
     66         mTableRatio = resources.getInteger(R.integer.table_ratio) / 1000000f;
     67         mEnableFling = resources.getBoolean(R.bool.enable_fling);
     68         mManualImageRotation = resources.getBoolean(R.bool.enable_manual_image_rotation);
     69     }
     70 
     71     /** Get angle defined by first two touches, in degrees */
     72     private float getAngle(View target, MotionEvent ev) {
     73         float alpha = 0f;
     74         int a = ev.findPointerIndex(mA);
     75         int b = ev.findPointerIndex(mB);
     76         if (a >=0 && b >=0) {
     77             alpha = (float) (Math.atan2(pts[2*a + 1] - pts[2*b + 1],
     78                                         pts[2*a] - pts[2*b]) *
     79                              180f / Math.PI);
     80         }
     81         return alpha;
     82     }
     83 
     84     private void resetTouch(View target) {
     85         mInitialTouchX = -1;
     86         mInitialTouchY = -1;
     87         mInitialTouchA = 0f;
     88         mInitialTargetX = (float) target.getX();
     89         mInitialTargetY = (float) target.getY();
     90         mInitialTargetA = (float) target.getRotation();
     91     }
     92 
     93     public void onFling(View target, float dX, float dY) {
     94         if (!mEnableFling) {
     95             return;
     96         }
     97         log("fling " + dX + ", " + dY);
     98 
     99         // convert to pixel per frame
    100         dX /= 60f;
    101         dY /= 60f;
    102 
    103         // starting position compionents in global corrdinate frame
    104         final float x0 = pts[0];
    105         final float y0 = pts[1];
    106 
    107         // velocity
    108         final float v = (float) Math.hypot(dX, dY);
    109 
    110         if (v == 0f) {
    111             return;
    112         }
    113 
    114         // number of steps to come to a stop
    115         final float n = (float) Math.max(1.0, (- Math.log(v) / Math.log(mBeta)));
    116         // distance travelled before stopping
    117         final float s = (float) Math.max(0.0, (v * (1f - Math.pow(mBeta, n)) / (1f - mBeta)));
    118 
    119         // ending posiiton after stopping
    120         final float x1 = x0 + s * dX / v;
    121         final float y1 = y0 + s * dY / v;
    122 
    123         final float photoWidth = ((Integer) target.getTag(R.id.photo_width)).floatValue();
    124         final float photoHeight = ((Integer) target.getTag(R.id.photo_height)).floatValue();
    125         final float tableWidth = mTable.getWidth();
    126         final float tableHeight = mTable.getHeight();
    127         final float halfShortSide =
    128                 Math.min(photoWidth * mTableRatio, photoHeight * mTableRatio) / 2f;
    129         final View photo = target;
    130         ViewPropertyAnimator animator = photo.animate()
    131                 .xBy(x1 - x0)
    132                 .yBy(y1 - y0)
    133                 .setDuration((int) (1000f * n / 60f))
    134                 .setInterpolator(new DecelerateInterpolator(2f));
    135 
    136         if (y1 + halfShortSide < 0f || y1 - halfShortSide > tableHeight ||
    137             x1 + halfShortSide < 0f || x1 - halfShortSide > tableWidth) {
    138             log("fling away");
    139             animator.withEndAction(new Runnable() {
    140                     @Override
    141                     public void run() {
    142                         mTable.fadeAway(photo, true);
    143                     }
    144                 });
    145         }
    146     }
    147 
    148     @Override
    149     public boolean onTouch(View target, MotionEvent ev) {
    150         final int action = ev.getActionMasked();
    151 
    152         // compute raw coordinates
    153         for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) {
    154             pts[i*2] = ev.getX(i);
    155             pts[i*2 + 1] = ev.getY(i);
    156         }
    157         target.getMatrix().mapPoints(pts);
    158 
    159         switch (action) {
    160         case MotionEvent.ACTION_DOWN:
    161             mTable.moveToBackOfQueue(target);
    162             mInitialTouchTime = ev.getEventTime();
    163             mA = ev.getPointerId(ev.getActionIndex());
    164             resetTouch(target);
    165             break;
    166 
    167         case MotionEvent.ACTION_POINTER_DOWN:
    168             if (mB == INVALID_POINTER) {
    169                 mB = ev.getPointerId(ev.getActionIndex());
    170                 mInitialTouchA = getAngle(target, ev);
    171             }
    172             break;
    173 
    174         case MotionEvent.ACTION_POINTER_UP:
    175             if (mB == ev.getPointerId(ev.getActionIndex())) {
    176                 mB = INVALID_POINTER;
    177                 mInitialTargetA = (float) target.getRotation();
    178             }
    179             if (mA == ev.getPointerId(ev.getActionIndex())) {
    180                 log("primary went up!");
    181                 mA = mB;
    182                 resetTouch(target);
    183                 mB = INVALID_POINTER;
    184             }
    185             break;
    186 
    187         case MotionEvent.ACTION_MOVE: {
    188                 if (mA != INVALID_POINTER) {
    189                     int idx = ev.findPointerIndex(mA);
    190                     float x = pts[2 * idx];
    191                     float y = pts[2 * idx + 1];
    192                     if (mInitialTouchX == -1 && mInitialTouchY == -1) {
    193                         mInitialTouchX = x;
    194                         mInitialTouchY = y;
    195                     } else {
    196                         float dt = (float) (ev.getEventTime() - mLastEventTime) / 1000f;
    197                         float tmpDX = (x - mLastTouchX) / dt;
    198                         float tmpDY = (y - mLastTouchY) / dt;
    199                         if (dt > 0f && (Math.abs(tmpDX) > 5f || Math.abs(tmpDY) > 5f)) {
    200                             // work around odd bug with multi-finger flings
    201                             mDX = tmpDX;
    202                             mDY = tmpDY;
    203                         }
    204                         log("move " + mDX + ", " + mDY);
    205 
    206                         mLastEventTime = ev.getEventTime();
    207                         mLastTouchX = x;
    208                         mLastTouchY = y;
    209                     }
    210 
    211                     if (mTable.getSelected() != target) {
    212                         target.animate().cancel();
    213 
    214                         target.setX((int) (mInitialTargetX + x - mInitialTouchX));
    215                         target.setY((int) (mInitialTargetY + y - mInitialTouchY));
    216                         if (mManualImageRotation && mB != INVALID_POINTER) {
    217                             float a = getAngle(target, ev);
    218                             target.setRotation(
    219                                     (int) (mInitialTargetA + a - mInitialTouchA));
    220                         }
    221                     }
    222                 }
    223             }
    224             break;
    225 
    226         case MotionEvent.ACTION_UP: {
    227                 if (mA != INVALID_POINTER) {
    228                     int idx = ev.findPointerIndex(mA);
    229                     float x0 = pts[2 * idx];
    230                     float y0 = pts[2 * idx + 1];
    231                     if (mInitialTouchX == -1 && mInitialTouchY == -1) {
    232                         mInitialTouchX = x0;
    233                         mInitialTouchY = y0;
    234                     }
    235                     double distance = Math.hypot(x0 - mInitialTouchX,
    236                                                  y0 - mInitialTouchY);
    237                     if (mTable.getSelected() == target) {
    238                         mTable.dropOnTable(target);
    239                         mTable.clearSelection();
    240                     } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout &&
    241                                distance < mTouchSlop) {
    242                         // tap
    243                         target.animate().cancel();
    244                         mTable.setSelection(target);
    245                     } else {
    246                         onFling(target, mDX, mDY);
    247                     }
    248                     mA = INVALID_POINTER;
    249                     mB = INVALID_POINTER;
    250                 }
    251             }
    252             break;
    253 
    254         case MotionEvent.ACTION_CANCEL:
    255             log("action cancel!");
    256             break;
    257         }
    258 
    259         return true;
    260     }
    261 
    262     private static void log(String message) {
    263         if (DEBUG) {
    264             Log.i(TAG, message);
    265         }
    266     }
    267 }
    268