package com.moral.andbrickslib.views.bubbleseekbar; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.animation.LinearInterpolator; import com.moral.andbrickslib.R; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.math.BigDecimal; import static com.moral.andbrickslib.views.bubbleseekbar.BubbleSeekBar.TextPosition.BELOW_SECTION_MARK; import static com.moral.andbrickslib.views.bubbleseekbar.BubbleSeekBar.TextPosition.BOTTOM_SIDES; import static com.moral.andbrickslib.views.bubbleseekbar.BubbleSeekBar.TextPosition.SIDES; import static com.moral.andbrickslib.views.bubbleseekbar.BubbleUtils.dp2px; import static com.moral.andbrickslib.views.bubbleseekbar.BubbleUtils.sp2px; /** * A beautiful and powerful Android custom seek bar, which has a bubble view with progress * appearing upon when seeking. Highly customizable, mostly demands has been considered. *
* Created by woxingxiao on 2016-10-27. */ public class BubbleSeekBar extends View { static final int NONE = -1; @IntDef({NONE, SIDES, BOTTOM_SIDES, BELOW_SECTION_MARK}) @Retention(RetentionPolicy.SOURCE) public @interface TextPosition { int SIDES = 0, BOTTOM_SIDES = 1, BELOW_SECTION_MARK = 2; } private float mMin; // min private float mMax; // max private float mProgress; // real time value private boolean isFloatType; // support for float type output private int mTrackSize; // height of right-track(on the right of thumb) private int mSecondTrackSize; // height of left-track(on the left of thumb) private int mThumbRadius; // radius of thumb private int mThumbRadiusOnDragging; // radius of thumb when be dragging private int mTrackColor; // color of right-track private int mSecondTrackColor; // color of left-track private int mThumbColor; // color of thumb private int mSectionCount; // shares of whole progress(max - min) private boolean isShowSectionMark; // show demarcation points or not private boolean isAutoAdjustSectionMark; // auto scroll to the nearest section_mark or not private boolean isShowSectionText; // show section-text or not private int mSectionTextSize; // text size of section-text private int mSectionTextColor; // text color of section-text @TextPosition private int mSectionTextPosition = NONE; // text position of section-text relative to track private int mSectionTextInterval; // the interval of two section-text private boolean isShowThumbText; // show real time progress-text under thumb or not private int mThumbTextSize; // text size of progress-text private int mThumbTextColor; // text color of progress-text private boolean isShowProgressInFloat; // show bubble-progress in float or not private boolean isTouchToSeek; // touch anywhere on track to quickly seek private boolean isSeekBySection; // seek by section, the progress may not be linear private long mAnimDuration; // duration of animation private boolean isAlwaysShowBubble; // bubble shows all time private int mBubbleColor;// color of bubble private int mBubbleTextSize; // text size of bubble-progress private int mBubbleTextColor; // text color of bubble-progress private float mDelta; // max - min private float mSectionValue; // (mDelta / mSectionCount) private float mThumbCenterX; // X coordinate of thumb's center private float mTrackLength; // pixel length of whole track private float mSectionOffset; // pixel length of one section private boolean isThumbOnDragging; // is thumb on dragging or not private int mTextSpace; // space between text and track private boolean triggerBubbleShowing; private OnProgressChangedListener mProgressListener; // progress changing listener private float mLeft; // space between left of track and left of the view private float mRight; // space between right of track and left of the view private Paint mPaint; private Rect mRectText; private WindowManager mWindowManager; private BubbleView mBubbleView; private int mBubbleRadius; private float mBubbleCenterRawSolidX; private float mBubbleCenterRawSolidY; private float mBubbleCenterRawX; private WindowManager.LayoutParams mLayoutParams; private int[] mPoint = new int[2]; private boolean isTouchToSeekAnimEnd = true; private float mPreSecValue; // previous SectionValue private BubbleConfigBuilder mConfigBuilder; // config attributes public BubbleSeekBar(Context context) { this(context, null); } public BubbleSeekBar(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BubbleSeekBar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleSeekBar, defStyleAttr, 0); mMin = a.getFloat(R.styleable.BubbleSeekBar_bsb_min, 0.0f); mMax = a.getFloat(R.styleable.BubbleSeekBar_bsb_max, 100.0f); mProgress = a.getFloat(R.styleable.BubbleSeekBar_bsb_progress, mMin); isFloatType = a.getBoolean(R.styleable.BubbleSeekBar_bsb_is_float_type, false); mTrackSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_track_size, dp2px(2)); mSecondTrackSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_second_track_size, mTrackSize + dp2px(2)); mThumbRadius = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_radius, mSecondTrackSize + dp2px(2)); mThumbRadiusOnDragging = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_radius, mSecondTrackSize * 2); mSectionCount = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_count, 10); mTrackColor = a.getColor(R.styleable.BubbleSeekBar_bsb_track_color, ContextCompat.getColor(context, R.color.colorPrimary)); mSecondTrackColor = a.getColor(R.styleable.BubbleSeekBar_bsb_second_track_color, ContextCompat.getColor(context, R.color.colorAccent)); mThumbColor = a.getColor(R.styleable.BubbleSeekBar_bsb_thumb_color, mSecondTrackColor); isShowSectionText = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_section_text, false); mSectionTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_section_text_size, sp2px(14)); mSectionTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_section_text_color, mTrackColor); isSeekBySection = a.getBoolean(R.styleable.BubbleSeekBar_bsb_seek_by_section, false); int pos = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_text_position, NONE); if (pos == 0) { mSectionTextPosition = SIDES; } else if (pos == 1) { mSectionTextPosition = TextPosition.BOTTOM_SIDES; } else if (pos == 2) { mSectionTextPosition = TextPosition.BELOW_SECTION_MARK; } else { mSectionTextPosition = NONE; } mSectionTextInterval = a.getInteger(R.styleable.BubbleSeekBar_bsb_section_text_interval, 1); isShowThumbText = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_thumb_text, false); mThumbTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_thumb_text_size, sp2px(14)); mThumbTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_thumb_text_color, mSecondTrackColor); mBubbleColor = a.getColor(R.styleable.BubbleSeekBar_bsb_bubble_color, mSecondTrackColor); mBubbleTextSize = a.getDimensionPixelSize(R.styleable.BubbleSeekBar_bsb_bubble_text_size, sp2px(14)); mBubbleTextColor = a.getColor(R.styleable.BubbleSeekBar_bsb_bubble_text_color, Color.WHITE); isShowSectionMark = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_section_mark, false); isAutoAdjustSectionMark = a.getBoolean(R.styleable.BubbleSeekBar_bsb_auto_adjust_section_mark, false); isShowProgressInFloat = a.getBoolean(R.styleable.BubbleSeekBar_bsb_show_progress_in_float, false); int duration = a.getInteger(R.styleable.BubbleSeekBar_bsb_anim_duration, -1); mAnimDuration = duration < 0 ? 200 : duration; isTouchToSeek = a.getBoolean(R.styleable.BubbleSeekBar_bsb_touch_to_seek, false); isAlwaysShowBubble = a.getBoolean(R.styleable.BubbleSeekBar_bsb_always_show_bubble, false); a.recycle(); initConfigByPriority(); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStrokeCap(Paint.Cap.ROUND); mPaint.setTextAlign(Paint.Align.CENTER); mRectText = new Rect(); mTextSpace = dp2px(2); mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); // init BubbleView mBubbleView = new BubbleView(context); mBubbleView.setProgressText(isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress())); calculateRadiusOfBubble(); } private void initConfigByPriority() { if (mMin == mMax) { mMin = 0.0f; mMax = 100.0f; } if (mMin > mMax) { float tmp = mMax; mMax = mMin; mMin = tmp; } if (mProgress < mMin) { mProgress = mMin; } if (mProgress > mMax) { mProgress = mMax; } if (mSecondTrackSize < mTrackSize) { mSecondTrackSize = mTrackSize + dp2px(2); } if (mThumbRadius <= mSecondTrackSize) { mThumbRadius = mSecondTrackSize + dp2px(2); } if (mThumbRadiusOnDragging <= mSecondTrackSize) { mThumbRadiusOnDragging = mSecondTrackSize * 2; } if (mSectionCount <= 0) { mSectionCount = 10; } mDelta = mMax - mMin; mSectionValue = mDelta / mSectionCount; if (mSectionValue < 1) { isFloatType = true; } if (isFloatType) { isShowProgressInFloat = true; } if (mSectionTextPosition != NONE) { isShowSectionText = true; } if (isShowSectionText) { if (mSectionTextPosition == NONE) { mSectionTextPosition = TextPosition.SIDES; } if (mSectionTextPosition == TextPosition.BELOW_SECTION_MARK) { isShowSectionMark = true; } } if (mSectionTextInterval < 1) { mSectionTextInterval = 1; } if (isAutoAdjustSectionMark && !isShowSectionMark) { isAutoAdjustSectionMark = false; } if (isSeekBySection) { mPreSecValue = mMin; if (mProgress != mMin) { mPreSecValue = mSectionValue; } isShowSectionMark = true; isAutoAdjustSectionMark = true; isTouchToSeek = false; } mThumbTextSize = isFloatType || isSeekBySection || (isShowSectionText && mSectionTextPosition == TextPosition.BELOW_SECTION_MARK) ? mSectionTextSize : mThumbTextSize; } /** * Calculate radius of bubble according to the Min and the Max */ private void calculateRadiusOfBubble() { mPaint.setTextSize(mBubbleTextSize); // 计算滑到两端气泡里文字需要显示的宽度,比较取最大值为气泡的半径 String text; if (isShowProgressInFloat) { text = float2String(mMin); } else { text = getMinText(); } mPaint.getTextBounds(text, 0, text.length(), mRectText); int w1 = (mRectText.width() + mTextSpace * 2) >> 1; if (isShowProgressInFloat) { text = float2String(mMax); } else { text = getMaxText(); } mPaint.getTextBounds(text, 0, text.length(), mRectText); int w2 = (mRectText.width() + mTextSpace * 2) >> 1; mBubbleRadius = dp2px(14); // default 14dp int max = Math.max(mBubbleRadius, Math.max(w1, w2)); mBubbleRadius = max + mTextSpace; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int height = mThumbRadiusOnDragging * 2; // 默认高度为拖动时thumb圆的直径 if (isShowThumbText) { mPaint.setTextSize(mThumbTextSize); mPaint.getTextBounds("j", 0, 1, mRectText); // “j”是字母和阿拉伯数字中最高的 height += mRectText.height() + mTextSpace; // 如果显示实时进度,则原来基础上加上进度文字高度和间隔 } if (isShowSectionText && mSectionTextPosition >= TextPosition.BOTTOM_SIDES) { // 如果Section值在track之下显示,比较取较大值 mPaint.setTextSize(mSectionTextSize); mPaint.getTextBounds("j", 0, 1, mRectText); height = Math.max(height, mThumbRadiusOnDragging * 2 + mRectText.height() + mTextSpace); } setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), height); mLeft = getPaddingLeft() + mThumbRadiusOnDragging; mRight = getMeasuredWidth() - getPaddingRight() - mThumbRadiusOnDragging; if (isShowSectionText) { mPaint.setTextSize(mSectionTextSize); if (mSectionTextPosition == TextPosition.SIDES) { String text = getMinText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); mLeft += (mRectText.width() + mTextSpace); text = getMaxText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); mRight -= (mRectText.width() + mTextSpace); } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) { String text = getMinText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); float max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f); mLeft = getPaddingLeft() + max + mTextSpace; text = getMaxText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f); mRight = getMeasuredWidth() - getPaddingRight() - max - mTextSpace; } } else if (isShowThumbText && mSectionTextPosition == NONE) { mPaint.setTextSize(mThumbTextSize); String text = getMinText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); float max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f); mLeft = getPaddingLeft() + max + mTextSpace; text = getMaxText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); max = Math.max(mThumbRadiusOnDragging, mRectText.width() / 2f); mRight = getMeasuredWidth() - getPaddingRight() - max - mTextSpace; } mTrackLength = mRight - mLeft; mSectionOffset = mTrackLength * 1f / mSectionCount; mBubbleView.measure(widthMeasureSpec, heightMeasureSpec); locatePositionOnScreen(); } /** * In fact there two parts of the BubbleSeeBar, they are the BubbleView and the SeekBar. ** The BubbleView is added to Window by the WindowManager, so the only connection between * BubbleView and SeekBar is their origin raw coordinates on the screen. *
* It's easy to compute the coordinates(mBubbleCenterRawSolidX, mBubbleCenterRawSolidY) of point * when the Progress equals the Min. Then compute the pixel length increment when the Progress is * changing, the result is mBubbleCenterRawX. At last the WindowManager calls updateViewLayout() * to update the LayoutParameter.x of the BubbleView. *
* 气泡BubbleView实际是通过WindowManager动态添加的一个视图,因此与SeekBar唯一的位置联系就是它们在屏幕上的 * 绝对坐标。 * 先计算进度mProgress为mMin时BubbleView的中心坐标(mBubbleCenterRawSolidX,mBubbleCenterRawSolidY), * 然后根据进度来增量计算横坐标mBubbleCenterRawX,再动态设置LayoutParameter.x,就实现了气泡跟随滑动移动。 */ private void locatePositionOnScreen() { getLocationOnScreen(mPoint); mBubbleCenterRawSolidX = mPoint[0] + mLeft - mBubbleView.getMeasuredWidth() / 2f; mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta; mBubbleCenterRawSolidY = mPoint[1] - mBubbleView.getMeasuredHeight(); mBubbleCenterRawSolidY -= dp2px(24); if (BubbleUtils.isMIUI()) { mBubbleCenterRawSolidY += dp2px(4); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); float xLeft = getPaddingLeft(); float xRight = getMeasuredWidth() - getPaddingRight(); float yTop = getPaddingTop() + mThumbRadiusOnDragging; // draw sectionText SIDES or BOTTOM_SIDES if (isShowSectionText) { mPaint.setTextSize(mSectionTextSize); mPaint.setColor(mSectionTextColor); if (mSectionTextPosition == TextPosition.SIDES) { float y_ = yTop + mRectText.height() / 2f; String text = getMinText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); canvas.drawText(text, xLeft + mRectText.width() / 2f, y_, mPaint); xLeft += mRectText.width() + mTextSpace; text = getMaxText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); canvas.drawText(text, xRight - mRectText.width() / 2f, y_, mPaint); xRight -= (mRectText.width() + mTextSpace); } else if (mSectionTextPosition >= TextPosition.BOTTOM_SIDES) { float y_ = yTop + mThumbRadiusOnDragging + mTextSpace; String text = getMinText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); y_ += mRectText.height(); xLeft = mLeft; if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) { canvas.drawText(text, xLeft, y_, mPaint); } text = getMaxText(); mPaint.getTextBounds(text, 0, text.length(), mRectText); xRight = mRight; if (mSectionTextPosition == TextPosition.BOTTOM_SIDES) { canvas.drawText(text, xRight, y_, mPaint); } } } else if (isShowThumbText && mSectionTextPosition == NONE) { xLeft = mLeft; xRight = mRight; } if ((!isShowSectionText && !isShowThumbText) || mSectionTextPosition == TextPosition.SIDES) { xLeft += mThumbRadiusOnDragging; xRight -= mThumbRadiusOnDragging; } boolean isShowTextBelowSectionMark = isShowSectionText && mSectionTextPosition == TextPosition.BELOW_SECTION_MARK; boolean conditionInterval = mSectionCount % 2 == 0; // draw sectionMark & sectionText BELOW_SECTION_MARK if (isShowTextBelowSectionMark || isShowSectionMark) { float r = (mThumbRadiusOnDragging - dp2px(2)) / 2f; float junction = mTrackLength / mDelta * Math.abs(mProgress - mMin) + mLeft; // 交汇点 mPaint.setTextSize(mSectionTextSize); mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height float x_; float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace; for (int i = 0; i <= mSectionCount; i++) { x_ = xLeft + i * mSectionOffset; mPaint.setColor(x_ <= junction ? mSecondTrackColor : mTrackColor); // sectionMark canvas.drawCircle(x_, yTop, r, mPaint); // sectionText belows section if (isShowTextBelowSectionMark) { mPaint.setColor(mSectionTextColor); if (mSectionTextInterval > 1) { if (conditionInterval && i % mSectionTextInterval == 0) { float m = mMin + mSectionValue * i; canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint); } } else { float m = mMin + mSectionValue * i; canvas.drawText(isFloatType ? float2String(m) : (int) m + "", x_, y_, mPaint); } } } } if (!isThumbOnDragging || isAlwaysShowBubble) { mThumbCenterX = mTrackLength / mDelta * (mProgress - mMin) + xLeft; } // draw thumbText if (isShowThumbText && !isThumbOnDragging && isTouchToSeekAnimEnd) { mPaint.setColor(mThumbTextColor); mPaint.setTextSize(mThumbTextSize); mPaint.getTextBounds("0123456789", 0, "0123456789".length(), mRectText); // compute solid height float y_ = yTop + mRectText.height() + mThumbRadiusOnDragging + mTextSpace; if (isFloatType || (isShowProgressInFloat && mSectionTextPosition == TextPosition.BOTTOM_SIDES && mProgress != mMin && mProgress != mMax)) { canvas.drawText(String.valueOf(getProgressFloat()), mThumbCenterX, y_, mPaint); } else { canvas.drawText(String.valueOf(getProgress()), mThumbCenterX, y_, mPaint); } } // draw track mPaint.setColor(mSecondTrackColor); mPaint.setStrokeWidth(mSecondTrackSize); canvas.drawLine(xLeft, yTop, mThumbCenterX, yTop, mPaint); // draw second track mPaint.setColor(mTrackColor); mPaint.setStrokeWidth(mTrackSize); canvas.drawLine(mThumbCenterX, yTop, xRight, yTop, mPaint); // draw thumb mPaint.setColor(mThumbColor); canvas.drawCircle(mThumbCenterX, yTop, isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius, mPaint); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); post(new Runnable() { @Override public void run() { requestLayout(); } }); } @Override protected void onVisibilityChanged(@NonNull View changedView, int visibility) { if (!isAlwaysShowBubble) return; if (visibility != VISIBLE) { hideBubble(); } else { if (triggerBubbleShowing) { showBubble(); } } super.onVisibilityChanged(changedView, visibility); } @Override protected void onDetachedFromWindow() { hideBubble(); super.onDetachedFromWindow(); } float dx; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: isThumbOnDragging = isThumbTouched(event); if (isThumbOnDragging) { if (isAlwaysShowBubble) { triggerBubbleShowing = true; } showBubble(); invalidate(); } else if (isTouchToSeek && isTrackTouched(event)) { if (isAlwaysShowBubble) { hideBubble(); triggerBubbleShowing = true; } mThumbCenterX = event.getX(); if (mThumbCenterX < mLeft) { mThumbCenterX = mLeft; } if (mThumbCenterX > mRight) { mThumbCenterX = mRight; } mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin; mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta; showBubble(); invalidate(); } dx = mThumbCenterX - event.getX(); break; case MotionEvent.ACTION_MOVE: if (isThumbOnDragging) { mThumbCenterX = event.getX() + dx; if (mThumbCenterX < mLeft) { mThumbCenterX = mLeft; } if (mThumbCenterX > mRight) { mThumbCenterX = mRight; } mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin; mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta; mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f); mWindowManager.updateViewLayout(mBubbleView, mLayoutParams); mBubbleView.setProgressText(isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress())); invalidate(); if (mProgressListener != null) { mProgressListener.onProgressChanged(getProgress(), getProgressFloat()); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (isAutoAdjustSectionMark) { if (isTouchToSeek) { mBubbleView.postDelayed(new Runnable() { @Override public void run() { isTouchToSeekAnimEnd = false; autoAdjustSection(); } }, isThumbOnDragging ? 0 : 300); } else { autoAdjustSection(); } } else if (isThumbOnDragging || isTouchToSeek) { mBubbleView.postDelayed(new Runnable() { @Override public void run() { mBubbleView.animate() .alpha(isAlwaysShowBubble ? 1f : 0f) .setDuration(mAnimDuration) .setListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (!isAlwaysShowBubble) { hideBubble(); } isThumbOnDragging = false; invalidate(); if (mProgressListener != null) { mProgressListener.onProgressChanged(getProgress(), getProgressFloat()); } } @Override public void onAnimationCancel(Animator animation) { if (!isAlwaysShowBubble) { hideBubble(); } isThumbOnDragging = false; invalidate(); } }) .start(); } }, !isThumbOnDragging && isTouchToSeek ? 300 : 0); } if (mProgressListener != null) { mProgressListener.getProgressOnActionUp(getProgress(), getProgressFloat()); } break; } return isThumbOnDragging || isTouchToSeek || super.onTouchEvent(event); } /** * Detect effective touch of thumb */ private boolean isThumbTouched(MotionEvent event) { if (!isEnabled()) return false; float x = mTrackLength / mDelta * (mProgress - mMin) + mLeft; float y = getMeasuredHeight() / 2f; return (event.getX() - x) * (event.getX() - x) + (event.getY() - y) * (event.getY() - y) <= (mLeft + dp2px(8)) * (mLeft + dp2px(8)); } /** * Detect effective touch of track */ private boolean isTrackTouched(MotionEvent event) { if (!isEnabled()) return false; return event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight() && event.getY() >= getPaddingTop() && event.getY() <= getPaddingTop() + mThumbRadiusOnDragging * 2; } /** * Showing the Bubble depends the way that the WindowManager adds a Toast type view to the Window. *
* 显示气泡
* 原理是利用WindowManager动态添加一个与Toast相同类型的BubbleView,消失时再移除
*/
private void showBubble() {
if (mBubbleView.getParent() != null) {
return;
}
if (mLayoutParams == null) {
mLayoutParams = new WindowManager.LayoutParams();
mLayoutParams.gravity = Gravity.START | Gravity.TOP;
mLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
mLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
mLayoutParams.format = PixelFormat.TRANSLUCENT;
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
// MIUI禁止了开发者使用TYPE_TOAST,Android 7.1.1 对TYPE_TOAST的使用更严格
if (BubbleUtils.isMIUI() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
} else {
mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
}
}
mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
mLayoutParams.y = (int) (mBubbleCenterRawSolidY + 0.5f);
mBubbleView.setAlpha(0);
mBubbleView.setVisibility(VISIBLE);
mBubbleView.animate().alpha(1f).setDuration(mAnimDuration)
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mWindowManager.addView(mBubbleView, mLayoutParams);
}
}).start();
mBubbleView.setProgressText(isShowProgressInFloat ?
String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
}
/**
* Auto scroll to the nearest section mark
*/
private void autoAdjustSection() {
int i;
float x = 0;
for (i = 0; i <= mSectionCount; i++) {
x = i * mSectionOffset + mLeft;
if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
break;
}
}
BigDecimal bigDecimal = BigDecimal.valueOf(mThumbCenterX);
float x_ = bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
boolean onSection = x_ == x; // 就在section处,不作valueAnim,优化性能
AnimatorSet animatorSet = new AnimatorSet();
ValueAnimator valueAnim = null;
if (!onSection) {
if (mThumbCenterX - x <= mSectionOffset / 2f) {
valueAnim = ValueAnimator.ofFloat(mThumbCenterX, x);
} else {
valueAnim = ValueAnimator.ofFloat(mThumbCenterX, (i + 1) * mSectionOffset + mLeft);
}
valueAnim.setInterpolator(new LinearInterpolator());
valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mThumbCenterX = (float) animation.getAnimatedValue();
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
mBubbleCenterRawX = mBubbleCenterRawSolidX + mThumbCenterX - mLeft;
mLayoutParams.x = (int) (mBubbleCenterRawX + 0.5f);
if (mBubbleView.getParent() != null) {
mWindowManager.updateViewLayout(mBubbleView, mLayoutParams);
}
mBubbleView.setProgressText(isShowProgressInFloat ?
String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
invalidate();
if (mProgressListener != null) {
mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
}
}
});
}
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mBubbleView, View.ALPHA, isAlwaysShowBubble ? 1 : 0);
if (onSection) {
animatorSet.setDuration(mAnimDuration).play(alphaAnim);
} else {
animatorSet.setDuration(mAnimDuration).playTogether(valueAnim, alphaAnim);
}
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (!isAlwaysShowBubble) {
hideBubble();
}
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
isThumbOnDragging = false;
isTouchToSeekAnimEnd = true;
invalidate();
if (mProgressListener != null) {
mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
}
}
@Override
public void onAnimationCancel(Animator animation) {
if (!isAlwaysShowBubble) {
hideBubble();
}
mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
isThumbOnDragging = false;
isTouchToSeekAnimEnd = true;
invalidate();
}
});
animatorSet.start();
}
/**
* The WindowManager removes the BubbleView from the Window.
*/
private void hideBubble() {
mBubbleView.setVisibility(GONE); // 防闪烁
if (mBubbleView.getParent() != null) {
mWindowManager.removeViewImmediate(mBubbleView);
}
}
/**
* When BubbleSeekBar's parent view is scrollable, must listener to it's scrolling and call this
* method to correct the offsets.
*/
public void correctOffsetWhenContainerOnScrolling() {
locatePositionOnScreen();
if (mBubbleView.getParent() != null) {
postInvalidate();
}
}
private String getMinText() {
return isFloatType ? float2String(mMin) : String.valueOf((int) mMin);
}
private String getMaxText() {
return isFloatType ? float2String(mMax) : String.valueOf((int) mMax);
}
public float getMin() {
return mMin;
}
public float getMax() {
return mMax;
}
public void setProgress(float progress) {
mProgress = progress;
mBubbleCenterRawX = mBubbleCenterRawSolidX + mTrackLength * (mProgress - mMin) / mDelta;
if (mProgressListener != null) {
mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
}
if (isAlwaysShowBubble) {
hideBubble();
int[] location = new int[2];
getLocationOnScreen(location);
postDelayed(new Runnable() {
@Override
public void run() {
showBubble();
triggerBubbleShowing = true;
}
}, location[0] == 0 && location[1] == 0 ? 300 : 0);
}
postInvalidate();
}
public int getProgress() {
if (isSeekBySection) {
float half = mSectionValue / 2;
if (mProgress >= mPreSecValue) { // increasing
if (mProgress >= mPreSecValue + half) {
mPreSecValue += mSectionValue;
return Math.round(mPreSecValue);
} else {
return Math.round(mPreSecValue);
}
} else { // reducing
if (mProgress >= mPreSecValue - half) {
return Math.round(mPreSecValue);
} else {
mPreSecValue -= mSectionValue;
return Math.round(mPreSecValue);
}
}
}
return Math.round(mProgress);
}
public float getProgressFloat() {
return formatFloat(mProgress);
}
public OnProgressChangedListener getOnProgressChangedListener() {
return mProgressListener;
}
public void setOnProgressChangedListener(OnProgressChangedListener onProgressChangedListener) {
mProgressListener = onProgressChangedListener;
}
void config(BubbleConfigBuilder builder) {
mMin = builder.min;
mMax = builder.max;
mProgress = builder.progress;
isFloatType = builder.floatType;
mTrackSize = builder.trackSize;
mSecondTrackSize = builder.secondTrackSize;
mThumbRadius = builder.thumbRadius;
mThumbRadiusOnDragging = builder.thumbRadiusOnDragging;
mTrackColor = builder.trackColor;
mSecondTrackColor = builder.secondTrackColor;
mThumbColor = builder.thumbColor;
mSectionCount = builder.sectionCount;
isShowSectionMark = builder.showSectionMark;
isAutoAdjustSectionMark = builder.autoAdjustSectionMark;
isShowSectionText = builder.showSectionText;
mSectionTextSize = builder.sectionTextSize;
mSectionTextColor = builder.sectionTextColor;
mSectionTextPosition = builder.sectionTextPosition;
mSectionTextInterval = builder.sectionTextInterval;
isShowThumbText = builder.showThumbText;
mThumbTextSize = builder.thumbTextSize;
mThumbTextColor = builder.thumbTextColor;
isShowProgressInFloat = builder.showProgressInFloat;
isTouchToSeek = builder.touchToSeek;
isSeekBySection = builder.seekBySection;
mBubbleColor = builder.bubbleColor;
mBubbleTextSize = builder.bubbleTextSize;
mBubbleTextColor = builder.bubbleTextColor;
isAlwaysShowBubble = builder.alwaysShowBubble;
initConfigByPriority();
calculateRadiusOfBubble();
if (mProgressListener != null) {
mProgressListener.onProgressChanged(getProgress(), getProgressFloat());
mProgressListener.getProgressOnFinally(getProgress(), getProgressFloat());
}
mConfigBuilder = null;
requestLayout();
}
public BubbleConfigBuilder getConfigBuilder() {
if (mConfigBuilder == null) {
mConfigBuilder = new BubbleConfigBuilder(this);
}
mConfigBuilder.min = mMin;
mConfigBuilder.max = mMax;
mConfigBuilder.progress = mProgress;
mConfigBuilder.floatType = isFloatType;
mConfigBuilder.trackSize = mTrackSize;
mConfigBuilder.secondTrackSize = mSecondTrackSize;
mConfigBuilder.thumbRadius = mThumbRadius;
mConfigBuilder.thumbRadiusOnDragging = mThumbRadiusOnDragging;
mConfigBuilder.trackColor = mTrackColor;
mConfigBuilder.secondTrackColor = mSecondTrackColor;
mConfigBuilder.thumbColor = mThumbColor;
mConfigBuilder.sectionCount = mSectionCount;
mConfigBuilder.showSectionMark = isShowSectionMark;
mConfigBuilder.autoAdjustSectionMark = isAutoAdjustSectionMark;
mConfigBuilder.showSectionText = isShowSectionText;
mConfigBuilder.sectionTextSize = mSectionTextSize;
mConfigBuilder.sectionTextColor = mSectionTextColor;
mConfigBuilder.sectionTextPosition = mSectionTextPosition;
mConfigBuilder.sectionTextInterval = mSectionTextInterval;
mConfigBuilder.showThumbText = isShowThumbText;
mConfigBuilder.thumbTextSize = mThumbTextSize;
mConfigBuilder.thumbTextColor = mThumbTextColor;
mConfigBuilder.showProgressInFloat = isShowProgressInFloat;
mConfigBuilder.touchToSeek = isTouchToSeek;
mConfigBuilder.seekBySection = isSeekBySection;
mConfigBuilder.bubbleColor = mBubbleColor;
mConfigBuilder.bubbleTextSize = mBubbleTextSize;
mConfigBuilder.bubbleTextColor = mBubbleTextColor;
mConfigBuilder.alwaysShowBubble = isAlwaysShowBubble;
return mConfigBuilder;
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable("save_instance", super.onSaveInstanceState());
bundle.putFloat("progress", mProgress);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
mProgress = bundle.getFloat("progress");
super.onRestoreInstanceState(bundle.getParcelable("save_instance"));
mBubbleView.setProgressText(isShowProgressInFloat ?
String.valueOf(getProgressFloat()) : String.valueOf(getProgress()));
if (isAlwaysShowBubble) {
setProgress(mProgress);
}
return;
}
super.onRestoreInstanceState(state);
}
private String float2String(float value) {
return String.valueOf(formatFloat(value));
}
private float formatFloat(float value) {
BigDecimal bigDecimal = BigDecimal.valueOf(value);
return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
}
/**
* Listen to progress onChanged, onActionUp, onFinally
*/
public interface OnProgressChangedListener {
void onProgressChanged(int progress, float progressFloat);
void getProgressOnActionUp(int progress, float progressFloat);
void getProgressOnFinally(int progress, float progressFloat);
}
/**
* Listener adapter
*
* usage like {@link AnimatorListenerAdapter}
*/
public static abstract class OnProgressChangedListenerAdapter implements OnProgressChangedListener {
@Override
public void onProgressChanged(int progress, float progressFloat) {
}
@Override
public void getProgressOnActionUp(int progress, float progressFloat) {
}
@Override
public void getProgressOnFinally(int progress, float progressFloat) {
}
}
/*******************************************************************************************
* ************************************ custom bubble view ************************************
*******************************************************************************************/
private class BubbleView extends View {
private Paint mBubblePaint;
private Path mBubblePath;
private RectF mBubbleRectF;
private Rect mRect;
private String mProgressText = "";
BubbleView(Context context) {
this(context, null);
}
BubbleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBubblePaint = new Paint();
mBubblePaint.setAntiAlias(true);
mBubblePaint.setTextAlign(Paint.Align.CENTER);
mBubblePath = new Path();
mBubbleRectF = new RectF();
mRect = new Rect();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(3 * mBubbleRadius, 3 * mBubbleRadius);
mBubbleRectF.set(getMeasuredWidth() / 2f - mBubbleRadius, 0,
getMeasuredWidth() / 2f + mBubbleRadius, 2 * mBubbleRadius);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mBubblePath.reset();
float x0 = getMeasuredWidth() / 2f;
float y0 = getMeasuredHeight() - mBubbleRadius / 3f;
mBubblePath.moveTo(x0, y0);
float x1 = (float) (getMeasuredWidth() / 2f - Math.sqrt(3) / 2f * mBubbleRadius);
float y1 = 3 / 2f * mBubbleRadius;
mBubblePath.quadTo(
x1 - dp2px(2), y1 - dp2px(2),
x1, y1
);
mBubblePath.arcTo(mBubbleRectF, 150, 240);
float x2 = (float) (getMeasuredWidth() / 2f + Math.sqrt(3) / 2f * mBubbleRadius);
mBubblePath.quadTo(
x2 + dp2px(2), y1 - dp2px(2),
x0, y0
);
mBubblePath.close();
mBubblePaint.setColor(mBubbleColor);
canvas.drawPath(mBubblePath, mBubblePaint);
mBubblePaint.setTextSize(mBubbleTextSize);
mBubblePaint.setColor(mBubbleTextColor);
mBubblePaint.getTextBounds(mProgressText, 0, mProgressText.length(), mRect);
Paint.FontMetrics fm = mBubblePaint.getFontMetrics();
float baseline = mBubbleRadius + (fm.descent - fm.ascent) / 2f - fm.descent;
canvas.drawText(mProgressText, getMeasuredWidth() / 2f, baseline, mBubblePaint);
}
void setProgressText(String progressText) {
if (progressText != null && !mProgressText.equals(progressText)) {
mProgressText = progressText;
invalidate();
}
}
}
}