/* * 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. mZoomControl = (ZoomControlWheel) findViewById(R.id.zoom_control); if (isZoomSupported) { mZoomControl.setVisibility(View.VISIBLE); } else { // ZoomControlWheel is default added in res/layout-sw600dp/camera_control.xml, // If zoom not supported, should remove this view, or the radians of other view // will be calculated incorrectly, thus control wheel show abnormal. if (mZoomControl != null) { removeView(mZoomControl); } } // 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(); } } }