• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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         mZoomControl = (ZoomControlWheel) findViewById(R.id.zoom_control);
167         if (isZoomSupported) {
168             mZoomControl.setVisibility(View.VISIBLE);
169         } else {
170             // ZoomControlWheel is default added in res/layout-sw600dp/camera_control.xml,
171             // If zoom not supported, should remove this view, or the radians of other view
172             // will be calculated incorrectly, thus control wheel show abnormal.
173             if (mZoomControl != null) {
174                 removeView(mZoomControl);
175             }
176         }
177 
178         // Add CameraPicker.
179         initializeCameraPicker();
180 
181         // Add second-level Indicator Icon.
182         mSecondLevelIcon = addImageButton(context, R.drawable.ic_settings_holo_light, true);
183         mSecondLevelStartIndex = getChildCount();
184 
185         // Add second-level buttons.
186         mCloseIcon = addImageButton(context, R.drawable.btn_wheel_close_settings, false);
187         addControls(keys, otherSettingKeys);
188 
189         // The angle(in radians) of each icon for touch events.
190         mChildRadians = new double[getChildCount()];
191         presetFirstLevelChildRadians();
192         presetSecondLevelChildRadians();
193         mInitialized = true;
194     }
195 
addImageButton(Context context, int resourceId, boolean rotatable)196     private ImageView addImageButton(Context context, int resourceId, boolean rotatable) {
197         ImageView view;
198         if (rotatable) {
199             view = new RotateImageView(context);
200         } else {
201             view = new TwoStateImageView(context);
202         }
203         view.setImageResource(resourceId);
204         view.setOnClickListener(this);
205         addView(view);
206         return view;
207     }
208 
getTouchIndicatorIndex(double delta)209     private int getTouchIndicatorIndex(double delta) {
210         // The delta is the angle of touch point in radians.
211         if (mInAnimation) return -1;
212         int count = getChildCountByLevel(mCurrentLevel);
213         if (count == 0) return -1;
214         int sectors = count - 1;
215         int startIndex = (mCurrentLevel == 0) ? 0 : mSecondLevelStartIndex;
216         int endIndex;
217         if (mCurrentLevel == 0) {
218             // Skip the first component if it is zoom control, as we will
219             // deal with it specifically.
220             if (mZoomControl != null) startIndex++;
221             endIndex = mSecondLevelStartIndex - 1;
222         } else {
223             endIndex = getChildCount() - 1;
224         }
225         // Check which indicator is touched.
226         double halfTouchSectorRadians = mTouchSectorRadians[mCurrentLevel];
227         if ((delta >= (mChildRadians[startIndex] - halfTouchSectorRadians)) &&
228                 (delta <= (mChildRadians[endIndex] + halfTouchSectorRadians))) {
229             int index = 0;
230             if (mCurrentLevel == 1) {
231                 index = (int) ((delta - mChildRadians[startIndex])
232                         / mSectorRadians[mCurrentLevel]);
233                 // greater than the center of ending indicator
234                 if (index > sectors) return (startIndex + sectors);
235                 // less than the center of starting indicator
236                 if (index < 0) return startIndex;
237             }
238             if (delta <= (mChildRadians[startIndex + index]
239                     + halfTouchSectorRadians)) {
240                 return (startIndex + index);
241             }
242             if (delta >= (mChildRadians[startIndex + index + 1]
243                     - halfTouchSectorRadians)) {
244                 return (startIndex + index + 1);
245             }
246 
247             // It must be for zoom control if the touch event is in the visible
248             // range and not for other indicator buttons.
249             if ((mCurrentLevel == 0) && (mZoomControl != null)) return 0;
250         }
251         return -1;
252     }
253 
injectMotionEvent(int viewIndex, MotionEvent event, int action)254     private void injectMotionEvent(int viewIndex, MotionEvent event, int action) {
255         View v = getChildAt(viewIndex);
256         event.setAction(action);
257         v.dispatchTouchEvent(event);
258     }
259 
260     @Override
dispatchTouchEvent(MotionEvent event)261     public boolean dispatchTouchEvent(MotionEvent event) {
262         if (!onFilterTouchEventForSecurity(event)) return false;
263         mLastMotionEvent = event;
264         int action = event.getAction();
265 
266         double dx = event.getX() - mCenterX;
267         double dy = mCenterY - event.getY();
268         double radius = Math.sqrt(dx * dx + dy * dy);
269 
270         // Ignore the event if too far from the shutter button.
271         if ((radius <= (mWheelRadius + mStrokeWidth)) && (radius > mShutterButtonRadius)) {
272             double delta = Math.atan2(dy, dx);
273             if (delta < 0) delta += Math.PI * 2;
274             int index = getTouchIndicatorIndex(delta);
275             // Check if the touch event is for zoom control.
276             if ((mZoomControl != null) && (index == 0)) {
277                 mZoomControl.dispatchTouchEvent(event);
278             }
279             // Move over from one indicator to another.
280             if ((index != mPressedIndex) || (action == MotionEvent.ACTION_DOWN)) {
281                 if (mPressedIndex != -1) {
282                     injectMotionEvent(mPressedIndex, event, MotionEvent.ACTION_CANCEL);
283                 } else {
284                     // Cancel the popup if it is different from the selected.
285                     if (getSelectedIndicatorIndex() != index) dismissSettingPopup();
286                 }
287                 if ((index != -1) && (action == MotionEvent.ACTION_MOVE)) {
288                     if (mCurrentLevel != 0) {
289                         injectMotionEvent(index, event, MotionEvent.ACTION_DOWN);
290                     }
291                 }
292             }
293             if ((index != -1) && (action != MotionEvent.ACTION_MOVE)) {
294                 getChildAt(index).dispatchTouchEvent(event);
295             }
296             // Do not highlight the CameraPicker or Settings icon if we
297             // touch from the zoom control to one of them.
298             if ((mCurrentLevel == 0) && (index != 0)
299                     && (action == MotionEvent.ACTION_MOVE)) {
300                 return true;
301             }
302             // Once the button is up, reset the press index.
303             mPressedIndex = (action == MotionEvent.ACTION_UP) ? -1 : index;
304             invalidate();
305             return true;
306         }
307         // The event is not on any of the child.
308         onTouchOutBound();
309         return false;
310     }
311 
rotateWheel()312     private void rotateWheel() {
313         int totalDegrees = CLOSE_ICON_DEFAULT_DEGREES - SECOND_LEVEL_START_DEGREES;
314         int startAngle = ((mCurrentLevel == 0) ? CLOSE_ICON_DEFAULT_DEGREES
315                                                : SECOND_LEVEL_START_DEGREES);
316         if (mCurrentLevel == 0) totalDegrees = -totalDegrees;
317 
318         int elapsedTime = (int) (SystemClock.uptimeMillis() - mAnimationStartTime);
319         if (elapsedTime >= ANIMATION_TIME) {
320             elapsedTime = ANIMATION_TIME;
321             mCurrentLevel = (mCurrentLevel == 0) ? 1 : 0;
322             mInAnimation = false;
323         }
324 
325         int expectedAngle = startAngle + (totalDegrees * elapsedTime / ANIMATION_TIME);
326         double increment = Math.toRadians(expectedAngle)
327                 - mChildRadians[mSecondLevelStartIndex];
328         for (int i = 0 ; i < getChildCount(); ++i) mChildRadians[i] += increment;
329         // We also need to rotate the zoom control wheel as well.
330         if (mZoomControl != null) {
331             mZoomControl.rotate(mChildRadians[0]
332                     - Math.toRadians(MAX_ZOOM_CONTROL_DEGREES));
333         }
334     }
335 
336     @Override
onLayout( boolean changed, int left, int top, int right, int bottom)337     protected void onLayout(
338             boolean changed, int left, int top, int right, int bottom) {
339         if (!mInitialized) return;
340         if (mInAnimation) {
341             rotateWheel();
342             mHandler.post(mRunnable);
343         }
344         mCenterX = right - left - Util.dpToPixel(
345                 IndicatorControlWheelContainer.FULL_WHEEL_RADIUS);
346         mCenterY = (bottom - top) / 2;
347 
348         // Layout the indicators based on the current level.
349         // The icons are spreaded on the left side of the shutter button.
350         for (int i = 0; i < getChildCount(); ++i) {
351             View view = getChildAt(i);
352             // We still need to show the disabled indicators in the second level.
353             double radian = mChildRadians[i];
354             double startVisibleRadians = mInAnimation
355                     ? mStartVisibleRadians[1]
356                     : mStartVisibleRadians[mCurrentLevel];
357             double endVisibleRadians = mInAnimation
358                     ? mEndVisibleRadians[1]
359                     : mEndVisibleRadians[mCurrentLevel];
360             if ((!view.isEnabled() && (mCurrentLevel == 0))
361                     || (radian < (startVisibleRadians - HIGHLIGHT_RADIANS / 2))
362                     || (radian > (endVisibleRadians + HIGHLIGHT_RADIANS / 2))) {
363                 view.setVisibility(View.GONE);
364                 continue;
365             }
366             view.setVisibility(View.VISIBLE);
367             int x = mCenterX + (int)(mWheelRadius * Math.cos(radian));
368             int y = mCenterY - (int)(mWheelRadius * Math.sin(radian));
369             int width = view.getMeasuredWidth();
370             int height = view.getMeasuredHeight();
371             if (view == mZoomControl) {
372                 // ZoomControlWheel matches the size of its parent view.
373                 view.layout(0, 0, right - left, bottom - top);
374             } else {
375                 view.layout(x - width / 2, y - height / 2, x + width / 2,
376                         y + height / 2);
377             }
378         }
379     }
380 
presetFirstLevelChildRadians()381     private void presetFirstLevelChildRadians() {
382         // Set the visible range in the first-level indicator wheel.
383         mStartVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_START_DEGREES);
384         mTouchSectorRadians[0] = HIGHLIGHT_RADIANS;
385         mEndVisibleRadians[0] = Math.toRadians(FIRST_LEVEL_END_DEGREES);
386 
387         // Set the angle of each component in the first-level indicator wheel.
388         int startIndex = 0;
389         if (mZoomControl != null) {
390             mChildRadians[startIndex++] = Math.toRadians(MAX_ZOOM_CONTROL_DEGREES);
391         }
392         if (mCameraPicker != null) {
393             mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_START_DEGREES);
394         }
395         mChildRadians[startIndex++] = Math.toRadians(FIRST_LEVEL_END_DEGREES);
396     }
397 
presetSecondLevelChildRadians()398     private void presetSecondLevelChildRadians() {
399         int count = getChildCountByLevel(1);
400         int sectors = (count <= 1) ? 1 : (count - 1);
401         double sectorDegrees =
402                 ((SECOND_LEVEL_END_DEGREES - SECOND_LEVEL_START_DEGREES) / sectors);
403         mSectorRadians[1] = Math.toRadians(sectorDegrees);
404 
405         double degrees = CLOSE_ICON_DEFAULT_DEGREES;
406         mStartVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_START_DEGREES);
407 
408         int startIndex = mSecondLevelStartIndex;
409         for (int i = 0; i < count; i++) {
410             mChildRadians[startIndex + i] = Math.toRadians(degrees);
411             degrees += sectorDegrees;
412         }
413 
414         // The radians for the touch sector of an indicator.
415         mTouchSectorRadians[1] =
416                 Math.min(HIGHLIGHT_RADIANS, Math.toRadians(sectorDegrees));
417 
418         mEndVisibleRadians[1] = Math.toRadians(SECOND_LEVEL_END_DEGREES);
419     }
420 
startTimeLapseAnimation(int timeLapseInterval, long startTime)421     public void startTimeLapseAnimation(int timeLapseInterval, long startTime) {
422         mTimeLapseInterval = timeLapseInterval;
423         mRecordingStartTime = startTime;
424         mNumberOfFrames = 0;
425         invalidate();
426     }
427 
stopTimeLapseAnimation()428     public void stopTimeLapseAnimation() {
429         mTimeLapseInterval = 0;
430         invalidate();
431     }
432 
getSelectedIndicatorIndex()433     private int getSelectedIndicatorIndex() {
434         for (int i = 0; i < mIndicators.size(); i++) {
435             AbstractIndicatorButton b = mIndicators.get(i);
436             if (b.getPopupWindow() != null) {
437                 return indexOfChild(b);
438             }
439         }
440         if (mPressedIndex != -1) {
441             View v = getChildAt(mPressedIndex);
442             if (!(v instanceof AbstractIndicatorButton) && v.isEnabled()) {
443                 return mPressedIndex;
444             }
445         }
446         return -1;
447     }
448 
449     @Override
onDraw(Canvas canvas)450     protected void onDraw(Canvas canvas) {
451         int selectedIndex = getSelectedIndicatorIndex();
452 
453         // Draw the highlight arc if an indicator is selected or being pressed.
454         // And skip the zoom control which index is zero.
455         if (selectedIndex >= 1) {
456             int degree = (int) Math.toDegrees(mChildRadians[selectedIndex]);
457             float innerR = (float) mShutterButtonRadius;
458             float outerR = (float) (mShutterButtonRadius + mStrokeWidth +
459                     EDGE_STROKE_WIDTH * 0.5);
460 
461             // Construct the path of the fan-shaped semi-transparent area.
462             Path fanPath = new Path();
463             mBackgroundRect.set(mCenterX - innerR, mCenterY - innerR,
464                     mCenterX + innerR, mCenterY + innerR);
465             fanPath.arcTo(mBackgroundRect, -degree + HIGHLIGHT_DEGREES / 2,
466                     -HIGHLIGHT_DEGREES);
467             mBackgroundRect.set(mCenterX - outerR, mCenterY - outerR,
468                     mCenterX + outerR, mCenterY + outerR);
469             fanPath.arcTo(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2,
470                     HIGHLIGHT_DEGREES);
471             fanPath.close();
472 
473             mBackgroundPaint.setStrokeWidth(HIGHLIGHT_WIDTH);
474             mBackgroundPaint.setStrokeCap(Paint.Cap.SQUARE);
475             mBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE);
476             mBackgroundPaint.setColor(HIGHLIGHT_FAN_COLOR);
477             canvas.drawPath(fanPath, mBackgroundPaint);
478 
479             // Draw the highlight edge
480             mBackgroundPaint.setStyle(Paint.Style.STROKE);
481             mBackgroundPaint.setColor(HIGHLIGHT_COLOR);
482             canvas.drawArc(mBackgroundRect, -degree - HIGHLIGHT_DEGREES / 2,
483                     HIGHLIGHT_DEGREES, false, mBackgroundPaint);
484         }
485 
486         // Draw arc shaped indicator in time lapse recording.
487         if (mTimeLapseInterval != 0) {
488             // Setup rectangle and paint.
489             mBackgroundRect.set((float)(mCenterX - mShutterButtonRadius),
490                     (float)(mCenterY - mShutterButtonRadius),
491                     (float)(mCenterX + mShutterButtonRadius),
492                     (float)(mCenterY + mShutterButtonRadius));
493             mBackgroundRect.inset(3f, 3f);
494             mBackgroundPaint.setStrokeWidth(TIME_LAPSE_ARC_WIDTH);
495             mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
496             mBackgroundPaint.setColor(TIME_LAPSE_ARC_COLOR);
497 
498             // Compute the start angle and sweep angle.
499             long timeDelta = SystemClock.uptimeMillis() - mRecordingStartTime;
500             long numberOfFrames = timeDelta / mTimeLapseInterval;
501             float sweepAngle;
502             if (numberOfFrames > mNumberOfFrames) {
503                 // The arc just acrosses 0 degree. Draw a full circle so it
504                 // looks better.
505                 sweepAngle = 360;
506                 mNumberOfFrames = numberOfFrames;
507             } else {
508                 sweepAngle = timeDelta % mTimeLapseInterval * 360f / mTimeLapseInterval;
509             }
510 
511             canvas.drawArc(mBackgroundRect, 0, sweepAngle, false, mBackgroundPaint);
512             invalidate();
513         }
514 
515         super.onDraw(canvas);
516     }
517 
518     @Override
setEnabled(boolean enabled)519     public void setEnabled(boolean enabled) {
520         super.setEnabled(enabled);
521         if (!mInitialized) return;
522         if (mCurrentMode == MODE_VIDEO) {
523             mSecondLevelIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
524             mCloseIcon.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
525             requestLayout();
526         } else {
527             // We also disable the zoom button during snapshot.
528             enableZoom(enabled);
529         }
530         mSecondLevelIcon.setEnabled(enabled);
531         mCloseIcon.setEnabled(enabled);
532     }
533 
enableZoom(boolean enabled)534     public void enableZoom(boolean enabled) {
535         if (mZoomControl != null) mZoomControl.setEnabled(enabled);
536     }
537 
onTouchOutBound()538     public void onTouchOutBound() {
539         dismissSettingPopup();
540         if (mPressedIndex != -1) {
541             injectMotionEvent(mPressedIndex, mLastMotionEvent, MotionEvent.ACTION_CANCEL);
542             mPressedIndex = -1;
543             invalidate();
544         }
545     }
546 
dismissSecondLevelIndicator()547     public void dismissSecondLevelIndicator() {
548         if (mCurrentLevel == 1) {
549             changeIndicatorsLevel();
550         }
551     }
552 }
553