BottomBar.java revision a1fab413bcbe5f62ae1d829bac0539519fef96a6
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.content.res.Configuration; 24import android.graphics.Canvas; 25import android.graphics.Path; 26import android.graphics.Paint; 27import android.graphics.RectF; 28import android.graphics.drawable.Drawable; 29import android.graphics.drawable.TransitionDrawable; 30import android.util.AttributeSet; 31import android.util.TypedValue; 32import android.view.Gravity; 33import android.view.MotionEvent; 34import android.view.View; 35import android.widget.FrameLayout; 36import android.widget.ImageButton; 37import android.widget.LinearLayout; 38 39import com.android.camera.ShutterButton; 40import com.android.camera.MultiToggleImageButton; 41import com.android.camera.ToggleImageButton; 42import com.android.camera.util.Gusterpolator; 43import com.android.camera2.R; 44 45 46/** 47 * BottomBar swaps its width and height on rotation. In addition, it also changes 48 * gravity and layout orientation based on the new orientation. Specifically, in 49 * landscape it aligns to the right side of its parent and lays out its children 50 * vertically, whereas in portrait, it stays at the bottom of the parent and has 51 * a horizontal layout orientation. 52*/ 53public class BottomBar extends FrameLayout 54 implements PreviewStatusListener.PreviewAreaSizeChangedListener, 55 PreviewOverlay.OnPreviewTouchedListener { 56 57 private static final String TAG = "BottomBar"; 58 59 private static final int BOTTOMBAR_OPTIONS_TIMEOUT_MS = 2000; 60 61 private static final int CIRCLE_ANIM_DURATION_MS = 300; 62 63 private static final int MODE_CAPTURE = 0; 64 private static final int MODE_OPTIONS = 1; 65 private static final int MODE_INTENT = 2; 66 private static final int MODE_INTENT_REVIEW = 3; 67 private int mMode; 68 69 private int mWidth; 70 private int mHeight; 71 private float mOffsetShorterEdge; 72 private float mOffsetLongerEdge; 73 74 private final int mOptimalHeight; 75 private boolean mOverLayBottomBar; 76 77 private ToggleImageButton mOptionsToggle; 78 79 private TopRightMostOverlay mOptionsOverlay; 80 private TopRightWeightedLayout mOptionsLayout; 81 private FrameLayout mCaptureLayout; 82 private TopRightWeightedLayout mIntentLayout; 83 private boolean mIsCaptureIntent = false; 84 85 /** 86 * A generic Runnable for setting the options toggle 87 * to the capture layout state and performing the state 88 * transition. 89 */ 90 private final Runnable mCloseOptionsRunnable = 91 new Runnable() { 92 @Override 93 public void run() { 94 if (mOptionsToggle != null) { 95 mOptionsToggle.setState(0, true); 96 } 97 } 98 }; 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 public BottomBar(Context context, AttributeSet attrs) { 113 super(context, attrs); 114 mOptimalHeight = getResources().getDimensionPixelSize(R.dimen.bottom_bar_height_optimal); 115 mCircleRadius = getResources() 116 .getDimensionPixelSize(R.dimen.video_capture_circle_diameter) / 2; 117 mCirclePaint.setAntiAlias(true); 118 } 119 120 private void setPaintColor(int alpha, int color, boolean isCaptureChange) { 121 int computedColor = (alpha << 24) | (color & 0x00ffffff); 122 mCirclePaint.setColor(computedColor); 123 if (mOptionsToggle == null) { 124 mOptionsToggle = (ToggleImageButton) findViewById(R.id.bottombar_options_toggle); 125 } 126 if (!isCaptureChange) { 127 mOptionsToggle.setBackgroundColor(computedColor); 128 } 129 invalidate(); 130 } 131 132 private void setPaintColor(int alpha, int color) { 133 setPaintColor(alpha, color, false); 134 } 135 136 @Override 137 public void onFinishInflate() { 138 mOptionsOverlay 139 = (TopRightMostOverlay) findViewById(R.id.bottombar_options_overlay); 140 mOptionsLayout 141 = (TopRightWeightedLayout) findViewById(R.id.bottombar_options); 142 mCaptureLayout 143 = (FrameLayout) findViewById(R.id.bottombar_capture); 144 mIntentLayout 145 = (TopRightWeightedLayout) findViewById(R.id.bottombar_intent); 146 147 mShutterButton 148 = (ShutterButton) findViewById(R.id.shutter_button); 149 mShutterButton.setOnTouchListener(new OnTouchListener() { 150 @Override 151 public boolean onTouch(View v, MotionEvent event) { 152 if (MotionEvent.ACTION_DOWN == event.getActionMasked()) { 153 setPaintColor(mBackgroundAlpha, mBackgroundPressedColor, true); 154 invalidate(); 155 } else if (MotionEvent.ACTION_UP == event.getActionMasked()) { 156 setPaintColor(mBackgroundAlpha, mBackgroundColor, true); 157 invalidate(); 158 } 159 160 return false; 161 } 162 }); 163 164 mOptionsOverlay.setOnTouchListener(new View.OnTouchListener() { 165 @Override 166 public boolean onTouch(View v, MotionEvent event) { 167 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 168 // close options immediately. 169 closeModeOptionsDelayed(BOTTOMBAR_OPTIONS_TIMEOUT_MS); 170 } 171 // Let touch event reach mode options or shutter. 172 return false; 173 } 174 }); 175 } 176 177 @Override 178 public void onPreviewTouched(MotionEvent ev) { 179 // close options immediately. 180 closeModeOptionsDelayed(0); 181 } 182 183 /** 184 * Schedule (or re-schedule) the options menu to be closed 185 * after a number of milliseconds. If the options menu 186 * is already closed, nothing is scheduled. 187 */ 188 private void closeModeOptionsDelayed(int milliseconds) { 189 // Check that the bottom bar options are visible. 190 if (mOptionsLayout.getVisibility() != View.VISIBLE) { 191 return; 192 } 193 194 // Remove queued callbacks. 195 removeCallbacks(mCloseOptionsRunnable); 196 197 // Close the bottom bar options view in n milliseconds. 198 postDelayed(mCloseOptionsRunnable, milliseconds); 199 } 200 201 /** 202 * Initializes the bottom bar toggle for switching between 203 * capture and the bottom bar options. 204 */ 205 public void setupToggle(boolean isCaptureIntent) { 206 mIsCaptureIntent = isCaptureIntent; 207 208 // Of type ToggleImageButton because ToggleButton 209 // has a non-removable spacing for text on the right-hand side. 210 mOptionsToggle = (ToggleImageButton) findViewById(R.id.bottombar_options_toggle); 211 mOptionsToggle.setState(0, false); 212 mOptionsToggle.setOnStateChangeListener(new ToggleImageButton.OnStateChangeListener() { 213 @Override 214 public void stateChanged(View view, boolean toOptions) { 215 if (toOptions) { 216 if (mIsCaptureIntent) { 217 hideIntentLayout(); 218 } 219 transitionToOptions(); 220 } else { 221 if (mIsCaptureIntent) { 222 transitionToIntentLayout(); 223 } else { 224 transitionToCapture(); 225 } 226 } 227 } 228 }); 229 mOptionsOverlay.setReferenceViewParent(mOptionsLayout); 230 } 231 232 /** 233 * Hide the intent layout. This is necessary for switching between 234 * the intent capture layout and the bottom bar options. 235 */ 236 private void hideIntentLayout() { 237 mIntentLayout.setVisibility(View.INVISIBLE); 238 } 239 240 /** 241 * Perform a transition from the bottom bar options layout to the 242 * bottom bar capture layout. 243 */ 244 public void transitionToCapture() { 245 mOptionsOverlay.setVisibility(View.VISIBLE); 246 mOptionsLayout.setVisibility(View.INVISIBLE); 247 mCaptureLayout.setVisibility(View.VISIBLE); 248 if (mMode == MODE_INTENT || mMode == MODE_INTENT_REVIEW) { 249 mIntentLayout.setVisibility(View.INVISIBLE); 250 } 251 252 mMode = MODE_CAPTURE; 253 } 254 255 /** 256 * Perform a transition from the bottom bar capture layout to the 257 * bottom bar options layout. 258 */ 259 public void transitionToOptions() { 260 mCaptureLayout.setVisibility(View.INVISIBLE); 261 mOptionsLayout.setVisibility(View.VISIBLE); 262 263 mMode = MODE_OPTIONS; 264 } 265 266 /** 267 * Perform a transition to the global intent layout. The current 268 * layout state of the bottom bar is irrelevant. 269 */ 270 public void transitionToIntentLayout() { 271 mCaptureLayout.setVisibility(View.VISIBLE); 272 mOptionsLayout.setVisibility(View.INVISIBLE); 273 mOptionsOverlay.setVisibility(View.VISIBLE); 274 mIntentLayout.setVisibility(View.VISIBLE); 275 276 View button; 277 button = mIntentLayout.findViewById(R.id.done_button); 278 button.setVisibility(View.INVISIBLE); 279 button = mIntentLayout.findViewById(R.id.retake_button); 280 button.setVisibility(View.INVISIBLE); 281 282 mMode = MODE_INTENT; 283 } 284 285 /** 286 * Perform a transition to the global intent review layout. 287 * The current layout state of the bottom bar is irrelevant. 288 */ 289 public void transitionToIntentReviewLayout() { 290 mCaptureLayout.setVisibility(View.INVISIBLE); 291 mOptionsLayout.setVisibility(View.INVISIBLE); 292 mOptionsOverlay.setVisibility(View.INVISIBLE); 293 294 View button; 295 button = mIntentLayout.findViewById(R.id.done_button); 296 button.setVisibility(View.VISIBLE); 297 button = mIntentLayout.findViewById(R.id.retake_button); 298 button.setVisibility(View.VISIBLE); 299 mIntentLayout.setVisibility(View.VISIBLE); 300 301 mMode = MODE_INTENT_REVIEW; 302 } 303 304 private void setButtonImageLevels(int level) { 305 ((MultiToggleImageButton) findViewById(R.id.flash_toggle_button)).setImageLevel(level); 306 ((MultiToggleImageButton) findViewById(R.id.camera_toggle_button)).setImageLevel(level); 307 ((MultiToggleImageButton) findViewById(R.id.hdr_plus_toggle_button)).setImageLevel(level); 308 ((MultiToggleImageButton) findViewById(R.id.refocus_toggle_button)).setImageLevel(level); 309 ((ImageButton) findViewById(R.id.cancel_button)).setImageLevel(level); 310 ((ImageButton) findViewById(R.id.done_button)).setImageLevel(level); 311 ((ImageButton) findViewById(R.id.retake_button)).setImageLevel(level); 312 mOptionsToggle.setImageLevel(level); 313 } 314 315 @Override 316 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 317 mWidth = MeasureSpec.getSize(widthMeasureSpec); 318 mHeight = MeasureSpec.getSize(heightMeasureSpec); 319 if (mWidth == 0 || mHeight == 0) { 320 return; 321 } 322 323 if (mOffsetShorterEdge != 0 && mOffsetLongerEdge != 0) { 324 float previewAspectRatio = 325 mOffsetLongerEdge / mOffsetShorterEdge; 326 if (previewAspectRatio < 1.0) { 327 previewAspectRatio = 1.0f/previewAspectRatio; 328 } 329 float screenAspectRatio = (float) mWidth / (float) mHeight; 330 if (screenAspectRatio < 1.0) { 331 screenAspectRatio = 1.0f/screenAspectRatio; 332 } 333 if (previewAspectRatio >= screenAspectRatio) { 334 mOverLayBottomBar = true; 335 setBackgroundAlpha(128); 336 setButtonImageLevels(1); 337 } else { 338 mOverLayBottomBar = false; 339 setBackgroundAlpha(255); 340 setButtonImageLevels(0); 341 } 342 } 343 344 // Calculates the width and height needed for the bar. 345 int barWidth, barHeight; 346 if (mWidth > mHeight) { 347 // TODO: The bottom bar should not need to care about the 348 // the type of its parent. Handle this in the parent layout. 349 ((LinearLayout.LayoutParams) getLayoutParams()).gravity = Gravity.RIGHT; 350 if ((mOffsetLongerEdge == 0 && mOffsetShorterEdge == 0) || mOverLayBottomBar) { 351 barWidth = mOptimalHeight; 352 barHeight = mHeight; 353 } else { 354 barWidth = (int) (mWidth - mOffsetLongerEdge); 355 barHeight = mHeight; 356 } 357 } else { 358 ((LinearLayout.LayoutParams) getLayoutParams()).gravity = Gravity.BOTTOM; 359 if ((mOffsetLongerEdge == 0 && mOffsetShorterEdge == 0) || mOverLayBottomBar) { 360 barWidth = mWidth; 361 barHeight = mOptimalHeight; 362 } else { 363 barWidth = mWidth; 364 barHeight = (int) (mHeight - mOffsetLongerEdge); 365 } 366 } 367 368 super.onMeasure(MeasureSpec.makeMeasureSpec(barWidth, MeasureSpec.EXACTLY), 369 MeasureSpec.makeMeasureSpec(barHeight, MeasureSpec.EXACTLY)); 370 } 371 372 @Override 373 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 374 super.onLayout(changed, left, top, right, bottom); 375 376 int width = right - left; 377 int height = bottom - top; 378 379 if (changed) { 380 mCirclePath.reset(); 381 mCirclePath.addCircle( 382 width/2, 383 height/2, 384 (int)(diagonalLength(width, height)/2), 385 Path.Direction.CW); 386 387 int shortEdge = mOptionsToggle.getWidth(); 388 if (mOptionsToggle.getHeight() < shortEdge) { 389 shortEdge = mOptionsToggle.getHeight(); 390 } 391 mRectPath.reset(); 392 if (width > height) { 393 mRectPath.addRect( 394 0.0f, 395 0.0f, 396 (float) width - shortEdge, 397 (float) height, 398 Path.Direction.CW); 399 } else { 400 mRectPath.addRect( 401 0.0f, 402 (float) shortEdge, 403 (float) width, 404 (float) height, 405 Path.Direction.CW); 406 } 407 } 408 } 409 410 private void adjustBottomBar(float scaledTextureWidth, 411 float scaledTextureHeight) { 412 setOffset(scaledTextureWidth, scaledTextureHeight); 413 } 414 415 @Override 416 public void onPreviewAreaSizeChanged(RectF previewArea) { 417 adjustBottomBar(previewArea.width(), previewArea.height()); 418 } 419 420 private void setOffset(float scaledTextureWidth, float scaledTextureHeight) { 421 float offsetLongerEdge, offsetShorterEdge; 422 if (scaledTextureHeight > scaledTextureWidth) { 423 offsetLongerEdge = scaledTextureHeight; 424 offsetShorterEdge = scaledTextureWidth; 425 } else { 426 offsetLongerEdge = scaledTextureWidth; 427 offsetShorterEdge = scaledTextureHeight; 428 } 429 if (mOffsetLongerEdge != offsetLongerEdge || mOffsetShorterEdge != offsetShorterEdge) { 430 mOffsetLongerEdge = offsetLongerEdge; 431 mOffsetShorterEdge = offsetShorterEdge; 432 requestLayout(); 433 } 434 } 435 436 @Override 437 protected void onConfigurationChanged(Configuration config) { 438 super.onConfigurationChanged(config); 439 } 440 441 // prevent touches on bottom bar (not its children) 442 // from triggering a touch event on preview area 443 @Override 444 public boolean onTouchEvent(MotionEvent event) { 445 return true; 446 } 447 448 @Override 449 public void onDraw(Canvas canvas) { 450 switch (mMode) { 451 case MODE_CAPTURE: // intentional fallthrough 452 case MODE_OPTIONS: 453 if (mDrawCircle) { 454 canvas.drawPath(mCirclePath, mCirclePaint); 455 } else { 456 canvas.drawPath(mRectPath, mCirclePaint); 457 } 458 break; 459 case MODE_INTENT: 460 canvas.drawPaint(mCirclePaint); // TODO make this case handle capture button 461 // highlighting correctly 462 break; 463 case MODE_INTENT_REVIEW: 464 canvas.drawPaint(mCirclePaint); 465 } 466 467 super.onDraw(canvas); 468 } 469 470 @Override 471 public void setBackgroundColor(int color) { 472 mBackgroundColor = color; 473 setPaintColor(mBackgroundAlpha, mBackgroundColor); 474 } 475 476 public void setBackgroundPressedColor(int color) { 477 mBackgroundPressedColor = color; 478 } 479 480 public void setBackgroundAlpha(int alpha) { 481 mBackgroundAlpha = alpha; 482 setPaintColor(mBackgroundAlpha, mBackgroundColor); 483 } 484 485 private double diagonalLength(double w, double h) { 486 return Math.sqrt((w*w) + (h*h)); 487 } 488 private double diagonalLength() { 489 return diagonalLength(getWidth(), getHeight()); 490 } 491 492 private TransitionDrawable crossfadeDrawable(Drawable from, Drawable to) { 493 Drawable [] arrayDrawable = new Drawable[2]; 494 arrayDrawable[0] = from; 495 arrayDrawable[1] = to; 496 TransitionDrawable transitionDrawable = new TransitionDrawable(arrayDrawable); 497 transitionDrawable.setCrossFadeEnabled(true); 498 return transitionDrawable; 499 } 500 501 /** 502 * Set the shutter button's icon resource 503 */ 504 public void setShutterButtonIcon(int resId) { 505 mShutterButton.setImageResource(resId); 506 } 507 508 /** 509 * Animates bar to a single stop button 510 */ 511 public void animateToCircle(int resId) { 512 final ValueAnimator radiusAnimator = ValueAnimator.ofFloat( 513 (float) diagonalLength()/2, 514 mCircleRadius); 515 radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS); 516 radiusAnimator.setInterpolator(Gusterpolator.INSTANCE); 517 518 radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 519 @Override 520 public void onAnimationUpdate(ValueAnimator animation) { 521 mCirclePath.reset(); 522 mCirclePath.addCircle( 523 getWidth()/2, 524 getHeight()/2, 525 (Float) animation.getAnimatedValue(), 526 Path.Direction.CW); 527 528 invalidate(); 529 } 530 }); 531 532 TransitionDrawable transitionDrawable = crossfadeDrawable( 533 mShutterButton.getDrawable(), 534 getResources().getDrawable(resId)); 535 mShutterButton.setImageDrawable(transitionDrawable); 536 537 View optionsOverlay = findViewById(R.id.bottombar_options_overlay); 538 optionsOverlay.setVisibility(View.INVISIBLE); 539 540 mDrawCircle = true; 541 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 542 radiusAnimator.start(); 543 } 544 545 /** 546 * Animates bar to full width / length with video capture icon 547 */ 548 public void animateToFullSize(int resId) { 549 final ValueAnimator radiusAnimator = ValueAnimator.ofFloat( 550 mCircleRadius, 551 (float) diagonalLength()/2); 552 radiusAnimator.setDuration(CIRCLE_ANIM_DURATION_MS); 553 radiusAnimator.setInterpolator(Gusterpolator.INSTANCE); 554 radiusAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 555 @Override 556 public void onAnimationUpdate(ValueAnimator animation) { 557 mCirclePath.reset(); 558 mCirclePath.addCircle( 559 getWidth()/2, 560 getHeight()/2, 561 (Float) animation.getAnimatedValue(), 562 Path.Direction.CW); 563 564 invalidate(); 565 } 566 }); 567 radiusAnimator.addListener(new AnimatorListenerAdapter() { 568 @Override 569 public void onAnimationEnd(Animator animation) { 570 View optionsOverlay = findViewById(R.id.bottombar_options_overlay); 571 optionsOverlay.setVisibility(View.VISIBLE); 572 mDrawCircle = false; 573 } 574 }); 575 576 TransitionDrawable transitionDrawable = crossfadeDrawable( 577 mShutterButton.getDrawable(), 578 getResources().getDrawable(resId)); 579 mShutterButton.setImageDrawable(transitionDrawable); 580 581 transitionDrawable.startTransition(CIRCLE_ANIM_DURATION_MS); 582 radiusAnimator.start(); 583 } 584} 585