PhotoTouchListener.java revision c1501041b64faa6c205a93baf403c4c87a0c1acf
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 */ 16package com.android.dreams.phototable; 17 18import android.content.Context; 19import android.content.res.Resources; 20import android.util.Log; 21import android.view.MotionEvent; 22import android.view.View; 23import android.view.ViewConfiguration; 24import android.view.ViewPropertyAnimator; 25import android.view.animation.DecelerateInterpolator; 26 27/** 28 * Touch listener that implements phototable interactions. 29 */ 30public 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 Table 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, Table 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 .withLayer() 132 .xBy(x1 - x0) 133 .yBy(y1 - y0) 134 .setDuration((int) (1000f * n / 60f)) 135 .setInterpolator(new DecelerateInterpolator(2f)); 136 137 if (y1 + halfShortSide < 0f || y1 - halfShortSide > tableHeight || 138 x1 + halfShortSide < 0f || x1 - halfShortSide > tableWidth) { 139 log("fling away"); 140 animator.withEndAction(new Runnable() { 141 @Override 142 public void run() { 143 mTable.fadeAway(photo, true); 144 } 145 }); 146 } 147 } 148 149 @Override 150 public boolean onTouch(View target, MotionEvent ev) { 151 final int action = ev.getActionMasked(); 152 153 // compute raw coordinates 154 for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) { 155 pts[i*2] = ev.getX(i); 156 pts[i*2 + 1] = ev.getY(i); 157 } 158 target.getMatrix().mapPoints(pts); 159 160 switch (action) { 161 case MotionEvent.ACTION_DOWN: 162 mTable.moveToBackOfQueue(target); 163 mInitialTouchTime = ev.getEventTime(); 164 mA = ev.getPointerId(ev.getActionIndex()); 165 resetTouch(target); 166 break; 167 168 case MotionEvent.ACTION_POINTER_DOWN: 169 if (mB == INVALID_POINTER) { 170 mB = ev.getPointerId(ev.getActionIndex()); 171 mInitialTouchA = getAngle(target, ev); 172 } 173 break; 174 175 case MotionEvent.ACTION_POINTER_UP: 176 if (mB == ev.getPointerId(ev.getActionIndex())) { 177 mB = INVALID_POINTER; 178 mInitialTargetA = (float) target.getRotation(); 179 } 180 if (mA == ev.getPointerId(ev.getActionIndex())) { 181 log("primary went up!"); 182 mA = mB; 183 resetTouch(target); 184 mB = INVALID_POINTER; 185 } 186 break; 187 188 case MotionEvent.ACTION_MOVE: { 189 if (mA != INVALID_POINTER) { 190 int idx = ev.findPointerIndex(mA); 191 float x = pts[2 * idx]; 192 float y = pts[2 * idx + 1]; 193 if (mInitialTouchX == -1 && mInitialTouchY == -1) { 194 mInitialTouchX = x; 195 mInitialTouchY = y; 196 } else { 197 float dt = (float) (ev.getEventTime() - mLastEventTime) / 1000f; 198 float tmpDX = (x - mLastTouchX) / dt; 199 float tmpDY = (y - mLastTouchY) / dt; 200 if (dt > 0f && (Math.abs(tmpDX) > 5f || Math.abs(tmpDY) > 5f)) { 201 // work around odd bug with multi-finger flings 202 mDX = tmpDX; 203 mDY = tmpDY; 204 } 205 log("move " + mDX + ", " + mDY); 206 207 mLastEventTime = ev.getEventTime(); 208 mLastTouchX = x; 209 mLastTouchY = y; 210 } 211 212 if (mTable.getSelected() != target) { 213 target.animate().cancel(); 214 215 target.setX((int) (mInitialTargetX + x - mInitialTouchX)); 216 target.setY((int) (mInitialTargetY + y - mInitialTouchY)); 217 if (mManualImageRotation && mB != INVALID_POINTER) { 218 float a = getAngle(target, ev); 219 target.setRotation( 220 (int) (mInitialTargetA + a - mInitialTouchA)); 221 } 222 } 223 } 224 } 225 break; 226 227 case MotionEvent.ACTION_UP: { 228 if (mA != INVALID_POINTER) { 229 int idx = ev.findPointerIndex(mA); 230 float x0 = pts[2 * idx]; 231 float y0 = pts[2 * idx + 1]; 232 if (mInitialTouchX == -1 && mInitialTouchY == -1) { 233 mInitialTouchX = x0; 234 mInitialTouchY = y0; 235 } 236 double distance = Math.hypot(x0 - mInitialTouchX, 237 y0 - mInitialTouchY); 238 if (mTable.getSelected() == target) { 239 mTable.dropOnTable(target); 240 mTable.clearSelection(); 241 } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout && 242 distance < mTouchSlop) { 243 // tap 244 target.animate().cancel(); 245 mTable.setSelection(target); 246 } else { 247 onFling(target, mDX, mDY); 248 } 249 mA = INVALID_POINTER; 250 mB = INVALID_POINTER; 251 } 252 } 253 break; 254 255 case MotionEvent.ACTION_CANCEL: 256 log("action cancel!"); 257 break; 258 } 259 260 return true; 261 } 262 263 private static void log(String message) { 264 if (DEBUG) { 265 Log.i(TAG, message); 266 } 267 } 268} 269