CameraAppUI.java revision c050a9475312db95bcd6d3ec8480065ee8104954
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.Matrix;
21import android.graphics.RectF;
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.view.ViewGroup;
31import android.widget.FrameLayout;
32import android.widget.FrameLayout.LayoutParams;
33
34import com.android.camera.AnimationManager;
35import com.android.camera.TextureViewHelper;
36import com.android.camera.filmstrip.FilmstripContentPanel;
37import com.android.camera.ui.BottomBar;
38import com.android.camera.ui.CaptureAnimationOverlay;
39import com.android.camera.ui.MainActivityLayout;
40import com.android.camera.ui.ModeListView;
41import com.android.camera.ui.ModeTransitionView;
42import com.android.camera.ui.PreviewOverlay;
43import com.android.camera.ui.PreviewStatusListener;
44import com.android.camera.widget.FilmstripLayout;
45import com.android.camera2.R;
46
47/**
48 * CameraAppUI centralizes control of views shared across modules. Whereas module
49 * specific views will be handled in each Module UI. For example, we can now
50 * bring the flash animation and capture animation up from each module to app
51 * level, as these animations are largely the same for all modules.
52 *
53 * This class also serves to disambiguate touch events. It recognizes all the
54 * swipe gestures that happen on the preview by attaching a touch listener to
55 * a full-screen view on top of preview TextureView. Since CameraAppUI has knowledge
56 * of how swipe from each direction should be handled, it can then redirect these
57 * events to appropriate recipient views.
58 */
59public class CameraAppUI implements ModeListView.ModeSwitchListener,
60        TextureView.SurfaceTextureListener {
61
62    /**
63     * The bottom controls on the filmstrip.
64     */
65    public static interface BottomControls {
66        /** Values for the view state of the button. */
67        public final int VIEW_NONE = 0;
68        public final int VIEW_PHOTO_SPHERE = 1;
69        public final int VIEW_RGBZ = 2;
70
71        /**
72         * Sets a new or replaces an existing listener for bottom control events.
73         */
74        void setListener(Listener listener);
75
76        /**
77         * Set if the bottom controls are visible.
78         * @param visible {@code true} if visible.
79         */
80        void setVisible(boolean visible);
81
82        /**
83         * @param visible Whether the button is visible.
84         */
85        void setEditButtonVisibility(boolean visible);
86
87        /**
88         * @param enabled Whether the button is enabled.
89         */
90        void setEditEnabled(boolean enabled);
91
92        /**
93         * Sets the visibility of the view-photosphere button.
94         *
95         * @param state one of {@link #VIEW_NONE}, {@link #VIEW_PHOTO_SPHERE},
96         *            {@link #VIEW_RGBZ}.
97         */
98        void setViewButtonVisibility(int state);
99
100        /**
101         * @param enabled Whether the button is enabled.
102         */
103        void setViewEnabled(boolean enabled);
104
105        /**
106         * @param visible Whether the button is visible.
107         */
108        void setTinyPlanetButtonVisibility(boolean visible);
109
110        /**
111         * @param enabled Whether the button is enabled.
112         */
113        void setTinyPlanetEnabled(boolean enabled);
114
115        /**
116         * @param visible Whether the button is visible.
117         */
118        void setDeleteButtonVisibility(boolean visible);
119
120        /**
121         * @param enabled Whether the button is enabled.
122         */
123        void setDeleteEnabled(boolean enabled);
124
125        /**
126         * @param visible Whether the button is visible.
127         */
128        void setShareButtonVisibility(boolean visible);
129
130        /**
131         * @param enabled Whether the button is enabled.
132         */
133        void setShareEnabled(boolean enabled);
134
135        /**
136         * @param visible Whether the button is visible.
137         */
138        void setGalleryButtonVisibility(boolean visible);
139
140        /**
141         * Classes implementing this interface can listen for events on the bottom
142         * controls.
143         */
144        public static interface Listener {
145            /**
146             * Called when the user pressed the "view" button to e.g. view a photo
147             * sphere or RGBZ image.
148             */
149            public void onExternalViewer();
150
151            /**
152             * Called when the "edit" button is pressed.
153             */
154            public void onEdit();
155
156            /**
157             * Called when the "tiny planet" button is pressed.
158             */
159            public void onTinyPlanet();
160
161            /**
162             * Called when the "delete" button is pressed.
163             */
164            public void onDelete();
165
166            /**
167             * Called when the "share" button is pressed.
168             */
169            public void onShare();
170
171            /**
172             * Called when the "gallery" button is pressed.
173             */
174            public void onGallery();
175        }
176    }
177
178    private final static String TAG = "CameraAppUI";
179
180    private final AppController mController;
181    private final boolean mIsCaptureIntent;
182    private final boolean mIsSecureCamera;
183    private final AnimationManager mAnimationManager;
184
185    // Swipe states:
186    private final static int IDLE = 0;
187    private final static int SWIPE_UP = 1;
188    private final static int SWIPE_DOWN = 2;
189    private final static int SWIPE_LEFT = 3;
190    private final static int SWIPE_RIGHT = 4;
191
192    // Touch related measures:
193    private final int mSlop;
194    private final static int SWIPE_TIME_OUT_MS = 500;
195
196    private final static int SHIMMY_DELAY_MS = 1000;
197
198    // Mode cover states:
199    private final static int COVER_HIDDEN = 0;
200    private final static int COVER_SHOWN = 1;
201    private final static int COVER_WILL_HIDE_AT_NEXT_FRAME = 2;
202
203    // App level views:
204    private final FrameLayout mCameraRootView;
205    private final ModeTransitionView mModeTransitionView;
206    private final MainActivityLayout mAppRootView;
207    private final ModeListView mModeListView;
208    private final FilmstripLayout mFilmstripLayout;
209    private TextureView mTextureView;
210    private FrameLayout mModuleUI;
211
212    private BottomBar mBottomBar;
213    private TextureViewHelper mTextureViewHelper;
214    private final GestureDetector mGestureDetector;
215    private int mSwipeState = IDLE;
216    private PreviewOverlay mPreviewOverlay;
217    private CaptureAnimationOverlay mCaptureOverlay;
218    private PreviewStatusListener mPreviewStatusListener;
219    private int mModeCoverState = COVER_HIDDEN;
220    private final FilmstripBottomControls mFilmstripBottomControls;
221    private final FilmstripContentPanel mFilmstripPanel;
222    private Runnable mHideCoverRunnable;
223    private final View.OnLayoutChangeListener mPreviewLayoutChangeListener
224            = new View.OnLayoutChangeListener() {
225        @Override
226        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
227                int oldTop, int oldRight, int oldBottom) {
228            if (mPreviewStatusListener != null) {
229                mPreviewStatusListener.onPreviewLayoutChanged(v, left, top, right, bottom, oldLeft,
230                        oldTop, oldRight, oldBottom);
231            }
232        }
233    };
234
235    public void updatePreviewAspectRatio(float aspectRatio) {
236        mTextureViewHelper.updateAspectRatio(aspectRatio);
237    }
238
239    /**
240     * This is to support modules that calculate their own transform matrix because
241     * they need to use a transform matrix to rotate the preview.
242     *
243     * @param matrix transform matrix to be set on the TextureView
244     */
245    public void updatePreviewTransform(Matrix matrix) {
246        mTextureViewHelper.updateTransform(matrix);
247    }
248
249    public interface AnimationFinishedListener {
250        public void onAnimationFinished(boolean success);
251    }
252
253    private class MyTouchListener implements View.OnTouchListener {
254        private boolean mScaleStarted = false;
255        @Override
256        public boolean onTouch(View v, MotionEvent event) {
257            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
258                mScaleStarted = false;
259            } else if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
260                mScaleStarted = true;
261            }
262            return (!mScaleStarted) && mGestureDetector.onTouchEvent(event);
263        }
264    }
265
266    /**
267     * This gesture listener finds out the direction of the scroll gestures and
268     * sends them to CameraAppUI to do further handling.
269     */
270    private class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
271        private MotionEvent mDown;
272
273        @Override
274        public boolean onScroll(MotionEvent e1, MotionEvent ev, float distanceX, float distanceY) {
275            if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT_MS
276                    || mSwipeState != IDLE
277                    || mIsCaptureIntent) {
278                return false;
279            }
280
281            int deltaX = (int) (ev.getX() - mDown.getX());
282            int deltaY = (int) (ev.getY() - mDown.getY());
283            if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
284                if (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop) {
285                    // Calculate the direction of the swipe.
286                    if (deltaX >= Math.abs(deltaY)) {
287                        // Swipe right.
288                        setSwipeState(SWIPE_RIGHT);
289                    } else if (deltaX <= -Math.abs(deltaY)) {
290                        // Swipe left.
291                        setSwipeState(SWIPE_LEFT);
292                    } else if (deltaY >= Math.abs(deltaX)) {
293                        // Swipe down.
294                        setSwipeState(SWIPE_DOWN);
295                    } else if (deltaY <= -Math.abs(deltaX)) {
296                        // Swipe up.
297                        setSwipeState(SWIPE_UP);
298                    }
299                }
300            }
301            return true;
302        }
303
304        private void setSwipeState(int swipeState) {
305            mSwipeState = swipeState;
306            // Notify new swipe detected.
307            onSwipeDetected(swipeState);
308        }
309
310        @Override
311        public boolean onDown(MotionEvent ev) {
312            mDown = MotionEvent.obtain(ev);
313            mSwipeState = IDLE;
314            return false;
315        }
316    }
317
318    public CameraAppUI(AppController controller, MainActivityLayout appRootView,
319                       boolean isSecureCamera, boolean isCaptureIntent) {
320        mSlop = ViewConfiguration.get(controller.getAndroidContext()).getScaledTouchSlop();
321        mController = controller;
322        mIsSecureCamera = isSecureCamera;
323        mIsCaptureIntent = isCaptureIntent;
324
325        mAppRootView = appRootView;
326        mFilmstripLayout = (FilmstripLayout) appRootView.findViewById(R.id.filmstrip_layout);
327        mCameraRootView = (FrameLayout) appRootView.findViewById(R.id.camera_app_root);
328        mModeTransitionView = (ModeTransitionView)
329                mAppRootView.findViewById(R.id.mode_transition_view);
330        mFilmstripBottomControls = new FilmstripBottomControls(
331                (ViewGroup) mAppRootView.findViewById(R.id.filmstrip_bottom_controls));
332        mFilmstripPanel = (FilmstripContentPanel) mAppRootView.findViewById(R.id.filmstrip_layout);
333        mGestureDetector = new GestureDetector(controller.getAndroidContext(),
334                new MyGestureListener());
335        mModeListView = (ModeListView) appRootView.findViewById(R.id.mode_list_layout);
336        if (mModeListView != null) {
337            mModeListView.setModeSwitchListener(this);
338        } else {
339            Log.e(TAG, "Cannot find mode list in the view hierarchy");
340        }
341        mAnimationManager = new AnimationManager();
342    }
343
344    /**
345     * Redirects touch events to appropriate recipient views based on swipe direction.
346     * More specifically, swipe up and swipe down will be handled by the view that handles
347     * mode transition; swipe left will be send to filmstrip; swipe right will be redirected
348     * to mode list in order to bring up mode list.
349     */
350    private void onSwipeDetected(int swipeState) {
351        if (swipeState == SWIPE_UP || swipeState == SWIPE_DOWN) {
352            // Quick switch between photo/video.
353            if (mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ||
354                    mController.getCurrentModuleIndex() == ModeListView.MODE_VIDEO) {
355                mAppRootView.redirectTouchEventsTo(mModeTransitionView);
356
357                final int moduleToTransitionTo =
358                        mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ?
359                        ModeListView.MODE_VIDEO : ModeListView.MODE_PHOTO;
360                int shadeColorId = ModeListView.getModeThemeColor(moduleToTransitionTo);
361                int iconRes = ModeListView.getModeIconResourceId(moduleToTransitionTo);
362
363                AnimationFinishedListener listener = new AnimationFinishedListener() {
364                    @Override
365                    public void onAnimationFinished(boolean success) {
366                        if (success) {
367                            mHideCoverRunnable = new Runnable() {
368                                @Override
369                                public void run() {
370                                    mModeTransitionView.startPeepHoleAnimation();
371                                }
372                            };
373                            mModeCoverState = COVER_SHOWN;
374                            // Go to new module when the previous operation is successful.
375                            mController.onModeSelected(moduleToTransitionTo);
376                        }
377                    }
378                };
379                if (mSwipeState == SWIPE_UP) {
380                    mModeTransitionView.prepareToPullUpShade(shadeColorId, iconRes, listener);
381                } else {
382                    mModeTransitionView.prepareToPullDownShade(shadeColorId, iconRes, listener);
383                }
384            }
385        } else if (swipeState == SWIPE_LEFT) {
386            // Pass the touch sequence to filmstrip layout.
387            mAppRootView.redirectTouchEventsTo(mFilmstripLayout);
388        } else if (swipeState == SWIPE_RIGHT) {
389            // Pass the touch to mode switcher
390            mAppRootView.redirectTouchEventsTo(mModeListView);
391        }
392    }
393
394    /**
395     * Gets called when activity resumes in preview.
396     */
397    public void resume() {
398        if (mTextureView == null || mTextureView.getSurfaceTexture() != null) {
399            if (!mIsCaptureIntent) {
400                mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
401            }
402        } else {
403            // Show mode theme cover until preview is ready
404            showModeCoverUntilPreviewReady();
405        }
406        // Hide action bar first since we are in full screen mode first, and
407        // switch the system UI to lights-out mode.
408        mFilmstripPanel.hide();
409    }
410
411    /**
412     * A cover view showing the mode theme color and mode icon will be visible on
413     * top of preview until preview is ready (i.e. camera preview is started and
414     * the first frame has been received).
415     */
416    private void showModeCoverUntilPreviewReady() {
417        int modeId = mController.getCurrentModuleIndex();
418        int colorId = ModeListView.getModeThemeColor(modeId);
419        int iconId = ModeListView.getModeIconResourceId(modeId);
420        mModeTransitionView.setupModeCover(colorId, iconId);
421        mHideCoverRunnable = new Runnable() {
422            @Override
423            public void run() {
424                mModeTransitionView.hideModeCover(new AnimationFinishedListener() {
425                    @Override
426                    public void onAnimationFinished(boolean success) {
427                        if (success) {
428                            // Show shimmy in SHIMMY_DELAY_MS
429                            if (!mIsCaptureIntent) {
430                                mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
431                            }
432                        }
433                    }
434                });
435            }
436        };
437        mModeCoverState = COVER_SHOWN;
438    }
439
440    private void hideModeCover() {
441        if (mHideCoverRunnable != null) {
442            mAppRootView.post(mHideCoverRunnable);
443            mHideCoverRunnable = null;
444        }
445        mModeCoverState = COVER_HIDDEN;
446    }
447
448    /**
449     * Called when the back key is pressed.
450     *
451     * @return Whether the UI responded to the key event.
452     */
453    public boolean onBackPressed() {
454        if (mFilmstripLayout.getVisibility() == View.VISIBLE) {
455            return mFilmstripLayout.onBackPressed();
456        } else {
457            return mModeListView.onBackPressed();
458        }
459    }
460
461    /**
462     * Sets a {@link com.android.camera.ui.PreviewStatusListener} that
463     * listens to SurfaceTexture changes. In addition, the listener will also provide
464     * a {@link android.view.GestureDetector.OnGestureListener}, which will listen to
465     * gestures that happen on camera preview.
466     *
467     * @param previewStatusListener the listener that gets notified when SurfaceTexture
468     *                              changes
469     */
470    public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) {
471        mPreviewStatusListener = previewStatusListener;
472        if (mPreviewStatusListener != null) {
473            GestureDetector.OnGestureListener gestureListener
474                    = mPreviewStatusListener.getGestureListener();
475            if (gestureListener != null) {
476                mPreviewOverlay.setGestureListener(gestureListener);
477            }
478            mTextureViewHelper.setAutoAdjustTransform(
479                    mPreviewStatusListener.shouldAutoAdjustTransformMatrixOnLayout());
480            if (mPreviewStatusListener.shouldAutoAdjustBottomBar()) {
481                mBottomBar = (BottomBar) mCameraRootView.findViewById(R.id.bottom_bar);
482                mTextureViewHelper.setPreviewSizeChangedListener(mBottomBar);
483            }
484        }
485    }
486
487    /**
488     * This inflates generic_module layout, which contains all the shared views across
489     * modules. Then each module inflates their own views in the given view group. For
490     * now, this is called every time switching from a not-yet-refactored module to a
491     * refactored module. In the future, this should only need to be done once per app
492     * start.
493     */
494    public void prepareModuleUI() {
495        mCameraRootView.removeAllViews();
496        LayoutInflater inflater = (LayoutInflater) mController.getAndroidContext()
497                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
498        inflater.inflate(R.layout.generic_module, mCameraRootView, true);
499
500        mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout);
501        mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content);
502        mTextureViewHelper = new TextureViewHelper(mTextureView);
503        mTextureViewHelper.setSurfaceTextureListener(this);
504        mTextureViewHelper.setOnLayoutChangeListener(mPreviewLayoutChangeListener);
505
506        mPreviewOverlay = (PreviewOverlay) mCameraRootView.findViewById(R.id.preview_overlay);
507        mPreviewOverlay.setOnTouchListener(new MyTouchListener());
508        mCaptureOverlay = (CaptureAnimationOverlay)
509                mCameraRootView.findViewById(R.id.capture_overlay);
510    }
511
512    // TODO: Remove this when refactor is done.
513    // This is here to ensure refactored modules can work with not-yet-refactored ones.
514    public void clearCameraUI() {
515        mCameraRootView.removeAllViews();
516        mModuleUI = null;
517        mTextureView = null;
518        mPreviewOverlay = null;
519    }
520
521    /**
522     * Called indirectly from each module in their initialization to get a view group
523     * to inflate the module specific views in.
524     *
525     * @return a view group for modules to attach views to
526     */
527    public FrameLayout getModuleRootView() {
528        // TODO: Change it to mModuleUI when refactor is done
529        return mCameraRootView;
530    }
531
532    /**
533     * Remove all the module specific views.
534     */
535    public void clearModuleUI() {
536        if (mModuleUI != null) {
537            mModuleUI.removeAllViews();
538        }
539        // TODO: Remove this when bottom bar is at the app level
540        mBottomBar = null;
541        mTextureViewHelper.setPreviewSizeChangedListener(null);
542
543        mPreviewStatusListener = null;
544        mPreviewOverlay.reset();
545    }
546
547    /**
548     * Gets called when preview is started.
549     */
550    public void onPreviewStarted() {
551        if (mModeCoverState == COVER_SHOWN) {
552            mModeCoverState = COVER_WILL_HIDE_AT_NEXT_FRAME;
553        }
554    }
555
556    /**
557     * Gets called when a mode is selected from {@link com.android.camera.ui.ModeListView}
558     *
559     * @param modeIndex mode index of the selected mode
560     */
561    @Override
562    public void onModeSelected(int modeIndex) {
563        mHideCoverRunnable = new Runnable() {
564            @Override
565            public void run() {
566                mModeListView.startModeSelectionAnimation();
567            }
568        };
569        mModeCoverState = COVER_SHOWN;
570
571        int lastIndex = mController.getCurrentModuleIndex();
572        mController.onModeSelected(modeIndex);
573        int currentIndex = mController.getCurrentModuleIndex();
574
575        if (mTextureView == null) {
576            // TODO: Remove this when all the modules use TextureView
577            int temporaryDelay = 600; // ms
578            mModeListView.postDelayed(new Runnable() {
579                @Override
580                public void run() {
581                    hideModeCover();
582                }
583            }, temporaryDelay);
584        } else if (lastIndex == currentIndex) {
585            hideModeCover();
586        }
587    }
588
589    /********************** Capture animation **********************/
590    /* TODO: This session is subject to UX changes. In addition to the generic
591       flash animation and post capture animation, consider designating a parameter
592       for specifying the type of animation, as well as an animation finished listener
593       so that modules can have more knowledge of the status of the animation. */
594
595    /**
596     * Starts the pre-capture animation.
597     */
598    public void startPreCaptureAnimation() {
599        mCaptureOverlay.startFlashAnimation();
600    }
601
602    /**
603     * Cancels the pre-capture animation.
604     */
605    public void cancelPreCaptureAnimation() {
606        mAnimationManager.cancelAnimations();
607    }
608
609    /**
610     * Cancels the post-capture animation.
611     */
612    public void cancelPostCaptureAnimation() {
613        mAnimationManager.cancelAnimations();
614    }
615
616    public FilmstripContentPanel getFilmstripContentPanel() {
617        return mFilmstripPanel;
618    }
619
620    /**
621     * @return The {@link com.android.camera.app.CameraAppUI.BottomControls} on the
622     * bottom of the filmstrip.
623     */
624    public BottomControls getFilmstripBottomControls() {
625        return mFilmstripBottomControls;
626    }
627
628    /**
629     * @param listener The listener for bottom controls.
630     */
631    public void setFilmstripBottomControlsListener(BottomControls.Listener listener) {
632        mFilmstripBottomControls.setListener(listener);
633    }
634
635    /***************************SurfaceTexture Listener*********************************/
636
637    @Override
638    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
639        Log.v(TAG, "SurfaceTexture is available");
640        if (mPreviewStatusListener != null) {
641            mPreviewStatusListener.onSurfaceTextureAvailable(surface, width, height);
642        }
643    }
644
645    @Override
646    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
647        if (mPreviewStatusListener != null) {
648            mPreviewStatusListener.onSurfaceTextureSizeChanged(surface, width, height);
649        }
650    }
651
652    @Override
653    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
654        Log.v(TAG, "SurfaceTexture is destroyed");
655        if (mPreviewStatusListener != null) {
656            return mPreviewStatusListener.onSurfaceTextureDestroyed(surface);
657        }
658        return false;
659    }
660
661    @Override
662    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
663        if (mModeCoverState == COVER_WILL_HIDE_AT_NEXT_FRAME) {
664            hideModeCover();
665            mModeCoverState = COVER_HIDDEN;
666        }
667        if (mPreviewStatusListener != null) {
668            mPreviewStatusListener.onSurfaceTextureUpdated(surface);
669        }
670    }
671}
672