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
17package com.android.camera;
18
19import android.os.Handler;
20import android.os.Message;
21import android.view.MotionEvent;
22import android.view.ScaleGestureDetector;
23import android.view.View;
24import android.view.ViewConfiguration;
25
26import com.android.camera.ui.PieRenderer;
27import com.android.camera.ui.RenderOverlay;
28import com.android.camera.ui.ZoomRenderer;
29import com.android.gallery3d.R;
30
31import java.util.ArrayList;
32import java.util.List;
33
34public 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