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