IndicatorControlWheel.java revision da705aa1c8cbaaba105d9bc7799aef95c416a459
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 requestLayout(); 302 } 303 304 @Override 305 protected void onLayout( 306 boolean changed, int left, int top, int right, int bottom) { 307 if (mInAnimation) { 308 rotateWheel(); 309 mHandler.post(mRunnable); 310 } 311 mCenterX = right - left - Util.dpToPixel( 312 IndicatorControlWheelContainer.FULL_WHEEL_RADIUS); 313 mCenterY = (bottom - top) / 2; 314 315 // Layout the indicators based on the current level. 316 // The icons are spreaded on the left side of the shutter button. 317 for (int i = 0; i < getChildCount(); ++i) { 318 View view = getChildAt(i); 319 // We still need to show the disabled indicators in the second level. 320 if (!view.isEnabled() && (mCurrentLevel == 0)) continue; 321 double radian = mChildRadians[i]; 322 double startVisibleRadians = mInAnimation 323 ? mStartVisibleRadians[1] 324 : mStartVisibleRadians[mCurrentLevel]; 325 double endVisibleRadians = mInAnimation 326 ? mEndVisibleRadians[1] 327 : mEndVisibleRadians[mCurrentLevel]; 328 if ((radian < (startVisibleRadians - HIGHLIGHT_RADIANS / 2)) || 329 (radian > (endVisibleRadians + HIGHLIGHT_RADIANS / 2))) { 330 view.setVisibility(View.GONE); 331 continue; 332 } 333 view.setVisibility(View.VISIBLE); 334 int x = mCenterX + (int)(mWheelRadius * Math.cos(radian)); 335 int y = mCenterY - (int)(mWheelRadius * Math.sin(radian)); 336 int width = view.getMeasuredWidth(); 337 int height = view.getMeasuredHeight(); 338 view.layout(x - width / 2, y - height / 2, x + width / 2, 339 y + height / 2); 340 } 341 } 342 343 private void presetFirstLevelChildRadians() { 344 int count = getChildCountByLevel(0); 345 int sectors = (count <= 1) ? 0 : (count - 1); 346 double sectorDegrees = FIRST_LEVEL_SECTOR_DEGREES; 347 mSectorRadians[0] = Math.toRadians(sectorDegrees); 348 int zoomIndex = indexOfChild(mZoomIcon); 349 double degrees; 350 351 // Make sure the zoom button is located at 180 degrees. If there are 352 // more buttons than we could show in the visible angle from 90 degrees 353 // to 270 degrees, the modification of FIRST_LEVEL_SECTOR_DEGREES is 354 // required then. 355 if (zoomIndex >= 0) { 356 degrees = ZOOM_ICON_DEFAULT_DEGREES - (zoomIndex * sectorDegrees); 357 } else { 358 degrees = FIRST_LEVEL_END_DEGREES - (sectors * sectorDegrees); 359 } 360 mStartVisibleRadians[0] = Math.toRadians(degrees); 361 362 int startIndex = 0; 363 for (int i = 0; i < count; i++) { 364 mChildRadians[startIndex + i] = Math.toRadians(degrees); 365 degrees += sectorDegrees; 366 } 367 368 // The radians for the touch sector of an indicator. 369 mTouchSectorRadians[0] = HIGHLIGHT_RADIANS; 370 mEndVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_END_DEGREES); 371 } 372 373 private void presetSecondLevelChildRadians() { 374 int count = getChildCountByLevel(1); 375 int sectors = (count <= 1) ? 1 : (count - 1); 376 double sectorDegrees = 377 ((SECOND_LEVEL_END_DEGREES - SECOND_LEVEL_START_DEGREES) / sectors); 378 mSectorRadians[1] = Math.toRadians(sectorDegrees); 379 380 double degrees = CLOSE_ICON_DEFAULT_DEGREES; 381 mStartVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_START_DEGREES); 382 383 int startIndex = mSecondLevelStartIndex; 384 for (int i = 0; i < count; i++) { 385 mChildRadians[startIndex + i] = Math.toRadians(degrees); 386 degrees += sectorDegrees; 387 } 388 389 // The radians for the touch sector of an indicator. 390 mTouchSectorRadians[1] = 391 Math.min(HIGHLIGHT_RADIANS, Math.toRadians(sectorDegrees)); 392 393 mEndVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_END_DEGREES); 394 } 395 396 public void startTimeLapseAnimation(int timeLapseInterval, long startTime) { 397 mTimeLapseInterval = timeLapseInterval; 398 mRecordingStartTime = startTime; 399 mNumberOfFrames = 0; 400 invalidate(); 401 } 402 403 public void stopTimeLapseAnimation() { 404 mTimeLapseInterval = 0; 405 invalidate(); 406 } 407 408 private int getSelectedIndicatorIndex() { 409 for (int i = 0; i < mIndicators.size(); i++) { 410 AbstractIndicatorButton b = mIndicators.get(i); 411 if (b.getPopupWindow() != null) { 412 return indexOfChild(b); 413 } 414 } 415 if (mPressedIndex != -1) { 416 View v = getChildAt(mPressedIndex); 417 if (!(v instanceof AbstractIndicatorButton) && v.isEnabled()) { 418 return mPressedIndex; 419 } 420 } 421 return -1; 422 } 423 424 @Override 425 protected void onDraw(Canvas canvas) { 426 int selectedIndex = getSelectedIndicatorIndex(); 427 428 // Draw the highlight arc if an indicator is selected or being pressed. 429 if (selectedIndex >= 0) { 430 int degree = (int) Math.toDegrees(mChildRadians[selectedIndex]); 431 float innerR = (float) mShutterButtonRadius; 432 float outerR = (float) (mShutterButtonRadius + mStrokeWidth + 433 EDGE_STROKE_WIDTH * 0.5); 434 435 // Construct the path of the fan-shaped semi-transparent area. 436 Path fanPath = new Path(); 437 mBackgroundRect.set(mCenterX - innerR, mCenterY - innerR, 438 mCenterX + innerR, mCenterY + innerR); 439 fanPath.arcTo(mBackgroundRect, -degree + HIGHLIGHT_DEGREES / 2, 440 -HIGHLIGHT_DEGREES); 441 mBackgroundRect.set(mCenterX - outerR, mCenterY - outerR, 442 mCenterX + outerR, mCenterY + outerR); 443 fanPath.arcTo(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 444 HIGHLIGHT_DEGREES); 445 fanPath.close(); 446 447 mBackgroundPaint.setStrokeWidth(HIGHLIGHT_WIDTH); 448 mBackgroundPaint.setStrokeCap(Paint.Cap.SQUARE); 449 mBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE); 450 mBackgroundPaint.setColor(HIGHLIGHT_FAN_COLOR); 451 canvas.drawPath(fanPath, mBackgroundPaint); 452 453 // Draw the highlight edge 454 mBackgroundPaint.setStyle(Paint.Style.STROKE); 455 mBackgroundPaint.setColor(HIGHLIGHT_COLOR); 456 canvas.drawArc(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, 457 HIGHLIGHT_DEGREES, false, mBackgroundPaint); 458 } 459 460 // Draw arc shaped indicator in time lapse recording. 461 if (mTimeLapseInterval != 0) { 462 // Setup rectangle and paint. 463 mBackgroundRect.set((float)(mCenterX - mShutterButtonRadius), 464 (float)(mCenterY - mShutterButtonRadius), 465 (float)(mCenterX + mShutterButtonRadius), 466 (float)(mCenterY + mShutterButtonRadius)); 467 mBackgroundRect.inset(3f, 3f); 468 mBackgroundPaint.setStrokeWidth(TIME_LAPSE_ARC_WIDTH); 469 mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); 470 mBackgroundPaint.setColor(TIME_LAPSE_ARC_COLOR); 471 472 // Compute the start angle and sweep angle. 473 long timeDelta = SystemClock.uptimeMillis() - mRecordingStartTime; 474 long numberOfFrames = timeDelta / mTimeLapseInterval; 475 float sweepAngle; 476 if (numberOfFrames > mNumberOfFrames) { 477 // The arc just acrosses 0 degree. Draw a full circle so it 478 // looks better. 479 sweepAngle = 360; 480 mNumberOfFrames = numberOfFrames; 481 } else { 482 sweepAngle = timeDelta % mTimeLapseInterval * 360f / mTimeLapseInterval; 483 } 484 485 canvas.drawArc(mBackgroundRect, 0, sweepAngle, false, mBackgroundPaint); 486 invalidate(); 487 } 488 489 super.onDraw(canvas); 490 } 491 492 @Override 493 public void setEnabled(boolean enabled) { 494 super.setEnabled(enabled); 495 if (mCurrentMode == MODE_VIDEO) { 496 mSecondLevelIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 497 mCloseIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); 498 requestLayout(); 499 } else { 500 // We also disable the zoom button during snapshot. 501 enableZoom(enabled); 502 } 503 mSecondLevelIcon.setEnabled(enabled); 504 mCloseIcon.setEnabled(enabled); 505 } 506 507 public void enableZoom(boolean enabled) { 508 if (mZoomIcon != null) mZoomIcon.setEnabled(enabled); 509 } 510 511 public void onTouchOutBound() { 512 dismissSettingPopup(); 513 if (mPressedIndex != -1) { 514 injectMotionEvent(mPressedIndex, mLastMotionEvent, MotionEvent.ACTION_CANCEL); 515 mPressedIndex = -1; 516 invalidate(); 517 } 518 } 519 520 public void dismissSecondLevelIndicator() { 521 if (mCurrentLevel == 1) { 522 changeIndicatorsLevel(); 523 } 524 } 525} 526