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 
17 package com.android.camera.ui;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.graphics.Rect;
22 import android.graphics.RectF;
23 import android.graphics.drawable.ColorDrawable;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.LayerDrawable;
26 import android.graphics.drawable.TransitionDrawable;
27 import android.util.AttributeSet;
28 import android.view.MotionEvent;
29 import android.view.TouchDelegate;
30 import android.view.View;
31 import android.widget.FrameLayout;
32 import android.widget.ImageButton;
33 
34 import com.android.camera.CaptureLayoutHelper;
35 import com.android.camera.ShutterButton;
36 import com.android.camera.debug.Log;
37 import com.android.camera.util.ApiHelper;
38 import com.android.camera.util.CameraUtil;
39 import 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  */
48 public 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 
BottomBar(Context context, AttributeSet attrs)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 
setPaintColor(int alpha, int color)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 
refreshPaintColor()129     private void refreshPaintColor() {
130         setPaintColor(mBackgroundAlpha, mBackgroundColor);
131     }
132 
setCancelBackgroundColor(int alpha, int color)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 
setCaptureButtonUp()149     private void setCaptureButtonUp() {
150         setPaintColor(mBackgroundAlpha, mBackgroundColor);
151     }
152 
setCaptureButtonDown()153     private void setCaptureButtonDown() {
154         if (!ApiHelper.isLOrHigher()) {
155             setPaintColor(mBackgroundAlpha, mBackgroundPressedColor);
156         }
157     }
158 
setCancelButtonUp()159     private void setCancelButtonUp() {
160         setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor);
161     }
162 
setCancelButtonDown()163     private void setCancelButtonDown() {
164         setCancelBackgroundColor(mBackgroundAlpha, mBackgroundPressedColor);
165     }
166 
167     @Override
onFinishInflate()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 
extendTouchAreaToMatchParent(int id)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      */
transitionToCapture()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      */
transitionToCancel()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      */
transitionToIntentCaptureLayout()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      */
transitionToIntentReviewLayout()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      */
isInIntentReview()297     public boolean isInIntentReview() {
298         return mMode == MODE_INTENT_REVIEW;
299     }
300 
setButtonImageLevels(int level)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      */
setOverlayBottomBar(boolean overlay)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      */
setCaptureLayoutHelper(CaptureLayoutHelper helper)339     public void setCaptureLayoutHelper(CaptureLayoutHelper helper) {
340         mCaptureLayoutHelper = helper;
341     }
342 
343     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onTouchEvent(MotionEvent event)368     public boolean onTouchEvent(MotionEvent event) {
369         return true;
370     }
371 
372     @Override
setBackgroundColor(int color)373     public void setBackgroundColor(int color) {
374         mBackgroundColor = color;
375         setPaintColor(mBackgroundAlpha, mBackgroundColor);
376         setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor);
377     }
378 
setBackgroundPressedColor(int color)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 
applyCircleDrawableToShutterBackground(LayerDrawable shutterBackground)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 
newDrawableFromConstantState(Drawable.ConstantState constantState)404     private LayerDrawable newDrawableFromConstantState(Drawable.ConstantState constantState) {
405         return (LayerDrawable) constantState.newDrawable(getContext().getResources());
406     }
407 
setupShutterBackgroundForModeIndex(int index)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 
setColorsForModeIndex(int index)430     public void setColorsForModeIndex(int index) {
431         setupShutterBackgroundForModeIndex(index);
432     }
433 
setBackgroundAlpha(int alpha)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      */
setShutterButtonEnabled(final boolean enabled)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      */
setShutterButtonImportantToA11y(boolean important)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      */
isShutterButtonEnabled()471     public boolean isShutterButtonEnabled() {
472         return mShutterButton.isEnabled();
473     }
474 
crossfadeDrawable(Drawable from, Drawable to)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      */
setShutterButtonIcon(int resId)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      */
animateToVideoStop(int resId)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      */
animateToFullSize(int resId)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