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