BottomBar.java revision 9bfd6bddaa09ccdadabf3b0e7e2b81bc1bcf175d
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.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.RectF;
22import android.graphics.drawable.ColorDrawable;
23import android.graphics.drawable.Drawable;
24import android.graphics.drawable.LayerDrawable;
25import android.graphics.drawable.TransitionDrawable;
26import android.util.AttributeSet;
27import android.view.MotionEvent;
28import android.view.View;
29import android.widget.FrameLayout;
30import android.widget.ImageButton;
31
32import com.android.camera.CaptureLayoutHelper;
33import com.android.camera.ShutterButton;
34import com.android.camera.debug.Log;
35import com.android.camera.util.CameraUtil;
36import com.android.camera2.R;
37
38/**
39 * BottomBar swaps its width and height on rotation. In addition, it also
40 * changes gravity and layout orientation based on the new orientation.
41 * Specifically, in landscape it aligns to the right side of its parent and lays
42 * out its children vertically, whereas in portrait, it stays at the bottom of
43 * the parent and has a horizontal layout orientation.
44 */
45public class BottomBar extends FrameLayout {
46
47    private static final Log.Tag TAG = new Log.Tag("BottomBar");
48
49    private static final int CIRCLE_ANIM_DURATION_MS = 300;
50    private static final int DRAWABLE_MAX_LEVEL = 10000;
51    private static final int MODE_CAPTURE = 0;
52    private static final int MODE_INTENT = 1;
53    private static final int MODE_INTENT_REVIEW = 2;
54    private static final int MODE_CANCEL = 3;
55
56    private int mMode;
57
58    private final int mBackgroundAlphaOverlay;
59    private final int mBackgroundAlphaDefault;
60    private boolean mOverLayBottomBar;
61
62    private FrameLayout mCaptureLayout;
63    private FrameLayout mCancelLayout;
64    private TopRightWeightedLayout mIntentReviewLayout;
65
66    private ShutterButton mShutterButton;
67    private ImageButton mCancelButton;
68
69    private int mBackgroundColor;
70    private int mBackgroundPressedColor;
71    private int mBackgroundAlpha = 0xff;
72
73    private boolean mDrawCircle;
74    private final float mCircleRadius;
75    private CaptureLayoutHelper mCaptureLayoutHelper = null;
76
77    private final boolean mIsOsVersionL;
78    // for Android L, these backgrounds are RippleDrawables (ISA LayerDrawable)
79    // pre-L, they're plain old LayerDrawables
80    private final LayerDrawable[] mShutterButtonBackgrounds;
81    // a reference to the shutter background's first contained drawable
82    // if it's an animated circle drawable (for video mode)
83    private AnimatedCircleDrawable mAnimatedCircleDrawable;
84    // a reference to the shutter background's first contained drawable
85    // if it's a color drawable (for all other modes)
86    private ColorDrawable mColorDrawable;
87
88    private RectF mRect = new RectF();
89
90    public BottomBar(Context context, AttributeSet attrs) {
91        super(context, attrs);
92        mCircleRadius = getResources()
93                .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2;
94        mBackgroundAlphaOverlay = getResources()
95                .getInteger(R.integer.bottom_bar_background_alpha_overlay);
96        mBackgroundAlphaDefault = getResources()
97                .getInteger(R.integer.bottom_bar_background_alpha);
98
99        mIsOsVersionL = context.getResources().getBoolean(R.bool.is_os_version_l);
100
101        // preload all the drawable BGs
102        TypedArray ar = context.getResources()
103                .obtainTypedArray(R.array.shutter_button_backgrounds);
104        int len = ar.length();
105
106        mShutterButtonBackgrounds = new LayerDrawable[len];
107        for (int i = 0; i < len; i++) {
108            int drawableId = ar.getResourceId(i, -1);
109            LayerDrawable shutterBackground = mShutterButtonBackgrounds[i] =
110                    (LayerDrawable) context.getResources().getDrawable(drawableId).mutate();
111
112            // the background for video has a circle_item drawable placeholder
113            // that gets replaced by an AnimatedCircleDrawable for the cool
114            // shrink-down-to-a-circle effect
115            // all other modes need not do this replace
116            Drawable d = shutterBackground.findDrawableByLayerId(R.id.circle_item);
117            if (d != null) {
118                Drawable animatedCircleDrawable =
119                        new AnimatedCircleDrawable((int) mCircleRadius);
120                animatedCircleDrawable.setLevel(DRAWABLE_MAX_LEVEL);
121                shutterBackground
122                        .setDrawableByLayerId(R.id.circle_item, animatedCircleDrawable);
123            }
124        }
125        ar.recycle();
126    }
127
128    private void setPaintColor(int alpha, int color) {
129        if (mAnimatedCircleDrawable != null) {
130            mAnimatedCircleDrawable.setColor(color);
131            mAnimatedCircleDrawable.setAlpha(alpha);
132            invalidateDrawable(mAnimatedCircleDrawable);
133            invalidate();
134        } else if (mColorDrawable != null) {
135            mColorDrawable.setColor(color);
136            mColorDrawable.setAlpha(alpha);
137            invalidateDrawable(mColorDrawable);
138            invalidate();
139        }
140
141        if (mIntentReviewLayout != null) {
142            ColorDrawable intentBackground = (ColorDrawable) mIntentReviewLayout
143                    .getBackground();
144            intentBackground.setColor(color);
145            intentBackground.setAlpha(alpha);
146        }
147    }
148
149    private void refreshPaintColor() {
150        setPaintColor(mBackgroundAlpha, mBackgroundColor);
151    }
152
153    private void setCancelBackgroundColor(int alpha, int color) {
154        LayerDrawable layerDrawable = (LayerDrawable) mCancelButton.getBackground();
155        ColorDrawable colorDrawable = (ColorDrawable) layerDrawable.getDrawable(0);
156        if (!mIsOsVersionL) {
157            colorDrawable.setColor(color);
158        }
159        colorDrawable.setAlpha(alpha);
160    }
161
162    private void setCaptureButtonUp() {
163        setPaintColor(mBackgroundAlpha, mBackgroundColor);
164    }
165
166    private void setCaptureButtonDown() {
167        if (!mIsOsVersionL) {
168            setPaintColor(mBackgroundAlpha, mBackgroundPressedColor);
169        }
170    }
171
172    private void setCancelButtonUp() {
173        setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor);
174    }
175
176    private void setCancelButtonDown() {
177        setCancelBackgroundColor(mBackgroundAlpha, mBackgroundPressedColor);
178    }
179
180    @Override
181    public void onFinishInflate() {
182        mCaptureLayout =
183                (FrameLayout) findViewById(R.id.bottombar_capture);
184        mCancelLayout =
185                (FrameLayout) findViewById(R.id.bottombar_cancel);
186        mCancelLayout.setVisibility(View.GONE);
187
188        mIntentReviewLayout =
189                (TopRightWeightedLayout) findViewById(R.id.bottombar_intent_review);
190
191        mShutterButton =
192                (ShutterButton) findViewById(R.id.shutter_button);
193        mShutterButton.setOnTouchListener(new OnTouchListener() {
194            @Override
195            public boolean onTouch(View v, MotionEvent event) {
196                if (MotionEvent.ACTION_DOWN == event.getActionMasked()) {
197                    setCaptureButtonDown();
198                } else if (MotionEvent.ACTION_UP == event.getActionMasked() ||
199                        MotionEvent.ACTION_CANCEL == event.getActionMasked()) {
200                    setCaptureButtonUp();
201                } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) {
202                    mRect.set(0, 0, getWidth(), getHeight());
203                    if (!mRect.contains(event.getX(), event.getY())) {
204                        setCaptureButtonUp();
205                    }
206                }
207                return false;
208            }
209        });
210
211        mCancelButton =
212                (ImageButton) findViewById(R.id.shutter_cancel_button);
213        mCancelButton.setOnTouchListener(new OnTouchListener() {
214            @Override
215            public boolean onTouch(View v, MotionEvent event) {
216                if (MotionEvent.ACTION_DOWN == event.getActionMasked()) {
217                    setCancelButtonDown();
218                } else if (MotionEvent.ACTION_UP == event.getActionMasked() ||
219                        MotionEvent.ACTION_CANCEL == event.getActionMasked()) {
220                    setCancelButtonUp();
221                } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) {
222                    mRect.set(0, 0, getWidth(), getHeight());
223                    if (!mRect.contains(event.getX(), event.getY())) {
224                        setCancelButtonUp();
225                    }
226                }
227                return false;
228            }
229        });
230
231    }
232
233    /**
234     * Perform a transition from the bottom bar options layout to the bottom bar
235     * capture layout.
236     */
237    public void transitionToCapture() {
238        mCaptureLayout.setVisibility(View.VISIBLE);
239        mCancelLayout.setVisibility(View.GONE);
240        mIntentReviewLayout.setVisibility(View.GONE);
241
242        mMode = MODE_CAPTURE;
243    }
244
245    /**
246     * Perform a transition from the bottom bar options layout to the bottom bar
247     * capture layout.
248     */
249    public void transitionToCancel() {
250        mCaptureLayout.setVisibility(View.GONE);
251        mIntentReviewLayout.setVisibility(View.GONE);
252        mCancelLayout.setVisibility(View.VISIBLE);
253
254        mMode = MODE_CANCEL;
255    }
256
257    /**
258     * Perform a transition to the global intent layout. The current layout
259     * state of the bottom bar is irrelevant.
260     */
261    public void transitionToIntentCaptureLayout() {
262        mIntentReviewLayout.setVisibility(View.GONE);
263        mCaptureLayout.setVisibility(View.VISIBLE);
264        mCancelLayout.setVisibility(View.GONE);
265
266        mMode = MODE_INTENT;
267    }
268
269    /**
270     * Perform a transition to the global intent review layout. The current
271     * layout state of the bottom bar is irrelevant.
272     */
273    public void transitionToIntentReviewLayout() {
274        mCaptureLayout.setVisibility(View.GONE);
275        mIntentReviewLayout.setVisibility(View.VISIBLE);
276        mCancelLayout.setVisibility(View.GONE);
277
278        mMode = MODE_INTENT_REVIEW;
279    }
280
281    /**
282     * @return whether UI is in intent review mode
283     */
284    public boolean isInIntentReview() {
285        return mMode == MODE_INTENT_REVIEW;
286    }
287
288    private void setButtonImageLevels(int level) {
289        ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level);
290        ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level);
291        ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level);
292    }
293
294    private void setOverlayBottomBar(boolean overlay) {
295        mOverLayBottomBar = overlay;
296        if (overlay) {
297            setBackgroundAlpha(mBackgroundAlphaOverlay);
298            setButtonImageLevels(1);
299        } else {
300            setBackgroundAlpha(mBackgroundAlphaDefault);
301            setButtonImageLevels(0);
302        }
303    }
304
305    /**
306     * Sets a capture layout helper to query layout rect from.
307     */
308    public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
309        mCaptureLayoutHelper = helper;
310    }
311
312    @Override
313    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
314        final int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
315        final int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
316        if (measureWidth == 0 || measureHeight == 0) {
317            return;
318        }
319
320        if (mCaptureLayoutHelper == null) {
321            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
322            Log.e(TAG, "Capture layout helper needs to be set first.");
323        } else {
324            RectF bottomBarRect = mCaptureLayoutHelper.getBottomBarRect();
325            super.onMeasure(MeasureSpec.makeMeasureSpec(
326                    (int) bottomBarRect.width(), MeasureSpec.EXACTLY),
327                    MeasureSpec.makeMeasureSpec((int) bottomBarRect.height(), MeasureSpec.EXACTLY)
328                    );
329            boolean shouldOverlayBottomBar = mCaptureLayoutHelper.shouldOverlayBottomBar();
330            setOverlayBottomBar(shouldOverlayBottomBar);
331        }
332    }
333
334    // prevent touches on bottom bar (not its children)
335    // from triggering a touch event on preview area
336    @Override
337    public boolean onTouchEvent(MotionEvent event) {
338        return true;
339    }
340
341    @Override
342    public void setBackgroundColor(int color) {
343        mBackgroundColor = color;
344        setPaintColor(mBackgroundAlpha, mBackgroundColor);
345        setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor);
346    }
347
348    private void setBackgroundPressedColor(int color) {
349        if (mIsOsVersionL) {
350            // not supported (setting a color on a RippleDrawable is hard =[ )
351        } else {
352            mBackgroundPressedColor = color;
353        }
354    }
355
356    private void setupShutterBackgroundForModeIndex(int index) {
357        LayerDrawable shutterBackground = mShutterButtonBackgrounds[index];
358        mShutterButton.setBackground(shutterBackground);
359
360        Drawable d = shutterBackground.getDrawable(0);
361        mAnimatedCircleDrawable = null;
362        mColorDrawable = null;
363        if (d instanceof AnimatedCircleDrawable) {
364            mAnimatedCircleDrawable = (AnimatedCircleDrawable) d;
365        } else if (d instanceof ColorDrawable) {
366            mColorDrawable = (ColorDrawable) d;
367        }
368
369        int colorId = CameraUtil.getCameraThemeColorId(index, getContext());
370        int pressedColor = getContext().getResources().getColor(colorId);
371        setBackgroundPressedColor(pressedColor);
372        refreshPaintColor();
373    }
374
375    public void setColorsForModeIndex(int index) {
376        setupShutterBackgroundForModeIndex(index);
377    }
378
379    public void setBackgroundAlpha(int alpha) {
380        mBackgroundAlpha = alpha;
381        setPaintColor(mBackgroundAlpha, mBackgroundColor);
382        setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor);
383    }
384
385    /**
386     * Sets the shutter button enabled if true, disabled if false.
387     * <p>
388     * Disabled means that the shutter button is not clickable and is greyed
389     * out.
390     */
391    public void setShutterButtonEnabled(final boolean enabled) {
392        mShutterButton.post(new Runnable() {
393            @Override
394            public void run() {
395                mShutterButton.setEnabled(enabled);
396                setShutterButtonImportantToA11y(enabled);
397            }
398        });
399    }
400
401    /**
402     * Sets whether shutter button should be included in a11y announcement and
403     * navigation
404     */
405    public void setShutterButtonImportantToA11y(boolean important) {
406        if (important) {
407            mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
408        } else {
409            mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
410        }
411    }
412
413    /**
414     * Returns whether the capture button is enabled.
415     */
416    public boolean isShutterButtonEnabled() {
417        return mShutterButton.isEnabled();
418    }
419
420    private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) {
421        Drawable[] arrayDrawable = new Drawable[2];
422        arrayDrawable[0] = from;
423        arrayDrawable[1] = to;
424        TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable);
425        transitionDrawable.setCrossFadeEnabled(true);
426        return transitionDrawable;
427    }
428
429    /**
430     * Sets the shutter button's icon resource. By default, all drawables
431     * instances loaded from the same resource share a common state; if you
432     * modify the state of one instance, all the other instances will receive
433     * the same modification. In order to modify properties of this icon
434     * drawable without affecting other drawables, here we use a mutable
435     * drawable which is guaranteed to not share states with other drawables.
436     */
437    public void setShutterButtonIcon(int resId) {
438        Drawable iconDrawable = getResources().getDrawable(resId);
439        if (iconDrawable != null) {
440            iconDrawable = iconDrawable.mutate();
441        }
442        mShutterButton.setImageDrawable(iconDrawable);
443    }
444
445    /**
446     * Animates bar to a single stop button
447     */
448    public void animateToVideoStop(int resId) {
449        if (mOverLayBottomBar && mAnimatedCircleDrawable != null) {
450            mAnimatedCircleDrawable.animateToSmallRadius();
451            mDrawCircle = true;
452        }
453
454        TransitionDrawable transitionDrawable = crossfadeDrawable(
455                mShutterButton.getDrawable(),
456                getResources().getDrawable(resId));
457        mShutterButton.setImageDrawable(transitionDrawable);
458        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
459    }
460
461    /**
462     * Animates bar to full width / length with video capture icon
463     */
464    public void animateToFullSize(int resId) {
465        if (mDrawCircle && mAnimatedCircleDrawable != null) {
466            mAnimatedCircleDrawable.animateToFullSize();
467            mDrawCircle = false;
468        }
469
470        TransitionDrawable transitionDrawable = crossfadeDrawable(
471                mShutterButton.getDrawable(),
472                getResources().getDrawable(resId));
473        mShutterButton.setImageDrawable(transitionDrawable);
474        transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS);
475    }
476}
477