1 /* 2 * Copyright (C) 2010 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 com.android.camera.PreferenceGroup; 20 import com.android.camera.R; 21 import com.android.camera.Util; 22 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.graphics.Canvas; 26 import android.graphics.Paint; 27 import android.graphics.Path; 28 import android.graphics.RectF; 29 import android.os.Handler; 30 import android.os.SystemClock; 31 import android.util.AttributeSet; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.widget.ImageView; 35 36 /** 37 * A view that contains camera setting indicators in two levels. The first-level 38 * indicators including the zoom, camera picker, flash and second-level control. 39 * The second-level indicators are the merely for the camera settings. 40 */ 41 public class IndicatorControlWheel extends IndicatorControl implements 42 View.OnClickListener { 43 public static final int HIGHLIGHT_WIDTH = 4; 44 45 private static final String TAG = "IndicatorControlWheel"; 46 private static final int HIGHLIGHT_DEGREES = 30; 47 private static final double HIGHLIGHT_RADIANS = Math.toRadians(HIGHLIGHT_DEGREES); 48 49 // The following angles are based in the zero degree on the right. Here we 50 // have the CameraPicker, ZoomControl and the Settings icons in the 51 // first-level. For consistency, we treat the zoom control as one of the 52 // indicator buttons but it needs additional efforts for rotation animation. 53 // For second-level indicators, the indicators are located evenly between start 54 // and end angle. In addition, these indicators for the second-level hidden 55 // in the same wheel with larger angle values are visible after rotation. 56 private static final int FIRST_LEVEL_START_DEGREES = 74; 57 private static final int FIRST_LEVEL_END_DEGREES = 286; 58 private static final int FIRST_LEVEL_SECTOR_DEGREES = 45; 59 private static final int SECOND_LEVEL_START_DEGREES = 60; 60 private static final int SECOND_LEVEL_END_DEGREES = 300; 61 private static final int MAX_ZOOM_CONTROL_DEGREES = 264; 62 private static final int CLOSE_ICON_DEFAULT_DEGREES = 315; 63 64 private static final int ANIMATION_TIME = 300; // milliseconds 65 66 // The width of the edges on both sides of the wheel, which has less alpha. 67 private static final float EDGE_STROKE_WIDTH = 6f; 68 private static final int TIME_LAPSE_ARC_WIDTH = 6; 69 70 private final int HIGHLIGHT_COLOR; 71 private final int HIGHLIGHT_FAN_COLOR; 72 private final int TIME_LAPSE_ARC_COLOR; 73 74 // The center of the shutter button. 75 private int mCenterX, mCenterY; 76 // The width of the wheel stroke. 77 private int mStrokeWidth; 78 private double mShutterButtonRadius; 79 private double mWheelRadius; 80 private double mChildRadians[]; 81 private Paint mBackgroundPaint; 82 private RectF mBackgroundRect; 83 // The index of the child that is being pressed. -1 means no child is being 84 // pressed. 85 private int mPressedIndex = -1; 86 87 // Time lapse recording variables. 88 private int mTimeLapseInterval; // in ms 89 private long mRecordingStartTime = 0; 90 private long mNumberOfFrames = 0; 91 92 // Remember the last event for event cancelling if out of bound. 93 private MotionEvent mLastMotionEvent; 94 95 private ImageView mSecondLevelIcon; 96 private ImageView mCloseIcon; 97 98 // Variables for animation. 99 private long mAnimationStartTime; 100 private boolean mInAnimation = false; 101 private Handler mHandler = new Handler(); 102 private final Runnable mRunnable = new Runnable() { 103 public void run() { 104 requestLayout(); 105 } 106 }; 107 108 // Variables for level control. 109 private int mCurrentLevel = 0; 110 private int mSecondLevelStartIndex = -1; 111 private double mStartVisibleRadians[] = new double[2]; 112 private double mEndVisibleRadians[] = new double[2]; 113 private double mSectorRadians[] = new double[2]; 114 private double mTouchSectorRadians[] = new double[2]; 115 116 private ZoomControlWheel mZoomControl; 117 private boolean mInitialized; 118 119 public IndicatorControlWheel(Context context, AttributeSet attrs) { 120 super(context, attrs); 121 Resources resources = context.getResources(); 122 HIGHLIGHT_COLOR = resources.getColor(R.color.review_control_pressed_color); 123 HIGHLIGHT_FAN_COLOR = resources.getColor(R.color.review_control_pressed_fan_color); 124 TIME_LAPSE_ARC_COLOR = resources.getColor(R.color.time_lapse_arc); 125 126 setWillNotDraw(false); 127 128 mBackgroundPaint = new Paint(); 129 mBackgroundPaint.setStyle(Paint.Style.STROKE); 130 mBackgroundPaint.setAntiAlias(true); 131 132 mBackgroundRect = new RectF(); 133 } 134 135 private int getChildCountByLevel(int level) { 136 // Get current child count by level. 137 if (level == 1) { 138 return (getChildCount() - mSecondLevelStartIndex); 139 } else { 140 return mSecondLevelStartIndex; 141 } 142 } 143 144 private void changeIndicatorsLevel() { 145 mPressedIndex = -1; 146 dismissSettingPopup(); 147 mInAnimation = true; 148 mAnimationStartTime = SystemClock.uptimeMillis(); 149 requestLayout(); 150 } 151 152 @Override 153 public void onClick(View view) { 154 changeIndicatorsLevel(); 155 } 156 157 public void initialize(Context context, PreferenceGroup group, 158 boolean isZoomSupported, String[] keys, String[] otherSettingKeys) { 159 mShutterButtonRadius = IndicatorControlWheelContainer.SHUTTER_BUTTON_RADIUS; 160 mStrokeWidth = Util.dpToPixel(IndicatorControlWheelContainer.STROKE_WIDTH); 161 mWheelRadius = mShutterButtonRadius + mStrokeWidth * 0.5; 162 163 setPreferenceGroup(group); 164 165 // Add the ZoomControl if supported. 166 if (isZoomSupported) { 167 mZoomControl = (ZoomControlWheel) findViewById(R.id.zoom_control); 168 mZoomControl.setVisibility(View.VISIBLE); 169 } 170 171 // Add CameraPicker. 172 initializeCameraPicker(); 173 174 // Add second-level Indicator Icon. 175 mSecondLevelIcon = addImageButton(context, R.drawable.ic_settings_holo_light, true); 176 mSecondLevelStartIndex = getChildCount(); 177 178 // Add second-level buttons. 179 mCloseIcon = addImageButton(context, R.drawable.btn_wheel_close_settings, false); 180 addControls(keys, otherSettingKeys); 181 182 // The angle(in radians) of each icon for touch events. 183 mChildRadians = new double[getChildCount()]; 184 presetFirstLevelChildRadians(); 185 presetSecondLevelChildRadians(); 186 mInitialized = true; 187 } 188 189 private ImageView addImageButton(Context context, int resourceId, boolean rotatable) { 190 ImageView view; 191 if (rotatable) { 192 view = new RotateImageView(context); 193 } else { 194 view = new TwoStateImageView(context); 195 } 196 view.setImageResource(resourceId); 197 view.setOnClickListener(this); 198 addView(view); 199 return view; 200 } 201 202 private int getTouchIndicatorIndex(double delta) { 203 // The delta is the angle of touch point in radians. 204 if (mInAnimation) return -1; 205 int count = getChildCountByLevel(mCurrentLevel); 206 if (count == 0) return -1; 207 int sectors = count - 1; 208 int startIndex = (mCurrentLevel == 0) ? 0 : mSecondLevelStartIndex; 209 int endIndex; 210 if (mCurrentLevel == 0) { 211 // Skip the first component if it is zoom control, as we will 212 // deal with it specifically. 213 if (mZoomControl != null) startIndex++; 214 endIndex = mSecondLevelStartIndex - 1; 215 } else { 216 endIndex = getChildCount() - 1; 217 } 218 // Check which indicator is touched. 219 double halfTouchSectorRadians = mTouchSectorRadians[mCurrentLevel]; 220 if ((delta >= (mChildRadians[startIndex] - halfTouchSectorRadians)) && 221 (delta <= (mChildRadians[endIndex] + halfTouchSectorRadians))) { 222 int index = 0; 223 if (mCurrentLevel == 1) { 224 index = (int) ((delta - mChildRadians[startIndex]) 225 / mSectorRadians[mCurrentLevel]); 226 // greater than the center of ending indicator 227 if (index > sectors) return (startIndex + sectors); 228 // less than the center of starting indicator 229 if (index < 0) return startIndex; 230 } 231 if (delta <= (mChildRadians[startIndex + index] 232 + halfTouchSectorRadians)) { 233 return (startIndex + index); 234 } 235 if (delta >= (mChildRadians[startIndex + index + 1] 236 - halfTouchSectorRadians)) { 237 return (startIndex + index + 1); 238 } 239 240 // It must be for zoom control if the touch event is in the visible 241 // range and not for other indicator buttons. 242 if ((mCurrentLevel == 0) && (mZoomControl != null)) return 0; 243 } 244 return -1; 245 } 246 247 private void injectMotionEvent(int viewIndex, MotionEvent event, int action) { 248 View v = getChildAt(viewIndex); 249 event.setAction(action); 250 v.dispatchTouchEvent(event); 251 } 252 253 @Override 254 public boolean dispatchTouchEvent(MotionEvent event) { 255 if (!onFilterTouchEventForSecurity(event)) return false; 256 mLastMotionEvent = event; 257 int action = event.getAction(); 258 259 double dx = event.getX() - mCenterX; 260 double dy = mCenterY - event.getY(); 261 double radius = Math.sqrt(dx * dx + dy * dy); 262 263 // Ignore the event if too far from the shutter button. 264 if ((radius <= (mWheelRadius + mStrokeWidth)) && (radius > mShutterButtonRadius)) { 265 double delta = Math.atan2(dy, dx); 266 if (delta < 0) delta += Math.PI * 2; 267 int index = getTouchIndicatorIndex(delta); 268 // Check if the touch event is for zoom control. 269 if ((mZoomControl != null) && (index == 0)) { 270 mZoomControl.dispatchTouchEvent(event); 271 } 272 // Move over from one indicator to another. 273 if ((index != mPressedIndex) || (action == MotionEvent.ACTION_DOWN)) { 274 if (mPressedIndex != -1) { 275 injectMotionEvent(mPressedIndex, event, MotionEvent.ACTION_CANCEL); 276 } else { 277 // Cancel the popup if it is different from the selected. 278 if (getSelectedIndicatorIndex() != index) dismissSettingPopup(); 279 } 280 if ((index != -1) && (action == MotionEvent.ACTION_MOVE)) { 281 if (mCurrentLevel != 0) { 282 injectMotionEvent(index, event, MotionEvent.ACTION_DOWN); 283 } 284 } 285 } 286 if ((index != -1) && (action != MotionEvent.ACTION_MOVE)) { 287 getChildAt(index).dispatchTouchEvent(event); 288 } 289 // Do not highlight the CameraPicker or Settings icon if we 290 // touch from the zoom control to one of them. 291 if ((mCurrentLevel == 0) && (index != 0) 292 && (action == MotionEvent.ACTION_MOVE)) { 293 return true; 294 } 295 // Once the button is up, reset the press index. 296 mPressedIndex = (action == MotionEvent.ACTION_UP) ? -1 : index; 297 invalidate(); 298 return true; 299 } 300 // The event is not on any of the child. 301 onTouchOutBound(); 302 return false; 303 } 304 305 private void rotateWheel() { 306 int totalDegrees = CLOSE_ICON_DEFAULT_DEGREES - SECOND_LEVEL_START_DEGREES; 307 int startAngle = ((mCurrentLevel == 0) ? CLOSE_ICON_DEFAULT_DEGREES 308 : SECOND_LEVEL_START_DEGREES); 309 if (mCurrentLevel == 0) totalDegrees = -totalDegrees; 310 311 int elapsedTime = (int) (SystemClock.uptimeMillis() - mAnimationStartTime); 312 if (elapsedTime >= ANIMATION_TIME) { 313 elapsedTime = ANIMATION_TIME; 314 mCurrentLevel = (mCurrentLevel == 0) ? 1 : 0; 315 mInAnimation = false; 316 } 317 318 int expectedAngle = startAngle + (totalDegrees * elapsedTime / ANIMATION_TIME); 319 double increment = Math.toRadians(expectedAngle) 320 - mChildRadians[mSecondLevelStartIndex]; 321 for (int i = 0 ; i < getChildCount(); ++i) mChildRadians[i] += increment; 322 // We also need to rotate the zoom control wheel as well. 323 if (mZoomControl != null) { 324 mZoomControl.rotate(mChildRadians[0] 325 - Math.toRadians(MAX_ZOOM_CONTROL_DEGREES)); 326 } 327 } 328 329 @Override 330 protected void onLayout( 331 boolean changed, int left, int top, int right, int bottom) { 332 if (!mInitialized) return; 333 if (mInAnimation) { 334 rotateWheel(); 335 mHandler.post(mRunnable); 336 } 337 mCenterX = right - left - Util.dpToPixel( 338 IndicatorControlWheelContainer.FULL_WHEEL_RADIUS); 339 mCenterY = (bottom - top) / 2; 340 341 // Layout the indicators based on the current level. 342 // The icons are spreaded on the left side of the shutter button. 343 for (int i = 0; i < getChildCount(); ++i) { 344 View view = getChildAt(i); 345 // We still need to show the disabled indicators in the second level. 346 double radian = mChildRadians[i]; 347 double startVisibleRadians = mInAnimation 348 ? mStartVisibleRadians[1] 349 : mStartVisibleRadians[mCurrentLevel]; 350 double endVisibleRadians = mInAnimation 351 ? mEndVisibleRadians[1] 352 : mEndVisibleRadians[mCurrentLevel]; 353 if ((!view.isEnabled() && (mCurrentLevel == 0)) 354 || (radian < (startVisibleRadians - HIGHLIGHT_RADIANS / 2)) 355 || (radian > (endVisibleRadians + HIGHLIGHT_RADIANS / 2))) { 356 view.setVisibility(View.GONE); 357 continue; 358 } 359 view.setVisibility(View.VISIBLE); 360 int x = mCenterX + (int)(mWheelRadius * Math.cos(radian)); 361 int y = mCenterY - (int)(mWheelRadius * Math.sin(radian)); 362 int width = view.getMeasuredWidth(); 363 int height = view.getMeasuredHeight(); 364 if (view == mZoomControl) { 365 // ZoomControlWheel matches the size of its parent view. 366 view.layout(0, 0, right - left, bottom - top); 367 } else { 368 view.layout(x - width / 2, y - height / 2, x + width / 2, 369 y + height / 2); 370 } 371 } 372 } 373 374 private void presetFirstLevelChildRadians() { 375 // Set the visible range in the first-level indicator wheel. 376 mStartVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_START_DEGREES); 377 mTouchSectorRadians[0] = HIGHLIGHT_RADIANS; 378 mEndVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_END_DEGREES); 379 380 // Set the angle of each component in the first-level indicator wheel. 381 int startIndex = 0; 382 if (mZoomControl != null) { 383 mChildRadians[startIndex++] = Math.toRadians(MAX_ZOOM_CONTROL_DEGREES); 384 } 385 if (mCameraPicker != null) { 386 mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_START_DEGREES); 387 } 388 mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_END_DEGREES); 389 } 390 391 private void presetSecondLevelChildRadians() { 392 int count = getChildCountByLevel(1); 393 int sectors = (count <= 1) ? 1 : (count - 1); 394 double sectorDegrees = 395 ((SECOND_LEVEL_END_DEGREES - SECOND_LEVEL_START_DEGREES) / sectors); 396 mSectorRadians[1] = Math.toRadians(sectorDegrees); 397 398 double degrees = CLOSE_ICON_DEFAULT_DEGREES; 399 mStartVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_START_DEGREES); 400 401 int startIndex = mSecondLevelStartIndex; 402 for (int i = 0; i < count; i++) { 403 mChildRadians[startIndex + i] = Math.toRadians(degrees); 404 degrees += sectorDegrees; 405 } 406 407 // The radians for the touch sector of an indicator. 408 mTouchSectorRadians[1] = 409 Math.min(HIGHLIGHT_RADIANS, Math.toRadians(sectorDegrees)); 410 411 mEndVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_END_DEGREES); 412 } 413 414 public void startTimeLapseAnimation(int timeLapseInterval, long startTime) { 415 mTimeLapseInterval = timeLapseInterval; 416 mRecordingStartTime = startTime; 417 mNumberOfFrames = 0; 418 invalidate(); 419 } 420 421 public void stopTimeLapseAnimation() { 422 mTimeLapseInterval = 0; 423 invalidate(); 424 } 425 426 private int getSelectedIndicatorIndex() { 427 for (int i = 0; i < mIndicators.size(); i++) { 428 AbstractIndicatorButton b = mIndicators.get(i); 429 if (b.getPopupWindow() != null) { 430 return indexOfChild(b); 431 } 432 } 433 if (mPressedIndex != -1) { 434 View v = getChildAt(mPressedIndex); 435 if (!(v instanceof AbstractIndicatorButton) && v.isEnabled()) { 436 return mPressedIndex; 437 } 438 } 439 return -1; 440 } 441 442 @Override 443 protected void onDraw(Canvas canvas) { 444 int selectedIndex = getSelectedIndicatorIndex(); 445 446 // Draw the highlight arc if an indicator is selected or being pressed. 447 // And skip the zoom control which index is zero. 448 if (selectedIndex >= 1) { 449 int degree = (int) Math.toDegrees(mChildRadians[selectedIndex]); 450 float innerR = (float) mShutterButtonRadius; 451 float outerR = (float) (mShutterButtonRadius + mStrokeWidth + 452 EDGE_STROKE_WIDTH * 0.5); 453 454 // Construct the path of the fan-shaped semi-transparent area. 455 Path fanPath = new Path(); 456 mBackgroundRect.set(mCenterX - innerR, mCenterY - innerR, 457 mCenterX + innerR, mCenterY + innerR); 458 fanPath.arcTo(mBackgroundRect, -degree + HIGHLIGHT_DEGREES / 2, 459 -HIGHLIGHT_DEGREES); 460 mBackgroundRect.set(mCenterX - outerR, mCenterY - outerR, 461 mCenterX + outerR, mCenterY + outerR); 462 fanPath.arcTo(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 463 HIGHLIGHT_DEGREES); 464 fanPath.close(); 465 466 mBackgroundPaint.setStrokeWidth(HIGHLIGHT_WIDTH); 467 mBackgroundPaint.setStrokeCap(Paint.Cap.SQUARE); 468 mBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE); 469 mBackgroundPaint.setColor(HIGHLIGHT_FAN_COLOR); 470 canvas.drawPath(fanPath, mBackgroundPaint); 471 472 // Draw the highlight edge 473 mBackgroundPaint.setStyle(Paint.Style.STROKE); 474 mBackgroundPaint.setColor(HIGHLIGHT_COLOR); 475 canvas.drawArc(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 476 HIGHLIGHT_DEGREES, false, mBackgroundPaint); 477 } 478 479 // Draw arc shaped indicator in time lapse recording. 480 if (mTimeLapseInterval != 0) { 481 // Setup rectangle and paint. 482 mBackgroundRect.set((float)(mCenterX - mShutterButtonRadius), 483 (float)(mCenterY - mShutterButtonRadius), 484 (float)(mCenterX + mShutterButtonRadius), 485 (float)(mCenterY + mShutterButtonRadius)); 486 mBackgroundRect.inset(3f, 3f); 487 mBackgroundPaint.setStrokeWidth(TIME_LAPSE_ARC_WIDTH); 488 mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); 489 mBackgroundPaint.setColor(TIME_LAPSE_ARC_COLOR); 490 491 // Compute the start angle and sweep angle. 492 long timeDelta = SystemClock.uptimeMillis() - mRecordingStartTime; 493 long numberOfFrames = timeDelta / mTimeLapseInterval; 494 float sweepAngle; 495 if (numberOfFrames > mNumberOfFrames) { 496 // The arc just acrosses 0 degree. Draw a full circle so it 497 // looks better. 498 sweepAngle = 360; 499 mNumberOfFrames = numberOfFrames; 500 } else { 501 sweepAngle = timeDelta % mTimeLapseInterval * 360f / mTimeLapseInterval; 502 } 503 504 canvas.drawArc(mBackgroundRect, 0, sweepAngle, false, mBackgroundPaint); 505 invalidate(); 506 } 507 508 super.onDraw(canvas); 509 } 510 511 @Override 512 public void setEnabled(boolean enabled) { 513 super.setEnabled(enabled); 514 if (!mInitialized) return; 515 if (mCurrentMode == MODE_VIDEO) { 516 mSecondLevelIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 517 mCloseIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 518 requestLayout(); 519 } else { 520 // We also disable the zoom button during snapshot. 521 enableZoom(enabled); 522 } 523 mSecondLevelIcon.setEnabled(enabled); 524 mCloseIcon.setEnabled(enabled); 525 } 526 527 public void enableZoom(boolean enabled) { 528 if (mZoomControl != null) mZoomControl.setEnabled(enabled); 529 } 530 531 public void onTouchOutBound() { 532 dismissSettingPopup(); 533 if (mPressedIndex != -1) { 534 injectMotionEvent(mPressedIndex, mLastMotionEvent, MotionEvent.ACTION_CANCEL); 535 mPressedIndex = -1; 536 invalidate(); 537 } 538 } 539 540 public void dismissSecondLevelIndicator() { 541 if (mCurrentLevel == 1) { 542 changeIndicatorsLevel(); 543 } 544 } 545 } 546