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 25 /** 26 * Touch listener that implements phototable interactions. 27 */ 28 public class PhotoTouchListener implements View.OnTouchListener { 29 private static final String TAG = "PhotoTouchListener"; 30 private static final boolean DEBUG = false; 31 private static final int INVALID_POINTER = -1; 32 private static final int MAX_POINTER_COUNT = 10; 33 private final int mTouchSlop; 34 private final int mTapTimeout; 35 private final PhotoTable mTable; 36 private final float mBeta; 37 private final boolean mEnableFling; 38 private final boolean mManualImageRotation; 39 private long mLastEventTime; 40 private float mLastTouchX; 41 private float mLastTouchY; 42 private float mInitialTouchX; 43 private float mInitialTouchY; 44 private float mInitialTouchA; 45 private long mInitialTouchTime; 46 private float mInitialTargetX; 47 private float mInitialTargetY; 48 private float mInitialTargetA; 49 private float mDX; 50 private float mDY; 51 private int mA = INVALID_POINTER; 52 private int mB = INVALID_POINTER; 53 private float[] pts = new float[MAX_POINTER_COUNT]; 54 55 public PhotoTouchListener(Context context, PhotoTable table) { 56 mTable = table; 57 final ViewConfiguration configuration = ViewConfiguration.get(context); 58 mTouchSlop = configuration.getScaledTouchSlop(); 59 mTapTimeout = ViewConfiguration.getTapTimeout(); 60 final Resources resources = context.getResources(); 61 mBeta = resources.getInteger(R.integer.table_damping) / 1000000f; 62 mEnableFling = resources.getBoolean(R.bool.enable_fling); 63 mManualImageRotation = resources.getBoolean(R.bool.enable_manual_image_rotation); 64 } 65 66 /** Get angle defined by first two touches, in degrees */ 67 private float getAngle(View target, MotionEvent ev) { 68 float alpha = 0f; 69 int a = ev.findPointerIndex(mA); 70 int b = ev.findPointerIndex(mB); 71 if (a >=0 && b >=0) { 72 alpha = (float) (Math.atan2(pts[2*a + 1] - pts[2*b + 1], 73 pts[2*a] - pts[2*b]) * 74 180f / Math.PI); 75 } 76 return alpha; 77 } 78 79 private void resetTouch(View target) { 80 mInitialTouchX = -1; 81 mInitialTouchY = -1; 82 mInitialTouchA = 0f; 83 mInitialTargetX = (float) target.getX(); 84 mInitialTargetY = (float) target.getY(); 85 mInitialTargetA = (float) target.getRotation(); 86 } 87 88 public void onFling(View target, float dX, float dY) { 89 if (!mEnableFling) { 90 return; 91 } 92 log("fling " + dX + ", " + dY); 93 94 // convert to pixel per frame 95 dX /= 60f; 96 dY /= 60f; 97 98 // starting position compionents in global corrdinate frame 99 final float x0 = pts[0]; 100 final float y0 = pts[1]; 101 102 // velocity 103 final float v = (float) Math.hypot(dX, dY); 104 105 if (v == 0f) { 106 return; 107 } 108 109 // number of steps to come to a stop 110 final float n = (float) Math.max(1.0, (- Math.log(v) / Math.log(mBeta))); 111 // distance travelled before stopping 112 final float s = (float) Math.max(0.0, (v * (1f - Math.pow(mBeta, n)) / (1f - mBeta))); 113 114 // ending posiiton after stopping 115 final float x1 = x0 + s * dX / v; 116 final float y1 = y0 + s * dY / v; 117 118 mTable.fling(target, x1 - x0, y1 - y0, (int) (1000f * n / 60f), false); 119 } 120 121 @Override 122 public boolean onTouch(View target, MotionEvent ev) { 123 final int action = ev.getActionMasked(); 124 125 // compute raw coordinates 126 for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) { 127 pts[i*2] = ev.getX(i); 128 pts[i*2 + 1] = ev.getY(i); 129 } 130 target.getMatrix().mapPoints(pts); 131 132 switch (action) { 133 case MotionEvent.ACTION_DOWN: 134 mTable.moveToTopOfPile(target); 135 mInitialTouchTime = ev.getEventTime(); 136 mA = ev.getPointerId(ev.getActionIndex()); 137 resetTouch(target); 138 break; 139 140 case MotionEvent.ACTION_POINTER_DOWN: 141 if (mB == INVALID_POINTER) { 142 mB = ev.getPointerId(ev.getActionIndex()); 143 mInitialTouchA = getAngle(target, ev); 144 } 145 break; 146 147 case MotionEvent.ACTION_POINTER_UP: 148 if (mB == ev.getPointerId(ev.getActionIndex())) { 149 mB = INVALID_POINTER; 150 mInitialTargetA = (float) target.getRotation(); 151 } 152 if (mA == ev.getPointerId(ev.getActionIndex())) { 153 log("primary went up!"); 154 mA = mB; 155 resetTouch(target); 156 mB = INVALID_POINTER; 157 } 158 break; 159 160 case MotionEvent.ACTION_MOVE: { 161 if (mA != INVALID_POINTER) { 162 int idx = ev.findPointerIndex(mA); 163 float x = pts[2 * idx]; 164 float y = pts[2 * idx + 1]; 165 if (mInitialTouchX == -1 && mInitialTouchY == -1) { 166 mInitialTouchX = x; 167 mInitialTouchY = y; 168 } else { 169 float dt = (float) (ev.getEventTime() - mLastEventTime) / 1000f; 170 float tmpDX = (x - mLastTouchX) / dt; 171 float tmpDY = (y - mLastTouchY) / dt; 172 if (dt > 0f && (Math.abs(tmpDX) > 5f || Math.abs(tmpDY) > 5f)) { 173 // work around odd bug with multi-finger flings 174 mDX = tmpDX; 175 mDY = tmpDY; 176 } 177 log("move " + mDX + ", " + mDY); 178 179 mLastEventTime = ev.getEventTime(); 180 mLastTouchX = x; 181 mLastTouchY = y; 182 } 183 184 if (!mTable.hasSelection()) { 185 float rotation = target.getRotation(); 186 if (mManualImageRotation && mB != INVALID_POINTER) { 187 float a = getAngle(target, ev); 188 rotation = mInitialTargetA + a - mInitialTouchA; 189 } 190 mTable.move(target, 191 mInitialTargetX + x - mInitialTouchX, 192 mInitialTargetY + y - mInitialTouchY, 193 rotation); 194 } 195 } 196 } 197 break; 198 199 case MotionEvent.ACTION_UP: { 200 if (mA != INVALID_POINTER) { 201 int idx = ev.findPointerIndex(mA); 202 float x0 = pts[2 * idx]; 203 float y0 = pts[2 * idx + 1]; 204 if (mInitialTouchX == -1 && mInitialTouchY == -1) { 205 mInitialTouchX = x0; 206 mInitialTouchY = y0; 207 } 208 double distance = Math.hypot(x0 - mInitialTouchX, 209 y0 - mInitialTouchY); 210 if (mTable.hasSelection()) { 211 if (distance < mTouchSlop) { 212 mTable.clearSelection(); 213 } else { 214 if ((x0 - mInitialTouchX) > 0f) { 215 mTable.selectPrevious(); 216 } else { 217 mTable.selectNext(); 218 } 219 } 220 } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout && 221 distance < mTouchSlop) { 222 // tap 223 mTable.setSelection(target); 224 } else { 225 onFling(target, mDX, mDY); 226 } 227 mA = INVALID_POINTER; 228 mB = INVALID_POINTER; 229 } 230 } 231 break; 232 233 case MotionEvent.ACTION_CANCEL: 234 log("action cancel!"); 235 break; 236 } 237 238 return true; 239 } 240 241 private static void log(String message) { 242 if (DEBUG) { 243 Log.i(TAG, message); 244 } 245 } 246 } 247