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