53 | * Created by woxingxiao on 2016-10-27.
54 | */
55 | public class BubbleSeekBar extends View {
56 |
57 | static final int NONE = -1;
58 |
59 | @IntDef({NONE, SIDES, BOTTOM_SIDES, BELOW_SECTION_MARK})
60 | @Retention(RetentionPolicy.SOURCE)
61 | public @interface TextPosition {
62 | int SIDES = 0, BOTTOM_SIDES = 1, BELOW_SECTION_MARK = 2;
63 | }
64 |
65 | private float mMin; // min
66 | private float mMax; // max
67 | private float mProgress; // real time value
68 | private boolean isFloatType; // support for float type output
69 | private int mTrackSize; // height of right-track(on the right of thumb)
70 | private int mSecondTrackSize; // height of left-track(on the left of thumb)
71 | private int mThumbRadius; // radius of thumb
72 | private int mThumbRadiusOnDragging; // radius of thumb when be dragging
73 | private int mTrackColor; // color of right-track
74 | private int mSecondTrackColor; // color of left-track
75 | private int mThumbColor; // color of thumb
76 | private int mSectionCount; // shares of whole progress(max - min)
77 | private boolean isShowSectionMark; // show demarcation points or not
78 | private boolean isAutoAdjustSectionMark; // auto scroll to the nearest section_mark or not
79 | private boolean isShowSectionText; // show section-text or not
80 | private int mSectionTextSize; // text size of section-text
81 | private int mSectionTextColor; // text color of section-text
82 | @TextPosition
83 | private int mSectionTextPosition = NONE; // text position of section-text relative to track
84 | private int mSectionTextInterval; // the interval of two section-text
85 | private boolean isShowThumbText; // show real time progress-text under thumb or not
86 | private int mThumbTextSize; // text size of progress-text
87 | private int mThumbTextColor; // text color of progress-text
88 | private boolean isShowProgressInFloat; // show bubble-progress in float or not
89 | private boolean isTouchToSeek; // touch anywhere on track to quickly seek
90 | private boolean isSeekStepSection; // seek one step by one section, the progress is discrete
91 | private boolean isSeekBySection; // seek by section, the progress may not be linear
92 | private long mAnimDuration; // duration of animation
93 | private boolean isAlwaysShowBubble; // bubble shows all time
94 | private long mAlwaysShowBubbleDelay; // the delay duration before bubble shows all the time
95 | private boolean isHideBubble; // hide bubble
96 | private boolean isRtl; // right to left
97 |
98 | private int mBubbleColor;// color of bubble
99 | private int mBubbleTextSize; // text size of bubble-progress
100 | private int mBubbleTextColor; // text color of bubble-progress
101 |
102 | private float mDelta; // max - min
103 | private float mSectionValue; // (mDelta / mSectionCount)
104 | private float mThumbCenterX; // X coordinate of thumb's center
105 | private float mTrackLength; // pixel length of whole track
106 | private float mSectionOffset; // pixel length of one section
107 | private boolean isThumbOnDragging; // is thumb on dragging or not
108 | private int mTextSpace; // space between text and track
109 | private boolean triggerBubbleShowing;
110 | private SparseArray
453 | * The BubbleView is added to Window by the WindowManager, so the only connection between
454 | * BubbleView and SeekBar is their origin raw coordinates on the screen.
455 | *
456 | * It's easy to compute the coordinates(mBubbleCenterRawSolidX, mBubbleCenterRawSolidY) of point
457 | * when the Progress equals the Min. Then compute the pixel length increment when the Progress is
458 | * changing, the result is mBubbleCenterRawX. At last the WindowManager calls updateViewLayout()
459 | * to update the LayoutParameter.x of the BubbleView.
460 | *
461 | * 气泡BubbleView实际是通过WindowManager动态添加的一个视图,因此与SeekBar唯一的位置联系就是它们在屏幕上的
462 | * 绝对坐标。
463 | * 先计算进度mProgress为mMin时BubbleView的中心坐标(mBubbleCenterRawSolidX,mBubbleCenterRawSolidY),
464 | * 然后根据进度来增量计算横坐标mBubbleCenterRawX,再动态设置LayoutParameter.x,就实现了气泡跟随滑动移动。
465 | */
466 | private void locatePositionInWindow() {
467 | getLocationInWindow(mPoint);
468 |
469 | ViewParent parent = getParent();
470 | if (parent instanceof View && ((View) parent).getMeasuredWidth() > 0) {
471 | mPoint[0] %= ((View) parent).getMeasuredWidth();
472 | }
473 |
474 | if (isRtl) {
475 | mBubbleCenterRawSolidX = mPoint[0] + mRight - mBubbleView.getMeasuredWidth() / 2f;
476 | } else {
477 | mBubbleCenterRawSolidX = mPoint[0] + mLeft - mBubbleView.getMeasuredWidth() / 2f;
478 | }
479 | mBubbleCenterRawX = calculateCenterRawXofBubbleView();
480 | mBubbleCenterRawSolidY = mPoint[1] - mBubbleView.getMeasuredHeight();
481 | mBubbleCenterRawSolidY -= dp2px(24);
482 | if (BubbleUtils.isMIUI()) {
483 | mBubbleCenterRawSolidY -= dp2px(4);
484 | }
485 |
486 | Context context = getContext();
487 | if (context instanceof Activity) {
488 | Window window = ((Activity) context).getWindow();
489 | if (window != null) {
490 | int flags = window.getAttributes().flags;
491 | if ((flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0) {
492 | Resources res = Resources.getSystem();
493 | int id = res.getIdentifier("status_bar_height", "dimen", "android");
494 | mBubbleCenterRawSolidY += res.getDimensionPixelSize(id);
495 | }
496 | }
497 | }
498 | }
499 |
500 | @Override
501 | protected void onDraw(Canvas canvas) {
502 | super.onDraw(canvas);
503 |
504 | float xLeft = getPaddingLeft();
505 | float xRight = getMeasuredWidth() - getPaddingRight();
506 | float yTop = getPaddingTop() + mThumbRadiusOnDragging;
507 |
508 | // draw sectionText SIDES or BOTTOM_SIDES
509 | if (isShowSectionText) {
510 | mPaint.setColor(mSectionTextColor);
511 | mPaint.setTextSize(mSectionTextSize);
512 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
513 |
514 | if (mSectionTextPosition == TextPosition.SIDES) {
515 | float y_ = yTop + mRectText.height() / 2f;
516 |
517 | String text = mSectionTextArray.get(0);
518 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
519 | canvas.drawText(text, xLeft + mRectText.width() / 2f, y_, mPaint);
520 | xLeft += mRectText.width() + mTextSpace;
521 |
522 | text = mSectionTextArray.get(mSectionCount);
523 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
524 | canvas.drawText(text, xRight - (mRectText.width() + 0.5f) / 2f, y_, mPaint);
525 | xRight -= (mRectText.width() + mTextSpace);
526 |
527 | } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) {
528 | float y_ = yTop + mThumbRadiusOnDragging + mTextSpace;
529 |
530 | String text = mSectionTextArray.get(0);
531 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
532 | y_ += mRectText.height();
533 | xLeft = mLeft;
534 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
535 | canvas.drawText(text, xLeft, y_, mPaint);
536 | }
537 |
538 | text = mSectionTextArray.get(mSectionCount);
539 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
540 | xRight = mRight;
541 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
542 | canvas.drawText(text, xRight, y_, mPaint);
543 | }
544 | }
545 | } else if (isShowThumbText && mSectionTextPosition == NONE) {
546 | xLeft = mLeft;
547 | xRight = mRight;
548 | }
549 |
550 | if ((!isShowSectionText && !isShowThumbText) || mSectionTextPosition == TextPosition.SIDES) {
551 | xLeft += mThumbRadiusOnDragging;
552 | xRight -= mThumbRadiusOnDragging;
553 | }
554 |
555 | boolean isShowTextBelowSectionMark = isShowSectionText && mSectionTextPosition ==
556 | TextPosition.BELOW_SECTION_MARK;
557 |
558 | // draw sectionMark & sectionText BELOW_SECTION_MARK
559 | if (isShowTextBelowSectionMark || isShowSectionMark) {
560 | mPaint.setTextSize(mSectionTextSize);
561 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
562 |
563 | float x_;
564 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
565 | float r = (mThumbRadiusOnDragging - dp2px(2)) / 2f;
566 | float junction; // where secondTrack meets firstTrack
567 | if (isRtl) {
568 | junction = mRight - mTrackLength / mDelta * Math.abs(mProgress - mMin);
569 | } else {
570 | junction = mLeft + mTrackLength / mDelta * Math.abs(mProgress - mMin);
571 | }
572 |
573 | for (int i = 0; i <= mSectionCount; i++) {
574 | x_ = xLeft + i * mSectionOffset;
575 | if (isRtl) {
576 | mPaint.setColor(x_ <= junction ? mTrackColor : mSecondTrackColor);
577 | } else {
578 | mPaint.setColor(x_ <= junction ? mSecondTrackColor : mTrackColor);
579 | }
580 | // sectionMark
581 | canvas.drawCircle(x_, yTop, r, mPaint);
582 |
583 | // sectionText belows section
584 | if (isShowTextBelowSectionMark) {
585 | mPaint.setColor(mSectionTextColor);
586 | if (mSectionTextArray.get(i, null) != null) {
587 | canvas.drawText(mSectionTextArray.get(i), x_, y_, mPaint);
588 | }
589 | }
590 | }
591 | }
592 |
593 | if (!isThumbOnDragging || isAlwaysShowBubble) {
594 | if (isRtl) {
595 | mThumbCenterX = xRight - mTrackLength / mDelta * (mProgress - mMin);
596 | } else {
597 | mThumbCenterX = xLeft + mTrackLength / mDelta * (mProgress - mMin);
598 | }
599 | }
600 |
601 | // draw thumbText
602 | if (isShowThumbText && !isThumbOnDragging && isTouchToSeekAnimEnd) {
603 | mPaint.setColor(mThumbTextColor);
604 | mPaint.setTextSize(mThumbTextSize);
605 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
606 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
607 |
608 | if (isFloatType || (isShowProgressInFloat && mSectionTextPosition == TextPosition.BOTTOM_SIDES &&
609 | mProgress != mMin && mProgress != mMax)) {
610 | canvas.drawText(String.valueOf(getProgressFloat()), mThumbCenterX, y_, mPaint);
611 | } else {
612 | canvas.drawText(String.valueOf(getProgress()), mThumbCenterX, y_, mPaint);
613 | }
614 | }
615 |
616 | // draw track
617 | mPaint.setColor(mSecondTrackColor);
618 | mPaint.setStrokeWidth(mSecondTrackSize);
619 | if (isRtl) {
620 | canvas.drawLine(xRight, yTop, mThumbCenterX, yTop, mPaint);
621 | } else {
622 | canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint);
623 | }
624 |
625 | // draw second track
626 | mPaint.setColor(mTrackColor);
627 | mPaint.setStrokeWidth(mTrackSize);
628 | if (isRtl) {
629 | canvas.drawLine(mThumbCenterX, yTop, xLeft, yTop, mPaint);
630 | } else {
631 | canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint);
632 | }
633 |
634 | // draw thumb
635 | mPaint.setColor(mThumbColor);
636 | canvas.drawCircle(mThumbCenterX, yTop, isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius, mPaint);
637 | }
638 |
639 | @Override
640 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
641 | super.onSizeChanged(w, h, oldw, oldh);
642 |
643 | post(new Runnable() {
644 | @Override
645 | public void run() {
646 | requestLayout();
647 | }
648 | });
649 | }
650 |
651 | @Override
652 | protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
653 | if (isHideBubble || !isAlwaysShowBubble)
654 | return;
655 |
656 | if (visibility != VISIBLE) {
657 | hideBubble();
658 | } else {
659 | if (triggerBubbleShowing) {
660 | showBubble();
661 | }
662 | }
663 | super.onVisibilityChanged(changedView, visibility);
664 | }
665 |
666 | @Override
667 | protected void onDetachedFromWindow() {
668 | hideBubble();
669 | super.onDetachedFromWindow();
670 | }
671 |
672 | @Override
673 | public boolean performClick() {
674 | return super.performClick();
675 | }
676 |
677 | float dx;
678 |
679 | @Override
680 | public boolean onTouchEvent(MotionEvent event) {
681 | switch (event.getActionMasked()) {
682 | case MotionEvent.ACTION_DOWN:
683 | performClick();
684 | getParent().requestDisallowInterceptTouchEvent(true);
685 |
686 | isThumbOnDragging = isThumbTouched(event);
687 | if (isThumbOnDragging) {
688 | if (isSeekBySection && !triggerSeekBySection) {
689 | triggerSeekBySection = true;
690 | }
691 | if (isAlwaysShowBubble && !triggerBubbleShowing) {
692 | triggerBubbleShowing = true;
693 | }
694 | if (!isHideBubble) {
695 | showBubble();
696 | }
697 |
698 | invalidate();
699 | } else if (isTouchToSeek && isTrackTouched(event)) {
700 | isThumbOnDragging = true;
701 | if (isSeekBySection && !triggerSeekBySection) {
702 | triggerSeekBySection = true;
703 | }
704 | if (isAlwaysShowBubble) {
705 | hideBubble();
706 | triggerBubbleShowing = true;
707 | }
708 |
709 | if (isSeekStepSection) {
710 | mThumbCenterX = mPreThumbCenterX = calThumbCxWhenSeekStepSection(event.getX());
711 | } else {
712 | mThumbCenterX = event.getX();
713 | if (mThumbCenterX < mLeft) {
714 | mThumbCenterX = mLeft;
715 | }
716 | if (mThumbCenterX > mRight) {
717 | mThumbCenterX = mRight;
718 | }
719 | }
720 |
721 | mProgress = calculateProgress();
722 |
723 | if (!isHideBubble) {
724 | mBubbleCenterRawX = calculateCenterRawXofBubbleView();
725 | showBubble();
726 | }
727 |
728 | invalidate();
729 | }
730 |
731 | dx = mThumbCenterX - event.getX();
732 |
733 | break;
734 | case MotionEvent.ACTION_MOVE:
735 | if (isThumbOnDragging) {
736 | boolean flag = true;
737 |
738 | if (isSeekStepSection) {
739 | float x = calThumbCxWhenSeekStepSection(event.getX());
740 | if (x != mPreThumbCenterX) {
741 | mThumbCenterX = mPreThumbCenterX = x;
742 | } else {
743 | flag = false;
744 | }
745 | } else {
746 | mThumbCenterX = event.getX() + dx;
747 | if (mThumbCenterX < mLeft) {
748 | mThumbCenterX = mLeft;
749 | }
750 | if (mThumbCenterX > mRight) {
751 | mThumbCenterX = mRight;
752 | }
753 | }
754 |
755 | if (flag) {
756 | mProgress = calculateProgress();
757 |
758 | if (!isHideBubble && mBubbleView.getParent() != null) {
759 | mBubbleCenterRawX = calculateCenterRawXofBubbleView();
760 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
761 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
762 | mBubbleView.setProgressText(isShowProgressInFloat ?
763 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
764 | } else {
765 | processProgress();
766 | }
767 |
768 | invalidate();
769 |
770 | if (mProgressListener != null) {
771 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat(), true);
772 | }
773 | }
774 | }
775 |
776 | break;
777 | case MotionEvent.ACTION_UP:
778 | case MotionEvent.ACTION_CANCEL:
779 | getParent().requestDisallowInterceptTouchEvent(false);
780 |
781 | if (isAutoAdjustSectionMark) {
782 | if (isTouchToSeek) {
783 | postDelayed(new Runnable() {
784 | @Override
785 | public void run() {
786 | isTouchToSeekAnimEnd = false;
787 | autoAdjustSection();
788 | }
789 | }, mAnimDuration);
790 | } else {
791 | autoAdjustSection();
792 | }
793 | } else if (isThumbOnDragging || isTouchToSeek) {
794 | if (isHideBubble) {
795 | animate()
796 | .setDuration(mAnimDuration)
797 | .setStartDelay(!isThumbOnDragging && isTouchToSeek ? 300 : 0)
798 | .setListener(new AnimatorListenerAdapter() {
799 | @Override
800 | public void onAnimationEnd(Animator animation) {
801 | isThumbOnDragging = false;
802 | invalidate();
803 | }
804 |
805 | @Override
806 | public void onAnimationCancel(Animator animation) {
807 | isThumbOnDragging = false;
808 | invalidate();
809 | }
810 | }).start();
811 | } else {
812 | postDelayed(new Runnable() {
813 | @Override
814 | public void run() {
815 | mBubbleView.animate()
816 | .alpha(isAlwaysShowBubble ? 1f : 0f)
817 | .setDuration(mAnimDuration)
818 | .setListener(new AnimatorListenerAdapter() {
819 | @Override
820 | public void onAnimationEnd(Animator animation) {
821 | if (!isAlwaysShowBubble) {
822 | hideBubble();
823 | }
824 |
825 | isThumbOnDragging = false;
826 | invalidate();
827 | }
828 |
829 | @Override
830 | public void onAnimationCancel(Animator animation) {
831 | if (!isAlwaysShowBubble) {
832 | hideBubble();
833 | }
834 |
835 | isThumbOnDragging = false;
836 | invalidate();
837 | }
838 | }).start();
839 | }
840 | }, mAnimDuration);
841 | }
842 | }
843 |
844 | if (mProgressListener != null) {
845 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat(), true);
846 | mProgressListener.getProgressOnActionUp(this, getProgress(), getProgressFloat());
847 | }
848 |
849 | break;
850 | }
851 |
852 | return isThumbOnDragging || isTouchToSeek || super.onTouchEvent(event);
853 | }
854 |
855 | /**
856 | * Detect effective touch of thumb
857 | */
858 | private boolean isThumbTouched(MotionEvent event) {
859 | if (!isEnabled())
860 | return false;
861 |
862 | float distance = mTrackLength / mDelta * (mProgress - mMin);
863 | float x = isRtl ? mRight - distance : mLeft + distance;
864 | float y = getMeasuredHeight() / 2f;
865 | return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y)
866 | <= (mLeft + dp2px(8)) * (mLeft + dp2px(8));
867 | }
868 |
869 | /**
870 | * Detect effective touch of track
871 | */
872 | private boolean isTrackTouched(MotionEvent event) {
873 | return isEnabled() && event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
874 | && event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom();
875 | }
876 |
877 | /**
878 | * If the thumb is being dragged, calculate the thumbCenterX when the seek_step_section is true.
879 | */
880 | private float calThumbCxWhenSeekStepSection(float touchedX) {
881 | if (touchedX <= mLeft) return mLeft;
882 | if (touchedX >= mRight) return mRight;
883 |
884 | int i;
885 | float x = 0;
886 | for (i = 0; i <= mSectionCount; i++) {
887 | x = i * mSectionOffset + mLeft;
888 | if (x <= touchedX && touchedX - x <= mSectionOffset) {
889 | break;
890 | }
891 | }
892 |
893 | if (touchedX - x <= mSectionOffset / 2f) {
894 | return x;
895 | } else {
896 | return (i + 1) * mSectionOffset + mLeft;
897 | }
898 | }
899 |
900 | /**
901 | * Auto scroll to the nearest section mark
902 | */
903 | private void autoAdjustSection() {
904 | int i;
905 | float x = 0;
906 | for (i = 0; i <= mSectionCount; i++) {
907 | x = i * mSectionOffset + mLeft;
908 | if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
909 | break;
910 | }
911 | }
912 |
913 | BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
914 | float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
915 | boolean onSection = x_ == x; // 就在section处,不作valueAnim,优化性能
916 |
917 | AnimatorSet animatorSet = new AnimatorSet();
918 |
919 | ValueAnimator valueAnim = null;
920 | if (!onSection) {
921 | if (mThumbCenterX - x <= mSectionOffset / 2f) {
922 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
923 | } else {
924 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
925 | }
926 | valueAnim.setInterpolator(new LinearInterpolator());
927 | valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
928 | @Override
929 | public void onAnimationUpdate(ValueAnimator animation) {
930 | mThumbCenterX = (float) animation.getAnimatedValue();
931 | mProgress = calculateProgress();
932 |
933 | if (!isHideBubble) {
934 | mBubbleCenterRawX = calculateCenterRawXofBubbleView();
935 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
936 | if (mBubbleView.getParent() != null) {
937 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
938 | }
939 | mBubbleView.setProgressText(isShowProgressInFloat ?
940 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
941 | } else {
942 | processProgress();
943 | }
944 |
945 | invalidate();
946 |
947 | if (mProgressListener != null) {
948 | mProgressListener.onProgressChanged(BubbleSeekBar.this, getProgress(),
949 | getProgressFloat(), true);
950 | }
951 | }
952 | });
953 | }
954 |
955 | if (isHideBubble) {
956 | if (!onSection) {
957 | animatorSet.setDuration(mAnimDuration).playTogether(valueAnim);
958 | }
959 | } else {
960 | ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mBubbleView, View.ALPHA, isAlwaysShowBubble ? 1 : 0);
961 | if (onSection) {
962 | animatorSet.setDuration(mAnimDuration).play(alphaAnim);
963 | } else {
964 | animatorSet.setDuration(mAnimDuration).playTogether(valueAnim, alphaAnim);
965 | }
966 | }
967 | animatorSet.addListener(new AnimatorListenerAdapter() {
968 | @Override
969 | public void onAnimationEnd(Animator animation) {
970 | if (!isHideBubble && !isAlwaysShowBubble) {
971 | hideBubble();
972 | }
973 |
974 | mProgress = calculateProgress();
975 | isThumbOnDragging = false;
976 | isTouchToSeekAnimEnd = true;
977 | invalidate();
978 |
979 | if (mProgressListener != null) {
980 | mProgressListener.getProgressOnFinally(BubbleSeekBar.this, getProgress(),
981 | getProgressFloat(), true);
982 | }
983 | }
984 |
985 | @Override
986 | public void onAnimationCancel(Animator animation) {
987 | if (!isHideBubble && !isAlwaysShowBubble) {
988 | hideBubble();
989 | }
990 |
991 | mProgress = calculateProgress();
992 | isThumbOnDragging = false;
993 | isTouchToSeekAnimEnd = true;
994 | invalidate();
995 | }
996 | });
997 | animatorSet.start();
998 | }
999 |
1000 | /**
1001 | * Showing the Bubble depends the way that the WindowManager adds a Toast type view to the Window.
1002 | *
1003 | * 显示气泡
1004 | * 原理是利用WindowManager动态添加一个与Toast相同类型的BubbleView,消失时再移除
1005 | */
1006 | private void showBubble() {
1007 | if (mBubbleView == null || mBubbleView.getParent() != null) {
1008 | return;
1009 | }
1010 |
1011 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
1012 | mLayoutParams.y = (int) (mBubbleCenterRawSolidY + 0.5f);
1013 |
1014 | mBubbleView.setAlpha(0);
1015 | mBubbleView.setVisibility(VISIBLE);
1016 | mBubbleView.animate().alpha(1f).setDuration(isTouchToSeek ? 0 : mAnimDuration)
1017 | .setListener(new AnimatorListenerAdapter() {
1018 | @Override
1019 | public void onAnimationStart(Animator animation) {
1020 | mWindowManager.addView(mBubbleView, mLayoutParams);
1021 | }
1022 | }).start();
1023 | mBubbleView.setProgressText(isShowProgressInFloat ?
1024 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
1025 | }
1026 |
1027 | /**
1028 | * The WindowManager removes the BubbleView from the Window.
1029 | */
1030 | private void hideBubble() {
1031 | if (mBubbleView == null)
1032 | return;
1033 |
1034 | mBubbleView.setVisibility(GONE); // 防闪烁
1035 | if (mBubbleView.getParent() != null) {
1036 | mWindowManager.removeViewImmediate(mBubbleView);
1037 | }
1038 | }
1039 |
1040 | private String float2String(float value) {
1041 | return String.valueOf(formatFloat(value));
1042 | }
1043 |
1044 | private float formatFloat(float value) {
1045 | BigDecimal bigDecimal = BigDecimal.valueOf(value);
1046 | return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
1047 | }
1048 |
1049 | private float calculateCenterRawXofBubbleView() {
1050 | if (isRtl) {
1051 | return mBubbleCenterRawSolidX - mTrackLength * (mProgress - mMin) / mDelta;
1052 | } else {
1053 | return mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
1054 | }
1055 | }
1056 |
1057 | private float calculateProgress() {
1058 | if (isRtl) {
1059 | return (mRight - mThumbCenterX) * mDelta / mTrackLength + mMin;
1060 | } else {
1061 | return (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
1062 | }
1063 | }
1064 |
1065 | /////// Api begins /////////////////////////////////////////////////////////////////////////////
1066 |
1067 | /**
1068 | * When BubbleSeekBar's parent view is scrollable, must listener to it's scrolling and call this
1069 | * method to correct the offsets.
1070 | */
1071 | public void correctOffsetWhenContainerOnScrolling() {
1072 | if (isHideBubble)
1073 | return;
1074 |
1075 | locatePositionInWindow();
1076 |
1077 | if (mBubbleView.getParent() != null) {
1078 | if (isAlwaysShowBubble) {
1079 | mLayoutParams.y = (int) (mBubbleCenterRawSolidY + 0.5f);
1080 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
1081 | } else {
1082 | postInvalidate();
1083 | }
1084 | }
1085 | }
1086 |
1087 | public float getMin() {
1088 | return mMin;
1089 | }
1090 |
1091 | public float getMax() {
1092 | return mMax;
1093 | }
1094 |
1095 | public void setProgress(float progress) {
1096 | mProgress = progress;
1097 |
1098 | if (mProgressListener != null) {
1099 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat(), false);
1100 | mProgressListener.getProgressOnFinally(this, getProgress(), getProgressFloat(), false);
1101 | }
1102 | if (!isHideBubble) {
1103 | mBubbleCenterRawX = calculateCenterRawXofBubbleView();
1104 | }
1105 | if (isAlwaysShowBubble) {
1106 | hideBubble();
1107 |
1108 | postDelayed(new Runnable() {
1109 | @Override
1110 | public void run() {
1111 | showBubble();
1112 | triggerBubbleShowing = true;
1113 | }
1114 | }, mAlwaysShowBubbleDelay);
1115 | }
1116 | if (isSeekBySection) {
1117 | triggerSeekBySection = false;
1118 | }
1119 |
1120 | postInvalidate();
1121 | }
1122 |
1123 | public int getProgress() {
1124 | return Math.round(processProgress());
1125 | }
1126 |
1127 | public float getProgressFloat() {
1128 | return formatFloat(processProgress());
1129 | }
1130 |
1131 | private float processProgress() {
1132 | final float progress = mProgress;
1133 |
1134 | if (isSeekBySection && triggerSeekBySection) {
1135 | float half = mSectionValue / 2;
1136 |
1137 | if (isTouchToSeek) {
1138 | if (progress == mMin || progress == mMax) {
1139 | return progress;
1140 | }
1141 |
1142 | float secValue;
1143 | for (int i = 0; i <= mSectionCount; i++) {
1144 | secValue = i * mSectionValue;
1145 | if (secValue < progress && secValue + mSectionValue >= progress) {
1146 | if (secValue + half > progress) {
1147 | return secValue;
1148 | } else {
1149 | return secValue + mSectionValue;
1150 | }
1151 | }
1152 | }
1153 | }
1154 |
1155 | if (progress >= mPreSecValue) { // increasing
1156 | if (progress >= mPreSecValue + half) {
1157 | mPreSecValue += mSectionValue;
1158 | return mPreSecValue;
1159 | } else {
1160 | return mPreSecValue;
1161 | }
1162 | } else { // reducing
1163 | if (progress >= mPreSecValue - half) {
1164 | return mPreSecValue;
1165 | } else {
1166 | mPreSecValue -= mSectionValue;
1167 | return mPreSecValue;
1168 | }
1169 | }
1170 | }
1171 |
1172 | return progress;
1173 | }
1174 |
1175 | public OnProgressChangedListener getOnProgressChangedListener() {
1176 | return mProgressListener;
1177 | }
1178 |
1179 | public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) {
1180 | mProgressListener = onProgressChangedListener;
1181 | }
1182 |
1183 | public void setTrackColor(@ColorInt int trackColor) {
1184 | if (mTrackColor != trackColor) {
1185 | mTrackColor = trackColor;
1186 | invalidate();
1187 | }
1188 | }
1189 |
1190 | public void setSecondTrackColor(@ColorInt int secondTrackColor) {
1191 | if (mSecondTrackColor != secondTrackColor) {
1192 | mSecondTrackColor = secondTrackColor;
1193 | invalidate();
1194 | }
1195 | }
1196 |
1197 | public void setThumbColor(@ColorInt int thumbColor) {
1198 | if (mThumbColor != thumbColor) {
1199 | mThumbColor = thumbColor;
1200 | invalidate();
1201 | }
1202 | }
1203 |
1204 | public void setBubbleColor(@ColorInt int bubbleColor) {
1205 | if (mBubbleColor != bubbleColor) {
1206 | mBubbleColor = bubbleColor;
1207 | if (mBubbleView != null) {
1208 | mBubbleView.invalidate();
1209 | }
1210 | }
1211 | }
1212 |
1213 | public void setCustomSectionTextArray(@NonNull CustomSectionTextArray customSectionTextArray) {
1214 | mSectionTextArray = customSectionTextArray.onCustomize(mSectionCount, mSectionTextArray);
1215 | for (int i = 0; i <= mSectionCount; i++) {
1216 | if (mSectionTextArray.get(i) == null) {
1217 | mSectionTextArray.put(i, "");
1218 | }
1219 | }
1220 |
1221 | isShowThumbText = false;
1222 | requestLayout();
1223 | invalidate();
1224 | }
1225 | /////// Api ends ///////////////////////////////////////////////////////////////////////////////
1226 |
1227 | @Override
1228 | protected Parcelable onSaveInstanceState() {
1229 | Bundle bundle = new Bundle();
1230 | bundle.putParcelable("save_instance", super.onSaveInstanceState());
1231 | bundle.putFloat("progress", mProgress);
1232 |
1233 | return bundle;
1234 | }
1235 |
1236 | @Override
1237 | protected void onRestoreInstanceState(Parcelable state) {
1238 | if (state instanceof Bundle) {
1239 | Bundle bundle = (Bundle) state;
1240 | mProgress = bundle.getFloat("progress");
1241 | super.onRestoreInstanceState(bundle.getParcelable("save_instance"));
1242 |
1243 | if (mBubbleView != null) {
1244 | mBubbleView.setProgressText(isShowProgressInFloat ?
1245 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
1246 | }
1247 | setProgress(mProgress);
1248 |
1249 | return;
1250 | }
1251 |
1252 | super.onRestoreInstanceState(state);
1253 | }
1254 |
1255 | /**
1256 | * Listen to progress onChanged, onActionUp, onFinally
1257 | */
1258 | public interface OnProgressChangedListener {
1259 |
1260 | void onProgressChanged(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat, boolean fromUser);
1261 |
1262 | void getProgressOnActionUp(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat);
1263 |
1264 | void getProgressOnFinally(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat, boolean fromUser);
1265 | }
1266 |
1267 | /**
1268 | * Listener adapter
1269 | *
1294 | * Customization goes here.
1295 | *
1270 | * usage like {@link AnimatorListenerAdapter}
1271 | */
1272 | public static abstract class OnProgressChangedListenerAdapter implements OnProgressChangedListener {
1273 |
1274 | @Override
1275 | public void onProgressChanged(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat, boolean fromUser) {
1276 | }
1277 |
1278 | @Override
1279 | public void getProgressOnActionUp(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat) {
1280 | }
1281 |
1282 | @Override
1283 | public void getProgressOnFinally(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat, boolean fromUser) {
1284 | }
1285 | }
1286 |
1287 | /**
1288 | * Customize the section texts under the track according to your demands by
1289 | * call {@link #setCustomSectionTextArray(CustomSectionTextArray)}.
1290 | */
1291 | public interface CustomSectionTextArray {
1292 | /**
1293 | * public SparseArray
1307 | *
1308 | * @param sectionCount The section count of the {@code BubbleSeekBar}.
1309 | * @param array The section texts array which had been initialized already. Customize
1310 | * the section text by changing one element's value of the SparseArray.
1311 | * The index key ∈[0, sectionCount].
1312 | * @return The customized section texts array. Can not be {@code null}.
1313 | */
1314 | @NonNull
1315 | SparseArray