Home | History | Annotate | Download | only in ui
      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 
    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 
    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 
    144     private void changeIndicatorsLevel() {
    145         mPressedIndex = -1;
    146         dismissSettingPopup();
    147         mInAnimation = true;
    148         mAnimationStartTime = SystemClock.uptimeMillis();
    149         requestLayout();
    150     }
    151 
    152     @Override
    153     public void onClick(View view) {
    154         changeIndicatorsLevel();
    155     }
    156 
    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 
    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 
    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 
    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
    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 
    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
    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 
    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 
    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 
    414     public void startTimeLapseAnimation(int timeLapseInterval, long startTime) {
    415         mTimeLapseInterval = timeLapseInterval;
    416         mRecordingStartTime = startTime;
    417         mNumberOfFrames = 0;
    418         invalidate();
    419     }
    420 
    421     public void stopTimeLapseAnimation() {
    422         mTimeLapseInterval = 0;
    423         invalidate();
    424     }
    425 
    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
    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
    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 
    527     public void enableZoom(boolean enabled) {
    528         if (mZoomControl != null) mZoomControl.setEnabled(enabled);
    529     }
    530 
    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 
    540     public void dismissSecondLevelIndicator() {
    541         if (mCurrentLevel == 1) {
    542             changeIndicatorsLevel();
    543         }
    544     }
    545 }
    546