CameraAppUI.java revision 2b906b8c9754b564d5113c7a342654c82f97f180
1/*
2 * Copyright (C) 2013 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.app;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Matrix;
22import android.graphics.SurfaceTexture;
23import android.util.Log;
24import android.view.GestureDetector;
25import android.view.LayoutInflater;
26import android.view.MotionEvent;
27import android.view.TextureView;
28import android.view.View;
29import android.view.ViewConfiguration;
30import android.widget.FrameLayout;
31import android.widget.ImageView;
32
33import com.android.camera.AnimationManager;
34import com.android.camera.widget.FilmstripLayout;
35import com.android.camera.ui.MainActivityLayout;
36import com.android.camera.ui.ModeListView;
37import com.android.camera.ui.ModeTransitionView;
38import com.android.camera.ui.PreviewOverlay;
39import com.android.camera.ui.PreviewStatusListener;
40import com.android.camera2.R;
41
42/**
43 * CameraAppUI centralizes control of views shared across modules. Whereas module
44 * specific views will be handled in each Module UI. For example, we can now
45 * bring the flash animation and capture animation up from each module to app
46 * level, as these animations are largely the same for all modules.
47 *
48 * This class also serves to disambiguate touch events. It recognizes all the
49 * swipe gestures that happen on the preview by attaching a touch listener to
50 * a full-screen view on top of preview TextureView. Since CameraAppUI has knowledge
51 * of how swipe from each direction should be handled, it can then redirect these
52 * events to appropriate recipient views.
53 */
54public class CameraAppUI implements ModeListView.ModeSwitchListener,
55        TextureView.SurfaceTextureListener {
56    private final static String TAG = "CameraAppUI";
57
58    private final AppController mController;
59    private final boolean mIsCaptureIntent;
60    private final boolean mIsSecureCamera;
61    private final AnimationManager mAnimationManager;
62
63    // Swipe states:
64    private final static int IDLE = 0;
65    private final static int SWIPE_UP = 1;
66    private final static int SWIPE_DOWN = 2;
67    private final static int SWIPE_LEFT = 3;
68    private final static int SWIPE_RIGHT = 4;
69
70    // Touch related measures:
71    private final int mSlop;
72    private final static int SWIPE_TIME_OUT_MS = 500;
73
74    private final static int SHIMMY_DELAY_MS = 1000;
75
76    // Mode cover states:
77    private final static int COVER_HIDDEN = 0;
78    private final static int COVER_SHOWN = 1;
79    private final static int COVER_WILL_HIDE_AT_NEXT_FRAME = 2;
80
81    // App level views:
82    private final FrameLayout mCameraRootView;
83    private final ModeTransitionView mModeTransitionView;
84    private final MainActivityLayout mAppRootView;
85    private final ModeListView mModeListView;
86    private final FilmstripLayout mFilmstripLayout;
87    private TextureView mTextureView;
88    private View mFlashOverlay;
89    private FrameLayout mModuleUI;
90
91    private GestureDetector mGestureDetector;
92    private int mSwipeState = IDLE;
93    private ImageView mPreviewThumbView;
94    private PreviewOverlay mPreviewOverlay;
95    private PreviewStatusListener mPreviewStatusListener;
96    private int mModeCoverState = COVER_HIDDEN;
97
98    public interface AnimationFinishedListener {
99        public void onAnimationFinished(boolean success);
100    }
101
102    private class MyTouchListener implements View.OnTouchListener {
103        private boolean mScaleStarted = false;
104        @Override
105        public boolean onTouch(View v, MotionEvent event) {
106            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
107                mScaleStarted = false;
108            } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
109                mScaleStarted = true;
110            }
111            return (!mScaleStarted) && mGestureDetector.onTouchEvent(event);
112        }
113    }
114
115    /**
116     * This gesture listener finds out the direction of the scroll gestures and
117     * sends them to CameraAppUI to do further handling.
118     */
119    private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
120        private MotionEvent mDown;
121
122        @Override
123        public boolean onScroll(MotionEvent e1, MotionEvent ev, float distanceX, float distanceY) {
124            if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT_MS
125                    || mSwipeState != IDLE) {
126                return true;
127            }
128
129            int deltaX = (int) (ev.getX() - mDown.getX());
130            int deltaY = (int) (ev.getY() - mDown.getY());
131            if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
132                if (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop) {
133                    // Calculate the direction of the swipe.
134                    if (deltaX >= Math.abs(deltaY)) {
135                        // Swipe right.
136                        setSwipeState(SWIPE_RIGHT);
137                    } else if (deltaX <= -Math.abs(deltaY)) {
138                        // Swipe left.
139                        setSwipeState(SWIPE_LEFT);
140                    } else if (deltaY >= Math.abs(deltaX)) {
141                        // Swipe down.
142                        setSwipeState(SWIPE_DOWN);
143                    } else if (deltaY <= -Math.abs(deltaX)) {
144                        // Swipe up.
145                        setSwipeState(SWIPE_UP);
146                    }
147                }
148            }
149            return true;
150        }
151
152        private void setSwipeState(int swipeState) {
153            mSwipeState = swipeState;
154            // Notify new swipe detected.
155            onSwipeDetected(swipeState);
156        }
157
158        @Override
159        public boolean onDown(MotionEvent ev) {
160            mDown = MotionEvent.obtain(ev);
161            mSwipeState = IDLE;
162            return false;
163        }
164    }
165
166    public CameraAppUI(AppController controller, MainActivityLayout appRootView,
167                       boolean isSecureCamera, boolean isCaptureIntent) {
168        mSlop = ViewConfiguration.get(controller.getAndroidContext()).getScaledTouchSlop();
169        mController = controller;
170        mIsSecureCamera = isSecureCamera;
171        mIsCaptureIntent = isCaptureIntent;
172
173        mAppRootView = appRootView;
174        mFilmstripLayout = (FilmstripLayout) appRootView.findViewById(R.id.filmstrip_layout);
175        mCameraRootView = (FrameLayout) appRootView.findViewById(R.id.camera_app_root);
176        mModeTransitionView = (ModeTransitionView)
177                mAppRootView.findViewById(R.id.mode_transition_view);
178        mGestureDetector = new GestureDetector(controller.getAndroidContext(),
179                new MyGestureListener());
180        mModeListView = (ModeListView) appRootView.findViewById(R.id.mode_list_layout);
181        if (mModeListView != null) {
182            mModeListView.setModeSwitchListener(this);
183        } else {
184            Log.e(TAG, "Cannot find mode list in the view hierarchy");
185        }
186        mAnimationManager = new AnimationManager();
187    }
188
189    /**
190     * Redirects touch events to appropriate recipient views based on swipe direction.
191     * More specifically, swipe up and swipe down will be handled by the view that handles
192     * mode transition; swipe left will be send to filmstrip; swipe right will be redirected
193     * to mode list in order to bring up mode list.
194     */
195    private void onSwipeDetected(int swipeState) {
196        if (swipeState == SWIPE_UP || swipeState == SWIPE_DOWN) {
197            // Quick switch between photo/video.
198            if (mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ||
199                    mController.getCurrentModuleIndex() == ModeListView.MODE_VIDEO) {
200                mAppRootView.redirectTouchEventsTo(mModeTransitionView);
201
202                final int moduleToTransitionTo =
203                        mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ?
204                        ModeListView.MODE_VIDEO : ModeListView.MODE_PHOTO;
205                int shadeColorId = ModeListView.getModeThemeColor(moduleToTransitionTo);
206                int iconRes = ModeListView.getModeIconResourceId(moduleToTransitionTo);
207
208                AnimationFinishedListener listener = new AnimationFinishedListener() {
209                    public void onAnimationFinished(boolean success) {
210                        if (success) {
211                            // Go to new module when the previous operation is successful.
212                            mController.onModeSelected(moduleToTransitionTo);
213                            mModeTransitionView.startPeepHoleAnimation();
214                        }
215                    }
216                };
217                if (mSwipeState == SWIPE_UP) {
218                    mModeTransitionView.prepareToPullUpShade(shadeColorId, iconRes, listener);
219                } else {
220                    mModeTransitionView.prepareToPullDownShade(shadeColorId, iconRes, listener);
221                }
222            }
223        } else if (swipeState == SWIPE_LEFT) {
224            // Pass the touch sequence to filmstrip layout.
225            mAppRootView.redirectTouchEventsTo(mFilmstripLayout);
226
227        } else if (swipeState == SWIPE_RIGHT) {
228            // Pass the touch to mode switcher
229            mAppRootView.redirectTouchEventsTo(mModeListView);
230        }
231    }
232
233    /**
234     * Gets called when activity resumes in preview.
235     */
236    public void resume() {
237        if (mTextureView == null || mTextureView.getSurfaceTexture() != null) {
238            mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
239        } else {
240            // Show mode theme cover until preview is ready
241            showModeCoverUntilPreviewReady();
242        }
243    }
244
245    /**
246     * A cover view showing the mode theme color and mode icon will be visible on
247     * top of preview until preview is ready (i.e. camera preview is started and
248     * the first frame has been received).
249     */
250    private void showModeCoverUntilPreviewReady() {
251        int modeId = mController.getCurrentModuleIndex();
252        int colorId = ModeListView.getModeThemeColor(modeId);
253        int iconId = ModeListView.getModeIconResourceId(modeId);
254        mModeTransitionView.setupModeCover(colorId, iconId);
255        mModeCoverState = COVER_SHOWN;
256    }
257
258    private void hideModeCover() {
259        mModeTransitionView.hideModeCover(new AnimationFinishedListener() {
260            @Override
261            public void onAnimationFinished(boolean success) {
262                if (success) {
263                    // Show shimmy in SHIMMY_DELAY_MS
264                    mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
265                }
266            }
267        });
268    }
269
270    /**
271     * Called when the back key is pressed.
272     *
273     * @return Whether the UI responded to the key event.
274     */
275    public boolean onBackPressed() {
276        return mFilmstripLayout.onBackPressed();
277    }
278
279    /**
280     * Sets a {@link com.android.camera.ui.PreviewStatusListener} that
281     * listens to SurfaceTexture changes. In addition, the listener will also provide
282     * a {@link android.view.GestureDetector.OnGestureListener}, which will listen to
283     * gestures that happen on camera preview.
284     *
285     * @param previewStatusListener the listener that gets notified when SurfaceTexture
286     *                              changes
287     */
288    public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) {
289        mPreviewStatusListener = previewStatusListener;
290        if (mPreviewStatusListener != null) {
291            GestureDetector.OnGestureListener gestureListener
292                    = mPreviewStatusListener.getGestureListener();
293            if (gestureListener != null) {
294                mPreviewOverlay.setGestureListener(gestureListener);
295            }
296        }
297    }
298
299    /**
300     * This inflates generic_module layout, which contains all the shared views across
301     * modules. Then each module inflates their own views in the given view group. For
302     * now, this is called every time switching from a not-yet-refactored module to a
303     * refactored module. In the future, this should only need to be done once per app
304     * start.
305     */
306    public void prepareModuleUI() {
307        mCameraRootView.removeAllViews();
308        LayoutInflater inflater = (LayoutInflater) mController.getAndroidContext()
309                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
310        inflater.inflate(R.layout.generic_module, mCameraRootView, true);
311
312        mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout);
313        mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content);
314        mTextureView.setSurfaceTextureListener(this);
315        mPreviewOverlay = (PreviewOverlay) mCameraRootView.findViewById(R.id.preview_overlay);
316        mPreviewOverlay.setOnTouchListener(new MyTouchListener());
317        mFlashOverlay = mCameraRootView.findViewById(R.id.flash_overlay);
318        mPreviewThumbView = (ImageView) mCameraRootView.findViewById(R.id.preview_thumb);
319
320    }
321
322    // TODO: Remove this when refactor is done.
323    // This is here to ensure refactored modules can work with not-yet-refactored ones.
324    public void clearCameraUI() {
325        mCameraRootView.removeAllViews();
326        mModuleUI = null;
327        mTextureView = null;
328        mPreviewOverlay = null;
329        mFlashOverlay = null;
330    }
331
332    /**
333     * Called indirectly from each module in their initialization to get a view group
334     * to inflate the module specific views in.
335     *
336     * @return a view group for modules to attach views to
337     */
338    public FrameLayout getModuleRootView() {
339        // TODO: Change it to mModuleUI when refactor is done
340        return mCameraRootView;
341    }
342
343    /**
344     * Remove all the module specific views.
345     */
346    public void clearModuleUI() {
347        if (mModuleUI != null) {
348            mModuleUI.removeAllViews();
349        }
350
351        // TODO: Bring TextureView up to the app level
352        mTextureView.removeOnLayoutChangeListener(null);
353
354        mPreviewStatusListener = null;
355        mPreviewOverlay.reset();
356    }
357
358    /**
359     * Gets called when preview is started.
360     */
361    public void onPreviewStarted() {
362        if (mModeCoverState == COVER_SHOWN) {
363            mModeCoverState = COVER_WILL_HIDE_AT_NEXT_FRAME;
364        }
365    }
366
367    @Override
368    public void onModeSelected(int modeIndex) {
369        mController.onModeSelected(modeIndex);
370    }
371
372    /**
373     * Sets the transform matrix on the preview TextureView
374     */
375    public void setPreviewTransformMatrix(Matrix transformMatrix) {
376        if (mTextureView == null) {
377            throw new UnsupportedOperationException("Cannot set transform matrix on a null" +
378                    " TextureView");
379        }
380        mTextureView.setTransform(transformMatrix);
381    }
382
383
384    /********************** Capture animation **********************/
385    /* TODO: This session is subject to UX changes. In addition to the generic
386       flash animation and post capture animation, consider designating a parameter
387       for specifying the type of animation, as well as an animation finished listener
388       so that modules can have more knowledge of the status of the animation. */
389
390    /**
391     * Starts the pre-capture animation.
392     */
393    public void startPreCaptureAnimation() {
394        mAnimationManager.startFlashAnimation(mFlashOverlay);
395    }
396
397    /**
398     * Cancels the pre-capture animation.
399     */
400    public void cancelPreCaptureAnimation() {
401        mAnimationManager.cancelAnimations();
402    }
403
404    /**
405     * Starts the post-capture animation with the current preview image.
406     */
407    public void startPostCaptureAnimation() {
408        if (mTextureView == null) {
409            Log.e(TAG, "Cannot get a frame from a null TextureView for animation");
410            return;
411        }
412        // TODO: Down sample bitmap
413        startPostCaptureAnimation(mTextureView.getBitmap());
414    }
415
416    /**
417     * Starts the post-capture animation with the given thumbnail.
418     *
419     * @param thumbnail The thumbnail for the animation.
420     */
421    public void startPostCaptureAnimation(Bitmap thumbnail) {
422        mPreviewThumbView.setImageBitmap(thumbnail);
423        mAnimationManager.startCaptureAnimation(mPreviewThumbView);
424    }
425
426    /**
427     * Cancels the post-capture animation.
428     */
429    public void cancelPostCaptureAnimation() {
430        mAnimationManager.cancelAnimations();
431    }
432
433    /***************************SurfaceTexture Listener*********************************/
434
435    @Override
436    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
437        Log.v(TAG, "SurfaceTexture is available");
438        if (mPreviewStatusListener != null) {
439            mPreviewStatusListener.onSurfaceTextureAvailable(surface, width, height);
440        }
441    }
442
443    @Override
444    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
445        if (mPreviewStatusListener != null) {
446            mPreviewStatusListener.onSurfaceTextureSizeChanged(surface, width, height);
447        }
448    }
449
450    @Override
451    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
452        Log.v(TAG, "SurfaceTexture is destroyed");
453        if (mPreviewStatusListener != null) {
454            return mPreviewStatusListener.onSurfaceTextureDestroyed(surface);
455        }
456        return false;
457    }
458
459    @Override
460    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
461        if (mModeCoverState == COVER_WILL_HIDE_AT_NEXT_FRAME) {
462            hideModeCover();
463            mModeCoverState = COVER_HIDDEN;
464        }
465        if (mPreviewStatusListener != null) {
466            mPreviewStatusListener.onSurfaceTextureUpdated(surface);
467        }
468    }
469}
470