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