Home | History | Annotate | Download | only in ui
      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 
     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