| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.camera.ui; |
| |
| import com.android.camera.PreferenceGroup; |
| import com.android.camera.R; |
| import com.android.camera.Util; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Canvas; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.RectF; |
| import android.os.Handler; |
| import android.os.SystemClock; |
| import android.util.AttributeSet; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| /** |
| * A view that contains camera setting indicators in two levels. The first-level |
| * indicators including the zoom, camera picker, flash and second-level control. |
| * The second-level indicators are the merely for the camera settings. |
| */ |
| public class IndicatorControlWheel extends IndicatorControl implements |
| View.OnClickListener { |
| public static final int HIGHLIGHT_WIDTH = 4; |
| |
| private static final String TAG = "IndicatorControlWheel"; |
| private static final int HIGHLIGHT_DEGREES = 30; |
| private static final double HIGHLIGHT_RADIANS = Math.toRadians(HIGHLIGHT_DEGREES); |
| |
| // The following angles are based in the zero degree on the right. Here we |
| // have the CameraPicker, ZoomControl and the Settings icons in the |
| // first-level. For consistency, we treat the zoom control as one of the |
| // indicator buttons but it needs additional efforts for rotation animation. |
| // For second-level indicators, the indicators are located evenly between start |
| // and end angle. In addition, these indicators for the second-level hidden |
| // in the same wheel with larger angle values are visible after rotation. |
| private static final int FIRST_LEVEL_START_DEGREES = 74; |
| private static final int FIRST_LEVEL_END_DEGREES = 286; |
| private static final int FIRST_LEVEL_SECTOR_DEGREES = 45; |
| private static final int SECOND_LEVEL_START_DEGREES = 60; |
| private static final int SECOND_LEVEL_END_DEGREES = 300; |
| private static final int MAX_ZOOM_CONTROL_DEGREES = 264; |
| private static final int CLOSE_ICON_DEFAULT_DEGREES = 315; |
| |
| private static final int ANIMATION_TIME = 300; // milliseconds |
| |
| // The width of the edges on both sides of the wheel, which has less alpha. |
| private static final float EDGE_STROKE_WIDTH = 6f; |
| private static final int TIME_LAPSE_ARC_WIDTH = 6; |
| |
| private final int HIGHLIGHT_COLOR; |
| private final int HIGHLIGHT_FAN_COLOR; |
| private final int TIME_LAPSE_ARC_COLOR; |
| |
| // The center of the shutter button. |
| private int mCenterX, mCenterY; |
| // The width of the wheel stroke. |
| private int mStrokeWidth; |
| private double mShutterButtonRadius; |
| private double mWheelRadius; |
| private double mChildRadians[]; |
| private Paint mBackgroundPaint; |
| private RectF mBackgroundRect; |
| // The index of the child that is being pressed. -1 means no child is being |
| // pressed. |
| private int mPressedIndex = -1; |
| |
| // Time lapse recording variables. |
| private int mTimeLapseInterval; // in ms |
| private long mRecordingStartTime = 0; |
| private long mNumberOfFrames = 0; |
| |
| // Remember the last event for event cancelling if out of bound. |
| private MotionEvent mLastMotionEvent; |
| |
| private ImageView mSecondLevelIcon; |
| private ImageView mCloseIcon; |
| |
| // Variables for animation. |
| private long mAnimationStartTime; |
| private boolean mInAnimation = false; |
| private Handler mHandler = new Handler(); |
| private final Runnable mRunnable = new Runnable() { |
| public void run() { |
| requestLayout(); |
| } |
| }; |
| |
| // Variables for level control. |
| private int mCurrentLevel = 0; |
| private int mSecondLevelStartIndex = -1; |
| private double mStartVisibleRadians[] = new double[2]; |
| private double mEndVisibleRadians[] = new double[2]; |
| private double mSectorRadians[] = new double[2]; |
| private double mTouchSectorRadians[] = new double[2]; |
| |
| private ZoomControlWheel mZoomControl; |
| private boolean mInitialized; |
| |
| public IndicatorControlWheel(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| Resources resources = context.getResources(); |
| HIGHLIGHT_COLOR = resources.getColor(R.color.review_control_pressed_color); |
| HIGHLIGHT_FAN_COLOR = resources.getColor(R.color.review_control_pressed_fan_color); |
| TIME_LAPSE_ARC_COLOR = resources.getColor(R.color.time_lapse_arc); |
| |
| setWillNotDraw(false); |
| |
| mBackgroundPaint = new Paint(); |
| mBackgroundPaint.setStyle(Paint.Style.STROKE); |
| mBackgroundPaint.setAntiAlias(true); |
| |
| mBackgroundRect = new RectF(); |
| } |
| |
| private int getChildCountByLevel(int level) { |
| // Get current child count by level. |
| if (level == 1) { |
| return (getChildCount() - mSecondLevelStartIndex); |
| } else { |
| return mSecondLevelStartIndex; |
| } |
| } |
| |
| private void changeIndicatorsLevel() { |
| mPressedIndex = -1; |
| dismissSettingPopup(); |
| mInAnimation = true; |
| mAnimationStartTime = SystemClock.uptimeMillis(); |
| requestLayout(); |
| } |
| |
| @Override |
| public void onClick(View view) { |
| changeIndicatorsLevel(); |
| } |
| |
| public void initialize(Context context, PreferenceGroup group, |
| boolean isZoomSupported, String[] keys, String[] otherSettingKeys) { |
| mShutterButtonRadius = IndicatorControlWheelContainer.SHUTTER_BUTTON_RADIUS; |
| mStrokeWidth = Util.dpToPixel(IndicatorControlWheelContainer.STROKE_WIDTH); |
| mWheelRadius = mShutterButtonRadius + mStrokeWidth * 0.5; |
| |
| setPreferenceGroup(group); |
| |
| // Add the ZoomControl if supported. |
| if (isZoomSupported) { |
| mZoomControl = (ZoomControlWheel) findViewById(R.id.zoom_control); |
| mZoomControl.setVisibility(View.VISIBLE); |
| } |
| |
| // Add CameraPicker. |
| initializeCameraPicker(); |
| |
| // Add second-level Indicator Icon. |
| mSecondLevelIcon = addImageButton(context, R.drawable.ic_settings_holo_light, true); |
| mSecondLevelStartIndex = getChildCount(); |
| |
| // Add second-level buttons. |
| mCloseIcon = addImageButton(context, R.drawable.btn_wheel_close_settings, false); |
| addControls(keys, otherSettingKeys); |
| |
| // The angle(in radians) of each icon for touch events. |
| mChildRadians = new double[getChildCount()]; |
| presetFirstLevelChildRadians(); |
| presetSecondLevelChildRadians(); |
| mInitialized = true; |
| } |
| |
| private ImageView addImageButton(Context context, int resourceId, boolean rotatable) { |
| ImageView view; |
| if (rotatable) { |
| view = new RotateImageView(context); |
| } else { |
| view = new TwoStateImageView(context); |
| } |
| view.setImageResource(resourceId); |
| view.setOnClickListener(this); |
| addView(view); |
| return view; |
| } |
| |
| private int getTouchIndicatorIndex(double delta) { |
| // The delta is the angle of touch point in radians. |
| if (mInAnimation) return -1; |
| int count = getChildCountByLevel(mCurrentLevel); |
| if (count == 0) return -1; |
| int sectors = count - 1; |
| int startIndex = (mCurrentLevel == 0) ? 0 : mSecondLevelStartIndex; |
| int endIndex; |
| if (mCurrentLevel == 0) { |
| // Skip the first component if it is zoom control, as we will |
| // deal with it specifically. |
| if (mZoomControl != null) startIndex++; |
| endIndex = mSecondLevelStartIndex - 1; |
| } else { |
| endIndex = getChildCount() - 1; |
| } |
| // Check which indicator is touched. |
| double halfTouchSectorRadians = mTouchSectorRadians[mCurrentLevel]; |
| if ((delta >= (mChildRadians[startIndex] - halfTouchSectorRadians)) && |
| (delta <= (mChildRadians[endIndex] + halfTouchSectorRadians))) { |
| int index = 0; |
| if (mCurrentLevel == 1) { |
| index = (int) ((delta - mChildRadians[startIndex]) |
| / mSectorRadians[mCurrentLevel]); |
| // greater than the center of ending indicator |
| if (index > sectors) return (startIndex + sectors); |
| // less than the center of starting indicator |
| if (index < 0) return startIndex; |
| } |
| if (delta <= (mChildRadians[startIndex + index] |
| + halfTouchSectorRadians)) { |
| return (startIndex + index); |
| } |
| if (delta >= (mChildRadians[startIndex + index + 1] |
| - halfTouchSectorRadians)) { |
| return (startIndex + index + 1); |
| } |
| |
| // It must be for zoom control if the touch event is in the visible |
| // range and not for other indicator buttons. |
| if ((mCurrentLevel == 0) && (mZoomControl != null)) return 0; |
| } |
| return -1; |
| } |
| |
| private void injectMotionEvent(int viewIndex, MotionEvent event, int action) { |
| View v = getChildAt(viewIndex); |
| event.setAction(action); |
| v.dispatchTouchEvent(event); |
| } |
| |
| @Override |
| public boolean dispatchTouchEvent(MotionEvent event) { |
| if (!onFilterTouchEventForSecurity(event)) return false; |
| mLastMotionEvent = event; |
| int action = event.getAction(); |
| |
| double dx = event.getX() - mCenterX; |
| double dy = mCenterY - event.getY(); |
| double radius = Math.sqrt(dx * dx + dy * dy); |
| |
| // Ignore the event if too far from the shutter button. |
| if ((radius <= (mWheelRadius + mStrokeWidth)) && (radius > mShutterButtonRadius)) { |
| double delta = Math.atan2(dy, dx); |
| if (delta < 0) delta += Math.PI * 2; |
| int index = getTouchIndicatorIndex(delta); |
| // Check if the touch event is for zoom control. |
| if ((mZoomControl != null) && (index == 0)) { |
| mZoomControl.dispatchTouchEvent(event); |
| } |
| // Move over from one indicator to another. |
| if ((index != mPressedIndex) || (action == MotionEvent.ACTION_DOWN)) { |
| if (mPressedIndex != -1) { |
| injectMotionEvent(mPressedIndex, event, MotionEvent.ACTION_CANCEL); |
| } else { |
| // Cancel the popup if it is different from the selected. |
| if (getSelectedIndicatorIndex() != index) dismissSettingPopup(); |
| } |
| if ((index != -1) && (action == MotionEvent.ACTION_MOVE)) { |
| if (mCurrentLevel != 0) { |
| injectMotionEvent(index, event, MotionEvent.ACTION_DOWN); |
| } |
| } |
| } |
| if ((index != -1) && (action != MotionEvent.ACTION_MOVE)) { |
| getChildAt(index).dispatchTouchEvent(event); |
| } |
| // Do not highlight the CameraPicker or Settings icon if we |
| // touch from the zoom control to one of them. |
| if ((mCurrentLevel == 0) && (index != 0) |
| && (action == MotionEvent.ACTION_MOVE)) { |
| return true; |
| } |
| // Once the button is up, reset the press index. |
| mPressedIndex = (action == MotionEvent.ACTION_UP) ? -1 : index; |
| invalidate(); |
| return true; |
| } |
| // The event is not on any of the child. |
| onTouchOutBound(); |
| return false; |
| } |
| |
| private void rotateWheel() { |
| int totalDegrees = CLOSE_ICON_DEFAULT_DEGREES - SECOND_LEVEL_START_DEGREES; |
| int startAngle = ((mCurrentLevel == 0) ? CLOSE_ICON_DEFAULT_DEGREES |
| : SECOND_LEVEL_START_DEGREES); |
| if (mCurrentLevel == 0) totalDegrees = -totalDegrees; |
| |
| int elapsedTime = (int) (SystemClock.uptimeMillis() - mAnimationStartTime); |
| if (elapsedTime >= ANIMATION_TIME) { |
| elapsedTime = ANIMATION_TIME; |
| mCurrentLevel = (mCurrentLevel == 0) ? 1 : 0; |
| mInAnimation = false; |
| } |
| |
| int expectedAngle = startAngle + (totalDegrees * elapsedTime / ANIMATION_TIME); |
| double increment = Math.toRadians(expectedAngle) |
| - mChildRadians[mSecondLevelStartIndex]; |
| for (int i = 0 ; i < getChildCount(); ++i) mChildRadians[i] += increment; |
| // We also need to rotate the zoom control wheel as well. |
| if (mZoomControl != null) { |
| mZoomControl.rotate(mChildRadians[0] |
| - Math.toRadians(MAX_ZOOM_CONTROL_DEGREES)); |
| } |
| } |
| |
| @Override |
| protected void onLayout( |
| boolean changed, int left, int top, int right, int bottom) { |
| if (!mInitialized) return; |
| if (mInAnimation) { |
| rotateWheel(); |
| mHandler.post(mRunnable); |
| } |
| mCenterX = right - left - Util.dpToPixel( |
| IndicatorControlWheelContainer.FULL_WHEEL_RADIUS); |
| mCenterY = (bottom - top) / 2; |
| |
| // Layout the indicators based on the current level. |
| // The icons are spreaded on the left side of the shutter button. |
| for (int i = 0; i < getChildCount(); ++i) { |
| View view = getChildAt(i); |
| // We still need to show the disabled indicators in the second level. |
| double radian = mChildRadians[i]; |
| double startVisibleRadians = mInAnimation |
| ? mStartVisibleRadians[1] |
| : mStartVisibleRadians[mCurrentLevel]; |
| double endVisibleRadians = mInAnimation |
| ? mEndVisibleRadians[1] |
| : mEndVisibleRadians[mCurrentLevel]; |
| if ((!view.isEnabled() && (mCurrentLevel == 0)) |
| || (radian < (startVisibleRadians - HIGHLIGHT_RADIANS / 2)) |
| || (radian > (endVisibleRadians + HIGHLIGHT_RADIANS / 2))) { |
| view.setVisibility(View.GONE); |
| continue; |
| } |
| view.setVisibility(View.VISIBLE); |
| int x = mCenterX + (int)(mWheelRadius * Math.cos(radian)); |
| int y = mCenterY - (int)(mWheelRadius * Math.sin(radian)); |
| int width = view.getMeasuredWidth(); |
| int height = view.getMeasuredHeight(); |
| if (view == mZoomControl) { |
| // ZoomControlWheel matches the size of its parent view. |
| view.layout(0, 0, right - left, bottom - top); |
| } else { |
| view.layout(x - width / 2, y - height / 2, x + width / 2, |
| y + height / 2); |
| } |
| } |
| } |
| |
| private void presetFirstLevelChildRadians() { |
| // Set the visible range in the first-level indicator wheel. |
| mStartVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_START_DEGREES); |
| mTouchSectorRadians[0] = HIGHLIGHT_RADIANS; |
| mEndVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_END_DEGREES); |
| |
| // Set the angle of each component in the first-level indicator wheel. |
| int startIndex = 0; |
| if (mZoomControl != null) { |
| mChildRadians[startIndex++] = Math.toRadians(MAX_ZOOM_CONTROL_DEGREES); |
| } |
| if (mCameraPicker != null) { |
| mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_START_DEGREES); |
| } |
| mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_END_DEGREES); |
| } |
| |
| private void presetSecondLevelChildRadians() { |
| int count = getChildCountByLevel(1); |
| int sectors = (count <= 1) ? 1 : (count - 1); |
| double sectorDegrees = |
| ((SECOND_LEVEL_END_DEGREES - SECOND_LEVEL_START_DEGREES) / sectors); |
| mSectorRadians[1] = Math.toRadians(sectorDegrees); |
| |
| double degrees = CLOSE_ICON_DEFAULT_DEGREES; |
| mStartVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_START_DEGREES); |
| |
| int startIndex = mSecondLevelStartIndex; |
| for (int i = 0; i < count; i++) { |
| mChildRadians[startIndex + i] = Math.toRadians(degrees); |
| degrees += sectorDegrees; |
| } |
| |
| // The radians for the touch sector of an indicator. |
| mTouchSectorRadians[1] = |
| Math.min(HIGHLIGHT_RADIANS, Math.toRadians(sectorDegrees)); |
| |
| mEndVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_END_DEGREES); |
| } |
| |
| public void startTimeLapseAnimation(int timeLapseInterval, long startTime) { |
| mTimeLapseInterval = timeLapseInterval; |
| mRecordingStartTime = startTime; |
| mNumberOfFrames = 0; |
| invalidate(); |
| } |
| |
| public void stopTimeLapseAnimation() { |
| mTimeLapseInterval = 0; |
| invalidate(); |
| } |
| |
| private int getSelectedIndicatorIndex() { |
| for (int i = 0; i < mIndicators.size(); i++) { |
| AbstractIndicatorButton b = mIndicators.get(i); |
| if (b.getPopupWindow() != null) { |
| return indexOfChild(b); |
| } |
| } |
| if (mPressedIndex != -1) { |
| View v = getChildAt(mPressedIndex); |
| if (!(v instanceof AbstractIndicatorButton) && v.isEnabled()) { |
| return mPressedIndex; |
| } |
| } |
| return -1; |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| int selectedIndex = getSelectedIndicatorIndex(); |
| |
| // Draw the highlight arc if an indicator is selected or being pressed. |
| // And skip the zoom control which index is zero. |
| if (selectedIndex >= 1) { |
| int degree = (int) Math.toDegrees(mChildRadians[selectedIndex]); |
| float innerR = (float) mShutterButtonRadius; |
| float outerR = (float) (mShutterButtonRadius + mStrokeWidth + |
| EDGE_STROKE_WIDTH * 0.5); |
| |
| // Construct the path of the fan-shaped semi-transparent area. |
| Path fanPath = new Path(); |
| mBackgroundRect.set(mCenterX - innerR, mCenterY - innerR, |
| mCenterX + innerR, mCenterY + innerR); |
| fanPath.arcTo(mBackgroundRect, -degree + HIGHLIGHT_DEGREES / 2, |
| -HIGHLIGHT_DEGREES); |
| mBackgroundRect.set(mCenterX - outerR, mCenterY - outerR, |
| mCenterX + outerR, mCenterY + outerR); |
| fanPath.arcTo(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, |
| HIGHLIGHT_DEGREES); |
| fanPath.close(); |
| |
| mBackgroundPaint.setStrokeWidth(HIGHLIGHT_WIDTH); |
| mBackgroundPaint.setStrokeCap(Paint.Cap.SQUARE); |
| mBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE); |
| mBackgroundPaint.setColor(HIGHLIGHT_FAN_COLOR); |
| canvas.drawPath(fanPath, mBackgroundPaint); |
| |
| // Draw the highlight edge |
| mBackgroundPaint.setStyle(Paint.Style.STROKE); |
| mBackgroundPaint.setColor(HIGHLIGHT_COLOR); |
| canvas.drawArc(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2, |
| HIGHLIGHT_DEGREES, false, mBackgroundPaint); |
| } |
| |
| // Draw arc shaped indicator in time lapse recording. |
| if (mTimeLapseInterval != 0) { |
| // Setup rectangle and paint. |
| mBackgroundRect.set((float)(mCenterX - mShutterButtonRadius), |
| (float)(mCenterY - mShutterButtonRadius), |
| (float)(mCenterX + mShutterButtonRadius), |
| (float)(mCenterY + mShutterButtonRadius)); |
| mBackgroundRect.inset(3f, 3f); |
| mBackgroundPaint.setStrokeWidth(TIME_LAPSE_ARC_WIDTH); |
| mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); |
| mBackgroundPaint.setColor(TIME_LAPSE_ARC_COLOR); |
| |
| // Compute the start angle and sweep angle. |
| long timeDelta = SystemClock.uptimeMillis() - mRecordingStartTime; |
| long numberOfFrames = timeDelta / mTimeLapseInterval; |
| float sweepAngle; |
| if (numberOfFrames > mNumberOfFrames) { |
| // The arc just acrosses 0 degree. Draw a full circle so it |
| // looks better. |
| sweepAngle = 360; |
| mNumberOfFrames = numberOfFrames; |
| } else { |
| sweepAngle = timeDelta % mTimeLapseInterval * 360f / mTimeLapseInterval; |
| } |
| |
| canvas.drawArc(mBackgroundRect, 0, sweepAngle, false, mBackgroundPaint); |
| invalidate(); |
| } |
| |
| super.onDraw(canvas); |
| } |
| |
| @Override |
| public void setEnabled(boolean enabled) { |
| super.setEnabled(enabled); |
| if (!mInitialized) return; |
| if (mCurrentMode == MODE_VIDEO) { |
| mSecondLevelIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); |
| mCloseIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); |
| requestLayout(); |
| } else { |
| // We also disable the zoom button during snapshot. |
| enableZoom(enabled); |
| } |
| mSecondLevelIcon.setEnabled(enabled); |
| mCloseIcon.setEnabled(enabled); |
| } |
| |
| public void enableZoom(boolean enabled) { |
| if (mZoomControl != null) mZoomControl.setEnabled(enabled); |
| } |
| |
| public void onTouchOutBound() { |
| dismissSettingPopup(); |
| if (mPressedIndex != -1) { |
| injectMotionEvent(mPressedIndex, mLastMotionEvent, MotionEvent.ACTION_CANCEL); |
| mPressedIndex = -1; |
| invalidate(); |
| } |
| } |
| |
| public void dismissSecondLevelIndicator() { |
| if (mCurrentLevel == 1) { |
| changeIndicatorsLevel(); |
| } |
| } |
| } |