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.RectF;
     22 import android.graphics.drawable.ColorDrawable;
     23 import android.graphics.drawable.Drawable;
     24 import android.graphics.drawable.LayerDrawable;
     25 import android.graphics.drawable.TransitionDrawable;
     26 import android.util.AttributeSet;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 import android.widget.FrameLayout;
     30 import android.widget.ImageButton;
     31 
     32 import com.android.camera.CaptureLayoutHelper;
     33 import com.android.camera.ShutterButton;
     34 import com.android.camera.debug.Log;
     35 import com.android.camera.util.ApiHelper;
     36 import com.android.camera.util.CameraUtil;
     37 import 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  */
     46 public 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