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 IndicatorControlWheel(Context context, AttributeSet attrs)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 getChildCountByLevel(int level)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 changeIndicatorsLevel()144 private void changeIndicatorsLevel() { 145 mPressedIndex = -1; 146 dismissSettingPopup(); 147 mInAnimation = true; 148 mAnimationStartTime = SystemClock.uptimeMillis(); 149 requestLayout(); 150 } 151 152 @Override onClick(View view)153 public void onClick(View view) { 154 changeIndicatorsLevel(); 155 } 156 initialize(Context context, PreferenceGroup group, boolean isZoomSupported, String[] keys, String[] otherSettingKeys)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 mZoomControl = (ZoomControlWheel) findViewById(R.id.zoom_control); 167 if (isZoomSupported) { 168 mZoomControl.setVisibility(View.VISIBLE); 169 } else { 170 // ZoomControlWheel is default added in res/layout-sw600dp/camera_control.xml, 171 // If zoom not supported, should remove this view, or the radians of other view 172 // will be calculated incorrectly, thus control wheel show abnormal. 173 if (mZoomControl != null) { 174 removeView(mZoomControl); 175 } 176 } 177 178 // Add CameraPicker. 179 initializeCameraPicker(); 180 181 // Add second-level Indicator Icon. 182 mSecondLevelIcon = addImageButton(context, R.drawable.ic_settings_holo_light, true); 183 mSecondLevelStartIndex = getChildCount(); 184 185 // Add second-level buttons. 186 mCloseIcon = addImageButton(context, R.drawable.btn_wheel_close_settings, false); 187 addControls(keys, otherSettingKeys); 188 189 // The angle(in radians) of each icon for touch events. 190 mChildRadians = new double[getChildCount()]; 191 presetFirstLevelChildRadians(); 192 presetSecondLevelChildRadians(); 193 mInitialized = true; 194 } 195 addImageButton(Context context, int resourceId, boolean rotatable)196 private ImageView addImageButton(Context context, int resourceId, boolean rotatable) { 197 ImageView view; 198 if (rotatable) { 199 view = new RotateImageView(context); 200 } else { 201 view = new TwoStateImageView(context); 202 } 203 view.setImageResource(resourceId); 204 view.setOnClickListener(this); 205 addView(view); 206 return view; 207 } 208 getTouchIndicatorIndex(double delta)209 private int getTouchIndicatorIndex(double delta) { 210 // The delta is the angle of touch point in radians. 211 if (mInAnimation) return -1; 212 int count = getChildCountByLevel(mCurrentLevel); 213 if (count == 0) return -1; 214 int sectors = count - 1; 215 int startIndex = (mCurrentLevel == 0) ? 0 : mSecondLevelStartIndex; 216 int endIndex; 217 if (mCurrentLevel == 0) { 218 // Skip the first component if it is zoom control, as we will 219 // deal with it specifically. 220 if (mZoomControl != null) startIndex++; 221 endIndex = mSecondLevelStartIndex - 1; 222 } else { 223 endIndex = getChildCount() - 1; 224 } 225 // Check which indicator is touched. 226 double halfTouchSectorRadians = mTouchSectorRadians[mCurrentLevel]; 227 if ((delta >= (mChildRadians[startIndex] - halfTouchSectorRadians)) && 228 (delta <= (mChildRadians[endIndex] + halfTouchSectorRadians))) { 229 int index = 0; 230 if (mCurrentLevel == 1) { 231 index = (int) ((delta - mChildRadians[startIndex]) 232 / mSectorRadians[mCurrentLevel]); 233 // greater than the center of ending indicator 234 if (index > sectors) return (startIndex + sectors); 235 // less than the center of starting indicator 236 if (index < 0) return startIndex; 237 } 238 if (delta <= (mChildRadians[startIndex + index] 239 + halfTouchSectorRadians)) { 240 return (startIndex + index); 241 } 242 if (delta >= (mChildRadians[startIndex + index + 1] 243 - halfTouchSectorRadians)) { 244 return (startIndex + index + 1); 245 } 246 247 // It must be for zoom control if the touch event is in the visible 248 // range and not for other indicator buttons. 249 if ((mCurrentLevel == 0) && (mZoomControl != null)) return 0; 250 } 251 return -1; 252 } 253 injectMotionEvent(int viewIndex, MotionEvent event, int action)254 private void injectMotionEvent(int viewIndex, MotionEvent event, int action) { 255 View v = getChildAt(viewIndex); 256 event.setAction(action); 257 v.dispatchTouchEvent(event); 258 } 259 260 @Override dispatchTouchEvent(MotionEvent event)261 public boolean dispatchTouchEvent(MotionEvent event) { 262 if (!onFilterTouchEventForSecurity(event)) return false; 263 mLastMotionEvent = event; 264 int action = event.getAction(); 265 266 double dx = event.getX() - mCenterX; 267 double dy = mCenterY - event.getY(); 268 double radius = Math.sqrt(dx * dx + dy * dy); 269 270 // Ignore the event if too far from the shutter button. 271 if ((radius <= (mWheelRadius + mStrokeWidth)) && (radius > mShutterButtonRadius)) { 272 double delta = Math.atan2(dy, dx); 273 if (delta < 0) delta += Math.PI * 2; 274 int index = getTouchIndicatorIndex(delta); 275 // Check if the touch event is for zoom control. 276 if ((mZoomControl != null) && (index == 0)) { 277 mZoomControl.dispatchTouchEvent(event); 278 } 279 // Move over from one indicator to another. 280 if ((index != mPressedIndex) || (action == MotionEvent.ACTION_DOWN)) { 281 if (mPressedIndex != -1) { 282 injectMotionEvent(mPressedIndex, event, MotionEvent.ACTION_CANCEL); 283 } else { 284 // Cancel the popup if it is different from the selected. 285 if (getSelectedIndicatorIndex() != index) dismissSettingPopup(); 286 } 287 if ((index != -1) && (action == MotionEvent.ACTION_MOVE)) { 288 if (mCurrentLevel != 0) { 289 injectMotionEvent(index, event, MotionEvent.ACTION_DOWN); 290 } 291 } 292 } 293 if ((index != -1) && (action != MotionEvent.ACTION_MOVE)) { 294 getChildAt(index).dispatchTouchEvent(event); 295 } 296 // Do not highlight the CameraPicker or Settings icon if we 297 // touch from the zoom control to one of them. 298 if ((mCurrentLevel == 0) && (index != 0) 299 && (action == MotionEvent.ACTION_MOVE)) { 300 return true; 301 } 302 // Once the button is up, reset the press index. 303 mPressedIndex = (action == MotionEvent.ACTION_UP) ? -1 : index; 304 invalidate(); 305 return true; 306 } 307 // The event is not on any of the child. 308 onTouchOutBound(); 309 return false; 310 } 311 rotateWheel()312 private void rotateWheel() { 313 int totalDegrees = CLOSE_ICON_DEFAULT_DEGREES - SECOND_LEVEL_START_DEGREES; 314 int startAngle = ((mCurrentLevel == 0) ? CLOSE_ICON_DEFAULT_DEGREES 315 : SECOND_LEVEL_START_DEGREES); 316 if (mCurrentLevel == 0) totalDegrees = -totalDegrees; 317 318 int elapsedTime = (int) (SystemClock.uptimeMillis() - mAnimationStartTime); 319 if (elapsedTime >= ANIMATION_TIME) { 320 elapsedTime = ANIMATION_TIME; 321 mCurrentLevel = (mCurrentLevel == 0) ? 1 : 0; 322 mInAnimation = false; 323 } 324 325 int expectedAngle = startAngle + (totalDegrees * elapsedTime / ANIMATION_TIME); 326 double increment = Math.toRadians(expectedAngle) 327 - mChildRadians[mSecondLevelStartIndex]; 328 for (int i = 0 ; i < getChildCount(); ++i) mChildRadians[i] += increment; 329 // We also need to rotate the zoom control wheel as well. 330 if (mZoomControl != null) { 331 mZoomControl.rotate(mChildRadians[0] 332 - Math.toRadians(MAX_ZOOM_CONTROL_DEGREES)); 333 } 334 } 335 336 @Override onLayout( boolean changed, int left, int top, int right, int bottom)337 protected void onLayout( 338 boolean changed, int left, int top, int right, int bottom) { 339 if (!mInitialized) return; 340 if (mInAnimation) { 341 rotateWheel(); 342 mHandler.post(mRunnable); 343 } 344 mCenterX = right - left - Util.dpToPixel( 345 IndicatorControlWheelContainer.FULL_WHEEL_RADIUS); 346 mCenterY = (bottom - top) / 2; 347 348 // Layout the indicators based on the current level. 349 // The icons are spreaded on the left side of the shutter button. 350 for (int i = 0; i < getChildCount(); ++i) { 351 View view = getChildAt(i); 352 // We still need to show the disabled indicators in the second level. 353 double radian = mChildRadians[i]; 354 double startVisibleRadians = mInAnimation 355 ? mStartVisibleRadians[1] 356 : mStartVisibleRadians[mCurrentLevel]; 357 double endVisibleRadians = mInAnimation 358 ? mEndVisibleRadians[1] 359 : mEndVisibleRadians[mCurrentLevel]; 360 if ((!view.isEnabled() && (mCurrentLevel == 0)) 361 || (radian < (startVisibleRadians - HIGHLIGHT_RADIANS / 2)) 362 || (radian > (endVisibleRadians + HIGHLIGHT_RADIANS / 2))) { 363 view.setVisibility(View.GONE); 364 continue; 365 } 366 view.setVisibility(View.VISIBLE); 367 int x = mCenterX + (int)(mWheelRadius * Math.cos(radian)); 368 int y = mCenterY - (int)(mWheelRadius * Math.sin(radian)); 369 int width = view.getMeasuredWidth(); 370 int height = view.getMeasuredHeight(); 371 if (view == mZoomControl) { 372 // ZoomControlWheel matches the size of its parent view. 373 view.layout(0, 0, right - left, bottom - top); 374 } else { 375 view.layout(x - width / 2, y - height / 2, x + width / 2, 376 y + height / 2); 377 } 378 } 379 } 380 presetFirstLevelChildRadians()381 private void presetFirstLevelChildRadians() { 382 // Set the visible range in the first-level indicator wheel. 383 mStartVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_START_DEGREES); 384 mTouchSectorRadians[0] = HIGHLIGHT_RADIANS; 385 mEndVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_END_DEGREES); 386 387 // Set the angle of each component in the first-level indicator wheel. 388 int startIndex = 0; 389 if (mZoomControl != null) { 390 mChildRadians[startIndex++] = Math.toRadians(MAX_ZOOM_CONTROL_DEGREES); 391 } 392 if (mCameraPicker != null) { 393 mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_START_DEGREES); 394 } 395 mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_END_DEGREES); 396 } 397 presetSecondLevelChildRadians()398 private void presetSecondLevelChildRadians() { 399 int count = getChildCountByLevel(1); 400 int sectors = (count <= 1) ? 1 : (count - 1); 401 double sectorDegrees = 402 ((SECOND_LEVEL_END_DEGREES - SECOND_LEVEL_START_DEGREES) / sectors); 403 mSectorRadians[1] = Math.toRadians(sectorDegrees); 404 405 double degrees = CLOSE_ICON_DEFAULT_DEGREES; 406 mStartVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_START_DEGREES); 407 408 int startIndex = mSecondLevelStartIndex; 409 for (int i = 0; i < count; i++) { 410 mChildRadians[startIndex + i] = Math.toRadians(degrees); 411 degrees += sectorDegrees; 412 } 413 414 // The radians for the touch sector of an indicator. 415 mTouchSectorRadians[1] = 416 Math.min(HIGHLIGHT_RADIANS, Math.toRadians(sectorDegrees)); 417 418 mEndVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_END_DEGREES); 419 } 420 startTimeLapseAnimation(int timeLapseInterval, long startTime)421 public void startTimeLapseAnimation(int timeLapseInterval, long startTime) { 422 mTimeLapseInterval = timeLapseInterval; 423 mRecordingStartTime = startTime; 424 mNumberOfFrames = 0; 425 invalidate(); 426 } 427 stopTimeLapseAnimation()428 public void stopTimeLapseAnimation() { 429 mTimeLapseInterval = 0; 430 invalidate(); 431 } 432 getSelectedIndicatorIndex()433 private int getSelectedIndicatorIndex() { 434 for (int i = 0; i < mIndicators.size(); i++) { 435 AbstractIndicatorButton b = mIndicators.get(i); 436 if (b.getPopupWindow() != null) { 437 return indexOfChild(b); 438 } 439 } 440 if (mPressedIndex != -1) { 441 View v = getChildAt(mPressedIndex); 442 if (!(v instanceof AbstractIndicatorButton) && v.isEnabled()) { 443 return mPressedIndex; 444 } 445 } 446 return -1; 447 } 448 449 @Override onDraw(Canvas canvas)450 protected void onDraw(Canvas canvas) { 451 int selectedIndex = getSelectedIndicatorIndex(); 452 453 // Draw the highlight arc if an indicator is selected or being pressed. 454 // And skip the zoom control which index is zero. 455 if (selectedIndex >= 1) { 456 int degree = (int) Math.toDegrees(mChildRadians[selectedIndex]); 457 float innerR = (float) mShutterButtonRadius; 458 float outerR = (float) (mShutterButtonRadius + mStrokeWidth + 459 EDGE_STROKE_WIDTH * 0.5); 460 461 // Construct the path of the fan-shaped semi-transparent area. 462 Path fanPath = new Path(); 463 mBackgroundRect.set(mCenterX - innerR, mCenterY - innerR, 464 mCenterX + innerR, mCenterY + innerR); 465 fanPath.arcTo(mBackgroundRect, -degree + HIGHLIGHT_DEGREES / 2, 466 -HIGHLIGHT_DEGREES); 467 mBackgroundRect.set(mCenterX - outerR, mCenterY - outerR, 468 mCenterX + outerR, mCenterY + outerR); 469 fanPath.arcTo(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 470 HIGHLIGHT_DEGREES); 471 fanPath.close(); 472 473 mBackgroundPaint.setStrokeWidth(HIGHLIGHT_WIDTH); 474 mBackgroundPaint.setStrokeCap(Paint.Cap.SQUARE); 475 mBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE); 476 mBackgroundPaint.setColor(HIGHLIGHT_FAN_COLOR); 477 canvas.drawPath(fanPath, mBackgroundPaint); 478 479 // Draw the highlight edge 480 mBackgroundPaint.setStyle(Paint.Style.STROKE); 481 mBackgroundPaint.setColor(HIGHLIGHT_COLOR); 482 canvas.drawArc(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 483 HIGHLIGHT_DEGREES, false, mBackgroundPaint); 484 } 485 486 // Draw arc shaped indicator in time lapse recording. 487 if (mTimeLapseInterval != 0) { 488 // Setup rectangle and paint. 489 mBackgroundRect.set((float)(mCenterX - mShutterButtonRadius), 490 (float)(mCenterY - mShutterButtonRadius), 491 (float)(mCenterX + mShutterButtonRadius), 492 (float)(mCenterY + mShutterButtonRadius)); 493 mBackgroundRect.inset(3f, 3f); 494 mBackgroundPaint.setStrokeWidth(TIME_LAPSE_ARC_WIDTH); 495 mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); 496 mBackgroundPaint.setColor(TIME_LAPSE_ARC_COLOR); 497 498 // Compute the start angle and sweep angle. 499 long timeDelta = SystemClock.uptimeMillis() - mRecordingStartTime; 500 long numberOfFrames = timeDelta / mTimeLapseInterval; 501 float sweepAngle; 502 if (numberOfFrames > mNumberOfFrames) { 503 // The arc just acrosses 0 degree. Draw a full circle so it 504 // looks better. 505 sweepAngle = 360; 506 mNumberOfFrames = numberOfFrames; 507 } else { 508 sweepAngle = timeDelta % mTimeLapseInterval * 360f / mTimeLapseInterval; 509 } 510 511 canvas.drawArc(mBackgroundRect, 0, sweepAngle, false, mBackgroundPaint); 512 invalidate(); 513 } 514 515 super.onDraw(canvas); 516 } 517 518 @Override setEnabled(boolean enabled)519 public void setEnabled(boolean enabled) { 520 super.setEnabled(enabled); 521 if (!mInitialized) return; 522 if (mCurrentMode == MODE_VIDEO) { 523 mSecondLevelIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 524 mCloseIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 525 requestLayout(); 526 } else { 527 // We also disable the zoom button during snapshot. 528 enableZoom(enabled); 529 } 530 mSecondLevelIcon.setEnabled(enabled); 531 mCloseIcon.setEnabled(enabled); 532 } 533 enableZoom(boolean enabled)534 public void enableZoom(boolean enabled) { 535 if (mZoomControl != null) mZoomControl.setEnabled(enabled); 536 } 537 onTouchOutBound()538 public void onTouchOutBound() { 539 dismissSettingPopup(); 540 if (mPressedIndex != -1) { 541 injectMotionEvent(mPressedIndex, mLastMotionEvent, MotionEvent.ACTION_CANCEL); 542 mPressedIndex = -1; 543 invalidate(); 544 } 545 } 546 dismissSecondLevelIndicator()547 public void dismissSecondLevelIndicator() { 548 if (mCurrentLevel == 1) { 549 changeIndicatorsLevel(); 550 } 551 } 552 } 553