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