Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2012 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 android.annotation.TargetApi;
     20 import android.content.Context;
     21 import android.content.res.ColorStateList;
     22 import android.content.res.Resources;
     23 import android.graphics.Canvas;
     24 import android.graphics.Paint;
     25 import android.graphics.Rect;
     26 import android.graphics.drawable.Drawable;
     27 import android.os.Build;
     28 import android.text.Layout;
     29 import android.text.StaticLayout;
     30 import android.text.TextPaint;
     31 import android.text.TextUtils;
     32 import android.util.AttributeSet;
     33 import android.util.DisplayMetrics;
     34 import android.view.Gravity;
     35 import android.view.MotionEvent;
     36 import android.view.VelocityTracker;
     37 import android.view.ViewConfiguration;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.view.accessibility.AccessibilityNodeInfo;
     40 import android.widget.CompoundButton;
     41 
     42 import com.android.camera2.R;
     43 
     44 /**
     45  * A Switch is a two-state toggle switch widget that can select between two
     46  * options. The user may drag the "thumb" back and forth to choose the selected option,
     47  * or simply tap to toggle as if it were a checkbox.
     48  */
     49 public class Switch extends CompoundButton {
     50     private static final int TOUCH_MODE_IDLE = 0;
     51     private static final int TOUCH_MODE_DOWN = 1;
     52     private static final int TOUCH_MODE_DRAGGING = 2;
     53 
     54     private Drawable mThumbDrawable;
     55     private Drawable mTrackDrawable;
     56     private int mThumbTextPadding;
     57     private int mSwitchMinWidth;
     58     private int mSwitchTextMaxWidth;
     59     private int mSwitchPadding;
     60     private CharSequence mTextOn;
     61     private CharSequence mTextOff;
     62 
     63     private int mTouchMode;
     64     private int mTouchSlop;
     65     private float mTouchX;
     66     private float mTouchY;
     67     private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
     68     private int mMinFlingVelocity;
     69 
     70     private float mThumbPosition;
     71     private int mSwitchWidth;
     72     private int mSwitchHeight;
     73     private int mThumbWidth; // Does not include padding
     74 
     75     private int mSwitchLeft;
     76     private int mSwitchTop;
     77     private int mSwitchRight;
     78     private int mSwitchBottom;
     79 
     80     private TextPaint mTextPaint;
     81     private ColorStateList mTextColors;
     82     private Layout mOnLayout;
     83     private Layout mOffLayout;
     84 
     85     private final Rect mTempRect = new Rect();
     86 
     87     private static final int[] CHECKED_STATE_SET = {
     88         android.R.attr.state_checked
     89     };
     90 
     91     /**
     92      * Construct a new Switch with default styling, overriding specific style
     93      * attributes as requested.
     94      *
     95      * @param context The Context that will determine this widget's theming.
     96      * @param attrs Specification of attributes that should deviate from default styling.
     97      */
     98     public Switch(Context context, AttributeSet attrs) {
     99         this(context, attrs, R.attr.switchStyle);
    100     }
    101 
    102     /**
    103      * Construct a new Switch with a default style determined by the given theme attribute,
    104      * overriding specific style attributes as requested.
    105      *
    106      * @param context The Context that will determine this widget's theming.
    107      * @param attrs Specification of attributes that should deviate from the default styling.
    108      * @param defStyle An attribute ID within the active theme containing a reference to the
    109      *                 default style for this widget. e.g. android.R.attr.switchStyle.
    110      */
    111     public Switch(Context context, AttributeSet attrs, int defStyle) {
    112         super(context, attrs, defStyle);
    113 
    114         mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
    115         Resources res = getResources();
    116         DisplayMetrics dm = res.getDisplayMetrics();
    117         mTextPaint.density = dm.density;
    118         mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark);
    119         mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark);
    120         mTextOn = res.getString(R.string.capital_on);
    121         mTextOff = res.getString(R.string.capital_off);
    122         mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding);
    123         mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width);
    124         mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width);
    125         mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding);
    126         setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small);
    127 
    128         ViewConfiguration config = ViewConfiguration.get(context);
    129         mTouchSlop = config.getScaledTouchSlop();
    130         mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
    131 
    132         // Refresh display with current params
    133         refreshDrawableState();
    134         setChecked(isChecked());
    135     }
    136 
    137     /**
    138      * Sets the switch text color, size, style, hint color, and highlight color
    139      * from the specified TextAppearance resource.
    140      */
    141     public void setSwitchTextAppearance(Context context, int resid) {
    142         Resources res = getResources();
    143         mTextColors = getTextColors();
    144         int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size);
    145         if (ts != mTextPaint.getTextSize()) {
    146             mTextPaint.setTextSize(ts);
    147             requestLayout();
    148         }
    149     }
    150 
    151     @Override
    152     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    153         if (mOnLayout == null) {
    154             mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth);
    155         }
    156         if (mOffLayout == null) {
    157             mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth);
    158         }
    159 
    160         mTrackDrawable.getPadding(mTempRect);
    161         final int maxTextWidth = Math.min(mSwitchTextMaxWidth,
    162                 Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()));
    163         final int switchWidth = Math.max(mSwitchMinWidth,
    164                 maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right);
    165         final int switchHeight = mTrackDrawable.getIntrinsicHeight();
    166 
    167         mThumbWidth = maxTextWidth + mThumbTextPadding * 2;
    168 
    169         mSwitchWidth = switchWidth;
    170         mSwitchHeight = switchHeight;
    171 
    172         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    173         final int measuredHeight = getMeasuredHeight();
    174         final int measuredWidth = getMeasuredWidth();
    175         if (measuredHeight < switchHeight) {
    176             setMeasuredDimension(measuredWidth, switchHeight);
    177         }
    178     }
    179 
    180     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    181     @Override
    182     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    183         super.onPopulateAccessibilityEvent(event);
    184         CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText();
    185         if (!TextUtils.isEmpty(text)) {
    186             event.getText().add(text);
    187         }
    188     }
    189 
    190     private Layout makeLayout(CharSequence text, int maxWidth) {
    191         int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint));
    192         StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint,
    193                 actual_width,
    194                 Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true,
    195                 TextUtils.TruncateAt.END,
    196                 (int) Math.min(actual_width, maxWidth));
    197         return l;
    198     }
    199 
    200     /**
    201      * @return true if (x, y) is within the target area of the switch thumb
    202      */
    203     private boolean hitThumb(float x, float y) {
    204         mThumbDrawable.getPadding(mTempRect);
    205         final int thumbTop = mSwitchTop - mTouchSlop;
    206         final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop;
    207         final int thumbRight = thumbLeft + mThumbWidth +
    208                 mTempRect.left + mTempRect.right + mTouchSlop;
    209         final int thumbBottom = mSwitchBottom + mTouchSlop;
    210         return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
    211     }
    212 
    213     @Override
    214     public boolean onTouchEvent(MotionEvent ev) {
    215         mVelocityTracker.addMovement(ev);
    216         final int action = ev.getActionMasked();
    217         switch (action) {
    218             case MotionEvent.ACTION_DOWN: {
    219                 final float x = ev.getX();
    220                 final float y = ev.getY();
    221                 if (isEnabled() && hitThumb(x, y)) {
    222                     mTouchMode = TOUCH_MODE_DOWN;
    223                     mTouchX = x;
    224                     mTouchY = y;
    225                 }
    226                 break;
    227             }
    228 
    229             case MotionEvent.ACTION_MOVE: {
    230                 switch (mTouchMode) {
    231                     case TOUCH_MODE_IDLE:
    232                         // Didn't target the thumb, treat normally.
    233                         break;
    234 
    235                     case TOUCH_MODE_DOWN: {
    236                         final float x = ev.getX();
    237                         final float y = ev.getY();
    238                         if (Math.abs(x - mTouchX) > mTouchSlop ||
    239                                 Math.abs(y - mTouchY) > mTouchSlop) {
    240                             mTouchMode = TOUCH_MODE_DRAGGING;
    241                             getParent().requestDisallowInterceptTouchEvent(true);
    242                             mTouchX = x;
    243                             mTouchY = y;
    244                             return true;
    245                         }
    246                         break;
    247                     }
    248 
    249                     case TOUCH_MODE_DRAGGING: {
    250                         final float x = ev.getX();
    251                         final float dx = x - mTouchX;
    252                         float newPos = Math.max(0,
    253                                 Math.min(mThumbPosition + dx, getThumbScrollRange()));
    254                         if (newPos != mThumbPosition) {
    255                             mThumbPosition = newPos;
    256                             mTouchX = x;
    257                             invalidate();
    258                         }
    259                         return true;
    260                     }
    261                 }
    262                 break;
    263             }
    264 
    265             case MotionEvent.ACTION_UP:
    266             case MotionEvent.ACTION_CANCEL: {
    267                 if (mTouchMode == TOUCH_MODE_DRAGGING) {
    268                     stopDrag(ev);
    269                     return true;
    270                 }
    271                 mTouchMode = TOUCH_MODE_IDLE;
    272                 mVelocityTracker.clear();
    273                 break;
    274             }
    275         }
    276 
    277         return super.onTouchEvent(ev);
    278     }
    279 
    280     private void cancelSuperTouch(MotionEvent ev) {
    281         MotionEvent cancel = MotionEvent.obtain(ev);
    282         cancel.setAction(MotionEvent.ACTION_CANCEL);
    283         super.onTouchEvent(cancel);
    284         cancel.recycle();
    285     }
    286 
    287     /**
    288      * Called from onTouchEvent to end a drag operation.
    289      *
    290      * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
    291      */
    292     private void stopDrag(MotionEvent ev) {
    293         mTouchMode = TOUCH_MODE_IDLE;
    294         // Up and not canceled, also checks the switch has not been disabled during the drag
    295         boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
    296 
    297         cancelSuperTouch(ev);
    298 
    299         if (commitChange) {
    300             boolean newState;
    301             mVelocityTracker.computeCurrentVelocity(1000);
    302             float xvel = mVelocityTracker.getXVelocity();
    303             if (Math.abs(xvel) > mMinFlingVelocity) {
    304                 newState = xvel > 0;
    305             } else {
    306                 newState = getTargetCheckedState();
    307             }
    308             animateThumbToCheckedState(newState);
    309         } else {
    310             animateThumbToCheckedState(isChecked());
    311         }
    312     }
    313 
    314     private void animateThumbToCheckedState(boolean newCheckedState) {
    315         setChecked(newCheckedState);
    316     }
    317 
    318     private boolean getTargetCheckedState() {
    319         return mThumbPosition >= getThumbScrollRange() / 2;
    320     }
    321 
    322     private void setThumbPosition(boolean checked) {
    323         mThumbPosition = checked ? getThumbScrollRange() : 0;
    324     }
    325 
    326     @Override
    327     public void setChecked(boolean checked) {
    328         super.setChecked(checked);
    329         setThumbPosition(checked);
    330         invalidate();
    331     }
    332 
    333     @Override
    334     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    335         super.onLayout(changed, left, top, right, bottom);
    336 
    337         setThumbPosition(isChecked());
    338 
    339         int switchRight;
    340         int switchLeft;
    341 
    342         switchRight = getWidth() - getPaddingRight();
    343         switchLeft = switchRight - mSwitchWidth;
    344 
    345         int switchTop = 0;
    346         int switchBottom = 0;
    347         switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
    348             default:
    349             case Gravity.TOP:
    350                 switchTop = getPaddingTop();
    351                 switchBottom = switchTop + mSwitchHeight;
    352                 break;
    353 
    354             case Gravity.CENTER_VERTICAL:
    355                 switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
    356                         mSwitchHeight / 2;
    357                 switchBottom = switchTop + mSwitchHeight;
    358                 break;
    359 
    360             case Gravity.BOTTOM:
    361                 switchBottom = getHeight() - getPaddingBottom();
    362                 switchTop = switchBottom - mSwitchHeight;
    363                 break;
    364         }
    365 
    366         mSwitchLeft = switchLeft;
    367         mSwitchTop = switchTop;
    368         mSwitchBottom = switchBottom;
    369         mSwitchRight = switchRight;
    370     }
    371 
    372     @Override
    373     protected void onDraw(Canvas canvas) {
    374         super.onDraw(canvas);
    375 
    376         // Draw the switch
    377         int switchLeft = mSwitchLeft;
    378         int switchTop = mSwitchTop;
    379         int switchRight = mSwitchRight;
    380         int switchBottom = mSwitchBottom;
    381 
    382         mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom);
    383         mTrackDrawable.draw(canvas);
    384 
    385         canvas.save();
    386 
    387         mTrackDrawable.getPadding(mTempRect);
    388         int switchInnerLeft = switchLeft + mTempRect.left;
    389         int switchInnerTop = switchTop + mTempRect.top;
    390         int switchInnerRight = switchRight - mTempRect.right;
    391         int switchInnerBottom = switchBottom - mTempRect.bottom;
    392         canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom);
    393 
    394         mThumbDrawable.getPadding(mTempRect);
    395         final int thumbPos = (int) (mThumbPosition + 0.5f);
    396         int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos;
    397         int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right;
    398 
    399         mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
    400         mThumbDrawable.draw(canvas);
    401 
    402         // mTextColors should not be null, but just in case
    403         if (mTextColors != null) {
    404             mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(),
    405                     mTextColors.getDefaultColor()));
    406         }
    407         mTextPaint.drawableState = getDrawableState();
    408 
    409         Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
    410 
    411         canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2,
    412                 (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2);
    413         switchText.draw(canvas);
    414 
    415         canvas.restore();
    416     }
    417 
    418     @Override
    419     public int getCompoundPaddingRight() {
    420         int padding = super.getCompoundPaddingRight() + mSwitchWidth;
    421         if (!TextUtils.isEmpty(getText())) {
    422             padding += mSwitchPadding;
    423         }
    424         return padding;
    425     }
    426 
    427     private int getThumbScrollRange() {
    428         if (mTrackDrawable == null) {
    429             return 0;
    430         }
    431         mTrackDrawable.getPadding(mTempRect);
    432         return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right;
    433     }
    434 
    435     @Override
    436     protected int[] onCreateDrawableState(int extraSpace) {
    437         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
    438 
    439         if (isChecked()) {
    440             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
    441         }
    442         return drawableState;
    443     }
    444 
    445     @Override
    446     protected void drawableStateChanged() {
    447         super.drawableStateChanged();
    448 
    449         int[] myDrawableState = getDrawableState();
    450 
    451         // Set the state of the Drawable
    452         // Drawable may be null when checked state is set from XML, from super constructor
    453         if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState);
    454         if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState);
    455 
    456         invalidate();
    457     }
    458 
    459     @Override
    460     protected boolean verifyDrawable(Drawable who) {
    461         return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
    462     }
    463 
    464     @Override
    465     public void jumpDrawablesToCurrentState() {
    466         super.jumpDrawablesToCurrentState();
    467         mThumbDrawable.jumpToCurrentState();
    468         mTrackDrawable.jumpToCurrentState();
    469     }
    470 
    471     @Override
    472     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    473         super.onInitializeAccessibilityEvent(event);
    474         event.setClassName(Switch.class.getName());
    475     }
    476 
    477     @Override
    478     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    479         super.onInitializeAccessibilityNodeInfo(info);
    480         info.setClassName(Switch.class.getName());
    481         CharSequence switchText = isChecked() ? mTextOn : mTextOff;
    482         if (!TextUtils.isEmpty(switchText)) {
    483             CharSequence oldText = info.getText();
    484             if (TextUtils.isEmpty(oldText)) {
    485                 info.setText(switchText);
    486             } else {
    487                 StringBuilder newText = new StringBuilder();
    488                 newText.append(oldText).append(' ').append(switchText);
    489                 info.setText(newText);
    490             }
    491         }
    492     }
    493 }
    494