BottomBar.java revision 45a821d43ae8d7287b649f670a66ab3d99eeccaf
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.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ValueAnimator; 22import android.content.Context; 23import android.graphics.Canvas; 24import android.graphics.Paint; 25import android.graphics.Path; 26import android.graphics.RectF; 27import android.graphics.drawable.Drawable; 28import android.graphics.drawable.TransitionDrawable; 29import android.util.AttributeSet; 30import android.view.Gravity; 31import android.view.MotionEvent; 32import android.view.View; 33import android.widget.FrameLayout; 34import android.widget.ImageButton; 35import android.widget.LinearLayout; 36 37import com.android.camera.ShutterButton; 38import com.android.camera.util.Gusterpolator; 39import com.android.camera2.R; 40 41/** 42 * BottomBar swaps its width and height on rotation. In addition, it also changes 43 * gravity and layout orientation based on the new orientation. Specifically, in 44 * landscape it aligns to the right side of its parent and lays out its children 45 * vertically, whereas in portrait, it stays at the bottom of the parent and has 46 * a horizontal layout orientation. 47*/ 48public class BottomBar extends FrameLayout 49 implements PreviewStatusListener.PreviewAreaChangedListener { 50 51 public interface AdjustPreviewAreaListener { 52 /** 53 * Called when the preview should be centered in the reference area. 54 * 55 * @param rect The reference area. 56 */ 57 public void fitAndCenterPreviewAreaInRect(RectF rect); 58 59 /** 60 * Called when the preview should be aligned to the bottom of the 61 * reference area. 62 * 63 * @param rect The reference area. 64 */ 65 public void fitAndAlignBottomInRect(RectF rect); 66 67 /** 68 * Called when the preview should be aligned to the right of the 69 * reference area. 70 * 71 * @param rect The reference area. 72 */ 73 public void fitAndAlignRightInRect(RectF rect); 74 } 75 76 private static final String TAG = "BottomBar"; 77 78 private static final int CIRCLE_ANIM_DURATION_MS = 300; 79 80 private static final int MODE_CAPTURE = 0; 81 private static final int MODE_INTENT = 1; 82 private static final int MODE_INTENT_REVIEW = 2; 83 private int mMode; 84 85 private float mPreviewShortEdge; 86 private float mPreviewLongEdge; 87 88 private final int mMinimumHeight; 89 private final int mMaximumHeight; 90 private final int mOptimalHeight; 91 private final int mBackgroundAlphaOverlay; 92 private final int mBackgroundAlphaDefault; 93 private boolean mOverLayBottomBar; 94 // To avoid multiple object allocations in onLayout(). 95 private final RectF mAlignArea = new RectF(); 96 97 private FrameLayout mCaptureLayout; 98 private TopRightWeightedLayout mIntentReviewLayout; 99 100 private ShutterButton mShutterButton; 101 102 private int mBackgroundColor; 103 private int mBackgroundPressedColor; 104 private int mBackgroundAlpha = 0xff; 105 106 private final Paint mCirclePaint = new Paint(); 107 private final Path mCirclePath = new Path(); 108 private boolean mDrawCircle; 109 private final float mCircleRadius; 110 private final Path mRectPath = new Path(); 111 112 private final RectF mRect = new RectF(); 113 114 private AdjustPreviewAreaListener mAdjustPreviewAreaListener; 115 116 public void setAdjustPreviewAreaListener(AdjustPreviewAreaListener listener) { 117 mAdjustPreviewAreaListener = listener; 118 notifyAreaAdjust(); 119 } 120 121 public BottomBar(Context context, AttributeSet attrs) { 122 super(context, attrs); 123 mMinimumHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_min); 124 mMaximumHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_max); 125 mOptimalHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_optimal); 126 mCircleRadius = getResources() 127 .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2; 128 mCirclePaint.setAntiAlias(true); 129 mBackgroundAlphaOverlay = getResources().getInteger(R.integer.bottom_bar_background_alpha_overlay); 130 mBackgroundAlphaDefault = getResources().getInteger(R.integer 131 .bottom_bar_background_alpha); 132 } 133 134 private void setPaintColor(int alpha, int color, boolean isCaptureChange) { 135 int computedColor = (alpha << 24) | (color & 0x00ffffff); 136 mCirclePaint.setColor(computedColor); 137 invalidate(); 138 } 139 140 private void setPaintColor(int alpha, int color) { 141 setPaintColor(alpha, color, false); 142 } 143 144 private void setCaptureButtonUp() { 145 setPaintColor(mBackgroundAlpha, mBackgroundColor, true); 146 invalidate(); 147 } 148 149 private void setCaptureButtonDown() { 150 setPaintColor(mBackgroundAlpha, mBackgroundPressedColor, true); 151 invalidate(); 152 } 153 154 @Override 155 public void onFinishInflate() { 156 mCaptureLayout 157 = (FrameLayout) findViewById(R.id.bottombar_capture); 158 mIntentReviewLayout 159 = (TopRightWeightedLayout) findViewById(R.id.bottombar_intent_review); 160 161 mShutterButton 162 = (ShutterButton) findViewById(R.id.shutter_button); 163 mShutterButton.setOnTouchListener(new OnTouchListener() { 164 @Override 165 public boolean onTouch(View v, MotionEvent event) { 166 if (MotionEvent.ACTION_DOWN == event.getActionMasked()) { 167 setCaptureButtonDown(); 168 } else if (MotionEvent.ACTION_UP == event.getActionMasked() || 169 MotionEvent.ACTION_CANCEL == event.getActionMasked()) { 170 setCaptureButtonUp(); 171 } else if (MotionEvent.ACTION_MOVE == event.getActionMasked()) { 172 if (!mRect.contains(event.getX(), event.getY())) { 173 setCaptureButtonUp(); 174 } 175 } 176 return false; 177 } 178 }); 179 } 180 181 /** 182 * Hide the intent layout. This is necessary for switching between 183 * the intent capture layout and the bottom bar options. 184 */ 185 private void hideIntentReviewLayout() { 186 mIntentReviewLayout.setVisibility(View.INVISIBLE); 187 } 188 189 /** 190 * Perform a transition from the bottom bar options layout to the 191 * bottom bar capture layout. 192 */ 193 public void transitionToCapture() { 194 mCaptureLayout.setVisibility(View.VISIBLE); 195 if (mMode == MODE_INTENT || mMode == MODE_INTENT_REVIEW) { 196 mIntentReviewLayout.setVisibility(View.INVISIBLE); 197 } 198 199 mMode = MODE_CAPTURE; 200 } 201 202 /** 203 * Perform a transition to the global intent layout. The current 204 * layout state of the bottom bar is irrelevant. 205 */ 206 public void transitionToIntentCaptureLayout() { 207 mIntentReviewLayout.setVisibility(View.INVISIBLE); 208 mCaptureLayout.setVisibility(View.VISIBLE); 209 210 mMode = MODE_INTENT; 211 } 212 213 /** 214 * Perform a transition to the global intent review layout. 215 * The current layout state of the bottom bar is irrelevant. 216 */ 217 public void transitionToIntentReviewLayout() { 218 mCaptureLayout.setVisibility(View.INVISIBLE); 219 mIntentReviewLayout.setVisibility(View.VISIBLE); 220 221 mMode = MODE_INTENT_REVIEW; 222 } 223 224 private void setButtonImageLevels(int level) { 225 ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level); 226 ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level); 227 ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level); 228 } 229 230 private void setOverlayBottomBar(boolean overlay) { 231 mOverLayBottomBar = overlay; 232 if (overlay) { 233 setBackgroundAlpha(mBackgroundAlphaOverlay); 234 setButtonImageLevels(1); 235 } else { 236 setBackgroundAlpha(mBackgroundAlphaDefault); 237 setButtonImageLevels(0); 238 } 239 } 240 241 @Override 242 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 243 final int measureWidth = MeasureSpec.getSize(widthMeasureSpec); 244 final int measureHeight = MeasureSpec.getSize(heightMeasureSpec); 245 if (measureWidth == 0 || measureHeight == 0) { 246 return; 247 } 248 249 if (mPreviewShortEdge != 0 && mPreviewLongEdge != 0) { 250 float previewAspectRatio = 251 mPreviewLongEdge / mPreviewShortEdge; 252 if (previewAspectRatio < 1.0) { 253 previewAspectRatio = 1.0f / previewAspectRatio; 254 } 255 float screenAspectRatio = (float) measureWidth / (float) measureHeight; 256 if (screenAspectRatio < 1.0) { 257 screenAspectRatio = 1.0f / screenAspectRatio; 258 } 259 // TODO: background alphas should be set by xml references to colors. 260 if (previewAspectRatio >= screenAspectRatio) { 261 setOverlayBottomBar(true); 262 } else { 263 setOverlayBottomBar(false); 264 } 265 } 266 267 // Calculates the width and height needed for the bar. 268 int barWidth, barHeight; 269 LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams(); 270 if (measureWidth > measureHeight) { 271 // Landscape. 272 // TODO: The bottom bar should not need to care about the 273 // the type of its parent. Handle this in the parent layout. 274 layoutParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL; 275 barHeight = (int) mPreviewShortEdge; 276 if ((mPreviewLongEdge == 0 && mPreviewShortEdge == 0) || mOverLayBottomBar) { 277 barWidth = mOptimalHeight; 278 } else { 279 float previewAspectRatio = mPreviewLongEdge / mPreviewShortEdge; 280 barWidth = (int) (measureWidth - mPreviewLongEdge); 281 if (barWidth < mMinimumHeight) { 282 barWidth = mOptimalHeight; 283 setOverlayBottomBar(previewAspectRatio > 14f / 9f); 284 } else if (barWidth > mMaximumHeight) { 285 barWidth = mMaximumHeight; 286 setOverlayBottomBar(false); 287 } 288 } 289 } else { 290 // Portrait 291 layoutParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 292 barWidth = (int) mPreviewShortEdge; 293 if ((mPreviewLongEdge == 0 && mPreviewShortEdge == 0) || mOverLayBottomBar) { 294 barHeight = mOptimalHeight; 295 } else { 296 float previewAspectRatio = mPreviewLongEdge / mPreviewShortEdge; 297 barHeight = (int) (measureHeight - mPreviewLongEdge); 298 if (barHeight < mMinimumHeight) { 299 barHeight = mOptimalHeight; 300 setOverlayBottomBar(previewAspectRatio > 14f / 9f); 301 } else if (barHeight > mMaximumHeight) { 302 barHeight = mMaximumHeight; 303 setOverlayBottomBar(false); 304 } 305 } 306 } 307 308 super.onMeasure(MeasureSpec.makeMeasureSpec(barWidth, MeasureSpec.EXACTLY), 309 MeasureSpec.makeMeasureSpec(barHeight, MeasureSpec.EXACTLY)); 310 } 311 312 @Override 313 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 314 super.onLayout(changed, left, top, right, bottom); 315 316 notifyAreaAdjust(); 317 318 final int width = getWidth(); 319 final int height = getHeight(); 320 321 if (changed) { 322 mCirclePath.reset(); 323 mCirclePath.addCircle( 324 width/2, 325 height/2, 326 (int)(diagonalLength(width, height)/2), 327 Path.Direction.CW); 328 329 mRect.set( 330 0.0f, 331 0.0f, 332 width, 333 height); 334 mRectPath.reset(); 335 mRectPath.addRect(mRect, Path.Direction.CW); 336 } 337 } 338 339 @Override 340 public void onPreviewAreaChanged(RectF previewArea) { 341 setOffset(previewArea.width(), previewArea.height()); 342 } 343 344 private void setOffset(float scaledTextureWidth, float scaledTextureHeight) { 345 float offsetLongerEdge, offsetShorterEdge; 346 if (scaledTextureHeight > scaledTextureWidth) { 347 offsetLongerEdge = scaledTextureHeight; 348 offsetShorterEdge = scaledTextureWidth; 349 } else { 350 offsetLongerEdge = scaledTextureWidth; 351 offsetShorterEdge = scaledTextureHeight; 352 } 353 if (mPreviewLongEdge != offsetLongerEdge || mPreviewShortEdge != offsetShorterEdge) { 354 mPreviewLongEdge = offsetLongerEdge; 355 mPreviewShortEdge = offsetShorterEdge; 356 requestLayout(); 357 } 358 } 359 360 // prevent touches on bottom bar (not its children) 361 // from triggering a touch event on preview area 362 @Override 363 public boolean onTouchEvent(MotionEvent event) { 364 return true; 365 } 366 367 @Override 368 public void onDraw(Canvas canvas) { 369 switch (mMode) { 370 case MODE_CAPTURE: 371 if (mDrawCircle) { 372 canvas.drawPath(mCirclePath, mCirclePaint); 373 } else { 374 canvas.drawPath(mRectPath, mCirclePaint); 375 } 376 break; 377 case MODE_INTENT: 378 canvas.drawPaint(mCirclePaint); // TODO make this case handle capture button 379 // highlighting correctly 380 break; 381 case MODE_INTENT_REVIEW: 382 canvas.drawPaint(mCirclePaint); 383 } 384 385 super.onDraw(canvas); 386 } 387 388 @Override 389 public void setBackgroundColor(int color) { 390 mBackgroundColor = color; 391 setPaintColor(mBackgroundAlpha, mBackgroundColor); 392 } 393 394 public void setBackgroundPressedColor(int color) { 395 mBackgroundPressedColor = color; 396 } 397 398 public void setBackgroundAlpha(int alpha) { 399 mBackgroundAlpha = alpha; 400 setPaintColor(mBackgroundAlpha, mBackgroundColor); 401 } 402 403 public void setCaptureButtonEnabled(boolean enabled) { 404 mShutterButton.setEnabled(enabled); 405 } 406 407 private double diagonalLength(double w, double h) { 408 return Math.sqrt((w*w) + (h*h)); 409 } 410 private double diagonalLength() { 411 return diagonalLength(getWidth(), getHeight()); 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 instances 425 * loaded from the same resource share a common state; if you modify the state 426 * of one instance, all the other instances will receive the same modification. 427 * In order to modify properties of this icon drawable without affecting other 428 * drawables, here we use a mutable drawable which is guaranteed to not share 429 * 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) { 444 final ValueAnimator radiusAnimator = 445 ValueAnimator.ofFloat((float) diagonalLength()/2, mCircleRadius); 446 radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS); 447 radiusAnimator.setInterpolator(Gusterpolator.INSTANCE); 448 449 radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 450 @Override 451 public void onAnimationUpdate(ValueAnimator animation) { 452 mCirclePath.reset(); 453 mCirclePath.addCircle( 454 getWidth()/2, 455 getHeight()/2, 456 (Float) animation.getAnimatedValue(), 457 Path.Direction.CW); 458 invalidate(); 459 } 460 }); 461 mDrawCircle = true; 462 radiusAnimator.start(); 463 } 464 465 TransitionDrawable transitionDrawable = crossfadeDrawable( 466 mShutterButton.getDrawable(), 467 getResources().getDrawable(resId)); 468 mShutterButton.setImageDrawable(transitionDrawable); 469 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 470 } 471 472 /** 473 * Animates bar to full width / length with video capture icon 474 */ 475 public void animateToFullSize(int resId) { 476 if (mDrawCircle) { 477 final ValueAnimator radiusAnimator = 478 ValueAnimator.ofFloat(mCircleRadius, (float) diagonalLength()/2); 479 radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS); 480 radiusAnimator.setInterpolator(Gusterpolator.INSTANCE); 481 radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 482 @Override 483 public void onAnimationUpdate(ValueAnimator animation) { 484 mCirclePath.reset(); 485 mCirclePath.addCircle( 486 getWidth()/2, 487 getHeight()/2, 488 (Float) animation.getAnimatedValue(), 489 Path.Direction.CW); 490 invalidate(); 491 } 492 }); 493 radiusAnimator.addListener(new AnimatorListenerAdapter() { 494 @Override 495 public void onAnimationEnd(Animator animation) { 496 mDrawCircle = false; 497 } 498 }); 499 radiusAnimator.start(); 500 } 501 502 TransitionDrawable transitionDrawable = crossfadeDrawable( 503 mShutterButton.getDrawable(), 504 getResources().getDrawable(resId)); 505 mShutterButton.setImageDrawable(transitionDrawable); 506 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 507 } 508 509 private void notifyAreaAdjust() { 510 final int width = getWidth(); 511 final int height = getHeight(); 512 513 if (width == 0 || height == 0 || mAdjustPreviewAreaListener == null) { 514 return; 515 } 516 if (width > height) { 517 // Portrait 518 if (!mOverLayBottomBar) { 519 mAlignArea.set(getLeft(), 0, getRight(), getTop()); 520 } else { 521 mAlignArea.set(getLeft(), 0, getRight(), getBottom()); 522 } 523 mAdjustPreviewAreaListener.fitAndAlignBottomInRect(mAlignArea); 524 } else { 525 // Landscape 526 if (!mOverLayBottomBar) { 527 mAlignArea.set(0, getTop(), getLeft(), getBottom()); 528 } else { 529 mAlignArea.set(0, getTop(), getRight(), getBottom()); 530 } 531 mAdjustPreviewAreaListener.fitAndAlignRightInRect(mAlignArea); 532 } 533 } 534} 535