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