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