Justin Klaassen | 10d07c8 | 2017-09-15 17:58:39 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2007 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 android.widget; |
| 18 | |
| 19 | import android.annotation.NonNull; |
| 20 | import android.annotation.Nullable; |
| 21 | import android.content.Context; |
| 22 | import android.content.res.ColorStateList; |
| 23 | import android.content.res.TypedArray; |
| 24 | import android.graphics.Canvas; |
| 25 | import android.graphics.Insets; |
| 26 | import android.graphics.PorterDuff; |
| 27 | import android.graphics.Rect; |
| 28 | import android.graphics.Region.Op; |
| 29 | import android.graphics.drawable.Drawable; |
| 30 | import android.os.Bundle; |
| 31 | import android.util.AttributeSet; |
| 32 | import android.view.KeyEvent; |
| 33 | import android.view.MotionEvent; |
| 34 | import android.view.ViewConfiguration; |
| 35 | import android.view.accessibility.AccessibilityNodeInfo; |
| 36 | |
| 37 | import com.android.internal.R; |
| 38 | |
| 39 | |
| 40 | /** |
| 41 | * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb. |
| 42 | */ |
| 43 | public abstract class AbsSeekBar extends ProgressBar { |
| 44 | private final Rect mTempRect = new Rect(); |
| 45 | |
| 46 | private Drawable mThumb; |
| 47 | private ColorStateList mThumbTintList = null; |
| 48 | private PorterDuff.Mode mThumbTintMode = null; |
| 49 | private boolean mHasThumbTint = false; |
| 50 | private boolean mHasThumbTintMode = false; |
| 51 | |
| 52 | private Drawable mTickMark; |
| 53 | private ColorStateList mTickMarkTintList = null; |
| 54 | private PorterDuff.Mode mTickMarkTintMode = null; |
| 55 | private boolean mHasTickMarkTint = false; |
| 56 | private boolean mHasTickMarkTintMode = false; |
| 57 | |
| 58 | private int mThumbOffset; |
| 59 | private boolean mSplitTrack; |
| 60 | |
| 61 | /** |
| 62 | * On touch, this offset plus the scaled value from the position of the |
| 63 | * touch will form the progress value. Usually 0. |
| 64 | */ |
| 65 | float mTouchProgressOffset; |
| 66 | |
| 67 | /** |
| 68 | * Whether this is user seekable. |
| 69 | */ |
| 70 | boolean mIsUserSeekable = true; |
| 71 | |
| 72 | /** |
| 73 | * On key presses (right or left), the amount to increment/decrement the |
| 74 | * progress. |
| 75 | */ |
| 76 | private int mKeyProgressIncrement = 1; |
| 77 | |
| 78 | private static final int NO_ALPHA = 0xFF; |
| 79 | private float mDisabledAlpha; |
| 80 | |
| 81 | private int mScaledTouchSlop; |
| 82 | private float mTouchDownX; |
| 83 | private boolean mIsDragging; |
| 84 | |
| 85 | public AbsSeekBar(Context context) { |
| 86 | super(context); |
| 87 | } |
| 88 | |
| 89 | public AbsSeekBar(Context context, AttributeSet attrs) { |
| 90 | super(context, attrs); |
| 91 | } |
| 92 | |
| 93 | public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { |
| 94 | this(context, attrs, defStyleAttr, 0); |
| 95 | } |
| 96 | |
| 97 | public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| 98 | super(context, attrs, defStyleAttr, defStyleRes); |
| 99 | |
| 100 | final TypedArray a = context.obtainStyledAttributes( |
| 101 | attrs, R.styleable.SeekBar, defStyleAttr, defStyleRes); |
| 102 | |
| 103 | final Drawable thumb = a.getDrawable(R.styleable.SeekBar_thumb); |
| 104 | setThumb(thumb); |
| 105 | |
| 106 | if (a.hasValue(R.styleable.SeekBar_thumbTintMode)) { |
| 107 | mThumbTintMode = Drawable.parseTintMode(a.getInt( |
| 108 | R.styleable.SeekBar_thumbTintMode, -1), mThumbTintMode); |
| 109 | mHasThumbTintMode = true; |
| 110 | } |
| 111 | |
| 112 | if (a.hasValue(R.styleable.SeekBar_thumbTint)) { |
| 113 | mThumbTintList = a.getColorStateList(R.styleable.SeekBar_thumbTint); |
| 114 | mHasThumbTint = true; |
| 115 | } |
| 116 | |
| 117 | final Drawable tickMark = a.getDrawable(R.styleable.SeekBar_tickMark); |
| 118 | setTickMark(tickMark); |
| 119 | |
| 120 | if (a.hasValue(R.styleable.SeekBar_tickMarkTintMode)) { |
| 121 | mTickMarkTintMode = Drawable.parseTintMode(a.getInt( |
| 122 | R.styleable.SeekBar_tickMarkTintMode, -1), mTickMarkTintMode); |
| 123 | mHasTickMarkTintMode = true; |
| 124 | } |
| 125 | |
| 126 | if (a.hasValue(R.styleable.SeekBar_tickMarkTint)) { |
| 127 | mTickMarkTintList = a.getColorStateList(R.styleable.SeekBar_tickMarkTint); |
| 128 | mHasTickMarkTint = true; |
| 129 | } |
| 130 | |
| 131 | mSplitTrack = a.getBoolean(R.styleable.SeekBar_splitTrack, false); |
| 132 | |
| 133 | // Guess thumb offset if thumb != null, but allow layout to override. |
| 134 | final int thumbOffset = a.getDimensionPixelOffset( |
| 135 | R.styleable.SeekBar_thumbOffset, getThumbOffset()); |
| 136 | setThumbOffset(thumbOffset); |
| 137 | |
| 138 | final boolean useDisabledAlpha = a.getBoolean(R.styleable.SeekBar_useDisabledAlpha, true); |
| 139 | a.recycle(); |
| 140 | |
| 141 | if (useDisabledAlpha) { |
| 142 | final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Theme, 0, 0); |
| 143 | mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f); |
| 144 | ta.recycle(); |
| 145 | } else { |
| 146 | mDisabledAlpha = 1.0f; |
| 147 | } |
| 148 | |
| 149 | applyThumbTint(); |
| 150 | applyTickMarkTint(); |
| 151 | |
| 152 | mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar. |
| 157 | * <p> |
| 158 | * If the thumb is a valid drawable (i.e. not null), half its width will be |
| 159 | * used as the new thumb offset (@see #setThumbOffset(int)). |
| 160 | * |
| 161 | * @param thumb Drawable representing the thumb |
| 162 | */ |
| 163 | public void setThumb(Drawable thumb) { |
| 164 | final boolean needUpdate; |
| 165 | // This way, calling setThumb again with the same bitmap will result in |
| 166 | // it recalcuating mThumbOffset (if for example it the bounds of the |
| 167 | // drawable changed) |
| 168 | if (mThumb != null && thumb != mThumb) { |
| 169 | mThumb.setCallback(null); |
| 170 | needUpdate = true; |
| 171 | } else { |
| 172 | needUpdate = false; |
| 173 | } |
| 174 | |
| 175 | if (thumb != null) { |
| 176 | thumb.setCallback(this); |
| 177 | if (canResolveLayoutDirection()) { |
| 178 | thumb.setLayoutDirection(getLayoutDirection()); |
| 179 | } |
| 180 | |
| 181 | // Assuming the thumb drawable is symmetric, set the thumb offset |
| 182 | // such that the thumb will hang halfway off either edge of the |
| 183 | // progress bar. |
| 184 | mThumbOffset = thumb.getIntrinsicWidth() / 2; |
| 185 | |
| 186 | // If we're updating get the new states |
| 187 | if (needUpdate && |
| 188 | (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth() |
| 189 | || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) { |
| 190 | requestLayout(); |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | mThumb = thumb; |
| 195 | |
| 196 | applyThumbTint(); |
| 197 | invalidate(); |
| 198 | |
| 199 | if (needUpdate) { |
| 200 | updateThumbAndTrackPos(getWidth(), getHeight()); |
| 201 | if (thumb != null && thumb.isStateful()) { |
| 202 | // Note that if the states are different this won't work. |
| 203 | // For now, let's consider that an app bug. |
| 204 | int[] state = getDrawableState(); |
| 205 | thumb.setState(state); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Return the drawable used to represent the scroll thumb - the component that |
| 212 | * the user can drag back and forth indicating the current value by its position. |
| 213 | * |
| 214 | * @return The current thumb drawable |
| 215 | */ |
| 216 | public Drawable getThumb() { |
| 217 | return mThumb; |
| 218 | } |
| 219 | |
| 220 | /** |
| 221 | * Applies a tint to the thumb drawable. Does not modify the current tint |
| 222 | * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. |
| 223 | * <p> |
| 224 | * Subsequent calls to {@link #setThumb(Drawable)} will automatically |
| 225 | * mutate the drawable and apply the specified tint and tint mode using |
| 226 | * {@link Drawable#setTintList(ColorStateList)}. |
| 227 | * |
| 228 | * @param tint the tint to apply, may be {@code null} to clear tint |
| 229 | * |
| 230 | * @attr ref android.R.styleable#SeekBar_thumbTint |
| 231 | * @see #getThumbTintList() |
| 232 | * @see Drawable#setTintList(ColorStateList) |
| 233 | */ |
| 234 | public void setThumbTintList(@Nullable ColorStateList tint) { |
| 235 | mThumbTintList = tint; |
| 236 | mHasThumbTint = true; |
| 237 | |
| 238 | applyThumbTint(); |
| 239 | } |
| 240 | |
| 241 | /** |
| 242 | * Returns the tint applied to the thumb drawable, if specified. |
| 243 | * |
| 244 | * @return the tint applied to the thumb drawable |
| 245 | * @attr ref android.R.styleable#SeekBar_thumbTint |
| 246 | * @see #setThumbTintList(ColorStateList) |
| 247 | */ |
| 248 | @Nullable |
| 249 | public ColorStateList getThumbTintList() { |
| 250 | return mThumbTintList; |
| 251 | } |
| 252 | |
| 253 | /** |
| 254 | * Specifies the blending mode used to apply the tint specified by |
| 255 | * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The |
| 256 | * default mode is {@link PorterDuff.Mode#SRC_IN}. |
| 257 | * |
| 258 | * @param tintMode the blending mode used to apply the tint, may be |
| 259 | * {@code null} to clear tint |
| 260 | * |
| 261 | * @attr ref android.R.styleable#SeekBar_thumbTintMode |
| 262 | * @see #getThumbTintMode() |
| 263 | * @see Drawable#setTintMode(PorterDuff.Mode) |
| 264 | */ |
| 265 | public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { |
| 266 | mThumbTintMode = tintMode; |
| 267 | mHasThumbTintMode = true; |
| 268 | |
| 269 | applyThumbTint(); |
| 270 | } |
| 271 | |
| 272 | /** |
| 273 | * Returns the blending mode used to apply the tint to the thumb drawable, |
| 274 | * if specified. |
| 275 | * |
| 276 | * @return the blending mode used to apply the tint to the thumb drawable |
| 277 | * @attr ref android.R.styleable#SeekBar_thumbTintMode |
| 278 | * @see #setThumbTintMode(PorterDuff.Mode) |
| 279 | */ |
| 280 | @Nullable |
| 281 | public PorterDuff.Mode getThumbTintMode() { |
| 282 | return mThumbTintMode; |
| 283 | } |
| 284 | |
| 285 | private void applyThumbTint() { |
| 286 | if (mThumb != null && (mHasThumbTint || mHasThumbTintMode)) { |
| 287 | mThumb = mThumb.mutate(); |
| 288 | |
| 289 | if (mHasThumbTint) { |
| 290 | mThumb.setTintList(mThumbTintList); |
| 291 | } |
| 292 | |
| 293 | if (mHasThumbTintMode) { |
| 294 | mThumb.setTintMode(mThumbTintMode); |
| 295 | } |
| 296 | |
| 297 | // The drawable (or one of its children) may not have been |
| 298 | // stateful before applying the tint, so let's try again. |
| 299 | if (mThumb.isStateful()) { |
| 300 | mThumb.setState(getDrawableState()); |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | /** |
| 306 | * @see #setThumbOffset(int) |
| 307 | */ |
| 308 | public int getThumbOffset() { |
| 309 | return mThumbOffset; |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Sets the thumb offset that allows the thumb to extend out of the range of |
| 314 | * the track. |
| 315 | * |
| 316 | * @param thumbOffset The offset amount in pixels. |
| 317 | */ |
| 318 | public void setThumbOffset(int thumbOffset) { |
| 319 | mThumbOffset = thumbOffset; |
| 320 | invalidate(); |
| 321 | } |
| 322 | |
| 323 | /** |
| 324 | * Specifies whether the track should be split by the thumb. When true, |
| 325 | * the thumb's optical bounds will be clipped out of the track drawable, |
| 326 | * then the thumb will be drawn into the resulting gap. |
| 327 | * |
| 328 | * @param splitTrack Whether the track should be split by the thumb |
| 329 | */ |
| 330 | public void setSplitTrack(boolean splitTrack) { |
| 331 | mSplitTrack = splitTrack; |
| 332 | invalidate(); |
| 333 | } |
| 334 | |
| 335 | /** |
| 336 | * Returns whether the track should be split by the thumb. |
| 337 | */ |
| 338 | public boolean getSplitTrack() { |
| 339 | return mSplitTrack; |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * Sets the drawable displayed at each progress position, e.g. at each |
| 344 | * possible thumb position. |
| 345 | * |
| 346 | * @param tickMark the drawable to display at each progress position |
| 347 | */ |
| 348 | public void setTickMark(Drawable tickMark) { |
| 349 | if (mTickMark != null) { |
| 350 | mTickMark.setCallback(null); |
| 351 | } |
| 352 | |
| 353 | mTickMark = tickMark; |
| 354 | |
| 355 | if (tickMark != null) { |
| 356 | tickMark.setCallback(this); |
| 357 | tickMark.setLayoutDirection(getLayoutDirection()); |
| 358 | if (tickMark.isStateful()) { |
| 359 | tickMark.setState(getDrawableState()); |
| 360 | } |
| 361 | applyTickMarkTint(); |
| 362 | } |
| 363 | |
| 364 | invalidate(); |
| 365 | } |
| 366 | |
| 367 | /** |
| 368 | * @return the drawable displayed at each progress position |
| 369 | */ |
| 370 | public Drawable getTickMark() { |
| 371 | return mTickMark; |
| 372 | } |
| 373 | |
| 374 | /** |
| 375 | * Applies a tint to the tick mark drawable. Does not modify the current tint |
| 376 | * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. |
| 377 | * <p> |
| 378 | * Subsequent calls to {@link #setTickMark(Drawable)} will automatically |
| 379 | * mutate the drawable and apply the specified tint and tint mode using |
| 380 | * {@link Drawable#setTintList(ColorStateList)}. |
| 381 | * |
| 382 | * @param tint the tint to apply, may be {@code null} to clear tint |
| 383 | * |
| 384 | * @attr ref android.R.styleable#SeekBar_tickMarkTint |
| 385 | * @see #getTickMarkTintList() |
| 386 | * @see Drawable#setTintList(ColorStateList) |
| 387 | */ |
| 388 | public void setTickMarkTintList(@Nullable ColorStateList tint) { |
| 389 | mTickMarkTintList = tint; |
| 390 | mHasTickMarkTint = true; |
| 391 | |
| 392 | applyTickMarkTint(); |
| 393 | } |
| 394 | |
| 395 | /** |
| 396 | * Returns the tint applied to the tick mark drawable, if specified. |
| 397 | * |
| 398 | * @return the tint applied to the tick mark drawable |
| 399 | * @attr ref android.R.styleable#SeekBar_tickMarkTint |
| 400 | * @see #setTickMarkTintList(ColorStateList) |
| 401 | */ |
| 402 | @Nullable |
| 403 | public ColorStateList getTickMarkTintList() { |
| 404 | return mTickMarkTintList; |
| 405 | } |
| 406 | |
| 407 | /** |
| 408 | * Specifies the blending mode used to apply the tint specified by |
| 409 | * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The |
| 410 | * default mode is {@link PorterDuff.Mode#SRC_IN}. |
| 411 | * |
| 412 | * @param tintMode the blending mode used to apply the tint, may be |
| 413 | * {@code null} to clear tint |
| 414 | * |
| 415 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode |
| 416 | * @see #getTickMarkTintMode() |
| 417 | * @see Drawable#setTintMode(PorterDuff.Mode) |
| 418 | */ |
| 419 | public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) { |
| 420 | mTickMarkTintMode = tintMode; |
| 421 | mHasTickMarkTintMode = true; |
| 422 | |
| 423 | applyTickMarkTint(); |
| 424 | } |
| 425 | |
| 426 | /** |
| 427 | * Returns the blending mode used to apply the tint to the tick mark drawable, |
| 428 | * if specified. |
| 429 | * |
| 430 | * @return the blending mode used to apply the tint to the tick mark drawable |
| 431 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode |
| 432 | * @see #setTickMarkTintMode(PorterDuff.Mode) |
| 433 | */ |
| 434 | @Nullable |
| 435 | public PorterDuff.Mode getTickMarkTintMode() { |
| 436 | return mTickMarkTintMode; |
| 437 | } |
| 438 | |
| 439 | private void applyTickMarkTint() { |
| 440 | if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) { |
| 441 | mTickMark = mTickMark.mutate(); |
| 442 | |
| 443 | if (mHasTickMarkTint) { |
| 444 | mTickMark.setTintList(mTickMarkTintList); |
| 445 | } |
| 446 | |
| 447 | if (mHasTickMarkTintMode) { |
| 448 | mTickMark.setTintMode(mTickMarkTintMode); |
| 449 | } |
| 450 | |
| 451 | // The drawable (or one of its children) may not have been |
| 452 | // stateful before applying the tint, so let's try again. |
| 453 | if (mTickMark.isStateful()) { |
| 454 | mTickMark.setState(getDrawableState()); |
| 455 | } |
| 456 | } |
| 457 | } |
| 458 | |
| 459 | /** |
| 460 | * Sets the amount of progress changed via the arrow keys. |
| 461 | * |
| 462 | * @param increment The amount to increment or decrement when the user |
| 463 | * presses the arrow keys. |
| 464 | */ |
| 465 | public void setKeyProgressIncrement(int increment) { |
| 466 | mKeyProgressIncrement = increment < 0 ? -increment : increment; |
| 467 | } |
| 468 | |
| 469 | /** |
| 470 | * Returns the amount of progress changed via the arrow keys. |
| 471 | * <p> |
| 472 | * By default, this will be a value that is derived from the progress range. |
| 473 | * |
| 474 | * @return The amount to increment or decrement when the user presses the |
| 475 | * arrow keys. This will be positive. |
| 476 | */ |
| 477 | public int getKeyProgressIncrement() { |
| 478 | return mKeyProgressIncrement; |
| 479 | } |
| 480 | |
| 481 | @Override |
| 482 | public synchronized void setMin(int min) { |
| 483 | super.setMin(min); |
| 484 | int range = getMax() - getMin(); |
| 485 | |
| 486 | if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { |
| 487 | |
| 488 | // It will take the user too long to change this via keys, change it |
| 489 | // to something more reasonable |
| 490 | setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20))); |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | @Override |
| 495 | public synchronized void setMax(int max) { |
| 496 | super.setMax(max); |
| 497 | int range = getMax() - getMin(); |
| 498 | |
| 499 | if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) { |
| 500 | // It will take the user too long to change this via keys, change it |
| 501 | // to something more reasonable |
| 502 | setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20))); |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | @Override |
| 507 | protected boolean verifyDrawable(@NonNull Drawable who) { |
| 508 | return who == mThumb || who == mTickMark || super.verifyDrawable(who); |
| 509 | } |
| 510 | |
| 511 | @Override |
| 512 | public void jumpDrawablesToCurrentState() { |
| 513 | super.jumpDrawablesToCurrentState(); |
| 514 | |
| 515 | if (mThumb != null) { |
| 516 | mThumb.jumpToCurrentState(); |
| 517 | } |
| 518 | |
| 519 | if (mTickMark != null) { |
| 520 | mTickMark.jumpToCurrentState(); |
| 521 | } |
| 522 | } |
| 523 | |
| 524 | @Override |
| 525 | protected void drawableStateChanged() { |
| 526 | super.drawableStateChanged(); |
| 527 | |
| 528 | final Drawable progressDrawable = getProgressDrawable(); |
| 529 | if (progressDrawable != null && mDisabledAlpha < 1.0f) { |
| 530 | progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); |
| 531 | } |
| 532 | |
| 533 | final Drawable thumb = mThumb; |
| 534 | if (thumb != null && thumb.isStateful() |
| 535 | && thumb.setState(getDrawableState())) { |
| 536 | invalidateDrawable(thumb); |
| 537 | } |
| 538 | |
| 539 | final Drawable tickMark = mTickMark; |
| 540 | if (tickMark != null && tickMark.isStateful() |
| 541 | && tickMark.setState(getDrawableState())) { |
| 542 | invalidateDrawable(tickMark); |
| 543 | } |
| 544 | } |
| 545 | |
| 546 | @Override |
| 547 | public void drawableHotspotChanged(float x, float y) { |
| 548 | super.drawableHotspotChanged(x, y); |
| 549 | |
| 550 | if (mThumb != null) { |
| 551 | mThumb.setHotspot(x, y); |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | @Override |
| 556 | void onVisualProgressChanged(int id, float scale) { |
| 557 | super.onVisualProgressChanged(id, scale); |
| 558 | |
| 559 | if (id == R.id.progress) { |
| 560 | final Drawable thumb = mThumb; |
| 561 | if (thumb != null) { |
| 562 | setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); |
| 563 | |
| 564 | // Since we draw translated, the drawable's bounds that it signals |
| 565 | // for invalidation won't be the actual bounds we want invalidated, |
| 566 | // so just invalidate this whole view. |
| 567 | invalidate(); |
| 568 | } |
| 569 | } |
| 570 | } |
| 571 | |
| 572 | @Override |
| 573 | protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| 574 | super.onSizeChanged(w, h, oldw, oldh); |
| 575 | |
| 576 | updateThumbAndTrackPos(w, h); |
| 577 | } |
| 578 | |
| 579 | private void updateThumbAndTrackPos(int w, int h) { |
| 580 | final int paddedHeight = h - mPaddingTop - mPaddingBottom; |
| 581 | final Drawable track = getCurrentDrawable(); |
| 582 | final Drawable thumb = mThumb; |
| 583 | |
| 584 | // The max height does not incorporate padding, whereas the height |
| 585 | // parameter does. |
| 586 | final int trackHeight = Math.min(mMaxHeight, paddedHeight); |
| 587 | final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); |
| 588 | |
| 589 | // Apply offset to whichever item is taller. |
| 590 | final int trackOffset; |
| 591 | final int thumbOffset; |
| 592 | if (thumbHeight > trackHeight) { |
| 593 | final int offsetHeight = (paddedHeight - thumbHeight) / 2; |
| 594 | trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2; |
| 595 | thumbOffset = offsetHeight; |
| 596 | } else { |
| 597 | final int offsetHeight = (paddedHeight - trackHeight) / 2; |
| 598 | trackOffset = offsetHeight; |
| 599 | thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2; |
| 600 | } |
| 601 | |
| 602 | if (track != null) { |
| 603 | final int trackWidth = w - mPaddingRight - mPaddingLeft; |
| 604 | track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight); |
| 605 | } |
| 606 | |
| 607 | if (thumb != null) { |
| 608 | setThumbPos(w, thumb, getScale(), thumbOffset); |
| 609 | } |
| 610 | } |
| 611 | |
| 612 | private float getScale() { |
| 613 | int min = getMin(); |
| 614 | int max = getMax(); |
| 615 | int range = max - min; |
| 616 | return range > 0 ? (getProgress() - min) / (float) range : 0; |
| 617 | } |
| 618 | |
| 619 | /** |
| 620 | * Updates the thumb drawable bounds. |
| 621 | * |
| 622 | * @param w Width of the view, including padding |
| 623 | * @param thumb Drawable used for the thumb |
| 624 | * @param scale Current progress between 0 and 1 |
| 625 | * @param offset Vertical offset for centering. If set to |
| 626 | * {@link Integer#MIN_VALUE}, the current offset will be used. |
| 627 | */ |
| 628 | private void setThumbPos(int w, Drawable thumb, float scale, int offset) { |
| 629 | int available = w - mPaddingLeft - mPaddingRight; |
| 630 | final int thumbWidth = thumb.getIntrinsicWidth(); |
| 631 | final int thumbHeight = thumb.getIntrinsicHeight(); |
| 632 | available -= thumbWidth; |
| 633 | |
| 634 | // The extra space for the thumb to move on the track |
| 635 | available += mThumbOffset * 2; |
| 636 | |
| 637 | final int thumbPos = (int) (scale * available + 0.5f); |
| 638 | |
| 639 | final int top, bottom; |
| 640 | if (offset == Integer.MIN_VALUE) { |
| 641 | final Rect oldBounds = thumb.getBounds(); |
| 642 | top = oldBounds.top; |
| 643 | bottom = oldBounds.bottom; |
| 644 | } else { |
| 645 | top = offset; |
| 646 | bottom = offset + thumbHeight; |
| 647 | } |
| 648 | |
| 649 | final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; |
| 650 | final int right = left + thumbWidth; |
| 651 | |
| 652 | final Drawable background = getBackground(); |
| 653 | if (background != null) { |
| 654 | final int offsetX = mPaddingLeft - mThumbOffset; |
| 655 | final int offsetY = mPaddingTop; |
| 656 | background.setHotspotBounds(left + offsetX, top + offsetY, |
| 657 | right + offsetX, bottom + offsetY); |
| 658 | } |
| 659 | |
| 660 | // Canvas will be translated, so 0,0 is where we start drawing |
| 661 | thumb.setBounds(left, top, right, bottom); |
| 662 | } |
| 663 | |
| 664 | /** |
| 665 | * @hide |
| 666 | */ |
| 667 | @Override |
| 668 | public void onResolveDrawables(int layoutDirection) { |
| 669 | super.onResolveDrawables(layoutDirection); |
| 670 | |
| 671 | if (mThumb != null) { |
| 672 | mThumb.setLayoutDirection(layoutDirection); |
| 673 | } |
| 674 | } |
| 675 | |
| 676 | @Override |
| 677 | protected synchronized void onDraw(Canvas canvas) { |
| 678 | super.onDraw(canvas); |
| 679 | drawThumb(canvas); |
| 680 | } |
| 681 | |
| 682 | @Override |
| 683 | void drawTrack(Canvas canvas) { |
| 684 | final Drawable thumbDrawable = mThumb; |
| 685 | if (thumbDrawable != null && mSplitTrack) { |
| 686 | final Insets insets = thumbDrawable.getOpticalInsets(); |
| 687 | final Rect tempRect = mTempRect; |
| 688 | thumbDrawable.copyBounds(tempRect); |
| 689 | tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop); |
| 690 | tempRect.left += insets.left; |
| 691 | tempRect.right -= insets.right; |
| 692 | |
| 693 | final int saveCount = canvas.save(); |
| 694 | canvas.clipRect(tempRect, Op.DIFFERENCE); |
| 695 | super.drawTrack(canvas); |
| 696 | drawTickMarks(canvas); |
| 697 | canvas.restoreToCount(saveCount); |
| 698 | } else { |
| 699 | super.drawTrack(canvas); |
| 700 | drawTickMarks(canvas); |
| 701 | } |
| 702 | } |
| 703 | |
| 704 | /** |
| 705 | * @hide |
| 706 | */ |
| 707 | protected void drawTickMarks(Canvas canvas) { |
| 708 | if (mTickMark != null) { |
| 709 | final int count = getMax() - getMin(); |
| 710 | if (count > 1) { |
| 711 | final int w = mTickMark.getIntrinsicWidth(); |
| 712 | final int h = mTickMark.getIntrinsicHeight(); |
| 713 | final int halfW = w >= 0 ? w / 2 : 1; |
| 714 | final int halfH = h >= 0 ? h / 2 : 1; |
| 715 | mTickMark.setBounds(-halfW, -halfH, halfW, halfH); |
| 716 | |
| 717 | final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count; |
| 718 | final int saveCount = canvas.save(); |
| 719 | canvas.translate(mPaddingLeft, getHeight() / 2); |
| 720 | for (int i = 0; i <= count; i++) { |
| 721 | mTickMark.draw(canvas); |
| 722 | canvas.translate(spacing, 0); |
| 723 | } |
| 724 | canvas.restoreToCount(saveCount); |
| 725 | } |
| 726 | } |
| 727 | } |
| 728 | |
| 729 | /** |
| 730 | * Draw the thumb. |
| 731 | */ |
| 732 | void drawThumb(Canvas canvas) { |
| 733 | if (mThumb != null) { |
| 734 | final int saveCount = canvas.save(); |
| 735 | // Translate the padding. For the x, we need to allow the thumb to |
| 736 | // draw in its extra space |
| 737 | canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop); |
| 738 | mThumb.draw(canvas); |
| 739 | canvas.restoreToCount(saveCount); |
| 740 | } |
| 741 | } |
| 742 | |
| 743 | @Override |
| 744 | protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| 745 | Drawable d = getCurrentDrawable(); |
| 746 | |
| 747 | int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight(); |
| 748 | int dw = 0; |
| 749 | int dh = 0; |
| 750 | if (d != null) { |
| 751 | dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth())); |
| 752 | dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight())); |
| 753 | dh = Math.max(thumbHeight, dh); |
| 754 | } |
| 755 | dw += mPaddingLeft + mPaddingRight; |
| 756 | dh += mPaddingTop + mPaddingBottom; |
| 757 | |
| 758 | setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), |
| 759 | resolveSizeAndState(dh, heightMeasureSpec, 0)); |
| 760 | } |
| 761 | |
| 762 | @Override |
| 763 | public boolean onTouchEvent(MotionEvent event) { |
| 764 | if (!mIsUserSeekable || !isEnabled()) { |
| 765 | return false; |
| 766 | } |
| 767 | |
| 768 | switch (event.getAction()) { |
| 769 | case MotionEvent.ACTION_DOWN: |
| 770 | if (isInScrollingContainer()) { |
| 771 | mTouchDownX = event.getX(); |
| 772 | } else { |
| 773 | startDrag(event); |
| 774 | } |
| 775 | break; |
| 776 | |
| 777 | case MotionEvent.ACTION_MOVE: |
| 778 | if (mIsDragging) { |
| 779 | trackTouchEvent(event); |
| 780 | } else { |
| 781 | final float x = event.getX(); |
| 782 | if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) { |
| 783 | startDrag(event); |
| 784 | } |
| 785 | } |
| 786 | break; |
| 787 | |
| 788 | case MotionEvent.ACTION_UP: |
| 789 | if (mIsDragging) { |
| 790 | trackTouchEvent(event); |
| 791 | onStopTrackingTouch(); |
| 792 | setPressed(false); |
| 793 | } else { |
| 794 | // Touch up when we never crossed the touch slop threshold should |
| 795 | // be interpreted as a tap-seek to that location. |
| 796 | onStartTrackingTouch(); |
| 797 | trackTouchEvent(event); |
| 798 | onStopTrackingTouch(); |
| 799 | } |
| 800 | // ProgressBar doesn't know to repaint the thumb drawable |
| 801 | // in its inactive state when the touch stops (because the |
| 802 | // value has not apparently changed) |
| 803 | invalidate(); |
| 804 | break; |
| 805 | |
| 806 | case MotionEvent.ACTION_CANCEL: |
| 807 | if (mIsDragging) { |
| 808 | onStopTrackingTouch(); |
| 809 | setPressed(false); |
| 810 | } |
| 811 | invalidate(); // see above explanation |
| 812 | break; |
| 813 | } |
| 814 | return true; |
| 815 | } |
| 816 | |
| 817 | private void startDrag(MotionEvent event) { |
| 818 | setPressed(true); |
| 819 | |
| 820 | if (mThumb != null) { |
| 821 | // This may be within the padding region. |
| 822 | invalidate(mThumb.getBounds()); |
| 823 | } |
| 824 | |
| 825 | onStartTrackingTouch(); |
| 826 | trackTouchEvent(event); |
| 827 | attemptClaimDrag(); |
| 828 | } |
| 829 | |
| 830 | private void setHotspot(float x, float y) { |
| 831 | final Drawable bg = getBackground(); |
| 832 | if (bg != null) { |
| 833 | bg.setHotspot(x, y); |
| 834 | } |
| 835 | } |
| 836 | |
| 837 | private void trackTouchEvent(MotionEvent event) { |
| 838 | final int x = Math.round(event.getX()); |
| 839 | final int y = Math.round(event.getY()); |
| 840 | final int width = getWidth(); |
| 841 | final int availableWidth = width - mPaddingLeft - mPaddingRight; |
| 842 | |
| 843 | final float scale; |
| 844 | float progress = 0.0f; |
| 845 | if (isLayoutRtl() && mMirrorForRtl) { |
| 846 | if (x > width - mPaddingRight) { |
| 847 | scale = 0.0f; |
| 848 | } else if (x < mPaddingLeft) { |
| 849 | scale = 1.0f; |
| 850 | } else { |
| 851 | scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth; |
| 852 | progress = mTouchProgressOffset; |
| 853 | } |
| 854 | } else { |
| 855 | if (x < mPaddingLeft) { |
| 856 | scale = 0.0f; |
| 857 | } else if (x > width - mPaddingRight) { |
| 858 | scale = 1.0f; |
| 859 | } else { |
| 860 | scale = (x - mPaddingLeft) / (float) availableWidth; |
| 861 | progress = mTouchProgressOffset; |
| 862 | } |
| 863 | } |
| 864 | |
| 865 | final int range = getMax() - getMin(); |
Justin Klaassen | 4d01eea | 2018-04-03 23:21:57 -0400 | [diff] [blame^] | 866 | progress += scale * range + getMin(); |
Justin Klaassen | 10d07c8 | 2017-09-15 17:58:39 -0400 | [diff] [blame] | 867 | |
| 868 | setHotspot(x, y); |
| 869 | setProgressInternal(Math.round(progress), true, false); |
| 870 | } |
| 871 | |
| 872 | /** |
| 873 | * Tries to claim the user's drag motion, and requests disallowing any |
| 874 | * ancestors from stealing events in the drag. |
| 875 | */ |
| 876 | private void attemptClaimDrag() { |
| 877 | if (mParent != null) { |
| 878 | mParent.requestDisallowInterceptTouchEvent(true); |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | /** |
| 883 | * This is called when the user has started touching this widget. |
| 884 | */ |
| 885 | void onStartTrackingTouch() { |
| 886 | mIsDragging = true; |
| 887 | } |
| 888 | |
| 889 | /** |
| 890 | * This is called when the user either releases his touch or the touch is |
| 891 | * canceled. |
| 892 | */ |
| 893 | void onStopTrackingTouch() { |
| 894 | mIsDragging = false; |
| 895 | } |
| 896 | |
| 897 | /** |
| 898 | * Called when the user changes the seekbar's progress by using a key event. |
| 899 | */ |
| 900 | void onKeyChange() { |
| 901 | } |
| 902 | |
| 903 | @Override |
| 904 | public boolean onKeyDown(int keyCode, KeyEvent event) { |
| 905 | if (isEnabled()) { |
| 906 | int increment = mKeyProgressIncrement; |
| 907 | switch (keyCode) { |
| 908 | case KeyEvent.KEYCODE_DPAD_LEFT: |
| 909 | case KeyEvent.KEYCODE_MINUS: |
| 910 | increment = -increment; |
| 911 | // fallthrough |
| 912 | case KeyEvent.KEYCODE_DPAD_RIGHT: |
| 913 | case KeyEvent.KEYCODE_PLUS: |
| 914 | case KeyEvent.KEYCODE_EQUALS: |
| 915 | increment = isLayoutRtl() ? -increment : increment; |
| 916 | |
| 917 | if (setProgressInternal(getProgress() + increment, true, true)) { |
| 918 | onKeyChange(); |
| 919 | return true; |
| 920 | } |
| 921 | break; |
| 922 | } |
| 923 | } |
| 924 | |
| 925 | return super.onKeyDown(keyCode, event); |
| 926 | } |
| 927 | |
| 928 | @Override |
| 929 | public CharSequence getAccessibilityClassName() { |
| 930 | return AbsSeekBar.class.getName(); |
| 931 | } |
| 932 | |
| 933 | /** @hide */ |
| 934 | @Override |
| 935 | public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { |
| 936 | super.onInitializeAccessibilityNodeInfoInternal(info); |
| 937 | |
| 938 | if (isEnabled()) { |
| 939 | final int progress = getProgress(); |
| 940 | if (progress > getMin()) { |
| 941 | info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); |
| 942 | } |
| 943 | if (progress < getMax()) { |
| 944 | info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); |
| 945 | } |
| 946 | } |
| 947 | } |
| 948 | |
| 949 | /** @hide */ |
| 950 | @Override |
| 951 | public boolean performAccessibilityActionInternal(int action, Bundle arguments) { |
| 952 | if (super.performAccessibilityActionInternal(action, arguments)) { |
| 953 | return true; |
| 954 | } |
| 955 | |
| 956 | if (!isEnabled()) { |
| 957 | return false; |
| 958 | } |
| 959 | |
| 960 | switch (action) { |
| 961 | case R.id.accessibilityActionSetProgress: { |
| 962 | if (!canUserSetProgress()) { |
| 963 | return false; |
| 964 | } |
| 965 | if (arguments == null || !arguments.containsKey( |
| 966 | AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) { |
| 967 | return false; |
| 968 | } |
| 969 | float value = arguments.getFloat( |
| 970 | AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE); |
| 971 | return setProgressInternal((int) value, true, true); |
| 972 | } |
| 973 | case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: |
| 974 | case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { |
| 975 | if (!canUserSetProgress()) { |
| 976 | return false; |
| 977 | } |
| 978 | int range = getMax() - getMin(); |
| 979 | int increment = Math.max(1, Math.round((float) range / 20)); |
| 980 | if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) { |
| 981 | increment = -increment; |
| 982 | } |
| 983 | |
| 984 | // Let progress bar handle clamping values. |
| 985 | if (setProgressInternal(getProgress() + increment, true, true)) { |
| 986 | onKeyChange(); |
| 987 | return true; |
| 988 | } |
| 989 | return false; |
| 990 | } |
| 991 | } |
| 992 | return false; |
| 993 | } |
| 994 | |
| 995 | /** |
| 996 | * @return whether user can change progress on the view |
| 997 | */ |
| 998 | boolean canUserSetProgress() { |
| 999 | return !isIndeterminate() && isEnabled(); |
| 1000 | } |
| 1001 | |
| 1002 | @Override |
| 1003 | public void onRtlPropertiesChanged(int layoutDirection) { |
| 1004 | super.onRtlPropertiesChanged(layoutDirection); |
| 1005 | |
| 1006 | final Drawable thumb = mThumb; |
| 1007 | if (thumb != null) { |
| 1008 | setThumbPos(getWidth(), thumb, getScale(), Integer.MIN_VALUE); |
| 1009 | |
| 1010 | // Since we draw translated, the drawable's bounds that it signals |
| 1011 | // for invalidation won't be the actual bounds we want invalidated, |
| 1012 | // so just invalidate this whole view. |
| 1013 | invalidate(); |
| 1014 | } |
| 1015 | } |
| 1016 | } |