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 setOverlayBottomBar(boolean overlay)307 private void setOverlayBottomBar(boolean overlay) { 308 mOverLayBottomBar = overlay; 309 if (overlay) { 310 setBackgroundAlpha(mBackgroundAlphaOverlay); 311 setButtonImageLevels(1); 312 } else { 313 setBackgroundAlpha(mBackgroundAlphaDefault); 314 setButtonImageLevels(0); 315 } 316 } 317 318 /** 319 * Sets a capture layout helper to query layout rect from. 320 */ setCaptureLayoutHelper(CaptureLayoutHelper helper)321 public void setCaptureLayoutHelper(CaptureLayoutHelper helper) { 322 mCaptureLayoutHelper = helper; 323 } 324 325 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)326 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 327 final int measureWidth = MeasureSpec.getSize(widthMeasureSpec); 328 final int measureHeight = MeasureSpec.getSize(heightMeasureSpec); 329 if (measureWidth == 0 || measureHeight == 0) { 330 return; 331 } 332 333 if (mCaptureLayoutHelper == null) { 334 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 335 Log.e(TAG, "Capture layout helper needs to be set first."); 336 } else { 337 RectF bottomBarRect = mCaptureLayoutHelper.getBottomBarRect(); 338 super.onMeasure(MeasureSpec.makeMeasureSpec( 339 (int) bottomBarRect.width(), MeasureSpec.EXACTLY), 340 MeasureSpec.makeMeasureSpec((int) bottomBarRect.height(), MeasureSpec.EXACTLY) 341 ); 342 boolean shouldOverlayBottomBar = mCaptureLayoutHelper.shouldOverlayBottomBar(); 343 setOverlayBottomBar(shouldOverlayBottomBar); 344 } 345 } 346 347 // prevent touches on bottom bar (not its children) 348 // from triggering a touch event on preview area 349 @Override onTouchEvent(MotionEvent event)350 public boolean onTouchEvent(MotionEvent event) { 351 return true; 352 } 353 354 @Override setBackgroundColor(int color)355 public void setBackgroundColor(int color) { 356 mBackgroundColor = color; 357 setPaintColor(mBackgroundAlpha, mBackgroundColor); 358 setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor); 359 } 360 setBackgroundPressedColor(int color)361 private void setBackgroundPressedColor(int color) { 362 if (ApiHelper.isLOrHigher()) { 363 // not supported (setting a color on a RippleDrawable is hard =[ ) 364 } else { 365 mBackgroundPressedColor = color; 366 } 367 } 368 applyCircleDrawableToShutterBackground(LayerDrawable shutterBackground)369 private LayerDrawable applyCircleDrawableToShutterBackground(LayerDrawable shutterBackground) { 370 // the background for video has a circle_item drawable placeholder 371 // that gets replaced by an AnimatedCircleDrawable for the cool 372 // shrink-down-to-a-circle effect 373 // all other modes need not do this replace 374 Drawable d = shutterBackground.findDrawableByLayerId(R.id.circle_item); 375 if (d != null) { 376 Drawable animatedCircleDrawable = 377 new AnimatedCircleDrawable((int) mCircleRadius); 378 animatedCircleDrawable.setLevel(DRAWABLE_MAX_LEVEL); 379 shutterBackground 380 .setDrawableByLayerId(R.id.circle_item, animatedCircleDrawable); 381 } 382 383 return shutterBackground; 384 } 385 newDrawableFromConstantState(Drawable.ConstantState constantState)386 private LayerDrawable newDrawableFromConstantState(Drawable.ConstantState constantState) { 387 return (LayerDrawable) constantState.newDrawable(getContext().getResources()); 388 } 389 setupShutterBackgroundForModeIndex(int index)390 private void setupShutterBackgroundForModeIndex(int index) { 391 LayerDrawable shutterBackground = applyCircleDrawableToShutterBackground( 392 newDrawableFromConstantState(mShutterButtonBackgroundConstantStates[index])); 393 mShutterButton.setBackground(shutterBackground); 394 mCancelButton.setBackground(applyCircleDrawableToShutterBackground( 395 newDrawableFromConstantState(mShutterButtonBackgroundConstantStates[index]))); 396 397 Drawable d = shutterBackground.getDrawable(0); 398 mAnimatedCircleDrawable = null; 399 mColorDrawable = null; 400 if (d instanceof AnimatedCircleDrawable) { 401 mAnimatedCircleDrawable = (AnimatedCircleDrawable) d; 402 } else if (d instanceof ColorDrawable) { 403 mColorDrawable = (ColorDrawable) d; 404 } 405 406 int colorId = CameraUtil.getCameraThemeColorId(index, getContext()); 407 int pressedColor = getContext().getResources().getColor(colorId); 408 setBackgroundPressedColor(pressedColor); 409 refreshPaintColor(); 410 } 411 setColorsForModeIndex(int index)412 public void setColorsForModeIndex(int index) { 413 setupShutterBackgroundForModeIndex(index); 414 } 415 setBackgroundAlpha(int alpha)416 public void setBackgroundAlpha(int alpha) { 417 mBackgroundAlpha = alpha; 418 setPaintColor(mBackgroundAlpha, mBackgroundColor); 419 setCancelBackgroundColor(mBackgroundAlpha, mBackgroundColor); 420 } 421 422 /** 423 * Sets the shutter button enabled if true, disabled if false. 424 * <p> 425 * Disabled means that the shutter button is not clickable and is greyed 426 * out. 427 */ setShutterButtonEnabled(final boolean enabled)428 public void setShutterButtonEnabled(final boolean enabled) { 429 mShutterButton.post(new Runnable() { 430 @Override 431 public void run() { 432 mShutterButton.setEnabled(enabled); 433 setShutterButtonImportantToA11y(enabled); 434 } 435 }); 436 } 437 438 /** 439 * Sets whether shutter button should be included in a11y announcement and 440 * navigation 441 */ setShutterButtonImportantToA11y(boolean important)442 public void setShutterButtonImportantToA11y(boolean important) { 443 if (important) { 444 mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); 445 } else { 446 mShutterButton.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); 447 } 448 } 449 450 /** 451 * Returns whether the capture button is enabled. 452 */ isShutterButtonEnabled()453 public boolean isShutterButtonEnabled() { 454 return mShutterButton.isEnabled(); 455 } 456 crossfadeDrawable(Drawable from, Drawable to)457 private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) { 458 Drawable[] arrayDrawable = new Drawable[2]; 459 arrayDrawable[0] = from; 460 arrayDrawable[1] = to; 461 TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable); 462 transitionDrawable.setCrossFadeEnabled(true); 463 return transitionDrawable; 464 } 465 466 /** 467 * Sets the shutter button's icon resource. By default, all drawables 468 * instances loaded from the same resource share a common state; if you 469 * modify the state of one instance, all the other instances will receive 470 * the same modification. In order to modify properties of this icon 471 * drawable without affecting other drawables, here we use a mutable 472 * drawable which is guaranteed to not share states with other drawables. 473 */ setShutterButtonIcon(int resId)474 public void setShutterButtonIcon(int resId) { 475 Drawable iconDrawable = getResources().getDrawable(resId); 476 if (iconDrawable != null) { 477 iconDrawable = iconDrawable.mutate(); 478 } 479 mShutterButton.setImageDrawable(iconDrawable); 480 } 481 482 /** 483 * Animates bar to a single stop button 484 */ animateToVideoStop(int resId)485 public void animateToVideoStop(int resId) { 486 if (mOverLayBottomBar && mAnimatedCircleDrawable != null) { 487 mAnimatedCircleDrawable.animateToSmallRadius(); 488 mDrawCircle = true; 489 } 490 491 TransitionDrawable transitionDrawable = crossfadeDrawable( 492 mShutterButton.getDrawable(), 493 getResources().getDrawable(resId)); 494 mShutterButton.setImageDrawable(transitionDrawable); 495 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 496 } 497 498 /** 499 * Animates bar to full width / length with video capture icon 500 */ animateToFullSize(int resId)501 public void animateToFullSize(int resId) { 502 if (mDrawCircle && mAnimatedCircleDrawable != null) { 503 mAnimatedCircleDrawable.animateToFullSize(); 504 mDrawCircle = false; 505 } 506 507 TransitionDrawable transitionDrawable = crossfadeDrawable( 508 mShutterButton.getDrawable(), 509 getResources().getDrawable(resId)); 510 mShutterButton.setImageDrawable(transitionDrawable); 511 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 512 } 513 } 514