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