├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values-v21
│ │ │ │ └── styles.xml
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── drawable
│ │ │ │ ├── shape_divider_shadow.xml
│ │ │ │ ├── shape_divider_vertical.xml
│ │ │ │ └── selector_radio_text_color.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ ├── menu
│ │ │ │ └── menu_main.xml
│ │ │ └── layout
│ │ │ │ ├── fragment_demo_3.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── fragment_demo_1.xml
│ │ │ │ ├── fragment_demo_4.xml
│ │ │ │ └── fragment_demo_2.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── com
│ │ │ └── xw
│ │ │ └── samlpe
│ │ │ └── bubbleseekbar
│ │ │ ├── ObservableScrollView.java
│ │ │ ├── DemoFragment1.java
│ │ │ ├── DemoFragment2.java
│ │ │ ├── DemoFragment4.java
│ │ │ ├── MainActivity.java
│ │ │ └── DemoFragment3.java
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── xw
│ │ │ └── samlpe
│ │ │ └── bubbleseekbar
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── xw
│ │ └── samlpe
│ │ └── bubbleseekbar
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── bubbleseekbar
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ └── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── attr.xml
│ │ └── java
│ │ └── com
│ │ └── xw
│ │ └── repo
│ │ ├── BubbleUtils.java
│ │ ├── BubbleConfigBuilder.java
│ │ └── BubbleSeekBar.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── apk
└── sample.apk
├── screenshot
├── demo1.gif
├── demo2.gif
├── demo3.gif
└── demo4.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── gradlew
└── README.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/bubbleseekbar/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':bubbleseekbar'
2 |
--------------------------------------------------------------------------------
/apk/sample.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sikeeoh/BubbleSeekBar/HEAD/apk/sample.apk
--------------------------------------------------------------------------------
/screenshot/demo1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sikeeoh/BubbleSeekBar/HEAD/screenshot/demo1.gif
--------------------------------------------------------------------------------
/screenshot/demo2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sikeeoh/BubbleSeekBar/HEAD/screenshot/demo2.gif
--------------------------------------------------------------------------------
/screenshot/demo3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sikeeoh/BubbleSeekBar/HEAD/screenshot/demo3.gif
--------------------------------------------------------------------------------
/screenshot/demo4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sikeeoh/BubbleSeekBar/HEAD/screenshot/demo4.gif
--------------------------------------------------------------------------------
/bubbleseekbar/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
27 |
28 | ******
29 |
30 |
31 |
32 | ## Download
33 | The **LATEST_VERSION**: [](https://bintray.com/sikeeoh/maven/bubbleseekbar)
34 |
35 | ```
36 | dependencies {
37 | // compile 'com.sikeeoh.repo:bubbleseekbar:${LATEST_VERSION}'
38 | }
39 | ```
40 |
41 | ## Usage
42 | ### xml
43 | ```xml
44 |
53 | * Created by woxingxiao on 2016-10-27.
54 | * Modified by sikeeoh
55 | */
56 | public class BubbleSeekBar extends View {
57 |
58 | static final int NONE = -1;
59 |
60 | @IntDef({NONE, SIDES, BOTTOM_SIDES, BELOW_SECTION_MARK})
61 | @Retention(RetentionPolicy.SOURCE)
62 | public @interface TextPosition {
63 | int SIDES = 0, BOTTOM_SIDES = 1, BELOW_SECTION_MARK = 2;
64 | }
65 |
66 | @IntDef({NONE, ALL, ONLY_DEFAULT, ONLY_CUSTOM})
67 | @Retention(RetentionPolicy.SOURCE)
68 | public @interface SectionTextShowOnlyCertainValues {
69 | int ALL = 0, ONLY_DEFAULT = 1, ONLY_CUSTOM = 2;
70 | }
71 |
72 | private float mMin; // min
73 | private float mMax; // max
74 | private float mProgress; // real time value
75 | private boolean isFloatType; // support for float type output
76 | private int mTrackSize; // height of right-track(on the right of thumb)
77 | private int mSecondTrackSize; // height of left-track(on the left of thumb)
78 | private int mThumbRadius; // radius of thumb
79 | private int mThumbRadiusOnDragging; // radius of thumb when be dragging
80 | private int mTrackColor; // color of right-track
81 | private int mSecondTrackColor; // color of left-track
82 | private int mThumbColor; // color of thumb
83 | private int mSectionCount; // shares of whole progress(max - min)
84 | private boolean isShowSectionMark; // show demarcation points or not
85 | private boolean isAutoAdjustSectionMark; // auto scroll to the nearest section_mark or not
86 | private boolean isShowSectionText; // show section-text or not
87 | private boolean isShowSecondTrack;
88 | private int mSectionTextSize; // text size of section-text
89 | private int mSectionTextColor; // text color of section-text
90 | @TextPosition
91 | private int mSectionTextPosition = NONE; // text position of section-text relative to track
92 | private int mSectionTextInterval; // the interval of two section-text
93 | private boolean isShowThumbText; // show real time progress-text under thumb or not
94 | private int mThumbTextSize; // text size of progress-text
95 | private int mThumbTextColor; // text color of progress-text
96 | private boolean isShowProgressInFloat; // show bubble-progress in float or not
97 | private boolean isTouchToSeek; // touch anywhere on track to quickly seek
98 | private boolean isSeekBySection; // seek by section, the progress may not be linear
99 | private long mAnimDuration; // duration of animation
100 | private boolean isAlwaysShowBubble; // bubble shows all time
101 | private long mAlwaysShowBubbleDelay; // the delay duration before bubble shows all the time
102 | private boolean isHideBubble; // hide bubble
103 |
104 | private int mBubbleColor;// color of bubble
105 | private int mBubbleTextSize; // text size of bubble-progress
106 | private int mBubbleTextColor; // text color of bubble-progress
107 |
108 | private float mDelta; // max - min
109 | private float mSectionValue; // (mDelta / mSectionCount)
110 | private float mThumbCenterX; // X coordinate of thumb's center
111 | private float mTrackLength; // pixel length of whole track
112 | private float mSectionOffset; // pixel length of one section
113 | private boolean isThumbOnDragging; // is thumb on dragging or not
114 | private int mTextSpace; // space between text and track
115 | private boolean triggerBubbleShowing;
116 | private boolean triggerSeekBySection;
117 |
118 | private OnProgressChangedListener mProgressListener; // progress changing listener
119 | private float mLeft; // space between left of track and left of the view
120 | private float mRight; // space between right of track and left of the view
121 | private Paint mPaint;
122 | private Rect mRectText;
123 |
124 | private WindowManager mWindowManager;
125 | private BubbleView mBubbleView;
126 | private int mBubbleRadius;
127 | private float mBubbleCenterRawSolidX;
128 | private float mBubbleCenterRawSolidY;
129 | private float mBubbleCenterRawX;
130 | private WindowManager.LayoutParams mLayoutParams;
131 | private int[] mPoint = new int[2];
132 | private boolean isTouchToSeekAnimEnd = true;
133 | private float mPreSecValue; // previous SectionValue
134 | private BubbleConfigBuilder mConfigBuilder; // config attributes
135 | private Map
414 | * The BubbleView is added to Window by the WindowManager, so the only connection between
415 | * BubbleView and SeekBar is their origin raw coordinates on the screen.
416 | *
417 | * It's easy to compute the coordinates(mBubbleCenterRawSolidX, mBubbleCenterRawSolidY) of point
418 | * when the Progress equals the Min. Then compute the pixel length increment when the Progress is
419 | * changing, the result is mBubbleCenterRawX. At last the WindowManager calls updateViewLayout()
420 | * to update the LayoutParameter.x of the BubbleView.
421 | *
422 | * 气泡BubbleView实际是通过WindowManager动态添加的一个视图,因此与SeekBar唯一的位置联系就是它们在屏幕上的
423 | * 绝对坐标。
424 | * 先计算进度mProgress为mMin时BubbleView的中心坐标(mBubbleCenterRawSolidX,mBubbleCenterRawSolidY),
425 | * 然后根据进度来增量计算横坐标mBubbleCenterRawX,再动态设置LayoutParameter.x,就实现了气泡跟随滑动移动。
426 | */
427 | private void locatePositionOnScreen() {
428 | getLocationOnScreen(mPoint);
429 |
430 | mBubbleCenterRawSolidX = mPoint[0] + mLeft - mBubbleView.getMeasuredWidth() / 2f;
431 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
432 | mBubbleCenterRawSolidY = mPoint[1] - mBubbleView.getMeasuredHeight();
433 | mBubbleCenterRawSolidY -= dp2px(24);
434 | if (BubbleUtils.isMIUI()) {
435 | mBubbleCenterRawSolidY += dp2px(4);
436 | }
437 | }
438 |
439 | @Override
440 | protected void onDraw(Canvas canvas) {
441 | super.onDraw(canvas);
442 |
443 | float xLeft = getPaddingLeft();
444 | float xRight = getMeasuredWidth() - getPaddingRight();
445 | float yTop = getPaddingTop() + mThumbRadiusOnDragging;
446 |
447 | // draw sectionText SIDES or BOTTOM_SIDES
448 | if (isShowSectionText) {
449 | mPaint.setTextSize(mSectionTextSize);
450 | mPaint.setColor(mSectionTextColor);
451 |
452 | if (mSectionTextPosition == TextPosition.SIDES) {
453 | float y_ = yTop + mRectText.height() / 2f;
454 |
455 | String text = getMinText();
456 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
457 | canvas.drawText(text, xLeft + mRectText.width() / 2f, y_, mPaint);
458 | xLeft += mRectText.width() + mTextSpace;
459 |
460 | text = getMaxText();
461 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
462 | canvas.drawText(text, xRight - mRectText.width() / 2f, y_, mPaint);
463 | xRight -= (mRectText.width() + mTextSpace);
464 |
465 | } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) {
466 | float y_ = yTop + mThumbRadiusOnDragging + mTextSpace;
467 |
468 | String text = getMinText();
469 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
470 | y_ += mRectText.height();
471 | xLeft = mLeft;
472 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
473 | canvas.drawText(text, xLeft, y_, mPaint);
474 | }
475 |
476 | text = getMaxText();
477 | mPaint.getTextBounds(text, 0, text.length(), mRectText);
478 | xRight = mRight;
479 | if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) {
480 | canvas.drawText(text, xRight, y_, mPaint);
481 | }
482 | }
483 | } else if (isShowThumbText && mSectionTextPosition == NONE) {
484 | xLeft = mLeft;
485 | xRight = mRight;
486 | }
487 |
488 | if ((!isShowSectionText && !isShowThumbText) || mSectionTextPosition == TextPosition.SIDES) {
489 | xLeft += mThumbRadiusOnDragging;
490 | xRight -= mThumbRadiusOnDragging;
491 | }
492 |
493 | boolean isShowTextBelowSectionMark = isShowSectionText && mSectionTextPosition ==
494 | TextPosition.BELOW_SECTION_MARK;
495 | boolean conditionInterval = mSectionCount % 2 == 0;
496 |
497 | // draw sectionMark & sectionText BELOW_SECTION_MARK
498 | if (isShowTextBelowSectionMark || isShowSectionMark) {
499 | float r = (mThumbRadiusOnDragging - dp2px(2)) / 2f;
500 | float junction = mTrackLength / mDelta * Math.abs(mProgress - mMin) + mLeft; // 交汇点
501 | mPaint.setTextSize(mSectionTextSize);
502 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
503 |
504 | float x_;
505 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
506 | sectionXPositionMap.clear();
507 | for (int i = 0; i <= mSectionCount; i++) {
508 | x_ = xLeft + i * mSectionOffset;
509 | mPaint.setColor(x_ <= junction ? (isShowSecondTrack ? mSecondTrackColor : mTrackColor) : mTrackColor);
510 | // sectionMark
511 | canvas.drawCircle(x_, yTop, r, mPaint);
512 |
513 | // sectionText belows section
514 | if (isShowTextBelowSectionMark) {
515 | mPaint.setColor(mSectionTextColor);
516 | int mapPosition = i + 1;
517 |
518 | if (mSectionTextInterval > 1) {
519 | if (conditionInterval && i % mSectionTextInterval == 0) {
520 | float m = mMin + mSectionValue * i;
521 | if (mSectionTextShowOnlyCertainValues == NONE || mSectionTextShowOnlyCertainValues == ALL) {
522 | String text = sectionTextMap == null || sectionTextMap.get(mapPosition) == null ? (isFloatType ? float2String(m) : (int) m + "") : sectionTextMap.get(mapPosition);
523 | canvas.drawText(text, x_, y_, mPaint);
524 | } else if (mSectionTextShowOnlyCertainValues == ONLY_DEFAULT) {
525 | canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint);
526 | } else if (mSectionTextShowOnlyCertainValues == ONLY_CUSTOM) {
527 | String text = sectionTextMap == null || sectionTextMap.get(mapPosition) == null ? "" : sectionTextMap.get(mapPosition);
528 | canvas.drawText(TextUtils.isEmpty(text) ? "" : text, x_, y_, mPaint);
529 | }
530 | }
531 | } else {
532 | float m = mMin + mSectionValue * i;
533 | if (mSectionTextShowOnlyCertainValues == NONE || mSectionTextShowOnlyCertainValues == ALL) {
534 | String text = sectionTextMap == null || sectionTextMap.get(mapPosition) == null ? (isFloatType ? float2String(m) : (int) m + "") : sectionTextMap.get(mapPosition);
535 | canvas.drawText(text, x_, y_, mPaint);
536 | } else if (mSectionTextShowOnlyCertainValues == ONLY_DEFAULT) {
537 | canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint);
538 | } else if (mSectionTextShowOnlyCertainValues == ONLY_CUSTOM) {
539 | String text = sectionTextMap == null || sectionTextMap.get(mapPosition) == null ? "" : sectionTextMap.get(mapPosition);
540 | canvas.drawText(TextUtils.isEmpty(text) ? "" : text, x_, y_, mPaint);
541 | }
542 | }
543 | sectionXPositionMap.put((Math.round(x_ / 10) * 10), mapPosition);
544 | }
545 | }
546 | }
547 |
548 | if (!isThumbOnDragging || isAlwaysShowBubble) {
549 | mThumbCenterX = mTrackLength / mDelta * (mProgress - mMin) + xLeft;
550 | }
551 |
552 | // draw thumbText
553 | if (isShowThumbText && !isThumbOnDragging && isTouchToSeekAnimEnd) {
554 | mPaint.setColor(mThumbTextColor);
555 | mPaint.setTextSize(mThumbTextSize);
556 | mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height
557 | float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace;
558 |
559 | if (isFloatType || (isShowProgressInFloat && mSectionTextPosition == TextPosition.BOTTOM_SIDES &&
560 | mProgress != mMin && mProgress != mMax)) {
561 |
562 | Integer sectionPosition = sectionXPositionMap.get((Math.round(mThumbCenterX / 10) * 10));
563 | if (sectionPosition == null) {
564 | canvas.drawText(String.valueOf(getProgressFloat()), mThumbCenterX, y_, mPaint);
565 | } else {
566 | String text = sectionTextMap == null || sectionTextMap.get(sectionPosition) == null ? String.valueOf(getProgressFloat()) : sectionTextMap.get(sectionPosition);
567 | canvas.drawText(text, mThumbCenterX, y_, mPaint);
568 | }
569 | } else {
570 | Integer sectionPosition = sectionXPositionMap.get((Math.round(mThumbCenterX / 10) * 10));
571 | if (sectionPosition == null) {
572 | canvas.drawText(String.valueOf(getProgress()), mThumbCenterX, y_, mPaint);
573 | } else {
574 | String text = sectionTextMap == null || sectionTextMap.get(sectionPosition) == null ? String.valueOf(getProgress()) : sectionTextMap.get(sectionPosition);
575 | canvas.drawText(text, mThumbCenterX, y_, mPaint);
576 | }
577 | }
578 | }
579 |
580 | // draw track
581 | mPaint.setColor(isShowSecondTrack ? mSecondTrackColor : mTrackColor);
582 | mPaint.setStrokeWidth(isShowSecondTrack ? mSecondTrackSize : mTrackSize);
583 | canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint);
584 |
585 | // draw second track
586 | mPaint.setColor(mTrackColor);
587 | mPaint.setStrokeWidth(mTrackSize);
588 | canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint);
589 |
590 | // draw thumb
591 | mPaint.setColor(mThumbColor);
592 | canvas.drawCircle(mThumbCenterX, yTop, isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius, mPaint);
593 | }
594 |
595 | @Override
596 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
597 | super.onSizeChanged(w, h, oldw, oldh);
598 |
599 | post(new Runnable() {
600 | @Override
601 | public void run() {
602 | requestLayout();
603 | }
604 | });
605 | }
606 |
607 | @Override
608 | protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
609 | if (isHideBubble || !isAlwaysShowBubble)
610 | return;
611 |
612 | if (visibility != VISIBLE) {
613 | hideBubble();
614 | } else {
615 | if (triggerBubbleShowing) {
616 | showBubble();
617 | }
618 | }
619 | super.onVisibilityChanged(changedView, visibility);
620 | }
621 |
622 | @Override
623 | protected void onDetachedFromWindow() {
624 | hideBubble();
625 | mBubbleView = null;
626 | super.onDetachedFromWindow();
627 | }
628 |
629 | float dx;
630 |
631 | @Override
632 | public boolean onTouchEvent(MotionEvent event) {
633 | switch (event.getActionMasked()) {
634 | case MotionEvent.ACTION_DOWN:
635 | getParent().requestDisallowInterceptTouchEvent(true);
636 |
637 | isThumbOnDragging = isThumbTouched(event);
638 | if (isThumbOnDragging) {
639 | if (isSeekBySection && !triggerSeekBySection) {
640 | triggerSeekBySection = true;
641 | }
642 | if (isAlwaysShowBubble && !triggerBubbleShowing) {
643 | triggerBubbleShowing = true;
644 | }
645 | if (!isHideBubble) {
646 | showBubble();
647 | }
648 |
649 | invalidate();
650 | } else if (isTouchToSeek && isTrackTouched(event)) {
651 | isThumbOnDragging = true;
652 | if (isAlwaysShowBubble) {
653 | hideBubble();
654 | triggerBubbleShowing = true;
655 | }
656 |
657 | mThumbCenterX = event.getX();
658 | if (mThumbCenterX < mLeft) {
659 | mThumbCenterX = mLeft;
660 | }
661 | if (mThumbCenterX > mRight) {
662 | mThumbCenterX = mRight;
663 | }
664 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
665 |
666 | if (!isHideBubble) {
667 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
668 | showBubble();
669 | }
670 |
671 | invalidate();
672 | }
673 |
674 | dx = mThumbCenterX - event.getX();
675 |
676 | break;
677 | case MotionEvent.ACTION_MOVE:
678 | if (isThumbOnDragging) {
679 | mThumbCenterX = event.getX() + dx;
680 | if (mThumbCenterX < mLeft) {
681 | mThumbCenterX = mLeft;
682 | }
683 | if (mThumbCenterX > mRight) {
684 | mThumbCenterX = mRight;
685 | }
686 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
687 |
688 | if (!isHideBubble) {
689 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
690 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
691 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
692 | mBubbleView.setProgressText(isShowProgressInFloat ?
693 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
694 | }
695 |
696 | invalidate();
697 |
698 | if (mProgressListener != null) {
699 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat());
700 | }
701 | }
702 |
703 | break;
704 | case MotionEvent.ACTION_UP:
705 | case MotionEvent.ACTION_CANCEL:
706 | getParent().requestDisallowInterceptTouchEvent(false);
707 |
708 | if (isAutoAdjustSectionMark) {
709 | if (isTouchToSeek) {
710 | postDelayed(new Runnable() {
711 | @Override
712 | public void run() {
713 | isTouchToSeekAnimEnd = false;
714 | autoAdjustSection();
715 | }
716 | }, isThumbOnDragging ? 0 : 300);
717 | } else {
718 | autoAdjustSection();
719 | }
720 | } else if (isThumbOnDragging || isTouchToSeek) {
721 | if (isHideBubble) {
722 | animate()
723 | .setDuration(mAnimDuration)
724 | .setStartDelay(!isThumbOnDragging && isTouchToSeek ? 300 : 0)
725 | .setListener(new AnimatorListenerAdapter() {
726 | @Override
727 | public void onAnimationEnd(Animator animation) {
728 | isThumbOnDragging = false;
729 | invalidate();
730 |
731 | if (mProgressListener != null) {
732 | mProgressListener.onProgressChanged(BubbleSeekBar.this, getProgress(),
733 | getProgressFloat());
734 | }
735 | }
736 |
737 | @Override
738 | public void onAnimationCancel(Animator animation) {
739 | isThumbOnDragging = false;
740 | invalidate();
741 | }
742 | })
743 | .start();
744 | } else {
745 | mBubbleView.animate()
746 | .alpha(isAlwaysShowBubble ? 1f : 0f)
747 | .setDuration(mAnimDuration)
748 | .setStartDelay(!isThumbOnDragging && isTouchToSeek ? 300 : 0)
749 | .setListener(new AnimatorListenerAdapter() {
750 | @Override
751 | public void onAnimationEnd(Animator animation) {
752 | if (!isAlwaysShowBubble) {
753 | hideBubble();
754 | }
755 |
756 | isThumbOnDragging = false;
757 | invalidate();
758 |
759 | if (mProgressListener != null) {
760 | mProgressListener.onProgressChanged(BubbleSeekBar.this, getProgress(),
761 | getProgressFloat());
762 | }
763 | }
764 |
765 | @Override
766 | public void onAnimationCancel(Animator animation) {
767 | if (!isAlwaysShowBubble) {
768 | hideBubble();
769 | }
770 |
771 | isThumbOnDragging = false;
772 | invalidate();
773 | }
774 | })
775 | .start();
776 |
777 | }
778 | }
779 |
780 | if (mProgressListener != null) {
781 | mProgressListener.getProgressOnActionUp(this, getProgress(), getProgressFloat());
782 | }
783 |
784 | break;
785 | }
786 |
787 | return isThumbOnDragging || isTouchToSeek || super.onTouchEvent(event);
788 | }
789 |
790 | /**
791 | * Detect effective touch of thumb
792 | */
793 | private boolean isThumbTouched(MotionEvent event) {
794 | if (!isEnabled())
795 | return false;
796 |
797 | float x = mTrackLength / mDelta * (mProgress - mMin) + mLeft;
798 | float y = getMeasuredHeight() / 2f;
799 | return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y)
800 | <= (mLeft + dp2px(8)) * (mLeft + dp2px(8));
801 | }
802 |
803 | /**
804 | * Detect effective touch of track
805 | */
806 | private boolean isTrackTouched(MotionEvent event) {
807 | return isEnabled() && event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
808 | && event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom();
809 | }
810 |
811 | /**
812 | * Showing the Bubble depends the way that the WindowManager adds a Toast type view to the Window.
813 | *
814 | * 显示气泡
815 | * 原理是利用WindowManager动态添加一个与Toast相同类型的BubbleView,消失时再移除
816 | */
817 | private void showBubble() {
818 | if (mBubbleView == null || mBubbleView.getParent() != null) {
819 | return;
820 | }
821 |
822 | if (mLayoutParams == null) {
823 | mLayoutParams = new WindowManager.LayoutParams();
824 | mLayoutParams.gravity = Gravity.START | Gravity.TOP;
825 | mLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
826 | mLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
827 | mLayoutParams.format = PixelFormat.TRANSLUCENT;
828 | mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
829 | | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
830 | | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
831 | // MIUI禁止了开发者使用TYPE_TOAST,Android 7.1.1 对TYPE_TOAST的使用更严格
832 | if (BubbleUtils.isMIUI() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
833 | mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
834 | } else {
835 | mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
836 | }
837 | }
838 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
839 | mLayoutParams.y = (int) (mBubbleCenterRawSolidY + 0.5f);
840 |
841 | mBubbleView.setAlpha(0);
842 | mBubbleView.setVisibility(VISIBLE);
843 | mBubbleView.animate().alpha(1f).setDuration(mAnimDuration)
844 | .setListener(new AnimatorListenerAdapter() {
845 | @Override
846 | public void onAnimationStart(Animator animation) {
847 | mWindowManager.addView(mBubbleView, mLayoutParams);
848 | }
849 | }).start();
850 | mBubbleView.setProgressText(isShowProgressInFloat ?
851 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
852 | }
853 |
854 | /**
855 | * Auto scroll to the nearest section mark
856 | */
857 | private void autoAdjustSection() {
858 | int i;
859 | float x = 0;
860 | for (i = 0; i <= mSectionCount; i++) {
861 | x = i * mSectionOffset + mLeft;
862 | if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
863 | break;
864 | }
865 | }
866 |
867 | BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
868 | float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
869 | boolean onSection = x_ == x; // 就在section处,不作valueAnim,优化性能
870 |
871 | AnimatorSet animatorSet = new AnimatorSet();
872 |
873 | ValueAnimator valueAnim = null;
874 | if (!onSection) {
875 | if (mThumbCenterX - x <= mSectionOffset / 2f) {
876 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
877 | } else {
878 | valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
879 | }
880 | valueAnim.setInterpolator(new LinearInterpolator());
881 | valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
882 | @Override
883 | public void onAnimationUpdate(ValueAnimator animation) {
884 | mThumbCenterX = (float) animation.getAnimatedValue();
885 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
886 |
887 | if (!isHideBubble) {
888 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mThumbCenterX - mLeft;
889 | mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
890 | if (mBubbleView.getParent() != null) {
891 | mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
892 | }
893 | mBubbleView.setProgressText(isShowProgressInFloat ?
894 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
895 | }
896 |
897 | invalidate();
898 |
899 | if (mProgressListener != null) {
900 | mProgressListener.onProgressChanged(BubbleSeekBar.this, getProgress(), getProgressFloat());
901 | }
902 | }
903 | });
904 | }
905 |
906 | if (isHideBubble) {
907 | if (!onSection) {
908 | animatorSet.setDuration(mAnimDuration).playTogether(valueAnim);
909 | }
910 | } else {
911 | ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mBubbleView, View.ALPHA, isAlwaysShowBubble ? 1 : 0);
912 | if (onSection) {
913 | animatorSet.setDuration(mAnimDuration).play(alphaAnim);
914 | } else {
915 | animatorSet.setDuration(mAnimDuration).playTogether(valueAnim, alphaAnim);
916 | }
917 | }
918 | animatorSet.addListener(new AnimatorListenerAdapter() {
919 | @Override
920 | public void onAnimationEnd(Animator animation) {
921 | if (!isHideBubble && !isAlwaysShowBubble) {
922 | hideBubble();
923 | }
924 |
925 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
926 | isThumbOnDragging = false;
927 | isTouchToSeekAnimEnd = true;
928 | invalidate();
929 |
930 | if (mProgressListener != null) {
931 | mProgressListener.getProgressOnFinally(BubbleSeekBar.this, getProgress(), getProgressFloat());
932 | }
933 | }
934 |
935 | @Override
936 | public void onAnimationCancel(Animator animation) {
937 | if (!isHideBubble && !isAlwaysShowBubble) {
938 | hideBubble();
939 | }
940 |
941 | mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
942 | isThumbOnDragging = false;
943 | isTouchToSeekAnimEnd = true;
944 | invalidate();
945 | }
946 | });
947 | animatorSet.start();
948 | }
949 |
950 | /**
951 | * The WindowManager removes the BubbleView from the Window.
952 | */
953 | private void hideBubble() {
954 | if (mBubbleView == null)
955 | return;
956 |
957 | mBubbleView.setVisibility(GONE); // 防闪烁
958 | if (mBubbleView.getParent() != null) {
959 | mWindowManager.removeViewImmediate(mBubbleView);
960 | }
961 | }
962 |
963 | /**
964 | * When BubbleSeekBar's parent view is scrollable, must listener to it's scrolling and call this
965 | * method to correct the offsets.
966 | */
967 | public void correctOffsetWhenContainerOnScrolling() {
968 | if (isHideBubble)
969 | return;
970 |
971 | locatePositionOnScreen();
972 |
973 | if (mBubbleView.getParent() != null) {
974 | postInvalidate();
975 | }
976 | }
977 |
978 | private String getMinText() {
979 | return isFloatType ? float2String(mMin) : String.valueOf((int) mMin);
980 | }
981 |
982 | private String getMaxText() {
983 | return isFloatType ? float2String(mMax) : String.valueOf((int) mMax);
984 | }
985 |
986 | public float getMin() {
987 | return mMin;
988 | }
989 |
990 | public float getMax() {
991 | return mMax;
992 | }
993 |
994 | public void setProgress(float progress) {
995 | mProgress = progress;
996 |
997 | if (mProgressListener != null) {
998 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat());
999 | mProgressListener.getProgressOnFinally(this, getProgress(), getProgressFloat());
1000 | }
1001 |
1002 | if (isHideBubble) {
1003 | mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
1004 | }
1005 |
1006 | if (isAlwaysShowBubble) {
1007 | hideBubble();
1008 |
1009 | int[] location = new int[2];
1010 | getLocationOnScreen(location);
1011 | postDelayed(new Runnable() {
1012 | @Override
1013 | public void run() {
1014 | showBubble();
1015 | triggerBubbleShowing = true;
1016 | }
1017 | }, location[0] == 0 && location[1] == 0 ? mAlwaysShowBubbleDelay : 0);
1018 | }
1019 |
1020 | postInvalidate();
1021 | }
1022 |
1023 | public int getProgress() {
1024 | if (isSeekBySection && triggerSeekBySection) {
1025 | float half = mSectionValue / 2;
1026 |
1027 | if (mProgress >= mPreSecValue) { // increasing
1028 | if (mProgress >= mPreSecValue + half) {
1029 | mPreSecValue += mSectionValue;
1030 | return Math.round(mPreSecValue);
1031 | } else {
1032 | return Math.round(mPreSecValue);
1033 | }
1034 | } else { // reducing
1035 | if (mProgress >= mPreSecValue - half) {
1036 | return Math.round(mPreSecValue);
1037 | } else {
1038 | mPreSecValue -= mSectionValue;
1039 | return Math.round(mPreSecValue);
1040 | }
1041 | }
1042 | }
1043 | return Math.round(mProgress);
1044 | }
1045 |
1046 | public float getProgressFloat() {
1047 | return formatFloat(mProgress);
1048 | }
1049 |
1050 | public OnProgressChangedListener getOnProgressChangedListener() {
1051 | return mProgressListener;
1052 | }
1053 |
1054 | public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) {
1055 | mProgressListener = onProgressChangedListener;
1056 | }
1057 |
1058 | void config(BubbleConfigBuilder builder) {
1059 | mMin = builder.min;
1060 | mMax = builder.max;
1061 | mProgress = builder.progress;
1062 | isFloatType = builder.floatType;
1063 | mTrackSize = builder.trackSize;
1064 | mSecondTrackSize = builder.secondTrackSize;
1065 | mThumbRadius = builder.thumbRadius;
1066 | mThumbRadiusOnDragging = builder.thumbRadiusOnDragging;
1067 | mTrackColor = builder.trackColor;
1068 | mSecondTrackColor = builder.secondTrackColor;
1069 | mThumbColor = builder.thumbColor;
1070 | mSectionCount = builder.sectionCount;
1071 | isShowSectionMark = builder.showSectionMark;
1072 | isAutoAdjustSectionMark = builder.autoAdjustSectionMark;
1073 | isShowSectionText = builder.showSectionText;
1074 | mSectionTextSize = builder.sectionTextSize;
1075 | mSectionTextColor = builder.sectionTextColor;
1076 | mSectionTextPosition = builder.sectionTextPosition;
1077 | mSectionTextInterval = builder.sectionTextInterval;
1078 | isShowThumbText = builder.showThumbText;
1079 | mThumbTextSize = builder.thumbTextSize;
1080 | mThumbTextColor = builder.thumbTextColor;
1081 | isShowProgressInFloat = builder.showProgressInFloat;
1082 | mAnimDuration = builder.animDuration;
1083 | isTouchToSeek = builder.touchToSeek;
1084 | isSeekBySection = builder.seekBySection;
1085 | mBubbleColor = builder.bubbleColor;
1086 | mBubbleTextSize = builder.bubbleTextSize;
1087 | mBubbleTextColor = builder.bubbleTextColor;
1088 | isAlwaysShowBubble = builder.alwaysShowBubble;
1089 | mAlwaysShowBubbleDelay = builder.alwaysShowBubbleDelay;
1090 | isHideBubble = builder.hideBubble;
1091 | isShowSecondTrack = builder.showSecondTrack;
1092 | sectionTextMap = builder.sectionTextMap;
1093 | mSectionTextShowOnlyCertainValues = builder.sectionTextShowOnlyCertainValues;
1094 |
1095 | initConfigByPriority();
1096 | calculateRadiusOfBubble();
1097 |
1098 | if (mProgressListener != null) {
1099 | mProgressListener.onProgressChanged(this, getProgress(), getProgressFloat());
1100 | mProgressListener.getProgressOnFinally(this, getProgress(), getProgressFloat());
1101 | }
1102 |
1103 | mConfigBuilder = null;
1104 |
1105 | requestLayout();
1106 | }
1107 |
1108 | public BubbleConfigBuilder getConfigBuilder() {
1109 | if (mConfigBuilder == null) {
1110 | mConfigBuilder = new BubbleConfigBuilder(this);
1111 | }
1112 |
1113 | mConfigBuilder.min = mMin;
1114 | mConfigBuilder.max = mMax;
1115 | mConfigBuilder.progress = mProgress;
1116 | mConfigBuilder.floatType = isFloatType;
1117 | mConfigBuilder.trackSize = mTrackSize;
1118 | mConfigBuilder.secondTrackSize = mSecondTrackSize;
1119 | mConfigBuilder.thumbRadius = mThumbRadius;
1120 | mConfigBuilder.thumbRadiusOnDragging = mThumbRadiusOnDragging;
1121 | mConfigBuilder.trackColor = mTrackColor;
1122 | mConfigBuilder.secondTrackColor = mSecondTrackColor;
1123 | mConfigBuilder.thumbColor = mThumbColor;
1124 | mConfigBuilder.sectionCount = mSectionCount;
1125 | mConfigBuilder.showSectionMark = isShowSectionMark;
1126 | mConfigBuilder.autoAdjustSectionMark = isAutoAdjustSectionMark;
1127 | mConfigBuilder.showSectionText = isShowSectionText;
1128 | mConfigBuilder.sectionTextSize = mSectionTextSize;
1129 | mConfigBuilder.sectionTextColor = mSectionTextColor;
1130 | mConfigBuilder.sectionTextPosition = mSectionTextPosition;
1131 | mConfigBuilder.sectionTextInterval = mSectionTextInterval;
1132 | mConfigBuilder.showThumbText = isShowThumbText;
1133 | mConfigBuilder.thumbTextSize = mThumbTextSize;
1134 | mConfigBuilder.thumbTextColor = mThumbTextColor;
1135 | mConfigBuilder.showProgressInFloat = isShowProgressInFloat;
1136 | mConfigBuilder.animDuration = mAnimDuration;
1137 | mConfigBuilder.touchToSeek = isTouchToSeek;
1138 | mConfigBuilder.seekBySection = isSeekBySection;
1139 | mConfigBuilder.bubbleColor = mBubbleColor;
1140 | mConfigBuilder.bubbleTextSize = mBubbleTextSize;
1141 | mConfigBuilder.bubbleTextColor = mBubbleTextColor;
1142 | mConfigBuilder.alwaysShowBubble = isAlwaysShowBubble;
1143 | mConfigBuilder.alwaysShowBubbleDelay = mAlwaysShowBubbleDelay;
1144 | mConfigBuilder.hideBubble = isHideBubble;
1145 | mConfigBuilder.showSecondTrack = isShowSecondTrack;
1146 | mConfigBuilder.sectionTextMap = sectionTextMap;
1147 | mConfigBuilder.sectionTextShowOnlyCertainValues = mSectionTextShowOnlyCertainValues;
1148 |
1149 | return mConfigBuilder;
1150 | }
1151 |
1152 | @Override
1153 | protected Parcelable onSaveInstanceState() {
1154 | Bundle bundle = new Bundle();
1155 | bundle.putParcelable("save_instance", super.onSaveInstanceState());
1156 | bundle.putFloat("progress", mProgress);
1157 |
1158 | return bundle;
1159 | }
1160 |
1161 | @Override
1162 | protected void onRestoreInstanceState(Parcelable state) {
1163 | if (state instanceof Bundle) {
1164 | Bundle bundle = (Bundle) state;
1165 | mProgress = bundle.getFloat("progress");
1166 | super.onRestoreInstanceState(bundle.getParcelable("save_instance"));
1167 |
1168 | if (mBubbleView != null) {
1169 | mBubbleView.setProgressText(isShowProgressInFloat ?
1170 | String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
1171 | }
1172 | setProgress(mProgress);
1173 |
1174 | return;
1175 | }
1176 |
1177 | super.onRestoreInstanceState(state);
1178 | }
1179 |
1180 | private String float2String(float value) {
1181 | return String.valueOf(formatFloat(value));
1182 | }
1183 |
1184 | private float formatFloat(float value) {
1185 | BigDecimal bigDecimal = BigDecimal.valueOf(value);
1186 | return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
1187 | }
1188 |
1189 | /**
1190 | * Listen to progress onChanged, onActionUp, onFinally
1191 | */
1192 | public interface OnProgressChangedListener {
1193 |
1194 | void onProgressChanged(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat);
1195 |
1196 | void getProgressOnActionUp(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat);
1197 |
1198 | void getProgressOnFinally(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat);
1199 | }
1200 |
1201 | /**
1202 | * Listener adapter
1203 | *
1204 | * usage like {@link AnimatorListenerAdapter}
1205 | */
1206 | public static abstract class OnProgressChangedListenerAdapter implements OnProgressChangedListener {
1207 |
1208 | @Override
1209 | public void onProgressChanged(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat) {
1210 | }
1211 |
1212 | @Override
1213 | public void getProgressOnActionUp(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat) {
1214 | }
1215 |
1216 | @Override
1217 | public void getProgressOnFinally(BubbleSeekBar bubbleSeekBar, int progress, float progressFloat) {
1218 | }
1219 | }
1220 |
1221 | /*******************************************************************************************
1222 | * ************************************ custom bubble view ************************************
1223 | *******************************************************************************************/
1224 | private class BubbleView extends View {
1225 |
1226 | private Paint mBubblePaint;
1227 | private Path mBubblePath;
1228 | private RectF mBubbleRectF;
1229 | private Rect mRect;
1230 | private String mProgressText = "";
1231 |
1232 | BubbleView(Context context) {
1233 | this(context, null);
1234 | }
1235 |
1236 | BubbleView(Context context, AttributeSet attrs) {
1237 | this(context, attrs, 0);
1238 | }
1239 |
1240 | BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
1241 | super(context, attrs, defStyleAttr);
1242 |
1243 | mBubblePaint = new Paint();
1244 | mBubblePaint.setAntiAlias(true);
1245 | mBubblePaint.setTextAlign(Paint.Align.CENTER);
1246 |
1247 | mBubblePath = new Path();
1248 | mBubbleRectF = new RectF();
1249 | mRect = new Rect();
1250 | }
1251 |
1252 | @Override
1253 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1254 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1255 |
1256 | setMeasuredDimension(3 * mBubbleRadius, 3 * mBubbleRadius);
1257 |
1258 | mBubbleRectF.set(getMeasuredWidth() / 2f - mBubbleRadius, 0,
1259 | getMeasuredWidth() / 2f + mBubbleRadius, 2 * mBubbleRadius);
1260 | }
1261 |
1262 | @Override
1263 | protected void onDraw(Canvas canvas) {
1264 | super.onDraw(canvas);
1265 |
1266 | mBubblePath.reset();
1267 | float x0 = getMeasuredWidth() / 2f;
1268 | float y0 = getMeasuredHeight() - mBubbleRadius / 3f;
1269 | mBubblePath.moveTo(x0, y0);
1270 | float x1 = (float) (getMeasuredWidth() / 2f - Math.sqrt(3) / 2f * mBubbleRadius);
1271 | float y1 = 3 / 2f * mBubbleRadius;
1272 | mBubblePath.quadTo(
1273 | x1 - dp2px(2), y1 - dp2px(2),
1274 | x1, y1
1275 | );
1276 | mBubblePath.arcTo(mBubbleRectF, 150, 240);
1277 |
1278 | float x2 = (float) (getMeasuredWidth() / 2f + Math.sqrt(3) / 2f * mBubbleRadius);
1279 | mBubblePath.quadTo(
1280 | x2 + dp2px(2), y1 - dp2px(2),
1281 | x0, y0
1282 | );
1283 | mBubblePath.close();
1284 |
1285 | mBubblePaint.setColor(mBubbleColor);
1286 | canvas.drawPath(mBubblePath, mBubblePaint);
1287 |
1288 | mBubblePaint.setTextSize(mBubbleTextSize);
1289 | mBubblePaint.setColor(mBubbleTextColor);
1290 | mBubblePaint.getTextBounds(mProgressText, 0, mProgressText.length(), mRect);
1291 | Paint.FontMetrics fm = mBubblePaint.getFontMetrics();
1292 | float baseline = mBubbleRadius + (fm.descent - fm.ascent) / 2f - fm.descent;
1293 | canvas.drawText(mProgressText, getMeasuredWidth() / 2f, baseline, mBubblePaint);
1294 | }
1295 |
1296 | void setProgressText(String progressText) {
1297 | if (progressText != null && !mProgressText.equals(progressText)) {
1298 | mProgressText = progressText;
1299 | invalidate();
1300 | }
1301 | }
1302 | }
1303 |
1304 | }
--------------------------------------------------------------------------------