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