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.ApiHelper; 36import com.android.camera.util.CameraUtil; 37import 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 */ 46public 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