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