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