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 17 package com.android.camera; 18 19 import android.os.Handler; 20 import android.os.Message; 21 import android.view.MotionEvent; 22 import android.view.ScaleGestureDetector; 23 import android.view.View; 24 import android.view.ViewConfiguration; 25 26 import com.android.camera.ui.PieRenderer; 27 import com.android.camera.ui.RenderOverlay; 28 import com.android.camera.ui.ZoomRenderer; 29 import com.android.gallery3d.R; 30 31 import java.util.ArrayList; 32 import java.util.List; 33 34 public class PreviewGestures 35 implements ScaleGestureDetector.OnScaleGestureListener { 36 37 private static final String TAG = "CAM_gestures"; 38 39 private static final long TIMEOUT_PIE = 200; 40 private static final int MSG_PIE = 1; 41 private static final int MODE_NONE = 0; 42 private static final int MODE_PIE = 1; 43 private static final int MODE_ZOOM = 2; 44 private static final int MODE_MODULE = 3; 45 private static final int MODE_ALL = 4; 46 private static final int MODE_SWIPE = 5; 47 48 public static final int DIR_UP = 0; 49 public static final int DIR_DOWN = 1; 50 public static final int DIR_LEFT = 2; 51 public static final int DIR_RIGHT = 3; 52 53 private CameraActivity mActivity; 54 private SingleTapListener mTapListener; 55 private RenderOverlay mOverlay; 56 private PieRenderer mPie; 57 private ZoomRenderer mZoom; 58 private MotionEvent mDown; 59 private MotionEvent mCurrent; 60 private ScaleGestureDetector mScale; 61 private List<View> mReceivers; 62 private List<View> mUnclickableAreas; 63 private int mMode; 64 private int mSlop; 65 private int mTapTimeout; 66 private boolean mEnabled; 67 private boolean mZoomOnly; 68 private int mOrientation; 69 private int[] mLocation; 70 private SwipeListener mSwipeListener; 71 72 private Handler mHandler = new Handler() { 73 public void handleMessage(Message msg) { 74 if (msg.what == MSG_PIE) { 75 mMode = MODE_PIE; 76 openPie(); 77 cancelActivityTouchHandling(mDown); 78 } 79 } 80 }; 81 82 public interface SingleTapListener { 83 public void onSingleTapUp(View v, int x, int y); 84 } 85 86 interface SwipeListener { 87 public void onSwipe(int direction); 88 } 89 90 public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener, 91 ZoomRenderer zoom, PieRenderer pie, SwipeListener swipe) { 92 mActivity = ctx; 93 mTapListener = tapListener; 94 mPie = pie; 95 mZoom = zoom; 96 mMode = MODE_ALL; 97 mScale = new ScaleGestureDetector(ctx, this); 98 mSlop = (int) ctx.getResources().getDimension(R.dimen.pie_touch_slop); 99 mTapTimeout = ViewConfiguration.getTapTimeout(); 100 mEnabled = true; 101 mLocation = new int[2]; 102 mSwipeListener = swipe; 103 } 104 105 public void setRenderOverlay(RenderOverlay overlay) { 106 mOverlay = overlay; 107 } 108 109 public void setOrientation(int orientation) { 110 mOrientation = orientation; 111 } 112 113 public void setEnabled(boolean enabled) { 114 mEnabled = enabled; 115 if (!enabled) { 116 cancelPie(); 117 } 118 } 119 120 public void setZoomOnly(boolean zoom) { 121 mZoomOnly = zoom; 122 } 123 124 public void addTouchReceiver(View v) { 125 if (mReceivers == null) { 126 mReceivers = new ArrayList<View>(); 127 } 128 mReceivers.add(v); 129 } 130 131 public void removeTouchReceiver(View v) { 132 if (mReceivers == null || v == null) return; 133 mReceivers.remove(v); 134 } 135 136 public void addUnclickableArea(View v) { 137 if (mUnclickableAreas == null) { 138 mUnclickableAreas = new ArrayList<View>(); 139 } 140 mUnclickableAreas.add(v); 141 } 142 143 public void clearTouchReceivers() { 144 if (mReceivers != null) { 145 mReceivers.clear(); 146 } 147 } 148 149 public void clearUnclickableAreas() { 150 if (mUnclickableAreas != null) { 151 mUnclickableAreas.clear(); 152 } 153 } 154 155 private boolean checkClickable(MotionEvent m) { 156 if (mUnclickableAreas != null) { 157 for (View v : mUnclickableAreas) { 158 if (isInside(m, v)) { 159 return false; 160 } 161 } 162 } 163 return true; 164 } 165 166 public void reset() { 167 clearTouchReceivers(); 168 clearUnclickableAreas(); 169 } 170 171 public boolean dispatchTouch(MotionEvent m) { 172 if (!mEnabled) { 173 return mActivity.superDispatchTouchEvent(m); 174 } 175 mCurrent = m; 176 if (MotionEvent.ACTION_DOWN == m.getActionMasked()) { 177 if (checkReceivers(m)) { 178 mMode = MODE_MODULE; 179 return mActivity.superDispatchTouchEvent(m); 180 } else { 181 mMode = MODE_ALL; 182 mDown = MotionEvent.obtain(m); 183 if (mPie != null && mPie.showsItems()) { 184 mMode = MODE_PIE; 185 return sendToPie(m); 186 } 187 if (mPie != null && !mZoomOnly && checkClickable(m)) { 188 mHandler.sendEmptyMessageDelayed(MSG_PIE, TIMEOUT_PIE); 189 } 190 if (mZoom != null) { 191 mScale.onTouchEvent(m); 192 } 193 // make sure this is ok 194 return mActivity.superDispatchTouchEvent(m); 195 } 196 } else if (mMode == MODE_NONE) { 197 return false; 198 } else if (mMode == MODE_SWIPE) { 199 if (MotionEvent.ACTION_UP == m.getActionMasked()) { 200 mSwipeListener.onSwipe(getSwipeDirection(m)); 201 } 202 return true; 203 } else if (mMode == MODE_PIE) { 204 if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) { 205 sendToPie(makeCancelEvent(m)); 206 if (mZoom != null) { 207 onScaleBegin(mScale); 208 } 209 } else { 210 return sendToPie(m); 211 } 212 return true; 213 } else if (mMode == MODE_ZOOM) { 214 mScale.onTouchEvent(m); 215 if (!mScale.isInProgress() && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) { 216 mMode = MODE_NONE; 217 onScaleEnd(mScale); 218 } 219 return true; 220 } else if (mMode == MODE_MODULE) { 221 return mActivity.superDispatchTouchEvent(m); 222 } else { 223 // didn't receive down event previously; 224 // assume module wasn't initialzed and ignore this event. 225 if (mDown == null) { 226 return true; 227 } 228 if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) { 229 if (!mZoomOnly) { 230 cancelPie(); 231 sendToPie(makeCancelEvent(m)); 232 } 233 if (mZoom != null) { 234 mScale.onTouchEvent(m); 235 onScaleBegin(mScale); 236 } 237 } else if ((mMode == MODE_ZOOM) && !mScale.isInProgress() 238 && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) { 239 // user initiated and stopped zoom gesture without zooming 240 mScale.onTouchEvent(m); 241 onScaleEnd(mScale); 242 } 243 // not zoom or pie mode and no timeout yet 244 if (mZoom != null) { 245 boolean res = mScale.onTouchEvent(m); 246 if (mScale.isInProgress()) { 247 cancelPie(); 248 cancelActivityTouchHandling(m); 249 return res; 250 } 251 } 252 if (MotionEvent.ACTION_UP == m.getActionMasked()) { 253 cancelPie(); 254 // must have been tap 255 if (m.getEventTime() - mDown.getEventTime() < mTapTimeout 256 && checkClickable(m)) { 257 cancelActivityTouchHandling(m); 258 mTapListener.onSingleTapUp(null, 259 (int) mDown.getX() - mOverlay.getWindowPositionX(), 260 (int) mDown.getY() - mOverlay.getWindowPositionY()); 261 return true; 262 } else { 263 return mActivity.superDispatchTouchEvent(m); 264 } 265 } else if (MotionEvent.ACTION_MOVE == m.getActionMasked()) { 266 if ((Math.abs(m.getX() - mDown.getX()) > mSlop) 267 || Math.abs(m.getY() - mDown.getY()) > mSlop) { 268 // moved too far and no timeout yet, no focus or pie 269 cancelPie(); 270 int dir = getSwipeDirection(m); 271 if (dir == DIR_LEFT) { 272 mMode = MODE_MODULE; 273 return mActivity.superDispatchTouchEvent(m); 274 } else { 275 cancelActivityTouchHandling(m); 276 mMode = MODE_NONE; 277 } 278 } 279 } 280 return false; 281 } 282 } 283 284 private boolean checkReceivers(MotionEvent m) { 285 if (mReceivers != null) { 286 for (View receiver : mReceivers) { 287 if (isInside(m, receiver)) { 288 return true; 289 } 290 } 291 } 292 return false; 293 } 294 295 // left tests for finger moving right to left 296 private int getSwipeDirection(MotionEvent m) { 297 float dx = 0; 298 float dy = 0; 299 switch (mOrientation) { 300 case 0: 301 dx = m.getX() - mDown.getX(); 302 dy = m.getY() - mDown.getY(); 303 break; 304 case 90: 305 dx = - (m.getY() - mDown.getY()); 306 dy = m.getX() - mDown.getX(); 307 break; 308 case 180: 309 dx = -(m.getX() - mDown.getX()); 310 dy = m.getY() - mDown.getY(); 311 break; 312 case 270: 313 dx = m.getY() - mDown.getY(); 314 dy = m.getX() - mDown.getX(); 315 break; 316 } 317 if (dx < 0 && (Math.abs(dy) / -dx < 2)) return DIR_LEFT; 318 if (dx > 0 && (Math.abs(dy) / dx < 2)) return DIR_RIGHT; 319 if (dy > 0) return DIR_DOWN; 320 return DIR_UP; 321 } 322 323 private boolean isInside(MotionEvent evt, View v) { 324 v.getLocationInWindow(mLocation); 325 // when view is flipped horizontally 326 if ((int) v.getRotationY() == 180) { 327 mLocation[0] -= v.getWidth(); 328 } 329 // when view is flipped vertically 330 if ((int) v.getRotationX() == 180) { 331 mLocation[1] -= v.getHeight(); 332 } 333 return (v.getVisibility() == View.VISIBLE 334 && evt.getX() >= mLocation[0] && evt.getX() < mLocation[0] + v.getWidth() 335 && evt.getY() >= mLocation[1] && evt.getY() < mLocation[1] + v.getHeight()); 336 } 337 338 public void cancelActivityTouchHandling(MotionEvent m) { 339 mActivity.superDispatchTouchEvent(makeCancelEvent(m)); 340 } 341 342 private MotionEvent makeCancelEvent(MotionEvent m) { 343 MotionEvent c = MotionEvent.obtain(m); 344 c.setAction(MotionEvent.ACTION_CANCEL); 345 return c; 346 } 347 348 private void openPie() { 349 mDown.offsetLocation(-mOverlay.getWindowPositionX(), 350 -mOverlay.getWindowPositionY()); 351 mOverlay.directDispatchTouch(mDown, mPie); 352 } 353 354 private void cancelPie() { 355 mHandler.removeMessages(MSG_PIE); 356 } 357 358 private boolean sendToPie(MotionEvent m) { 359 m.offsetLocation(-mOverlay.getWindowPositionX(), 360 -mOverlay.getWindowPositionY()); 361 return mOverlay.directDispatchTouch(m, mPie); 362 } 363 364 @Override 365 public boolean onScale(ScaleGestureDetector detector) { 366 return mZoom.onScale(detector); 367 } 368 369 @Override 370 public boolean onScaleBegin(ScaleGestureDetector detector) { 371 if (mMode != MODE_ZOOM) { 372 mMode = MODE_ZOOM; 373 cancelActivityTouchHandling(mCurrent); 374 } 375 if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) { 376 return mZoom.onScaleBegin(detector); 377 } else { 378 return true; 379 } 380 } 381 382 @Override 383 public void onScaleEnd(ScaleGestureDetector detector) { 384 if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) { 385 mZoom.onScaleEnd(detector); 386 } 387 } 388 } 389