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