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.util.Log;
22import android.view.MotionEvent;
23import android.view.ScaleGestureDetector;
24import android.view.View;
25import android.view.ViewConfiguration;
26
27import com.android.camera.ui.PieRenderer;
28import com.android.camera.ui.RenderOverlay;
29import com.android.camera.ui.ZoomRenderer;
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
47    private CameraActivity mActivity;
48    private CameraModule mModule;
49    private RenderOverlay mOverlay;
50    private PieRenderer mPie;
51    private ZoomRenderer mZoom;
52    private MotionEvent mDown;
53    private MotionEvent mCurrent;
54    private ScaleGestureDetector mScale;
55    private List<View> mReceivers;
56    private int mMode;
57    private int mSlop;
58    private int mTapTimeout;
59    private boolean mEnabled;
60    private boolean mZoomOnly;
61    private int mOrientation;
62    private int[] mLocation;
63
64    private Handler mHandler = new Handler() {
65        public void handleMessage(Message msg) {
66            if (msg.what == MSG_PIE) {
67                mMode = MODE_PIE;
68                openPie();
69                cancelActivityTouchHandling(mDown);
70            }
71        }
72    };
73
74    public PreviewGestures(CameraActivity ctx, CameraModule module,
75            ZoomRenderer zoom, PieRenderer pie) {
76        mActivity = ctx;
77        mModule = module;
78        mPie = pie;
79        mZoom = zoom;
80        mMode = MODE_ALL;
81        mScale = new ScaleGestureDetector(ctx, this);
82        mSlop = (int) ctx.getResources().getDimension(R.dimen.pie_touch_slop);
83        mTapTimeout = ViewConfiguration.getTapTimeout();
84        mEnabled = true;
85        mLocation = new int[2];
86    }
87
88    public void setRenderOverlay(RenderOverlay overlay) {
89        mOverlay = overlay;
90    }
91
92    public void setOrientation(int orientation) {
93        mOrientation = orientation;
94    }
95
96    public void setEnabled(boolean enabled) {
97        mEnabled = enabled;
98        if (!enabled) {
99            cancelPie();
100        }
101    }
102
103    public void setZoomOnly(boolean zoom) {
104        mZoomOnly = zoom;
105    }
106
107    public void addTouchReceiver(View v) {
108        if (mReceivers == null) {
109            mReceivers = new ArrayList<View>();
110        }
111        mReceivers.add(v);
112    }
113
114    public void clearTouchReceivers() {
115        if (mReceivers != null) {
116            mReceivers.clear();
117        }
118    }
119
120    public boolean dispatchTouch(MotionEvent m) {
121        if (!mEnabled) {
122            return mActivity.superDispatchTouchEvent(m);
123        }
124        mCurrent = m;
125        if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
126            if (checkReceivers(m)) {
127                mMode = MODE_MODULE;
128                return mActivity.superDispatchTouchEvent(m);
129            } else {
130                mMode = MODE_ALL;
131                mDown = MotionEvent.obtain(m);
132                if (mPie != null && mPie.showsItems()) {
133                    mMode = MODE_PIE;
134                    return sendToPie(m);
135                }
136                if (mPie != null && !mZoomOnly) {
137                    mHandler.sendEmptyMessageDelayed(MSG_PIE, TIMEOUT_PIE);
138                }
139                if (mZoom != null) {
140                    mScale.onTouchEvent(m);
141                }
142                // make sure this is ok
143                return mActivity.superDispatchTouchEvent(m);
144            }
145        } else if (mMode == MODE_NONE) {
146            return false;
147        } else if (mMode == MODE_PIE) {
148            if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
149                sendToPie(makeCancelEvent(m));
150                if (mZoom != null) {
151                    onScaleBegin(mScale);
152                }
153            } else {
154                return sendToPie(m);
155            }
156            return true;
157        } else if (mMode == MODE_ZOOM) {
158            mScale.onTouchEvent(m);
159            if (!mScale.isInProgress() && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
160                mMode = MODE_NONE;
161                onScaleEnd(mScale);
162            }
163            return true;
164        } else if (mMode == MODE_MODULE) {
165            return mActivity.superDispatchTouchEvent(m);
166        } else {
167            // didn't receive down event previously;
168            // assume module wasn't initialzed and ignore this event.
169            if (mDown == null) {
170                return true;
171            }
172            if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
173                if (!mZoomOnly) {
174                    cancelPie();
175                    sendToPie(makeCancelEvent(m));
176                }
177                if (mZoom != null) {
178                    mScale.onTouchEvent(m);
179                    onScaleBegin(mScale);
180                }
181            } else if ((mMode == MODE_ZOOM) && !mScale.isInProgress()
182                    && MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
183                // user initiated and stopped zoom gesture without zooming
184                mScale.onTouchEvent(m);
185                onScaleEnd(mScale);
186            }
187            // not zoom or pie mode and no timeout yet
188            if (mZoom != null) {
189                boolean res = mScale.onTouchEvent(m);
190                if (mScale.isInProgress()) {
191                    cancelPie();
192                    cancelActivityTouchHandling(m);
193                    return res;
194                }
195            }
196            if (MotionEvent.ACTION_UP == m.getActionMasked()) {
197                cancelPie();
198                cancelActivityTouchHandling(m);
199                // must have been tap
200                if (m.getEventTime() - mDown.getEventTime() < mTapTimeout) {
201                    mModule.onSingleTapUp(null,
202                            (int) mDown.getX() - mOverlay.getWindowPositionX(),
203                            (int) mDown.getY() - mOverlay.getWindowPositionY());
204                    return true;
205                } else {
206                    return mActivity.superDispatchTouchEvent(m);
207                }
208            } else if (MotionEvent.ACTION_MOVE == m.getActionMasked()) {
209                if ((Math.abs(m.getX() - mDown.getX()) > mSlop)
210                        || Math.abs(m.getY() - mDown.getY()) > mSlop) {
211                    // moved too far and no timeout yet, no focus or pie
212                    cancelPie();
213                    if (isSwipe(m, true)) {
214                        mMode = MODE_MODULE;
215                        return mActivity.superDispatchTouchEvent(m);
216                    } else {
217                        cancelActivityTouchHandling(m);
218                        if (isSwipe(m , false)) {
219                            mMode = MODE_NONE;
220                        } else if (!mZoomOnly) {
221                            mMode = MODE_PIE;
222                            openPie();
223                            sendToPie(m);
224                        }
225                    }
226                }
227            }
228            return false;
229        }
230    }
231
232    private boolean checkReceivers(MotionEvent m) {
233        if (mReceivers != null) {
234            for (View receiver : mReceivers) {
235                if (isInside(m, receiver)) {
236                    return true;
237                }
238            }
239        }
240        return false;
241    }
242
243    // left tests for finger moving right to left
244    private boolean isSwipe(MotionEvent m, boolean left) {
245        float dx = 0;
246        float dy = 0;
247        switch (mOrientation) {
248        case 0:
249            dx = m.getX() - mDown.getX();
250            dy = Math.abs(m.getY() - mDown.getY());
251            break;
252        case 90:
253            dx = - (m.getY() - mDown.getY());
254            dy = Math.abs(m.getX() - mDown.getX());
255            break;
256        case 180:
257            dx = -(m.getX() - mDown.getX());
258            dy = Math.abs(m.getY() - mDown.getY());
259            break;
260        case 270:
261            dx = m.getY() - mDown.getY();
262            dy = Math.abs(m.getX() - mDown.getX());
263            break;
264        }
265        if (left) {
266            return (dx < 0 && dy / -dx < 0.6f);
267        } else {
268            return (dx > 0 && dy / dx < 0.6f);
269        }
270    }
271
272    private boolean isInside(MotionEvent evt, View v) {
273        v.getLocationInWindow(mLocation);
274        return (v.getVisibility() == View.VISIBLE
275                && evt.getX() >= mLocation[0] && evt.getX() < mLocation[0] + v.getWidth()
276                && evt.getY() >= mLocation[1] && evt.getY() < mLocation[1] + v.getHeight());
277    }
278
279    public void cancelActivityTouchHandling(MotionEvent m) {
280        mActivity.superDispatchTouchEvent(makeCancelEvent(m));
281    }
282
283    private MotionEvent makeCancelEvent(MotionEvent m) {
284        MotionEvent c = MotionEvent.obtain(m);
285        c.setAction(MotionEvent.ACTION_CANCEL);
286        return c;
287    }
288
289    private void openPie() {
290        mDown.offsetLocation(-mOverlay.getWindowPositionX(),
291                -mOverlay.getWindowPositionY());
292        mOverlay.directDispatchTouch(mDown, mPie);
293    }
294
295    private void cancelPie() {
296        mHandler.removeMessages(MSG_PIE);
297    }
298
299    private boolean sendToPie(MotionEvent m) {
300        m.offsetLocation(-mOverlay.getWindowPositionX(),
301                -mOverlay.getWindowPositionY());
302        return mOverlay.directDispatchTouch(m, mPie);
303    }
304
305    @Override
306    public boolean onScale(ScaleGestureDetector detector) {
307        return mZoom.onScale(detector);
308    }
309
310    @Override
311    public boolean onScaleBegin(ScaleGestureDetector detector) {
312        if (mMode != MODE_ZOOM) {
313            mMode = MODE_ZOOM;
314            cancelActivityTouchHandling(mCurrent);
315        }
316        if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) {
317            return mZoom.onScaleBegin(detector);
318        } else {
319            return true;
320        }
321    }
322
323    @Override
324    public void onScaleEnd(ScaleGestureDetector detector) {
325        if (mCurrent.getActionMasked() != MotionEvent.ACTION_MOVE) {
326            mZoom.onScaleEnd(detector);
327        }
328    }
329}
330