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