BottomBar.java revision a1fab413bcbe5f62ae1d829bac0539519fef96a6
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.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.graphics.Canvas;
25import android.graphics.Path;
26import android.graphics.Paint;
27import android.graphics.RectF;
28import android.graphics.drawable.Drawable;
29import android.graphics.drawable.TransitionDrawable;
30import android.util.AttributeSet;
31import android.util.TypedValue;
32import android.view.Gravity;
33import android.view.MotionEvent;
34import android.view.View;
35import android.widget.FrameLayout;
36import android.widget.ImageButton;
37import android.widget.LinearLayout;
38
39import com.android.camera.ShutterButton;
40import com.android.camera.MultiToggleImageButton;
41import com.android.camera.ToggleImageButton;
42import com.android.camera.util.Gusterpolator;
43import com.android.camera2.R;
44
45
46/**
47 * BottomBar swaps its width and height on rotation. In addition, it also changes
48 * gravity and layout orientation based on the new orientation. Specifically, in
49 * landscape it aligns to the right side of its parent and lays out its children
50 * vertically, whereas in portrait, it stays at the bottom of the parent and has
51 * a horizontal layout orientation.
52*/
53public class BottomBar extends FrameLayout
54    implements PreviewStatusListener.PreviewAreaSizeChangedListener,
55               PreviewOverlay.OnPreviewTouchedListener {
56
57    private static final String TAG = "BottomBar";
58
59    private static final int BOTTOMBAR_OPTIONS_TIMEOUT_MS = 2000;
60
61    private static final int CIRCLE_ANIM_DURATION_MS = 300;
62
63    private static final int MODE_CAPTURE = 0;
64    private static final int MODE_OPTIONS = 1;
65    private static final int MODE_INTENT = 2;
66    private static final int MODE_INTENT_REVIEW = 3;
67    private int mMode;
68
69    private int mWidth;
70    private int mHeight;
71    private float mOffsetShorterEdge;
72    private float mOffsetLongerEdge;
73
74    private final int mOptimalHeight;
75    private boolean mOverLayBottomBar;
76
77    private ToggleImageButton mOptionsToggle;
78
79    private TopRightMostOverlay mOptionsOverlay;
80    private TopRightWeightedLayout mOptionsLayout;
81    private FrameLayout mCaptureLayout;
82    private TopRightWeightedLayout mIntentLayout;
83    private boolean mIsCaptureIntent = false;
84
85    /**
86     * A generic Runnable for setting the options toggle
87     * to the capture layout state and performing the state
88     * transition.
89     */
90    private final Runnable mCloseOptionsRunnable =
91        new Runnable() {
92            @Override
93            public void run() {
94                if (mOptionsToggle != null) {
95                    mOptionsToggle.setState(0, true);
96                }
97            }
98        };
99
100    private ShutterButton mShutterButton;
101
102    private int mBackgroundColor;
103    private int mBackgroundPressedColor;
104    private int mBackgroundAlpha = 0xff;
105
106    private final Paint mCirclePaint = new Paint();
107    private final Path mCirclePath = new Path();
108    private boolean mDrawCircle;
109    private final float mCircleRadius;
110    private final Path mRectPath = new Path();
111
112    public BottomBar(Context context, AttributeSet attrs) {
113        super(context, attrs);
114        mOptimalHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_optimal);
115        mCircleRadius = getResources()
116            .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2;
117        mCirclePaint.setAntiAlias(true);
118    }
119
120    private void setPaintColor(int alpha, int color, boolean isCaptureChange) {
121        int computedColor = (alpha << 24) | (color & 0x00ffffff);
122        mCirclePaint.setColor(computedColor);
123        if (mOptionsToggle == null) {
124            mOptionsToggle = (ToggleImageButton) findViewById(R.id.bottombar_options_toggle);
125        }
126        if (!isCaptureChange) {
127            mOptionsToggle.setBackgroundColor(computedColor);
128        }
129        invalidate();
130    }
131
132    private void setPaintColor(int alpha, int color) {
133        setPaintColor(alpha, color, false);
134    }
135
136    @Override
137    public void onFinishInflate() {
138        mOptionsOverlay
139            = (TopRightMostOverlay) findViewById(R.id.bottombar_options_overlay);
140        mOptionsLayout
141            = (TopRightWeightedLayout) findViewById(R.id.bottombar_options);
142        mCaptureLayout
143            = (FrameLayout) findViewById(R.id.bottombar_capture);
144        mIntentLayout
145            = (TopRightWeightedLayout) findViewById(R.id.bottombar_intent);
146
147        mShutterButton
148            = (ShutterButton) findViewById(R.id.shutter_button);
149        mShutterButton.setOnTouchListener(new OnTouchListener() {
150            @Override
151            public boolean onTouch(View v, MotionEvent event) {
152                if (MotionEvent.ACTION_DOWN == event.getActionMasked()) {
153                    setPaintColor(mBackgroundAlpha, mBackgroundPressedColor, true);
154                    invalidate();
155                } else if (MotionEvent.ACTION_UP == event.getActionMasked()) {
156                    setPaintColor(mBackgroundAlpha, mBackgroundColor, true);
157                    invalidate();
158                }
159
160                return false;
161            }
162        });
163
164        mOptionsOverlay.setOnTouchListener(new View.OnTouchListener() {
165            @Override
166            public boolean onTouch(View v, MotionEvent event) {
167                if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
168                    // close options immediately.
169                    closeModeOptionsDelayed(BOTTOMBAR_OPTIONS_TIMEOUT_MS);
170                }
171                // Let touch event reach mode options or shutter.
172                return false;
173            }
174        });
175    }
176
177    @Override
178    public void onPreviewTouched(MotionEvent ev) {
179        // close options immediately.
180        closeModeOptionsDelayed(0);
181    }
182
183    /**
184     * Schedule (or re-schedule) the options menu to be closed
185     * after a number of milliseconds.  If the options menu
186     * is already closed, nothing is scheduled.
187     */
188    private void closeModeOptionsDelayed(int milliseconds) {
189        // Check that the bottom bar options are visible.
190        if (mOptionsLayout.getVisibility() != View.VISIBLE) {
191            return;
192        }
193
194        // Remove queued callbacks.
195        removeCallbacks(mCloseOptionsRunnable);
196
197        // Close the bottom bar options view in n milliseconds.
198        postDelayed(mCloseOptionsRunnable, milliseconds);
199    }
200
201    /**
202     * Initializes the bottom bar toggle for switching between
203     * capture and the bottom bar options.
204     */
205    public void setupToggle(boolean isCaptureIntent) {
206        mIsCaptureIntent = isCaptureIntent;
207
208        // Of type ToggleImageButton because ToggleButton
209        // has a non-removable spacing for text on the right-hand side.
210        mOptionsToggle = (ToggleImageButton) findViewById(R.id.bottombar_options_toggle);
211        mOptionsToggle.setState(0, false);
212        mOptionsToggle.setOnStateChangeListener(new ToggleImageButton.OnStateChangeListener() {
213            @Override
214            public void stateChanged(View view, boolean toOptions) {
215                if (toOptions) {
216                    if (mIsCaptureIntent) {
217                        hideIntentLayout();
218                    }
219                    transitionToOptions();
220                } else {
221                    if (mIsCaptureIntent) {
222                        transitionToIntentLayout();
223                    } else {
224                        transitionToCapture();
225                    }
226                }
227            }
228        });
229        mOptionsOverlay.setReferenceViewParent(mOptionsLayout);
230    }
231
232    /**
233     * Hide the intent layout.  This is necessary for switching between
234     * the intent capture layout and the bottom bar options.
235     */
236    private void hideIntentLayout() {
237        mIntentLayout.setVisibility(View.INVISIBLE);
238    }
239
240    /**
241     * Perform a transition from the bottom bar options layout to the
242     * bottom bar capture layout.
243     */
244    public void transitionToCapture() {
245        mOptionsOverlay.setVisibility(View.VISIBLE);
246        mOptionsLayout.setVisibility(View.INVISIBLE);
247        mCaptureLayout.setVisibility(View.VISIBLE);
248        if (mMode == MODE_INTENT || mMode == MODE_INTENT_REVIEW) {
249            mIntentLayout.setVisibility(View.INVISIBLE);
250        }
251
252        mMode = MODE_CAPTURE;
253    }
254
255    /**
256     * Perform a transition from the bottom bar capture layout to the
257     * bottom bar options layout.
258     */
259    public void transitionToOptions() {
260        mCaptureLayout.setVisibility(View.INVISIBLE);
261        mOptionsLayout.setVisibility(View.VISIBLE);
262
263        mMode = MODE_OPTIONS;
264    }
265
266    /**
267     * Perform a transition to the global intent layout.  The current
268     * layout state of the bottom bar is irrelevant.
269     */
270    public void transitionToIntentLayout() {
271        mCaptureLayout.setVisibility(View.VISIBLE);
272        mOptionsLayout.setVisibility(View.INVISIBLE);
273        mOptionsOverlay.setVisibility(View.VISIBLE);
274        mIntentLayout.setVisibility(View.VISIBLE);
275
276        View button;
277        button = mIntentLayout.findViewById(R.id.done_button);
278        button.setVisibility(View.INVISIBLE);
279        button = mIntentLayout.findViewById(R.id.retake_button);
280        button.setVisibility(View.INVISIBLE);
281
282        mMode = MODE_INTENT;
283    }
284
285    /**
286     * Perform a transition to the global intent review layout.
287     * The current layout state of the bottom bar is irrelevant.
288     */
289    public void transitionToIntentReviewLayout() {
290        mCaptureLayout.setVisibility(View.INVISIBLE);
291        mOptionsLayout.setVisibility(View.INVISIBLE);
292        mOptionsOverlay.setVisibility(View.INVISIBLE);
293
294        View button;
295        button = mIntentLayout.findViewById(R.id.done_button);
296        button.setVisibility(View.VISIBLE);
297        button = mIntentLayout.findViewById(R.id.retake_button);
298        button.setVisibility(View.VISIBLE);
299        mIntentLayout.setVisibility(View.VISIBLE);
300
301        mMode = MODE_INTENT_REVIEW;
302    }
303
304    private void setButtonImageLevels(int level) {
305        ((MultiToggleImageButton) findViewById(R.id.flash_toggle_button)).setImageLevel(level);
306        ((MultiToggleImageButton) findViewById(R.id.camera_toggle_button)).setImageLevel(level);
307        ((MultiToggleImageButton) findViewById(R.id.hdr_plus_toggle_button)).setImageLevel(level);
308        ((MultiToggleImageButton) findViewById(R.id.refocus_toggle_button)).setImageLevel(level);
309        ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level);
310        ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level);
311        ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level);
312        mOptionsToggle.setImageLevel(level);
313    }
314
315    @Override
316    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
317        mWidth = MeasureSpec.getSize(widthMeasureSpec);
318        mHeight = MeasureSpec.getSize(heightMeasureSpec);
319        if (mWidth == 0 || mHeight == 0) {
320            return;
321        }
322
323        if (mOffsetShorterEdge != 0 && mOffsetLongerEdge != 0) {
324            float previewAspectRatio =
325                    mOffsetLongerEdge / mOffsetShorterEdge;
326            if (previewAspectRatio < 1.0) {
327                previewAspectRatio = 1.0f/previewAspectRatio;
328            }
329            float screenAspectRatio = (float) mWidth / (float) mHeight;
330            if (screenAspectRatio < 1.0) {
331                screenAspectRatio = 1.0f/screenAspectRatio;
332            }
333            if (previewAspectRatio >= screenAspectRatio) {
334                mOverLayBottomBar = true;
335                setBackgroundAlpha(128);
336                setButtonImageLevels(1);
337            } else {
338                mOverLayBottomBar = false;
339                setBackgroundAlpha(255);
340                setButtonImageLevels(0);
341            }
342        }
343
344        // Calculates the width and height needed for the bar.
345        int barWidth, barHeight;
346        if (mWidth > mHeight) {
347            // TODO: The bottom bar should not need to care about the
348            // the type of its parent.  Handle this in the parent layout.
349            ((LinearLayout.LayoutParams) getLayoutParams()).gravity = Gravity.RIGHT;
350            if ((mOffsetLongerEdge == 0 && mOffsetShorterEdge == 0) || mOverLayBottomBar) {
351                barWidth = mOptimalHeight;
352                barHeight = mHeight;
353            } else {
354                barWidth = (int) (mWidth - mOffsetLongerEdge);
355                barHeight = mHeight;
356            }
357        } else {
358            ((LinearLayout.LayoutParams) getLayoutParams()).gravity = Gravity.BOTTOM;
359            if ((mOffsetLongerEdge == 0 && mOffsetShorterEdge == 0) || mOverLayBottomBar) {
360                barWidth = mWidth;
361                barHeight = mOptimalHeight;
362            } else {
363                barWidth = mWidth;
364                barHeight = (int) (mHeight - mOffsetLongerEdge);
365            }
366        }
367
368        super.onMeasure(MeasureSpec.makeMeasureSpec(barWidth, MeasureSpec.EXACTLY),
369                MeasureSpec.makeMeasureSpec(barHeight, MeasureSpec.EXACTLY));
370    }
371
372    @Override
373    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
374        super.onLayout(changed, left, top, right, bottom);
375
376        int width = right - left;
377        int height = bottom - top;
378
379        if (changed) {
380            mCirclePath.reset();
381            mCirclePath.addCircle(
382                width/2,
383                height/2,
384                (int)(diagonalLength(width, height)/2),
385                Path.Direction.CW);
386
387            int shortEdge = mOptionsToggle.getWidth();
388            if (mOptionsToggle.getHeight() < shortEdge) {
389                shortEdge = mOptionsToggle.getHeight();
390            }
391            mRectPath.reset();
392            if (width > height) {
393                mRectPath.addRect(
394                    0.0f,
395                    0.0f,
396                    (float) width - shortEdge,
397                    (float) height,
398                    Path.Direction.CW);
399            } else {
400                mRectPath.addRect(
401                    0.0f,
402                    (float) shortEdge,
403                    (float) width,
404                    (float) height,
405                    Path.Direction.CW);
406            }
407        }
408    }
409
410    private void adjustBottomBar(float scaledTextureWidth,
411                                 float scaledTextureHeight) {
412        setOffset(scaledTextureWidth, scaledTextureHeight);
413    }
414
415    @Override
416    public void onPreviewAreaSizeChanged(RectF previewArea) {
417        adjustBottomBar(previewArea.width(), previewArea.height());
418    }
419
420    private void setOffset(float scaledTextureWidth, float scaledTextureHeight) {
421        float offsetLongerEdge, offsetShorterEdge;
422        if (scaledTextureHeight > scaledTextureWidth) {
423            offsetLongerEdge = scaledTextureHeight;
424            offsetShorterEdge = scaledTextureWidth;
425        } else {
426            offsetLongerEdge = scaledTextureWidth;
427            offsetShorterEdge = scaledTextureHeight;
428        }
429        if (mOffsetLongerEdge != offsetLongerEdge || mOffsetShorterEdge != offsetShorterEdge) {
430            mOffsetLongerEdge = offsetLongerEdge;
431            mOffsetShorterEdge = offsetShorterEdge;
432            requestLayout();
433        }
434    }
435
436    @Override
437    protected void onConfigurationChanged(Configuration config) {
438        super.onConfigurationChanged(config);
439    }
440
441    // prevent touches on bottom bar (not its children)
442    // from triggering a touch event on preview area
443    @Override
444    public boolean onTouchEvent(MotionEvent event) {
445        return true;
446    }
447
448    @Override
449    public void onDraw(Canvas canvas) {
450        switch (mMode) {
451            case MODE_CAPTURE: // intentional fallthrough
452            case MODE_OPTIONS:
453                if (mDrawCircle) {
454                    canvas.drawPath(mCirclePath, mCirclePaint);
455                } else {
456                    canvas.drawPath(mRectPath, mCirclePaint);
457                }
458                break;
459            case MODE_INTENT:
460                canvas.drawPaint(mCirclePaint); // TODO make this case handle capture button
461                                                // highlighting correctly
462                break;
463            case MODE_INTENT_REVIEW:
464                canvas.drawPaint(mCirclePaint);
465        }
466
467        super.onDraw(canvas);
468    }
469
470    @Override
471    public void setBackgroundColor(int color) {
472        mBackgroundColor = color;
473        setPaintColor(mBackgroundAlpha, mBackgroundColor);
474    }
475
476    public void setBackgroundPressedColor(int color) {
477        mBackgroundPressedColor = color;
478    }
479
480    public void setBackgroundAlpha(int alpha) {
481        mBackgroundAlpha = alpha;
482        setPaintColor(mBackgroundAlpha, mBackgroundColor);
483    }
484
485    private double diagonalLength(double w, double h) {
486        return Math.sqrt((w*w) + (h*h));
487    }
488    private double diagonalLength() {
489        return diagonalLength(getWidth(), getHeight());
490    }
491
492    private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) {
493        Drawable [] arrayDrawable = new Drawable[2];
494        arrayDrawable[0] = from;
495        arrayDrawable[1] = to;
496        TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable);
497        transitionDrawable.setCrossFadeEnabled(true);
498        return transitionDrawable;
499    }
500
501    /**
502     * Set the shutter button's icon resource
503     */
504    public void setShutterButtonIcon(int resId) {
505        mShutterButton.setImageResource(resId);
506    }
507
508    /**
509     * Animates bar to a single stop button
510     */
511    public void animateToCircle(int resId) {
512        final ValueAnimator radiusAnimator = ValueAnimator.ofFloat(
513                                                 (float) diagonalLength()/2,
514                                                 mCircleRadius);
515        radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS);
516        radiusAnimator.setInterpolator(Gusterpolator.INSTANCE);
517
518        radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
519            @Override
520            public void onAnimationUpdate(ValueAnimator animation) {
521                mCirclePath.reset();
522                mCirclePath.addCircle(
523                    getWidth()/2,
524                    getHeight()/2,
525                    (Float) animation.getAnimatedValue(),
526                    Path.Direction.CW);
527
528                invalidate();
529            }
530        });
531
532        TransitionDrawable transitionDrawable = crossfadeDrawable(
533                mShutterButton.getDrawable(),
534                getResources().getDrawable(resId));
535        mShutterButton.setImageDrawable(transitionDrawable);
536
537        View optionsOverlay = findViewById(R.id.bottombar_options_overlay);
538        optionsOverlay.setVisibility(View.INVISIBLE);
539
540        mDrawCircle = true;
541        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
542        radiusAnimator.start();
543    }
544
545    /**
546     * Animates bar to full width / length with video capture icon
547     */
548    public void animateToFullSize(int resId) {
549        final ValueAnimator radiusAnimator = ValueAnimator.ofFloat(
550                                                 mCircleRadius,
551                                                 (float) diagonalLength()/2);
552        radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS);
553        radiusAnimator.setInterpolator(Gusterpolator.INSTANCE);
554        radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
555            @Override
556            public void onAnimationUpdate(ValueAnimator animation) {
557                mCirclePath.reset();
558                mCirclePath.addCircle(
559                    getWidth()/2,
560                    getHeight()/2,
561                    (Float) animation.getAnimatedValue(),
562                    Path.Direction.CW);
563
564                invalidate();
565            }
566        });
567        radiusAnimator.addListener(new AnimatorListenerAdapter() {
568            @Override
569            public void onAnimationEnd(Animator animation) {
570                View optionsOverlay = findViewById(R.id.bottombar_options_overlay);
571                optionsOverlay.setVisibility(View.VISIBLE);
572                mDrawCircle = false;
573            }
574        });
575
576        TransitionDrawable transitionDrawable = crossfadeDrawable(
577                mShutterButton.getDrawable(),
578                getResources().getDrawable(resId));
579        mShutterButton.setImageDrawable(transitionDrawable);
580
581        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
582        radiusAnimator.start();
583    }
584}
585