275 | * If the thumb is a valid drawable (i.e. not null), half its width will be 276 | * used as the new thumb offset (@see #setThumbOffset(int)). 277 | * 278 | * @param thumb Drawable representing the thumb 279 | * @param which which thumb 280 | */ 281 | public void setThumb(Drawable thumb, WhichThumb which) { 282 | final boolean needUpdate; 283 | 284 | Drawable whichThumb = which == WhichThumb.Start ? mThumbStart : mThumbEnd; 285 | 286 | if (whichThumb != null && thumb != whichThumb) { 287 | whichThumb.setCallback(null); 288 | needUpdate = true; 289 | } else { 290 | needUpdate = false; 291 | } 292 | 293 | if (thumb != null) { 294 | thumb.setCallback(this); 295 | DrawableCompat.setLayoutDirection(thumb, ViewCompat.getLayoutDirection(this)); 296 | mThumbOffset = thumb.getIntrinsicWidth() / 2; 297 | mThumbWidth = thumb.getIntrinsicWidth(); 298 | mThumbHeight = thumb.getIntrinsicHeight(); 299 | 300 | if (needUpdate && 301 | (thumb.getIntrinsicWidth() != whichThumb.getIntrinsicWidth() 302 | || thumb.getIntrinsicHeight() != whichThumb.getIntrinsicHeight())) { 303 | requestLayout(); 304 | } 305 | } 306 | 307 | if (which == WhichThumb.Start) { 308 | mThumbStart = thumb; 309 | } else { 310 | mThumbEnd = thumb; 311 | } 312 | 313 | applyThumbTintInternal(which); 314 | invalidate(); 315 | 316 | if (needUpdate) { 317 | updateThumbAndTrackPos(getWidth(), getHeight()); 318 | if (thumb != null && thumb.isStateful()) { 319 | // Note that if the states are different this won't work. 320 | // For now, let's consider that an app bug. 321 | int[] state = getDrawableState(); 322 | thumb.setState(state); 323 | } 324 | } 325 | } 326 | 327 | public Drawable getThumbStart() { 328 | return mThumbStart; 329 | } 330 | 331 | public Drawable getThumbEnd() { 332 | return mThumbEnd; 333 | } 334 | 335 | public void setThumbTintList(@Nullable ColorStateList tint) { 336 | mThumbTintList = tint; 337 | mHasThumbTint = true; 338 | 339 | applyThumbTint(); 340 | } 341 | 342 | @Nullable 343 | public ColorStateList getThumbTintList() { 344 | return mThumbTintList; 345 | } 346 | 347 | public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) { 348 | mThumbTintMode = tintMode; 349 | mHasThumbTintMode = true; 350 | applyThumbTint(); 351 | } 352 | 353 | @Nullable 354 | public PorterDuff.Mode getThumbTintMode() { 355 | return mThumbTintMode; 356 | } 357 | 358 | private void applyThumbTint() { 359 | applyThumbTintInternal(WhichThumb.Start); 360 | applyThumbTintInternal(WhichThumb.End); 361 | } 362 | 363 | private void applyThumbTintInternal(final WhichThumb which) { 364 | Drawable thumb = which == WhichThumb.Start ? mThumbStart : mThumbEnd; 365 | 366 | if (thumb != null && (mHasThumbTint || mHasThumbTintMode)) { 367 | 368 | if (which == WhichThumb.Start) { 369 | mThumbStart = thumb.mutate(); 370 | thumb = mThumbStart; 371 | } else { 372 | mThumbEnd = thumb.mutate(); 373 | thumb = mThumbEnd; 374 | } 375 | 376 | if (mHasThumbTint) { 377 | DrawableCompat.setTintList(thumb, mThumbTintList); 378 | } 379 | 380 | if (mHasThumbTintMode) { 381 | DrawableCompat.setTintMode(thumb, mThumbTintMode); 382 | } 383 | 384 | if (thumb.isStateful()) { 385 | thumb.setState(getDrawableState()); 386 | } 387 | } 388 | } 389 | 390 | /** 391 | * @see #setThumbOffset(int) 392 | */ 393 | public int getThumbOffset() { 394 | return mThumbOffset; 395 | } 396 | 397 | /** 398 | * Sets the thumb offset that allows the thumb to extend out of the range of 399 | * the track. 400 | * 401 | * @param thumbOffset The offset amount in pixels. 402 | */ 403 | public void setThumbOffset(int thumbOffset) { 404 | mThumbOffset = thumbOffset; 405 | invalidate(); 406 | } 407 | 408 | /** 409 | * Specifies whether the track should be split by the thumb. When true, 410 | * the thumb's optical bounds will be clipped out of the track drawable, 411 | * then the thumb will be drawn into the resulting gap. 412 | * 413 | * @param splitTrack Whether the track should be split by the thumb 414 | */ 415 | public void setSplitTrack(boolean splitTrack) { 416 | mSplitTrack = splitTrack; 417 | invalidate(); 418 | } 419 | 420 | /** 421 | * Returns whether the track should be split by the thumb. 422 | */ 423 | public boolean getSplitTrack() { 424 | return mSplitTrack; 425 | } 426 | 427 | /** 428 | * Assign a new tick mark drawable 429 | * @param tickMark 430 | */ 431 | public void setTickMark(Drawable tickMark) { 432 | if (mTickMark != null) { 433 | mTickMark.setCallback(null); 434 | } 435 | 436 | mTickMark = tickMark; 437 | 438 | if (tickMark != null) { 439 | 440 | setProgressOffset(0); 441 | 442 | tickMark.setCallback(this); 443 | if (tickMark.isStateful()) { 444 | tickMark.setState(getDrawableState()); 445 | } 446 | 447 | final int w = tickMark.getIntrinsicWidth(); 448 | final int h = tickMark.getIntrinsicHeight(); 449 | final int halfW = w >= 0 ? w / 2 : 1; 450 | final int halfH = h >= 0 ? h / 2 : 1; 451 | tickMark.setBounds(-halfW, -halfH, halfW, halfH); 452 | applyTickMarkTint(); 453 | } 454 | invalidate(); 455 | } 456 | 457 | /** 458 | * @return the drawable displayed at each progress position 459 | */ 460 | public Drawable getTickMark() { 461 | return mTickMark; 462 | } 463 | 464 | /** 465 | * Applies a tint to the tick mark drawable. Does not modify the current tint 466 | * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. 467 | *
468 | * Subsequent calls to {@link #setTickMark(Drawable)} will automatically
469 | * mutate the drawable and apply the specified tint and tint mode using
470 | * {@link Drawable#setTintList(ColorStateList)}.
471 | *
472 | * @param tint the tint to apply, may be {@code null} to clear tint
473 | * @attr ref android.R.styleable#SeekBar_tickMarkTint
474 | * @see #getTickMarkTintList()
475 | * @see Drawable#setTintList(ColorStateList)
476 | */
477 | public void setTickMarkTintList(@Nullable ColorStateList tint) {
478 | mTickMarkTintList = tint;
479 | mHasTickMarkTint = true;
480 |
481 | applyTickMarkTint();
482 | }
483 |
484 | /**
485 | * Returns the tint applied to the tick mark drawable, if specified.
486 | *
487 | * @return the tint applied to the tick mark drawable
488 | * @attr ref android.R.styleable#SeekBar_tickMarkTint
489 | * @see #setTickMarkTintList(ColorStateList)
490 | */
491 | @Nullable
492 | public ColorStateList getTickMarkTintList() {
493 | return mTickMarkTintList;
494 | }
495 |
496 | /**
497 | * Specifies the blending mode used to apply the tint specified by
498 | * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The
499 | * default mode is {@link PorterDuff.Mode#SRC_IN}.
500 | *
501 | * @param tintMode the blending mode used to apply the tint, may be
502 | * {@code null} to clear tint
503 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
504 | * @see #getTickMarkTintMode()
505 | * @see Drawable#setTintMode(PorterDuff.Mode)
506 | */
507 | public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
508 | mTickMarkTintMode = tintMode;
509 | mHasTickMarkTintMode = true;
510 |
511 | applyTickMarkTint();
512 | }
513 |
514 | /**
515 | * Returns the blending mode used to apply the tint to the tick mark drawable,
516 | * if specified.
517 | *
518 | * @return the blending mode used to apply the tint to the tick mark drawable
519 | * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
520 | * @see #setTickMarkTintMode(PorterDuff.Mode)
521 | */
522 | @Nullable
523 | public PorterDuff.Mode getTickMarkTintMode() {
524 | return mTickMarkTintMode;
525 | }
526 |
527 | protected void applyTickMarkTint() {
528 | if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) {
529 | mTickMark = DrawableCompat.wrap(mTickMark.mutate());
530 |
531 | if (mHasTickMarkTint) {
532 | DrawableCompat.setTintList(mTickMark, mTickMarkTintList);
533 | }
534 |
535 | if (mHasTickMarkTintMode) {
536 | DrawableCompat.setTintMode(mTickMark, mTickMarkTintMode);
537 | }
538 |
539 | // The drawable (or one of its children) may not have been
540 | // stateful before applying the tint, so let's try again.
541 | if (mTickMark.isStateful()) {
542 | mTickMark.setState(getDrawableState());
543 | }
544 | }
545 | }
546 |
547 | public void setKeyProgressIncrement(int increment) {
548 | mKeyProgressIncrement = increment < 0 ? -increment : increment;
549 | }
550 |
551 | public float getKeyProgressIncrement() {
552 | return mKeyProgressIncrement;
553 | }
554 |
555 | public synchronized void setMax(int max) {
556 | super.setMax(max);
557 |
558 | if ((mKeyProgressIncrement == 0) || (getMax() / mKeyProgressIncrement > 20)) {
559 | setKeyProgressIncrement(getMax() / 20);
560 | }
561 | }
562 |
563 | @Override
564 | protected boolean verifyDrawable(@NonNull Drawable who) {
565 | return who == mThumbStart || who == mThumbEnd || who == mTickMark || super.verifyDrawable(who);
566 | }
567 |
568 | @Override
569 | public void jumpDrawablesToCurrentState() {
570 | super.jumpDrawablesToCurrentState();
571 |
572 | if (mThumbStart != null) {
573 | mThumbStart.jumpToCurrentState();
574 | }
575 |
576 | if (mThumbEnd != null) {
577 | mThumbEnd.jumpToCurrentState();
578 | }
579 |
580 | if (mTickMark != null) {
581 | mTickMark.jumpToCurrentState();
582 | }
583 | }
584 |
585 | @Override
586 | protected void drawableStateChanged() {
587 | super.drawableStateChanged();
588 |
589 | logger.info("drawableStateChanged(%s)", mWhichThumb);
590 |
591 | final Drawable progressDrawable = getProgressDrawable();
592 | if (progressDrawable != null && mDisabledAlpha < 1.0f) {
593 | progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
594 | }
595 |
596 | if (mWhichThumb != WhichThumb.None) {
597 | Drawable thumb = mWhichThumb == WhichThumb.Start ? mThumbStart : mThumbEnd;
598 | setDrawableState(thumb, getDrawableState());
599 | } else {
600 | setDrawableState(mThumbStart, getDrawableState());
601 | setDrawableState(mThumbEnd, getDrawableState());
602 | }
603 |
604 | final Drawable tickMark = mTickMark;
605 | if (tickMark != null && tickMark.isStateful()
606 | && tickMark.setState(getDrawableState())) {
607 | invalidateDrawable(tickMark);
608 | }
609 | }
610 |
611 | protected void setDrawableState(final Drawable drawable, final int[] drawableState) {
612 | if (null != drawable && drawable.isStateful() && drawable.setState(drawableState)) {
613 | invalidateDrawable(drawable);
614 | }
615 | }
616 |
617 | @Override
618 | public void drawableHotspotChanged(float x, float y) {
619 | super.drawableHotspotChanged(x, y);
620 |
621 | if (mThumbStart != null) {
622 | logger.verbose("setHotspot(mThumbStart, %.2f, %.2f)", x, y);
623 | DrawableCompat.setHotspot(mThumbStart, x, y);
624 | }
625 |
626 | if (mThumbEnd != null) {
627 | logger.verbose("setHotspot(mThumbEnd, %.2f, %.2f)", x, y);
628 | DrawableCompat.setHotspot(mThumbEnd, x, y);
629 | }
630 | }
631 |
632 | @Override
633 | public void onVisualProgressChanged(int id, float scaleStart, float scaleEnd) {
634 | super.onVisualProgressChanged(id, scaleStart, scaleEnd);
635 |
636 | if (id == android.R.id.progress) {
637 | if (mThumbStart != null && mThumbEnd != null) {
638 | setThumbPos(getWidth(), mThumbStart, scaleStart, WhichThumb.Start, Integer.MIN_VALUE);
639 | setThumbPos(getWidth(), mThumbEnd, scaleEnd, WhichThumb.End, Integer.MIN_VALUE);
640 | invalidate();
641 | }
642 | }
643 | }
644 |
645 | @Override
646 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
647 | super.onSizeChanged(w, h, oldw, oldh);
648 |
649 | updateThumbAndTrackPos(w, h);
650 | }
651 |
652 | private void updateThumbAndTrackPos(int w, int h) {
653 | final int paddedHeight = h - mPaddingTop - mPaddingBottom;
654 | final Drawable track = getCurrentDrawable();
655 | final Drawable thumb = mThumbStart;
656 |
657 | // The max height does not incorporate padding, whereas the height
658 | // parameter does.
659 | final int trackHeight = Math.min(mMaxHeight, paddedHeight);
660 | final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
661 |
662 | // Apply offset to whichever item is taller.
663 | final int trackOffset;
664 | final int thumbOffset;
665 | if (thumbHeight > trackHeight) {
666 | final int offsetHeight = (paddedHeight - thumbHeight) / 2;
667 | trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2;
668 | thumbOffset = offsetHeight;
669 | } else {
670 | final int offsetHeight = (paddedHeight - trackHeight) / 2;
671 | trackOffset = offsetHeight;
672 | thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2;
673 | }
674 |
675 | if (track != null) {
676 | final int trackWidth = w - mPaddingRight - mPaddingLeft;
677 | track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight);
678 | }
679 |
680 | if (mThumbStart != null && mThumbEnd != null) {
681 | setThumbPos(w, mThumbStart, getScaleStart(), WhichThumb.Start, thumbOffset);
682 | setThumbPos(w, mThumbEnd, getScaleEnd(), WhichThumb.End, thumbOffset);
683 | }
684 |
685 | final Drawable background = getBackground();
686 |
687 | if (background != null && thumb != null) {
688 | final Rect bounds = thumb.getBounds();
689 | background.setBounds(bounds);
690 | logger.verbose("setHotspot(background, %d, %d)", bounds.centerX(), bounds.centerY());
691 | DrawableCompat.setHotspotBounds(
692 | background, bounds.left, bounds.top, bounds.right, bounds.bottom);
693 |
694 | }
695 | }
696 |
697 | private float getScaleStart() {
698 | final float max = getMax();
699 | return max > 0 ? getProgressStart() / max : 0;
700 | }
701 |
702 | private float getScaleEnd() {
703 | final float max = getMax();
704 | return max > 0 ? getProgressEnd() / max : 0;
705 | }
706 |
707 | /**
708 | * Updates the thumb drawable bounds.
709 | *
710 | * @param w Width of the view, including padding
711 | * @param thumb Drawable used for the thumb
712 | * @param scale Current progress between 0 and 1
713 | * @param offset Vertical offset for centering. If set to Integer.MIN_VALUE, the current offset will be used.
714 | */
715 | private void setThumbPos(int w, Drawable thumb, float scale, WhichThumb which, int offset) {
716 | logger.info("setThumbPos(%d, %g, %s, %d)", w, scale, which, offset);
717 |
718 | int available = (w - mPaddingLeft - mPaddingRight) - getProgressOffset();
719 | available -= mThumbWidth;
720 |
721 | // The extra space for the thumb to move on the track
722 | available += mThumbOffset * 2;
723 |
724 | final int thumbPos = (int) (scale * available + 0.5f);
725 |
726 | final int top, bottom;
727 | if (offset == Integer.MIN_VALUE) {
728 | final Rect oldBounds = thumb.getBounds();
729 | top = oldBounds.top;
730 | bottom = oldBounds.bottom;
731 | } else {
732 | top = offset;
733 | bottom = offset + mThumbHeight;
734 | }
735 |
736 | int left = thumbPos;
737 | int right = left + mThumbWidth;
738 |
739 | if (which == WhichThumb.End) {
740 | left += getProgressOffset();
741 | right += getProgressOffset();
742 | }
743 |
744 | final Drawable background = getBackground();
745 |
746 | if (background != null && which == mWhichThumb) {
747 | final int offsetX = mPaddingLeft - mThumbOffset;
748 | final int offsetY = mPaddingTop;
749 |
750 | background.setBounds(
751 | left + offsetX,
752 | top + offsetY,
753 | right + offsetX,
754 | bottom + offsetY
755 | );
756 |
757 | DrawableCompat.setHotspotBounds(
758 | background,
759 | left + offsetX,
760 | top + offsetY,
761 | right + offsetX,
762 | bottom + offsetY
763 | );
764 | }
765 |
766 | thumb.setBounds(left, top, right, bottom);
767 | }
768 |
769 | @Override
770 | protected synchronized void onDraw(Canvas canvas) {
771 | super.onDraw(canvas);
772 | drawThumb(canvas);
773 | }
774 |
775 | @Override
776 | void drawTrack(Canvas canvas) {
777 | if (mThumbStart != null && mSplitTrack) {
778 | final Rect tempRect = mTempRect1;
779 |
780 | mThumbStart.copyBounds(tempRect);
781 | tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop);
782 | tempRect.left += mThumbClipInset;
783 | tempRect.right -= mThumbClipInset;
784 |
785 | final int saveCount = canvas.save();
786 | canvas.clipRect(tempRect, Op.DIFFERENCE);
787 |
788 | mThumbEnd.copyBounds(tempRect);
789 | tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop);
790 | tempRect.left += mThumbClipInset;
791 | tempRect.right -= mThumbClipInset;
792 |
793 | canvas.clipRect(tempRect, Op.DIFFERENCE);
794 |
795 | super.drawTrack(canvas);
796 | drawTickMarks(canvas);
797 | canvas.restoreToCount(saveCount);
798 |
799 | } else {
800 | super.drawTrack(canvas);
801 | drawTickMarks(canvas);
802 | }
803 | }
804 |
805 | void drawTickMarks(Canvas canvas) {
806 | if (mTickMark != null) {
807 | final float count = getMax();
808 | if (count > 1) {
809 | final float spacing = (getWidth() - (mPaddingLeft + mPaddingRight)) / (count / mStepSize);
810 | final int saveCount = canvas.save();
811 | canvas.translate(mPaddingLeft, getHeight() / 2f);
812 | for (int i = 0; i <= count; i++) {
813 | mTickMark.draw(canvas);
814 | canvas.translate(spacing, 0);
815 | }
816 | canvas.restoreToCount(saveCount);
817 | }
818 | }
819 | }
820 |
821 | void drawThumb(Canvas canvas) {
822 | if (mThumbStart != null && mThumbEnd != null) {
823 | final int saveCount = canvas.save();
824 | canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
825 | mThumbStart.draw(canvas);
826 | mThumbEnd.draw(canvas);
827 | canvas.restoreToCount(saveCount);
828 | }
829 | }
830 |
831 | @Override
832 | protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
833 | Drawable d = getCurrentDrawable();
834 |
835 | int thumbHeight = mThumbStart == null ? 0 : mThumbStart.getIntrinsicHeight();
836 | int dw = 0;
837 | int dh = 0;
838 | if (d != null) {
839 | dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
840 | dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
841 | dh = Math.max(thumbHeight, dh);
842 | }
843 | dw += mPaddingLeft + mPaddingRight;
844 | dh += mPaddingTop + mPaddingBottom;
845 |
846 | setMeasuredDimension(
847 | resolveSizeAndState(dw, widthMeasureSpec, 0),
848 | resolveSizeAndState(dh, heightMeasureSpec, 0)
849 | );
850 | }
851 |
852 | @SuppressLint ("ClickableViewAccessibility")
853 | @Override
854 | public boolean onTouchEvent(MotionEvent event) {
855 | if (!mIsUserSeekable || !isEnabled()) {
856 | return false;
857 | }
858 |
859 | switch (event.getAction()) {
860 | case MotionEvent.ACTION_DOWN:
861 | logger.info("ACTION_DOWN");
862 | if (SephirothViewCompat.isInScrollingContainer(this)) {
863 | logger.warn("isInScrollContainer");
864 | mTouchDownX = event.getX();
865 | } else {
866 | startDrag(event);
867 | }
868 | break;
869 |
870 | case MotionEvent.ACTION_MOVE:
871 | if (mIsDragging) {
872 | trackTouchEvent(event);
873 | } else {
874 | final float x = event.getX();
875 | if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
876 | startDrag(event);
877 | }
878 | }
879 | break;
880 |
881 | case MotionEvent.ACTION_UP:
882 | if (mIsDragging) {
883 | trackTouchEvent(event);
884 | onStopTrackingTouch();
885 | setPressed(false);
886 | } else {
887 | // Touch up when we never crossed the touch slop threshold should
888 | // be interpreted as a tap-seek to that location.
889 | onStartTrackingTouch();
890 | trackTouchEvent(event);
891 | onStopTrackingTouch();
892 | performClick();
893 | }
894 | // ProgressBar doesn't know to repaint the thumb drawable
895 | // in its inactive state when the touch stops (because the
896 | // value has not apparently changed)
897 | invalidate();
898 | break;
899 |
900 | case MotionEvent.ACTION_CANCEL:
901 | if (mIsDragging) {
902 | onStopTrackingTouch();
903 | setPressed(false);
904 | }
905 | invalidate(); // see above explanation
906 | break;
907 | }
908 | return true;
909 | }
910 |
911 | private WhichThumb getNearestThumb(float x, float y) {
912 |
913 | mThumbStart.copyBounds(mTempRect1);
914 | mTempRect1.inset(mTempRect1.width() / 4, mTempRect1.height() / 4);
915 |
916 | mThumbEnd.copyBounds(mTempRect2);
917 | mTempRect2.inset(mTempRect2.width() / 4, mTempRect2.height() / 4);
918 |
919 | float diff1 = Math.abs((x) - mTempRect1.centerX());
920 | float diff2 = Math.abs((x) - mTempRect2.centerX());
921 |
922 | if (mTempRect1.contains((int) x, (int) y)) {
923 | return WhichThumb.Start;
924 | }
925 | if (mTempRect2.contains((int) x, (int) y)) {
926 | return WhichThumb.End;
927 | }
928 |
929 | return diff1 < diff2 ? WhichThumb.Start : WhichThumb.End;
930 | }
931 |
932 | private void startDrag(MotionEvent event) {
933 | logger.info("startDrag");
934 |
935 | if (null == mThumbStart || null == mThumbEnd) {
936 | logger.error("missing one of the thumbs!");
937 | return;
938 | }
939 |
940 | mWhichThumb = getNearestThumb(event.getX() - (mPaddingLeft - mThumbOffset), event.getY());
941 | logger.verbose("mWhichThumb: %s", mWhichThumb);
942 |
943 | setPressed(true);
944 |
945 | if (mWhichThumb != WhichThumb.None) {
946 | final Drawable thumb = mWhichThumb == WhichThumb.Start ? mThumbStart : mThumbEnd;
947 |
948 | if (thumb != null) {
949 | final float scale = mWhichThumb == WhichThumb.Start ? getScaleStart() : getScaleEnd();
950 | setThumbPos(getWidth(), thumb, scale, mWhichThumb, Integer.MIN_VALUE);
951 | invalidate(thumb.getBounds());
952 | }
953 | }
954 |
955 | onStartTrackingTouch();
956 | trackTouchEvent(event);
957 | attemptClaimDrag();
958 | }
959 |
960 | @Override
961 | public void setPressed(final boolean pressed) {
962 | logger.debug("setPressed(%b, %s)", pressed, mWhichThumb);
963 |
964 | if (!pressed) {
965 | mWhichThumb = WhichThumb.None;
966 | }
967 |
968 | super.setPressed(pressed);
969 | }
970 |
971 | private void setHotspot(float x, float y) { }
972 |
973 | private void trackTouchEvent(MotionEvent event) {
974 | if (null == mThumbStart || null == mThumbEnd) {
975 | return;
976 | }
977 |
978 | float x = event.getX();
979 | float y = event.getY();
980 | final int width = getWidth();
981 |
982 | if (mWhichThumb == WhichThumb.End) {
983 | x -= getProgressOffset();
984 | }
985 |
986 | final int thumbWidth = mThumbStart.getIntrinsicWidth();
987 | final int availableWidth = width - mPaddingLeft - mPaddingRight - getProgressOffset() - thumbWidth + mThumbOffset * 2;
988 |
989 | x -= thumbWidth / 2f;
990 | x += mThumbOffset;
991 |
992 | final float scale;
993 | float progress = 0.0f;
994 |
995 | if (x < mPaddingLeft) {
996 | scale = 0.0f;
997 | } else if (x > width - mPaddingRight) {
998 | scale = 1.0f;
999 | } else {
1000 | scale = (x - mPaddingLeft) / (float) availableWidth;
1001 | progress = mTouchProgressOffset;
1002 | }
1003 |
1004 | final float max = getMax();
1005 | progress += scale * max;
1006 |
1007 | setHotspot(x, y);
1008 |
1009 | if (mWhichThumb == WhichThumb.Start) {
1010 | progress = MathUtils.constrain(progress, 0, getProgressStartMaxValue());
1011 | setProgressInternal(Math.round(progress), getProgressEnd(), true, false);
1012 | } else if (mWhichThumb == WhichThumb.End) {
1013 | progress = MathUtils.constrain(progress, getProgressEndMinValue(), getMax());
1014 | setProgressInternal(getProgressStart(), Math.round(progress), true, false);
1015 | }
1016 | }
1017 |
1018 | @Override
1019 | synchronized boolean setProgressInternal(
1020 | int startValue, int endValue, final boolean fromUser, final boolean animate) {
1021 |
1022 | if (mStepSize > 1) {
1023 | final int remainderStart = startValue % mStepSize;
1024 |
1025 | if (remainderStart > 0) {
1026 | if ((float) remainderStart / mStepSize > 0.5) {
1027 | // value + (step-(value%step))
1028 | startValue = startValue + (mStepSize - remainderStart);
1029 | } else {
1030 | // value - (value%step)
1031 | startValue = startValue - remainderStart;
1032 | }
1033 | }
1034 |
1035 | int remainderEnd = endValue % mStepSize;
1036 |
1037 | if (remainderEnd > 0) {
1038 | if ((float) remainderEnd / mStepSize > 0.5) {
1039 | endValue = endValue + (mStepSize - remainderEnd);
1040 | } else {
1041 | endValue = endValue - remainderEnd;
1042 | }
1043 | }
1044 | }
1045 |
1046 | if (mProgressStartMaxValue != -1 || mProgressEndMinValue != -1) {
1047 | if (mProgressStartMaxValue != -1) {
1048 | startValue = MathUtils.constrain(startValue, 0, mProgressStartMaxValue);
1049 |
1050 | }
1051 | if (mProgressEndMinValue != -1) {
1052 | endValue = MathUtils.constrain(endValue, mProgressEndMinValue, getMax());
1053 | }
1054 | } else if (mMinMaxStepSize != 0) {
1055 | if (endValue - startValue < mMinMaxStepSize) {
1056 | endValue = startValue + getMinMapStepSize();
1057 | }
1058 | }
1059 |
1060 | // startValue = MathUtils.constrain(startValue, 0, getProgressStartMaxValue());
1061 | // endValue = MathUtils.constrain(endValue, getProgressEndMinValue(), getMax());
1062 |
1063 | return super.setProgressInternal(startValue, endValue, fromUser, animate);
1064 | }
1065 |
1066 | private void attemptClaimDrag() {
1067 | if (getParent() != null) {
1068 | getParent().requestDisallowInterceptTouchEvent(true);
1069 | }
1070 | }
1071 |
1072 | void onStartTrackingTouch() {
1073 | mIsDragging = true;
1074 | if (mOnRangeSeekBarChangeListener != null) {
1075 | mOnRangeSeekBarChangeListener.onStartTrackingTouch(this);
1076 | }
1077 | }
1078 |
1079 | void onStopTrackingTouch() {
1080 | mIsDragging = false;
1081 | if (mOnRangeSeekBarChangeListener != null) {
1082 | mOnRangeSeekBarChangeListener.onStopTrackingTouch(this);
1083 | }
1084 | }
1085 |
1086 | void onKeyChange() {
1087 | }
1088 |
1089 | @Override
1090 | public boolean onKeyDown(int keyCode, KeyEvent event) {
1091 | if (isEnabled()) {
1092 | int increment = mKeyProgressIncrement;
1093 | switch (keyCode) {
1094 | case KeyEvent.KEYCODE_DPAD_LEFT:
1095 | case KeyEvent.KEYCODE_MINUS:
1096 | increment = -increment;
1097 | // fallthrough
1098 | case KeyEvent.KEYCODE_DPAD_RIGHT:
1099 | case KeyEvent.KEYCODE_PLUS:
1100 | case KeyEvent.KEYCODE_EQUALS:
1101 | if (setProgressInternal(getProgressStart() - increment, getProgressEnd() + increment, true, true)) {
1102 | onKeyChange();
1103 | return true;
1104 | }
1105 | break;
1106 | }
1107 | }
1108 |
1109 | return super.onKeyDown(keyCode, event);
1110 | }
1111 |
1112 | @Override
1113 | public CharSequence getAccessibilityClassName() {
1114 | return RangeSeekBar.class.getName();
1115 | }
1116 |
1117 | @Override
1118 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1119 | super.onInitializeAccessibilityNodeInfo(info);
1120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1121 | info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
1122 | }
1123 | }
1124 |
1125 | @Override
1126 | public boolean performAccessibilityAction(int action, Bundle arguments) {
1127 | return super.performAccessibilityAction(action, arguments);
1128 | // TODO: to be implemented
1129 | }
1130 | }
1131 |
--------------------------------------------------------------------------------
/rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/SephirothViewCompat.java:
--------------------------------------------------------------------------------
1 | package it.sephiroth.android.library.rangeseekbar;
2 |
3 | import android.view.View;
4 | import android.view.ViewGroup;
5 | import android.view.ViewParent;
6 |
7 | /**
8 | * Created by sephiroth on 2/9/17.
9 | */
10 |
11 | class SephirothViewCompat {
12 | static boolean isInScrollingContainer(final View view) {
13 | ViewParent p = view.getParent();
14 | while (p instanceof ViewGroup) {
15 | if (((ViewGroup) p).shouldDelayChildPressedState()) {
16 | return true;
17 | }
18 | p = p.getParent();
19 | }
20 | return false;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/rangeseekbar-library/src/main/java/it/sephiroth/android/library/rangeseekbar/ThemeUtils.java:
--------------------------------------------------------------------------------
1 | package it.sephiroth.android.library.rangeseekbar;
2 |
3 | import android.content.Context;
4 | import android.content.res.ColorStateList;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Color;
7 | import android.util.TypedValue;
8 |
9 | import androidx.core.graphics.ColorUtils;
10 |
11 | /**
12 | * Created by crugnola on 2/10/17.
13 | * RangeSeekBar
14 | */
15 |
16 | public class ThemeUtils {
17 | private static final ThreadLocal