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