BottomBar.java revision 45a821d43ae8d7287b649f670a66ab3d99eeccaf
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.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Path;
26import android.graphics.RectF;
27import android.graphics.drawable.Drawable;
28import android.graphics.drawable.TransitionDrawable;
29import android.util.AttributeSet;
30import android.view.Gravity;
31import android.view.MotionEvent;
32import android.view.View;
33import android.widget.FrameLayout;
34import android.widget.ImageButton;
35import android.widget.LinearLayout;
36
37import com.android.camera.ShutterButton;
38import com.android.camera.util.Gusterpolator;
39import com.android.camera2.R;
40
41/**
42 * BottomBar swaps its width and height on rotation. In addition, it also changes
43 * gravity and layout orientation based on the new orientation. Specifically, in
44 * landscape it aligns to the right side of its parent and lays out its children
45 * vertically, whereas in portrait, it stays at the bottom of the parent and has
46 * a horizontal layout orientation.
47*/
48public class BottomBar extends FrameLayout
49    implements PreviewStatusListener.PreviewAreaChangedListener {
50
51    public interface AdjustPreviewAreaListener {
52        /**
53         * Called when the preview should be centered in the reference area.
54         *
55         * @param rect The reference area.
56         */
57        public void fitAndCenterPreviewAreaInRect(RectF rect);
58
59        /**
60         * Called when the preview should be aligned to the bottom of the
61         * reference area.
62         *
63         * @param rect The reference area.
64         */
65        public void fitAndAlignBottomInRect(RectF rect);
66
67        /**
68         * Called when the preview should be aligned to the right of the
69         * reference area.
70         *
71         * @param rect The reference area.
72         */
73        public void fitAndAlignRightInRect(RectF rect);
74    }
75
76    private static final String TAG = "BottomBar";
77
78    private static final int CIRCLE_ANIM_DURATION_MS = 300;
79
80    private static final int MODE_CAPTURE = 0;
81    private static final int MODE_INTENT = 1;
82    private static final int MODE_INTENT_REVIEW = 2;
83    private int mMode;
84
85    private float mPreviewShortEdge;
86    private float mPreviewLongEdge;
87
88    private final int mMinimumHeight;
89    private final int mMaximumHeight;
90    private final int mOptimalHeight;
91    private final int mBackgroundAlphaOverlay;
92    private final int mBackgroundAlphaDefault;
93    private boolean mOverLayBottomBar;
94    // To avoid multiple object allocations in onLayout().
95    private final RectF mAlignArea = new RectF();
96
97    private FrameLayout mCaptureLayout;
98    private TopRightWeightedLayout mIntentReviewLayout;
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    private final RectF mRect = new RectF();
113
114    private AdjustPreviewAreaListener mAdjustPreviewAreaListener;
115
116    public void setAdjustPreviewAreaListener(AdjustPreviewAreaListener listener) {
117        mAdjustPreviewAreaListener = listener;
118        notifyAreaAdjust();
119    }
120
121    public BottomBar(Context context, AttributeSet attrs) {
122        super(context, attrs);
123        mMinimumHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_min);
124        mMaximumHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_max);
125        mOptimalHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_optimal);
126        mCircleRadius = getResources()
127            .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2;
128        mCirclePaint.setAntiAlias(true);
129        mBackgroundAlphaOverlay = getResources().getInteger(R.integer.bottom_bar_background_alpha_overlay);
130        mBackgroundAlphaDefault = getResources().getInteger(R.integer
131                .bottom_bar_background_alpha);
132    }
133
134    private void setPaintColor(int alpha, int color, boolean isCaptureChange) {
135        int computedColor = (alpha << 24) | (color & 0x00ffffff);
136        mCirclePaint.setColor(computedColor);
137        invalidate();
138    }
139
140    private void setPaintColor(int alpha, int color) {
141        setPaintColor(alpha, color, false);
142    }
143
144    private void setCaptureButtonUp() {
145        setPaintColor(mBackgroundAlpha, mBackgroundColor, true);
146        invalidate();
147    }
148
149    private void setCaptureButtonDown() {
150        setPaintColor(mBackgroundAlpha, mBackgroundPressedColor, true);
151        invalidate();
152    }
153
154    @Override
155    public void onFinishInflate() {
156        mCaptureLayout
157            = (FrameLayout) findViewById(R.id.bottombar_capture);
158        mIntentReviewLayout
159            = (TopRightWeightedLayout) findViewById(R.id.bottombar_intent_review);
160
161        mShutterButton
162            = (ShutterButton) findViewById(R.id.shutter_button);
163        mShutterButton.setOnTouchListener(new OnTouchListener() {
164            @Override
165            public boolean onTouch(View v, MotionEvent event) {
166                if (MotionEvent.ACTION_DOWN == event.getActionMasked()) {
167                    setCaptureButtonDown();
168                } else if (MotionEvent.ACTION_UP == event.getActionMasked() ||
169                        MotionEvent.ACTION_CANCEL == event.getActionMasked()) {
170                    setCaptureButtonUp();
171                } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) {
172                    if (!mRect.contains(event.getX(), event.getY())) {
173                        setCaptureButtonUp();
174                    }
175                }
176                return false;
177            }
178        });
179    }
180
181    /**
182     * Hide the intent layout.  This is necessary for switching between
183     * the intent capture layout and the bottom bar options.
184     */
185    private void hideIntentReviewLayout() {
186        mIntentReviewLayout.setVisibility(View.INVISIBLE);
187    }
188
189    /**
190     * Perform a transition from the bottom bar options layout to the
191     * bottom bar capture layout.
192     */
193    public void transitionToCapture() {
194        mCaptureLayout.setVisibility(View.VISIBLE);
195        if (mMode == MODE_INTENT || mMode == MODE_INTENT_REVIEW) {
196            mIntentReviewLayout.setVisibility(View.INVISIBLE);
197        }
198
199        mMode = MODE_CAPTURE;
200    }
201
202    /**
203     * Perform a transition to the global intent layout.  The current
204     * layout state of the bottom bar is irrelevant.
205     */
206    public void transitionToIntentCaptureLayout() {
207        mIntentReviewLayout.setVisibility(View.INVISIBLE);
208        mCaptureLayout.setVisibility(View.VISIBLE);
209
210        mMode = MODE_INTENT;
211    }
212
213    /**
214     * Perform a transition to the global intent review layout.
215     * The current layout state of the bottom bar is irrelevant.
216     */
217    public void transitionToIntentReviewLayout() {
218        mCaptureLayout.setVisibility(View.INVISIBLE);
219        mIntentReviewLayout.setVisibility(View.VISIBLE);
220
221        mMode = MODE_INTENT_REVIEW;
222    }
223
224    private void setButtonImageLevels(int level) {
225        ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level);
226        ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level);
227        ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level);
228    }
229
230    private void setOverlayBottomBar(boolean overlay) {
231        mOverLayBottomBar = overlay;
232        if (overlay) {
233            setBackgroundAlpha(mBackgroundAlphaOverlay);
234            setButtonImageLevels(1);
235        } else {
236            setBackgroundAlpha(mBackgroundAlphaDefault);
237            setButtonImageLevels(0);
238        }
239    }
240
241    @Override
242    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
243        final int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
244        final int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
245        if (measureWidth == 0 || measureHeight == 0) {
246            return;
247        }
248
249        if (mPreviewShortEdge != 0 && mPreviewLongEdge != 0) {
250            float previewAspectRatio =
251                    mPreviewLongEdge / mPreviewShortEdge;
252            if (previewAspectRatio < 1.0) {
253                previewAspectRatio = 1.0f / previewAspectRatio;
254            }
255            float screenAspectRatio = (float) measureWidth / (float) measureHeight;
256            if (screenAspectRatio < 1.0) {
257                screenAspectRatio = 1.0f / screenAspectRatio;
258            }
259            // TODO: background alphas should be set by xml references to colors.
260            if (previewAspectRatio >= screenAspectRatio) {
261                setOverlayBottomBar(true);
262            } else {
263                setOverlayBottomBar(false);
264            }
265        }
266
267        // Calculates the width and height needed for the bar.
268        int barWidth, barHeight;
269        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
270        if (measureWidth > measureHeight) {
271            // Landscape.
272            // TODO: The bottom bar should not need to care about the
273            // the type of its parent.  Handle this in the parent layout.
274            layoutParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
275            barHeight = (int) mPreviewShortEdge;
276            if ((mPreviewLongEdge == 0 && mPreviewShortEdge == 0) || mOverLayBottomBar) {
277                barWidth = mOptimalHeight;
278            } else {
279                float previewAspectRatio = mPreviewLongEdge / mPreviewShortEdge;
280                barWidth = (int) (measureWidth - mPreviewLongEdge);
281                if (barWidth < mMinimumHeight) {
282                    barWidth = mOptimalHeight;
283                    setOverlayBottomBar(previewAspectRatio > 14f / 9f);
284                } else if (barWidth > mMaximumHeight) {
285                    barWidth = mMaximumHeight;
286                    setOverlayBottomBar(false);
287                }
288            }
289        } else {
290            // Portrait
291            layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
292            barWidth = (int) mPreviewShortEdge;
293            if ((mPreviewLongEdge == 0 && mPreviewShortEdge == 0) || mOverLayBottomBar) {
294                barHeight = mOptimalHeight;
295            } else {
296                float previewAspectRatio = mPreviewLongEdge / mPreviewShortEdge;
297                barHeight = (int) (measureHeight - mPreviewLongEdge);
298                if (barHeight < mMinimumHeight) {
299                    barHeight = mOptimalHeight;
300                    setOverlayBottomBar(previewAspectRatio > 14f / 9f);
301                } else if (barHeight > mMaximumHeight) {
302                    barHeight = mMaximumHeight;
303                    setOverlayBottomBar(false);
304                }
305            }
306        }
307
308        super.onMeasure(MeasureSpec.makeMeasureSpec(barWidth, MeasureSpec.EXACTLY),
309                MeasureSpec.makeMeasureSpec(barHeight, MeasureSpec.EXACTLY));
310    }
311
312    @Override
313    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
314        super.onLayout(changed, left, top, right, bottom);
315
316        notifyAreaAdjust();
317
318        final int width = getWidth();
319        final int height = getHeight();
320
321        if (changed) {
322            mCirclePath.reset();
323            mCirclePath.addCircle(
324                width/2,
325                height/2,
326                (int)(diagonalLength(width, height)/2),
327                Path.Direction.CW);
328
329            mRect.set(
330                0.0f,
331                0.0f,
332                width,
333                height);
334            mRectPath.reset();
335            mRectPath.addRect(mRect, Path.Direction.CW);
336        }
337    }
338
339    @Override
340    public void onPreviewAreaChanged(RectF previewArea) {
341        setOffset(previewArea.width(), previewArea.height());
342    }
343
344    private void setOffset(float scaledTextureWidth, float scaledTextureHeight) {
345        float offsetLongerEdge, offsetShorterEdge;
346        if (scaledTextureHeight > scaledTextureWidth) {
347            offsetLongerEdge = scaledTextureHeight;
348            offsetShorterEdge = scaledTextureWidth;
349        } else {
350            offsetLongerEdge = scaledTextureWidth;
351            offsetShorterEdge = scaledTextureHeight;
352        }
353        if (mPreviewLongEdge != offsetLongerEdge || mPreviewShortEdge != offsetShorterEdge) {
354            mPreviewLongEdge = offsetLongerEdge;
355            mPreviewShortEdge = offsetShorterEdge;
356            requestLayout();
357        }
358    }
359
360    // prevent touches on bottom bar (not its children)
361    // from triggering a touch event on preview area
362    @Override
363    public boolean onTouchEvent(MotionEvent event) {
364        return true;
365    }
366
367    @Override
368    public void onDraw(Canvas canvas) {
369        switch (mMode) {
370            case MODE_CAPTURE:
371                if (mDrawCircle) {
372                    canvas.drawPath(mCirclePath, mCirclePaint);
373                } else {
374                    canvas.drawPath(mRectPath, mCirclePaint);
375                }
376                break;
377            case MODE_INTENT:
378                canvas.drawPaint(mCirclePaint); // TODO make this case handle capture button
379                                                // highlighting correctly
380                break;
381            case MODE_INTENT_REVIEW:
382                canvas.drawPaint(mCirclePaint);
383        }
384
385        super.onDraw(canvas);
386    }
387
388    @Override
389    public void setBackgroundColor(int color) {
390        mBackgroundColor = color;
391        setPaintColor(mBackgroundAlpha, mBackgroundColor);
392    }
393
394    public void setBackgroundPressedColor(int color) {
395        mBackgroundPressedColor = color;
396    }
397
398    public void setBackgroundAlpha(int alpha) {
399        mBackgroundAlpha = alpha;
400        setPaintColor(mBackgroundAlpha, mBackgroundColor);
401    }
402
403    public void setCaptureButtonEnabled(boolean enabled) {
404        mShutterButton.setEnabled(enabled);
405    }
406
407    private double diagonalLength(double w, double h) {
408        return Math.sqrt((w*w) + (h*h));
409    }
410    private double diagonalLength() {
411        return diagonalLength(getWidth(), getHeight());
412    }
413
414    private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) {
415        Drawable [] arrayDrawable = new Drawable[2];
416        arrayDrawable[0] = from;
417        arrayDrawable[1] = to;
418        TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable);
419        transitionDrawable.setCrossFadeEnabled(true);
420        return transitionDrawable;
421    }
422
423    /**
424     * Sets the shutter button's icon resource. By default, all drawables instances
425     * loaded from the same resource share a common state; if you modify the state
426     * of one instance, all the other instances will receive the same modification.
427     * In order to modify properties of this icon drawable without affecting other
428     * drawables, here we use a mutable drawable which is guaranteed to not share
429     * states with other drawables.
430     */
431    public void setShutterButtonIcon(int resId) {
432        Drawable iconDrawable = getResources().getDrawable(resId);
433        if (iconDrawable != null) {
434            iconDrawable = iconDrawable.mutate();
435        }
436        mShutterButton.setImageDrawable(iconDrawable);
437    }
438
439    /**
440     * Animates bar to a single stop button
441     */
442    public void animateToVideoStop(int resId) {
443        if (mOverLayBottomBar) {
444            final ValueAnimator radiusAnimator =
445                ValueAnimator.ofFloat((float) diagonalLength()/2, mCircleRadius);
446            radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS);
447            radiusAnimator.setInterpolator(Gusterpolator.INSTANCE);
448
449            radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
450                @Override
451                public void onAnimationUpdate(ValueAnimator animation) {
452                    mCirclePath.reset();
453                    mCirclePath.addCircle(
454                            getWidth()/2,
455                            getHeight()/2,
456                            (Float) animation.getAnimatedValue(),
457                            Path.Direction.CW);
458                    invalidate();
459                }
460            });
461            mDrawCircle = true;
462            radiusAnimator.start();
463        }
464
465        TransitionDrawable transitionDrawable = crossfadeDrawable(
466                mShutterButton.getDrawable(),
467                getResources().getDrawable(resId));
468        mShutterButton.setImageDrawable(transitionDrawable);
469        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
470    }
471
472    /**
473     * Animates bar to full width / length with video capture icon
474     */
475    public void animateToFullSize(int resId) {
476        if (mDrawCircle) {
477            final ValueAnimator radiusAnimator =
478                ValueAnimator.ofFloat(mCircleRadius, (float) diagonalLength()/2);
479            radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS);
480            radiusAnimator.setInterpolator(Gusterpolator.INSTANCE);
481            radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
482                @Override
483                public void onAnimationUpdate(ValueAnimator animation) {
484                    mCirclePath.reset();
485                    mCirclePath.addCircle(
486                            getWidth()/2,
487                            getHeight()/2,
488                            (Float) animation.getAnimatedValue(),
489                            Path.Direction.CW);
490                    invalidate();
491                }
492            });
493            radiusAnimator.addListener(new AnimatorListenerAdapter() {
494                @Override
495                public void onAnimationEnd(Animator animation) {
496                    mDrawCircle = false;
497                }
498            });
499            radiusAnimator.start();
500        }
501
502        TransitionDrawable transitionDrawable = crossfadeDrawable(
503                mShutterButton.getDrawable(),
504                getResources().getDrawable(resId));
505        mShutterButton.setImageDrawable(transitionDrawable);
506        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
507    }
508
509    private void notifyAreaAdjust() {
510        final int width = getWidth();
511        final int height = getHeight();
512
513        if (width == 0 || height == 0 || mAdjustPreviewAreaListener == null) {
514            return;
515        }
516        if (width > height) {
517            // Portrait
518            if (!mOverLayBottomBar) {
519                mAlignArea.set(getLeft(), 0, getRight(), getTop());
520            } else {
521                mAlignArea.set(getLeft(), 0, getRight(), getBottom());
522            }
523            mAdjustPreviewAreaListener.fitAndAlignBottomInRect(mAlignArea);
524        } else {
525            // Landscape
526            if (!mOverLayBottomBar) {
527                mAlignArea.set(0, getTop(), getLeft(), getBottom());
528            } else {
529                mAlignArea.set(0, getTop(), getRight(), getBottom());
530            }
531            mAdjustPreviewAreaListener.fitAndAlignRightInRect(mAlignArea);
532        }
533    }
534}
535