CameraAppUI.java revision b9b7240d62dd2ad245f2c0e09fd1056a28687978
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                return false;
278            }
279
280            int deltaX = (int) (ev.getX() - mDown.getX());
281            int deltaY = (int) (ev.getY() - mDown.getY());
282            if (ev.getActionMasked() == MotionEvent.ACTION_MOVE) {
283                if (Math.abs(deltaX) > mSlop || Math.abs(deltaY) > mSlop) {
284                    // Calculate the direction of the swipe.
285                    if (deltaX >= Math.abs(deltaY)) {
286                        // Swipe right.
287                        setSwipeState(SWIPE_RIGHT);
288                    } else if (deltaX <= -Math.abs(deltaY)) {
289                        // Swipe left.
290                        setSwipeState(SWIPE_LEFT);
291                    } else if (deltaY >= Math.abs(deltaX)) {
292                        // Swipe down.
293                        setSwipeState(SWIPE_DOWN);
294                    } else if (deltaY <= -Math.abs(deltaX)) {
295                        // Swipe up.
296                        setSwipeState(SWIPE_UP);
297                    }
298                }
299            }
300            return true;
301        }
302
303        private void setSwipeState(int swipeState) {
304            mSwipeState = swipeState;
305            // Notify new swipe detected.
306            onSwipeDetected(swipeState);
307        }
308
309        @Override
310        public boolean onDown(MotionEvent ev) {
311            mDown = MotionEvent.obtain(ev);
312            mSwipeState = IDLE;
313            return false;
314        }
315    }
316
317    public CameraAppUI(AppController controller, MainActivityLayout appRootView,
318                       boolean isSecureCamera, boolean isCaptureIntent) {
319        mSlop = ViewConfiguration.get(controller.getAndroidContext()).getScaledTouchSlop();
320        mController = controller;
321        mIsSecureCamera = isSecureCamera;
322        mIsCaptureIntent = isCaptureIntent;
323
324        mAppRootView = appRootView;
325        mFilmstripLayout = (FilmstripLayout) appRootView.findViewById(R.id.filmstrip_layout);
326        mCameraRootView = (FrameLayout) appRootView.findViewById(R.id.camera_app_root);
327        mModeTransitionView = (ModeTransitionView)
328                mAppRootView.findViewById(R.id.mode_transition_view);
329        mFilmstripBottomControls = new FilmstripBottomControls(
330                (ViewGroup) mAppRootView.findViewById(R.id.filmstrip_bottom_controls));
331        mFilmstripPanel = (FilmstripContentPanel) mAppRootView.findViewById(R.id.filmstrip_layout);
332        mGestureDetector = new GestureDetector(controller.getAndroidContext(),
333                new MyGestureListener());
334        mModeListView = (ModeListView) appRootView.findViewById(R.id.mode_list_layout);
335        if (mModeListView != null) {
336            mModeListView.setModeSwitchListener(this);
337        } else {
338            Log.e(TAG, "Cannot find mode list in the view hierarchy");
339        }
340        mAnimationManager = new AnimationManager();
341    }
342
343    /**
344     * Redirects touch events to appropriate recipient views based on swipe direction.
345     * More specifically, swipe up and swipe down will be handled by the view that handles
346     * mode transition; swipe left will be send to filmstrip; swipe right will be redirected
347     * to mode list in order to bring up mode list.
348     */
349    private void onSwipeDetected(int swipeState) {
350        if (swipeState == SWIPE_UP || swipeState == SWIPE_DOWN) {
351            // Quick switch between photo/video.
352            if (mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ||
353                    mController.getCurrentModuleIndex() == ModeListView.MODE_VIDEO) {
354                mAppRootView.redirectTouchEventsTo(mModeTransitionView);
355
356                final int moduleToTransitionTo =
357                        mController.getCurrentModuleIndex() == ModeListView.MODE_PHOTO ?
358                        ModeListView.MODE_VIDEO : ModeListView.MODE_PHOTO;
359                int shadeColorId = ModeListView.getModeThemeColor(moduleToTransitionTo);
360                int iconRes = ModeListView.getModeIconResourceId(moduleToTransitionTo);
361
362                AnimationFinishedListener listener = new AnimationFinishedListener() {
363                    @Override
364                    public void onAnimationFinished(boolean success) {
365                        if (success) {
366                            mHideCoverRunnable = new Runnable() {
367                                @Override
368                                public void run() {
369                                    mModeTransitionView.startPeepHoleAnimation();
370                                }
371                            };
372                            mModeCoverState = COVER_SHOWN;
373                            // Go to new module when the previous operation is successful.
374                            mController.onModeSelected(moduleToTransitionTo);
375                        }
376                    }
377                };
378                if (mSwipeState == SWIPE_UP) {
379                    mModeTransitionView.prepareToPullUpShade(shadeColorId, iconRes, listener);
380                } else {
381                    mModeTransitionView.prepareToPullDownShade(shadeColorId, iconRes, listener);
382                }
383            }
384        } else if (swipeState == SWIPE_LEFT) {
385            // Pass the touch sequence to filmstrip layout.
386            mAppRootView.redirectTouchEventsTo(mFilmstripLayout);
387
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            mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
400        } else {
401            // Show mode theme cover until preview is ready
402            showModeCoverUntilPreviewReady();
403        }
404        // Hide action bar first since we are in full screen mode first, and
405        // switch the system UI to lights-out mode.
406        mFilmstripPanel.hide();
407    }
408
409    /**
410     * A cover view showing the mode theme color and mode icon will be visible on
411     * top of preview until preview is ready (i.e. camera preview is started and
412     * the first frame has been received).
413     */
414    private void showModeCoverUntilPreviewReady() {
415        int modeId = mController.getCurrentModuleIndex();
416        int colorId = ModeListView.getModeThemeColor(modeId);
417        int iconId = ModeListView.getModeIconResourceId(modeId);
418        mModeTransitionView.setupModeCover(colorId, iconId);
419        mHideCoverRunnable = new Runnable() {
420            @Override
421            public void run() {
422                mModeTransitionView.hideModeCover(new AnimationFinishedListener() {
423                    @Override
424                    public void onAnimationFinished(boolean success) {
425                        if (success) {
426                            // Show shimmy in SHIMMY_DELAY_MS
427                            mModeListView.startAccordionAnimationWithDelay(SHIMMY_DELAY_MS);
428                        }
429                    }
430                });
431            }
432        };
433        mModeCoverState = COVER_SHOWN;
434    }
435
436    private void hideModeCover() {
437        if (mHideCoverRunnable != null) {
438            mAppRootView.post(mHideCoverRunnable);
439            mHideCoverRunnable = null;
440        }
441        mModeCoverState = COVER_HIDDEN;
442    }
443
444    /**
445     * Called when the back key is pressed.
446     *
447     * @return Whether the UI responded to the key event.
448     */
449    public boolean onBackPressed() {
450        if (mFilmstripLayout.getVisibility() == View.VISIBLE) {
451            return mFilmstripLayout.onBackPressed();
452        } else {
453            return mModeListView.onBackPressed();
454        }
455    }
456
457    /**
458     * Sets a {@link com.android.camera.ui.PreviewStatusListener} that
459     * listens to SurfaceTexture changes. In addition, the listener will also provide
460     * a {@link android.view.GestureDetector.OnGestureListener}, which will listen to
461     * gestures that happen on camera preview.
462     *
463     * @param previewStatusListener the listener that gets notified when SurfaceTexture
464     *                              changes
465     */
466    public void setPreviewStatusListener(PreviewStatusListener previewStatusListener) {
467        mPreviewStatusListener = previewStatusListener;
468        if (mPreviewStatusListener != null) {
469            GestureDetector.OnGestureListener gestureListener
470                    = mPreviewStatusListener.getGestureListener();
471            if (gestureListener != null) {
472                mPreviewOverlay.setGestureListener(gestureListener);
473            }
474            mTextureViewHelper.setAutoAdjustTransform(
475                    mPreviewStatusListener.shouldAutoAdjustTransformMatrixOnLayout());
476            if (mPreviewStatusListener.shouldAutoAdjustBottomBar()) {
477                mBottomBar = (BottomBar) mCameraRootView.findViewById(R.id.bottom_bar);
478                mTextureViewHelper.setPreviewSizeChangedListener(mBottomBar);
479            }
480        }
481    }
482
483    /**
484     * This inflates generic_module layout, which contains all the shared views across
485     * modules. Then each module inflates their own views in the given view group. For
486     * now, this is called every time switching from a not-yet-refactored module to a
487     * refactored module. In the future, this should only need to be done once per app
488     * start.
489     */
490    public void prepareModuleUI() {
491        mCameraRootView.removeAllViews();
492        LayoutInflater inflater = (LayoutInflater) mController.getAndroidContext()
493                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
494        inflater.inflate(R.layout.generic_module, mCameraRootView, true);
495
496        mModuleUI = (FrameLayout) mCameraRootView.findViewById(R.id.module_layout);
497        mTextureView = (TextureView) mCameraRootView.findViewById(R.id.preview_content);
498        mTextureViewHelper = new TextureViewHelper(mTextureView);
499        mTextureViewHelper.setSurfaceTextureListener(this);
500        mTextureViewHelper.setOnLayoutChangeListener(mPreviewLayoutChangeListener);
501
502        mPreviewOverlay = (PreviewOverlay) mCameraRootView.findViewById(R.id.preview_overlay);
503        mPreviewOverlay.setOnTouchListener(new MyTouchListener());
504        mCaptureOverlay = (CaptureAnimationOverlay)
505                mCameraRootView.findViewById(R.id.capture_overlay);
506    }
507
508    // TODO: Remove this when refactor is done.
509    // This is here to ensure refactored modules can work with not-yet-refactored ones.
510    public void clearCameraUI() {
511        mCameraRootView.removeAllViews();
512        mModuleUI = null;
513        mTextureView = null;
514        mPreviewOverlay = null;
515    }
516
517    /**
518     * Called indirectly from each module in their initialization to get a view group
519     * to inflate the module specific views in.
520     *
521     * @return a view group for modules to attach views to
522     */
523    public FrameLayout getModuleRootView() {
524        // TODO: Change it to mModuleUI when refactor is done
525        return mCameraRootView;
526    }
527
528    /**
529     * Remove all the module specific views.
530     */
531    public void clearModuleUI() {
532        if (mModuleUI != null) {
533            mModuleUI.removeAllViews();
534        }
535        // TODO: Remove this when bottom bar is at the app level
536        mBottomBar = null;
537        mTextureViewHelper.setPreviewSizeChangedListener(null);
538
539        mPreviewStatusListener = null;
540        mPreviewOverlay.reset();
541    }
542
543    /**
544     * Gets called when preview is started.
545     */
546    public void onPreviewStarted() {
547        if (mModeCoverState == COVER_SHOWN) {
548            mModeCoverState = COVER_WILL_HIDE_AT_NEXT_FRAME;
549        }
550    }
551
552    /**
553     * Gets called when a mode is selected from {@link com.android.camera.ui.ModeListView}
554     *
555     * @param modeIndex mode index of the selected mode
556     */
557    @Override
558    public void onModeSelected(int modeIndex) {
559        mHideCoverRunnable = new Runnable() {
560            @Override
561            public void run() {
562                mModeListView.startModeSelectionAnimation();
563            }
564        };
565        mModeCoverState = COVER_SHOWN;
566
567        int lastIndex = mController.getCurrentModuleIndex();
568        mController.onModeSelected(modeIndex);
569        int currentIndex = mController.getCurrentModuleIndex();
570
571        if (mTextureView == null) {
572            // TODO: Remove this when all the modules use TextureView
573            int temporaryDelay = 600; // ms
574            mModeListView.postDelayed(new Runnable() {
575                @Override
576                public void run() {
577                    hideModeCover();
578                }
579            }, temporaryDelay);
580        } else if (lastIndex == currentIndex) {
581            hideModeCover();
582        }
583    }
584
585    /********************** Capture animation **********************/
586    /* TODO: This session is subject to UX changes. In addition to the generic
587       flash animation and post capture animation, consider designating a parameter
588       for specifying the type of animation, as well as an animation finished listener
589       so that modules can have more knowledge of the status of the animation. */
590
591    /**
592     * Starts the pre-capture animation.
593     */
594    public void startPreCaptureAnimation() {
595        mCaptureOverlay.startFlashAnimation();
596    }
597
598    /**
599     * Cancels the pre-capture animation.
600     */
601    public void cancelPreCaptureAnimation() {
602        mAnimationManager.cancelAnimations();
603    }
604
605    /**
606     * Cancels the post-capture animation.
607     */
608    public void cancelPostCaptureAnimation() {
609        mAnimationManager.cancelAnimations();
610    }
611
612    public FilmstripContentPanel getFilmstripContentPanel() {
613        return mFilmstripPanel;
614    }
615
616    /**
617     * @return The {@link com.android.camera.app.CameraAppUI.BottomControls} on the
618     * bottom of the filmstrip.
619     */
620    public BottomControls getFilmstripBottomControls() {
621        return mFilmstripBottomControls;
622    }
623
624    /**
625     * @param listener The listener for bottom controls.
626     */
627    public void setFilmstripBottomControlsListener(BottomControls.Listener listener) {
628        mFilmstripBottomControls.setListener(listener);
629    }
630
631    /***************************SurfaceTexture Listener*********************************/
632
633    @Override
634    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
635        Log.v(TAG, "SurfaceTexture is available");
636        if (mPreviewStatusListener != null) {
637            mPreviewStatusListener.onSurfaceTextureAvailable(surface, width, height);
638        }
639    }
640
641    @Override
642    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
643        if (mPreviewStatusListener != null) {
644            mPreviewStatusListener.onSurfaceTextureSizeChanged(surface, width, height);
645        }
646    }
647
648    @Override
649    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
650        Log.v(TAG, "SurfaceTexture is destroyed");
651        if (mPreviewStatusListener != null) {
652            return mPreviewStatusListener.onSurfaceTextureDestroyed(surface);
653        }
654        return false;
655    }
656
657    @Override
658    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
659        if (mModeCoverState == COVER_WILL_HIDE_AT_NEXT_FRAME) {
660            hideModeCover();
661            mModeCoverState = COVER_HIDDEN;
662        }
663        if (mPreviewStatusListener != null) {
664            mPreviewStatusListener.onSurfaceTextureUpdated(surface);
665        }
666    }
667}
668