content: package com.faceunity.fulivedemo.ui.seekbar;
  
  import android.content.Context;
  import android.content.res.ColorStateList;
  import android.content.res.TypedArray;
  import android.graphics.Canvas;
  import android.graphics.Color;
  import android.graphics.Rect;
  import android.graphics.drawable.Drawable;
  import android.os.Build;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.support.annotation.NonNull;
  import android.support.annotation.Nullable;
  import android.support.v4.graphics.drawable.DrawableCompat;
  import android.support.v4.view.ViewCompat;
  import android.util.AttributeSet;
  import android.util.TypedValue;
  import android.view.KeyEvent;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewParent;
  
  import com.faceunity.fulivedemo.ui.seekbar.internal.PopupIndicator;
  import com.faceunity.fulivedemo.ui.seekbar.internal.compat.AnimatorCompat;
  import com.faceunity.fulivedemo.ui.seekbar.internal.compat.SeekBarCompat;
  import com.faceunity.fulivedemo.ui.seekbar.internal.drawable.MarkerDrawable;
  import com.faceunity.fulivedemo.ui.seekbar.internal.drawable.ThumbDrawable;
  import com.faceunity.fulivedemo.ui.seekbar.internal.drawable.TrackRectDrawable;
  
  import java.util.Formatter;
  import java.util.Locale;
  
  import io.agora.rtcwithfu.R;
  
  public class DiscreteSeekBar extends View {
      private static final String TAG = DiscreteSeekBar.class.getSimpleName();
  
      /**
       * Interface to propagate seekbar change event
       */
      public interface OnProgressChangeListener {
          /**
           * When the {@link DiscreteSeekBar} value changes
           *
           * @param seekBar  The DiscreteSeekBar
           * @param value    the new value
           * @param fromUser if the change was made from the user or not (i.e. the developer calling {@link #setProgress(int)}
           */
          public void onProgressChanged(DiscreteSeekBar seekBar, int value, boolean fromUser);
  
          public void onStartTrackingTouch(DiscreteSeekBar seekBar);
  
          public void onStopTrackingTouch(DiscreteSeekBar seekBar);
      }
  
      public static class OnSimpleProgressChangeListener implements OnProgressChangeListener {
          public void onProgressChanged(DiscreteSeekBar seekBar, int value, boolean fromUser) {
  
          }
  
          public void onStartTrackingTouch(DiscreteSeekBar seekBar) {
  
          }
  
          public void onStopTrackingTouch(DiscreteSeekBar seekBar) {
  
          }
      }
  
      /**
       * Interface to transform the current internal value of this DiscreteSeekBar to anther one for the visualization.
       * <p/>
       * This will be used on the floating bubble to display a different value if needed.
       * <p/>
       * Using this in conjunction with {@link #setIndicatorFormatter(String)} you will be able to manipulate the
       * value seen by the user
       *
       * @see #setIndicatorFormatter(String)
       * @see #setNumericTransformer(NumericTransformer)
       */
      public static abstract class NumericTransformer {
          /**
           * Return the desired value to be shown to the user.
           * This value will be formatted using the format specified by {@link #setIndicatorFormatter} before displaying it
           *
           * @param value The value to be transformed
           * @return The transformed int
           */
          public abstract int transform(int value);
  
          /**
           * Return the desired value to be shown to the user.
           * This value will be displayed 'as is' without further formatting.
           *
           * @param value The value to be transformed
           * @return A formatted string
           */
          public String transformToString(int value) {
              return String.valueOf(value);
          }
  
          /**
           * Used to indicate which transform will be used. If this method returns true,
           * {@link #transformToString(int)} will be used, otherwise {@link #transform(int)}
           * will be used
           */
          public boolean useStringTransform() {
              return false;
          }
      }
  
  
      private static class DefaultNumericTransformer extends NumericTransformer {
  
          @Override
          public int transform(int value) {
              return value;
          }
      }
  
  
      private static final boolean isLollipopOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
      //We want to always use a formatter so the indicator numbers are "translated" to specific locales.
      private static final String DEFAULT_FORMATTER = "%d";
  
      private static final int PRESSED_STATE = android.R.attr.state_pressed;
      private static final int FOCUSED_STATE = android.R.attr.state_focused;
      private static final int PROGRESS_ANIMATION_DURATION = 250;
      private static final int INDICATOR_DELAY_FOR_TAPS = 150;
      private static final int DEFAULT_THUMB_COLOR = 0xff009688;
      private static final int SEPARATION_DP = 5;
      private ThumbDrawable mThumb;
      private TrackRectDrawable mTrack;
      private TrackRectDrawable mTrackBase;
      private TrackRectDrawable mScrubber;
      private Drawable mRipple;
  
      private int mTrackHeight;
      private int mTrackBaseHeight;
      private int mScrubberHeight;
      private final int mAddedTouchBounds;
  
      private int mMax;
      private int mMin;
      private int mValue;
      private int mKeyProgressIncrement = 1;
      private boolean mMirrorForRtl = false;
      private boolean mAllowTrackClick = true;
      private boolean mIndicatorPopupEnabled = true;
      //We use our own Formatter to avoid creating new instances on every progress change
      Formatter mFormatter;
      private String mIndicatorFormatter;
      private NumericTransformer mNumericTransformer;
      private StringBuilder mFormatBuilder;
      private OnProgressChangeListener mPublicChangeListener;
      private boolean mIsDragging;
      private int mDragOffset;
  
      private Rect mInvalidateRect = new Rect();
      private Rect mTempRect = new Rect();
      private PopupIndicator mIndicator;
      private AnimatorCompat mPositionAnimator;
      private float mAnimationPosition;
      private int mAnimationTarget;
      private float mDownX;
      private float mTouchSlop;
  
      private int mValueBase;
  
      public DiscreteSeekBar(Context context) {
          this(context, null);
      }
  
      public DiscreteSeekBar(Context context, AttributeSet attrs) {
          this(context, attrs, R.attr.discreteSeekBarStyle);
      }
  
      public DiscreteSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
          setFocusable(true);
          setWillNotDraw(false);
  
          mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
          float density = context.getResources().getDisplayMetrics().density;
  
          TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DiscreteSeekBar,
                  defStyleAttr, R.style.Widget_DiscreteSeekBar);
  
          int max = 100;
          int min = 0;
          mMirrorForRtl = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_mirrorForRtl, mMirrorForRtl);
          mAllowTrackClick = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_allowTrackClickToDrag, mAllowTrackClick);
          mIndicatorPopupEnabled = a.getBoolean(R.styleable.DiscreteSeekBar_dsb_indicatorPopupEnabled, mIndicatorPopupEnabled);
          mTrackHeight = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_trackHeight, (int) (1 * density));
          mTrackBaseHeight = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_trackBaseHeight, (int) (1 * density));
          mScrubberHeight = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_scrubberHeight, (int) (4 * density));
          int thumbSize = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_thumbSize, (int) (density * ThumbDrawable.DEFAULT_SIZE_DP));
          int separation = a.getDimensionPixelSize(R.styleable.DiscreteSeekBar_dsb_indicatorSeparation,
                  (int) (SEPARATION_DP * density));
  
          int touchBounds = thumbSize * 3 / 2;
          mAddedTouchBounds = Math.max(0, (touchBounds - thumbSize) / 2);
  
          int indexMax = R.styleable.DiscreteSeekBar_dsb_max;
          int indexMin = R.styleable.DiscreteSeekBar_dsb_min;
          int indexValue = R.styleable.DiscreteSeekBar_dsb_value;
          final TypedValue out = new TypedValue();
          //Not sure why, but we wanted to be able to use dimensions here...
          if (a.getValue(indexMax, out)) {
              if (out.type == TypedValue.TYPE_DIMENSION) {
                  max = a.getDimensionPixelSize(indexMax, max);
              } else {
                  max = a.getInteger(indexMax, max);
              }
          }
          if (a.getValue(indexMin, out)) {
              if (out.type == TypedValue.TYPE_DIMENSION) {
                  min = a.getDimensionPixelSize(indexMin, min);
              } else {
                  min = a.getInteger(indexMin, min);
              }
          }
          if (a.getValue(indexValue, out)) {
              if (out.type == TypedValue.TYPE_DIMENSION) {
                  mValueBase = a.getDimensionPixelSize(indexValue, mValueBase);
              } else {
                  mValueBase = a.getInteger(indexValue, mValueBase);
              }
          }
  
          mMin = min;
          mMax = Math.max(min + 1, max);
          mValue = Math.max(min, Math.min(max, mValueBase));
          updateKeyboardRange();
  
          mIndicatorFormatter = a.getString(R.styleable.DiscreteSeekBar_dsb_indicatorFormatter);
  
          ColorStateList trackColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_trackColor);
          ColorStateList progressColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_progressColor);
          ColorStateList rippleColor = a.getColorStateList(R.styleable.DiscreteSeekBar_dsb_rippleColor);
          boolean editMode = isInEditMode();
          if (editMode || rippleColor == null) {
              rippleColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.DKGRAY});
          }
          if (editMode || trackColor == null) {
              trackColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{Color.GRAY});
          }
          if (editMode || progressColor == null) {
              progressColor = new ColorStateList(new int[][]{new int[]{}}, new int[]{DEFAULT_THUMB_COLOR});
          }
  
          mRipple = SeekBarCompat.getRipple(rippleColor);
          if (isLollipopOrGreater) {
              SeekBarCompat.setBackground(this, mRipple);
          } else {
              mRipple.setCallback(this);
          }
  
          TrackRectDrawable shapeDrawable = new TrackRectDrawable(trackColor);
          mTrack = shapeDrawable;
          mTrack.setCallback(this);
  
          mTrackBase = new TrackRectDrawable(trackColor);
          mTrackBase.setCallback(this);
  
          shapeDrawable = new TrackRectDrawable(progressColor);
          mScrubber = shapeDrawable;
          mScrubber.setCallback(this);
  
          mThumb = new ThumbDrawable(progressColor, thumbSize);
          mThumb.setCallback(this);
          mThumb.setBounds(0, 0, mThumb.getIntrinsicWidth(), mThumb.getIntrinsicHeight());
  
  
          if (!editMode) {
              mIndicator = new PopupIndicator(context, attrs, defStyleAttr, convertValueToMessage(mMax),
                      thumbSize, thumbSize + mAddedTouchBounds + separation);
              mIndicator.setListener(mFloaterListener);
          }
          a.recycle();
  
          setNumericTransformer(new DefaultNumericTransformer());
  
      }
  
      /**
       * Sets the current Indicator formatter string
       *
       * @param formatter
       * @see String#format(String, Object...)
       * @see #setNumericTransformer(NumericTransformer)
       */
      public void setIndicatorFormatter(@Nullable String formatter) {
          mIndicatorFormatter = formatter;
          updateProgressMessage(mValue);
      }
  
      /**
       * Sets the current {@link NumericTransformer}
       *
       * @param transformer
       * @see #getNumericTransformer()
       */
      public void setNumericTransformer(@Nullable NumericTransformer transformer) {
          mNumericTransformer = transformer != null ? transformer : new DefaultNumericTransformer();
          //We need to refresh the PopupIndicator view
          updateIndicatorSizes();
          updateProgressMessage(mValue);
      }
  
      /**
       * Retrieves the current {@link NumericTransformer}
       *
       * @return NumericTransformer
       * @see #setNumericTransformer
       */
      public NumericTransformer getNumericTransformer() {
          return mNumericTransformer;
      }
  
      /**
       * Sets the maximum value for this DiscreteSeekBar
       * if the supplied argument is smaller than the Current MIN value,
       * the MIN value will be set to MAX-1
       * <p/>
       * <p>
       * Also if the current progress is out of the new range, it will be set to MIN
       * </p>
       *
       * @param max
       * @see #setMin(int)
       * @see #setProgress(int)
       */
      public void setMax(int max) {
          if (mMax == max) return;
          mMax = max;
          if (mMax < mMin) {
              setMin(mMax - 1);
          }
          updateKeyboardRange();
  
          //We need to refresh the PopupIndicator view
          updateIndicatorSizes();
      }
  
      public int getMax() {
          return mMax;
      }
  
      /**
       * Sets the minimum value for this DiscreteSeekBar
       * if the supplied argument is bigger than the Current MAX value,
       * the MAX value will be set to MIN+1
       * <p>
       * Also if the current progress is out of the new range, it will be set to MIN
       * </p>
       *
       * @param min
       * @see #setMax(int)
       * @see #setProgress(int)
       */
      public void setMin(int min) {
          if (mMin == min) return;
          mMin = min;
          if (mMin > mMax) {
              setMax(mMin + 1);
          }
          updateKeyboardRange();
      }
  
      public int getMin() {
          return mMin;
      }
  
      /**
       * Sets the current progress for this DiscreteSeekBar
       * The supplied argument will be capped to the current MIN-MAX range
       *
       * @param progress
       * @see #setMax(int)
       * @see #setMin(int)
       */
      public void setProgress(int progress) {
          setProgress(progress, false);
      }
  
      private void setProgress(int value, boolean fromUser) {
          value = Math.max(mMin, Math.min(mMax, value));
          if (isAnimationRunning()) {
              mPositionAnimator.cancel();
          }
  
          mValue = value;
          notifyProgress(value, fromUser);
          updateProgressMessage(value);
          updateThumbPosFromCurrentProgress();
      }
  
      /**
       * Get the current progress
       *
       * @return the current progress :-P
       */
      public int getProgress() {
          return mValue;
      }
  
      /**
       * Sets a listener to receive notifications of changes to the DiscreteSeekBar's progress level. Also
       * provides notifications of when the DiscreteSeekBar shows/hides the bubble indicator.
       *
       * @param listener The seek bar notification listener
       * @see OnProgressChangeListener
       */
      public void setOnProgressChangeListener(@Nullable OnProgressChangeListener listener) {
          mPublicChangeListener = listener;
      }
  
      /**
       * Sets the color of the seek thumb, as well as the color of the popup indicator.
       *
       * @param thumbColor     The color the seek thumb will be changed to
       * @param indicatorColor The color the popup indicator will be changed to
       *                       The indicator will animate from thumbColor to indicatorColor
       *                       when opening
       */
      public void setThumbColor(int thumbColor, int indicatorColor) {
          mThumb.setColorStateList(ColorStateList.valueOf(thumbColor));
          mIndicator.setColors(indicatorColor, thumbColor);
      }
  
      /**
       * Sets the color of the seek thumb, as well as the color of the popup indicator.
       *
       * @param thumbColorStateList The ColorStateList the seek thumb will be changed to
       * @param indicatorColor      The color the popup indicator will be changed to
       *                            The indicator will animate from thumbColorStateList(pressed state) to indicatorColor
       *                            when opening
       */
      public void setThumbColor(@NonNull ColorStateList thumbColorStateList, int indicatorColor) {
          mThumb.setColorStateList(thumbColorStateList);
          //we use the "pressed" color to morph the indicator from it to its own color
          int thumbColor = thumbColorStateList.getColorForState(new int[]{PRESSED_STATE}, thumbColorStateList.getDefaultColor());
          mIndicator.setColors(indicatorColor, thumbColor);
      }
  
      /**
       * Sets the color of the seekbar scrubber
       *
       * @param color The color the track  scrubber will be changed to
       */
      public void setScrubberColor(int color) {
          mScrubber.setColorStateList(ColorStateList.valueOf(color));
      }
  
      /**
       * Sets the color of the seekbar scrubber
       *
       * @param colorStateList The ColorStateList the track scrubber will be changed to
       */
      public void setScrubberColor(@NonNull ColorStateList colorStateList) {
          mScrubber.setColorStateList(colorStateList);
      }
  
      /**
       * Sets the color of the seekbar ripple
       *
       * @param color The color the track  ripple will be changed to
       */
      public void setRippleColor(int color) {
          setRippleColor(new ColorStateList(new int[][]{new int[]{}}, new int[]{color}));
      }
  
      /**
       * Sets the color of the seekbar ripple
       *
       * @param colorStateList The ColorStateList the track ripple will be changed to
       */
      public void setRippleColor(@NonNull ColorStateList colorStateList) {
          SeekBarCompat.setRippleColor(mRipple, colorStateList);
      }
  
      /**
       * Sets the color of the seekbar scrubber
       *
       * @param color The color the track will be changed to
       */
      public void setTrackColor(int color) {
          mTrack.setColorStateList(ColorStateList.valueOf(color));
      }
  
      /**
       * Sets the color of the seekbar scrubber
       *
       * @param colorStateList The ColorStateList the track will be changed to
       */
      public void setTrackColor(@NonNull ColorStateList colorStateList) {
          mTrack.setColorStateList(colorStateList);
      }
  
      /**
       * If {@code enabled} is false the indicator won't appear. By default popup indicator is
       * enabled.
       */
      public void setIndicatorPopupEnabled(boolean enabled) {
          this.mIndicatorPopupEnabled = enabled;
      }
  
      private void updateIndicatorSizes() {
          if (!isInEditMode()) {
              if (mNumericTransformer.useStringTransform()) {
                  mIndicator.updateSizes(mNumericTransformer.transformToString(mMax));
              } else {
                  mIndicator.updateSizes(convertValueToMessage(mNumericTransformer.transform(mMax)));
              }
          }
  
      }
  
      private void notifyProgress(int value, boolean fromUser) {
          if (mPublicChangeListener != null) {
              mPublicChangeListener.onProgressChanged(DiscreteSeekBar.this, value, fromUser);
          }
          onValueChanged(value);
      }
  
      private void notifyBubble(boolean open) {
          if (open) {
              onShowBubble();
          } else {
              onHideBubble();
          }
      }
  
      /**
       * When the {@link DiscreteSeekBar} enters pressed or focused state
       * the bubble with the value will be shown, and this method called
       * <p>
       * Subclasses may override this to add functionality around this event
       * </p>
       */
      protected void onShowBubble() {
      }
  
      /**
       * When the {@link DiscreteSeekBar} exits pressed or focused state
       * the bubble with the value will be hidden, and this method called
       * <p>
       * Subclasses may override this to add functionality around this event
       * </p>
       */
      protected void onHideBubble() {
      }
  
      /**
       * When the {@link DiscreteSeekBar} value changes this method is called
       * <p>
       * Subclasses may override this to add functionality around this event
       * without having to specify a {@link OnProgressChangeListener}
       * </p>
       */
      protected void onValueChanged(int value) {
      }
  
      private void updateKeyboardRange() {
          int range = mMax - mMin;
          if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
              // It will take the user too long to change this via keys, change it
              // to something more reasonable
              mKeyProgressIncrement = Math.max(1, Math.round((float) range / 20));
          }
      }
  
  
      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          int widthSize = MeasureSpec.getSize(widthMeasureSpec);
          int height = mThumb.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom();
          height += (mAddedTouchBounds * 2);
          setMeasuredDimension(widthSize, height);
      }
  
      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
          super.onLayout(changed, left, top, right, bottom);
          if (changed) {
              removeCallbacks(mShowIndicatorRunnable);
              if (!isInEditMode()) {
                  mIndicator.dismissComplete();
              }
              updateFromDrawableState();
          }
      }
  
      @Override
      public void scheduleDrawable(Drawable who, Runnable what, long when) {
          super.scheduleDrawable(who, what, when);
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
          int thumbWidth = mThumb.getIntrinsicWidth();
          int thumbHeight = mThumb.getIntrinsicHeight();
          int addedThumb = mAddedTouchBounds;
          int halfThumb = thumbWidth / 2;
          int paddingLeft = getPaddingLeft() + addedThumb;
          int paddingRight = getPaddingRight();
          int bottom = getHeight() - getPaddingBottom() - addedThumb;
          mThumb.setBounds(paddingLeft, bottom - thumbHeight, paddingLeft + thumbWidth, bottom);
          int trackHeight = Math.max(mTrackHeight / 2, 1);
          mTrack.setBounds(paddingLeft + halfThumb, bottom - halfThumb - trackHeight,
                  getWidth() - halfThumb - paddingRight - addedThumb, bottom - halfThumb + trackHeight);
          int available = getWidth() - paddingRight - addedThumb - paddingLeft - thumbWidth;
          float scaleDrawBase = (mValueBase - mMin) / (float) (mMax - mMin);
          final int thumbPosBase = (int) (scaleDrawBase * available + 0.5f);
          mTrackBase.setBounds(paddingLeft + halfThumb + thumbPosBase - mTrackBaseHeight / 8, bottom - halfThumb - mTrackBaseHeight / 2,
                  paddingLeft + halfThumb + thumbPosBase + mTrackBaseHeight / 8, bottom - halfThumb + mTrackBaseHeight / 2);
          int scrubberHeight = Math.max(mScrubberHeight / 2, 2);
          mScrubber.setBounds(paddingLeft + halfThumb, bottom - halfThumb - scrubberHeight,
                  paddingLeft + halfThumb, bottom - halfThumb + scrubberHeight);
  
          //Update the thumb position after size changed
          updateThumbPosFromCurrentProgress();
      }
  
      @Override
      protected synchronized void onDraw(Canvas canvas) {
          if (!isLollipopOrGreater) {
              mRipple.draw(canvas);
          }
          super.onDraw(canvas);
          mTrack.draw(canvas);
          mScrubber.draw(canvas);
          if (mMin != mValueBase && mMax != mValueBase) {
              mTrackBase.draw(canvas);
          }
          mThumb.draw(canvas);
      }
  
      @Override
      protected void drawableStateChanged() {
          super.drawableStateChanged();
          updateFromDrawableState();
      }
  
      private void updateFromDrawableState() {
          int[] state = getDrawableState();
          boolean focused = false;
          boolean pressed = false;
          for (int i : state) {
              if (i == FOCUSED_STATE) {
                  focused = true;
              } else if (i == PRESSED_STATE) {
                  pressed = true;
              }
          }
          if (isEnabled() && (focused || pressed) && mIndicatorPopupEnabled) {
              //We want to add a small delay here to avoid
              //poping in/out on simple taps
              removeCallbacks(mShowIndicatorRunnable);
              postDelayed(mShowIndicatorRunnable, INDICATOR_DELAY_FOR_TAPS);
          } else {
              hideFloater();
          }
          mThumb.setState(state);
          mTrack.setState(state);
          mScrubber.setState(state);
          mRipple.setState(state);
      }
  
      private void updateProgressMessage(int value) {
          if (!isInEditMode()) {
              if (mNumericTransformer.useStringTransform()) {
                  mIndicator.setValue(mNumericTransformer.transformToString(value));
              } else {
                  mIndicator.setValue(convertValueToMessage(mNumericTransformer.transform(value)));
              }
          }
      }
  
      private String convertValueToMessage(int value) {
          String format = mIndicatorFormatter != null ? mIndicatorFormatter : DEFAULT_FORMATTER;
          //We're trying to re-use the Formatter here to avoid too much memory allocations
          //But I'm not completey sure if it's doing anything good... :(
          //Previously, this condition was wrong so the Formatter was always re-created
          //But as I fixed the condition, the formatter started outputting trash characters from previous
          //calls, so I mark the StringBuilder as empty before calling format again.
  
          //Anyways, I see the memory usage still go up on every call to this method
          //and I have no clue on how to fix that... damn Strings...
          if (mFormatter == null || !mFormatter.locale().equals(Locale.getDefault())) {
              int bufferSize = format.length() + String.valueOf(mMax).length();
              if (mFormatBuilder == null) {
                  mFormatBuilder = new StringBuilder(bufferSize);
              } else {
                  mFormatBuilder.ensureCapacity(bufferSize);
              }
              mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
          } else {
              mFormatBuilder.setLength(0);
          }
          return mFormatter.format(format, value).toString();
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent event) {
          if (!isEnabled()) {
              return false;
          }
          switch (event.getAction()) {
              case MotionEvent.ACTION_DOWN:
                  mDownX = event.getX();
                  startDragging(event, isInScrollingContainer());
                  break;
              case MotionEvent.ACTION_MOVE:
                  if (isDragging()) {
                      updateDragging(event);
                  } else {
                      final float x = event.getX();
                      if (Math.abs(x - mDownX) > mTouchSlop) {
                          startDragging(event, false);
                      }
                  }
                  break;
              case MotionEvent.ACTION_UP:
                  if (!isDragging() && mAllowTrackClick) {
                      startDragging(event, false);
                      updateDragging(event);
                  }
                  stopDragging();
                  break;
              case MotionEvent.ACTION_CANCEL:
                  stopDragging();
                  break;
          }
          return true;
      }
  
      private boolean isInScrollingContainer() {
          return SeekBarCompat.isInScrollingContainer(getParent());
      }
  
      private boolean startDragging(MotionEvent ev, boolean ignoreTrackIfInScrollContainer) {
          final Rect bounds = mTempRect;
          mThumb.copyBounds(bounds);
          //Grow the current thumb rect for a bigger touch area
          bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds);
          mIsDragging = (bounds.contains((int) ev.getX(), (int) ev.getY()));
          if (!mIsDragging && mAllowTrackClick && !ignoreTrackIfInScrollContainer) {
              //If the user clicked outside the thumb, we compute the current position
              //and force an immediate drag to it.
              mIsDragging = true;
              mDragOffset = (bounds.width() / 2) - mAddedTouchBounds;
              updateDragging(ev);
              //As the thumb may have moved, get the bounds again
              mThumb.copyBounds(bounds);
              bounds.inset(-mAddedTouchBounds, -mAddedTouchBounds);
          }
          if (mIsDragging) {
              setPressed(true);
              attemptClaimDrag();
              setHotspot(ev.getX(), ev.getY());
              mDragOffset = (int) (ev.getX() - bounds.left - mAddedTouchBounds);
              if (mPublicChangeListener != null) {
                  mPublicChangeListener.onStartTrackingTouch(this);
              }
          }
          return mIsDragging;
      }
  
      private boolean isDragging() {
          return mIsDragging;
      }
  
      private void stopDragging() {
          if (mPublicChangeListener != null) {
              mPublicChangeListener.onStopTrackingTouch(this);
          }
          mIsDragging = false;
          setPressed(false);
      }
  
      @Override
      public boolean onKeyDown(int keyCode, KeyEvent event) {
          //TODO: Should we reverse the keys for RTL? The framework's SeekBar does NOT....
          boolean handled = false;
          if (isEnabled()) {
              int progress = getAnimatedProgress();
              switch (keyCode) {
                  case KeyEvent.KEYCODE_DPAD_LEFT:
                      handled = true;
                      if (progress <= mMin) break;
                      animateSetProgress(progress - mKeyProgressIncrement);
                      break;
                  case KeyEvent.KEYCODE_DPAD_RIGHT:
                      handled = true;
                      if (progress >= mMax) break;
                      animateSetProgress(progress + mKeyProgressIncrement);
                      break;
              }
          }
  
          return handled || super.onKeyDown(keyCode, event);
      }
  
      private int getAnimatedProgress() {
          return isAnimationRunning() ? getAnimationTarget() : mValue;
      }
  
  
      boolean isAnimationRunning() {
          return mPositionAnimator != null && mPositionAnimator.isRunning();
      }
  
      void animateSetProgress(int progress) {
          final float curProgress = isAnimationRunning() ? getAnimationPosition() : getProgress();
  
          if (progress < mMin) {
              progress = mMin;
          } else if (progress > mMax) {
              progress = mMax;
          }
          //setProgressValueOnly(progress);
  
          if (mPositionAnimator != null) {
              mPositionAnimator.cancel();
          }
  
          mAnimationTarget = progress;
          mPositionAnimator = AnimatorCompat.create(curProgress,
                  progress, new AnimatorCompat.AnimationFrameUpdateListener() {
                      @Override
                      public void onAnimationFrame(float currentValue) {
                          setAnimationPosition(currentValue);
                      }
                  });
          mPositionAnimator.setDuration(PROGRESS_ANIMATION_DURATION);
          mPositionAnimator.start();
      }
  
      private int getAnimationTarget() {
          return mAnimationTarget;
      }
  
      void setAnimationPosition(float position) {
          mAnimationPosition = position;
          float currentScale = (position - mMin) / (float) (mMax - mMin);
          updateProgressFromAnimation(currentScale);
      }
  
      float getAnimationPosition() {
          return mAnimationPosition;
      }
  
  
      private void updateDragging(MotionEvent ev) {
          setHotspot(ev.getX(), ev.getY());
          int x = (int) ev.getX();
          Rect oldBounds = mThumb.getBounds();
          int halfThumb = oldBounds.width() / 2;
          int addedThumb = mAddedTouchBounds;
          int newX = x - mDragOffset + halfThumb;
          int left = getPaddingLeft() + halfThumb + addedThumb;
          int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb);
          if (newX < left) {
              newX = left;
          } else if (newX > right) {
              newX = right;
          }
  
          int available = right - left;
          float scale = (float) (newX - left) / (float) available;
          if (isRtl()) {
              scale = 1f - scale;
          }
          int progress = Math.round((scale * (mMax - mMin)) + mMin);
          setProgress(progress, true);
      }
  
      private void updateProgressFromAnimation(float scale) {
          Rect bounds = mThumb.getBounds();
          int halfThumb = bounds.width() / 2;
          int addedThumb = mAddedTouchBounds;
          int left = getPaddingLeft() + halfThumb + addedThumb;
          int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb);
          int available = right - left;
          int progress = Math.round((scale * (mMax - mMin)) + mMin);
          //we don't want to just call setProgress here to avoid the animation being cancelled,
          //and this position is not bound to a real progress value but interpolated
          if (progress != getProgress()) {
              mValue = progress;
              notifyProgress(mValue, true);
              updateProgressMessage(progress);
          }
          final int thumbPos = (int) (scale * available + 0.5f);
  
          float scaleDrawBase = (mValueBase - mMin) / (float) (mMax - mMin);
          final int thumbPosBase = (int) (scaleDrawBase * available + 0.5f);
  
          updateThumbPos(thumbPosBase, thumbPos);
      }
  
      private void updateThumbPosFromCurrentProgress() {
          int thumbWidth = mThumb.getIntrinsicWidth();
          int addedThumb = mAddedTouchBounds;
          int halfThumb = thumbWidth / 2;
  
          //This doesn't matter if RTL, as we just need the "avaiable" area
          int left = getPaddingLeft() + halfThumb + addedThumb;
          int right = getWidth() - (getPaddingRight() + halfThumb + addedThumb);
          int available = right - left;
  
          float scaleDraw = (mValue - mMin) / (float) (mMax - mMin);
          float scaleDrawBase = (mValueBase - mMin) / (float) (mMax - mMin);
  
          final int thumbPos = (int) (scaleDraw * available + 0.5f);
          final int thumbPosBase = (int) (scaleDrawBase * available + 0.5f);
          updateThumbPos(thumbPosBase, thumbPos);
      }
  
      private void updateThumbPos(int base, int value) {
          base += getPaddingLeft() + mAddedTouchBounds;
          value += getPaddingLeft() + mAddedTouchBounds;
          int start = Math.min(base, value);
          int end = Math.max(base, value);
  
          int thumbWidth = mThumb.getIntrinsicWidth();
          int halfThumb = thumbWidth / 2;
          mThumb.copyBounds(mInvalidateRect);
          mThumb.setBounds(value, mInvalidateRect.top, value + thumbWidth, mInvalidateRect.bottom);
          mScrubber.getBounds().left = start + halfThumb;
          mScrubber.getBounds().right = end + halfThumb;
          final Rect finalBounds = mTempRect;
          mThumb.copyBounds(finalBounds);
          if (!isInEditMode()) {
              mIndicator.move(finalBounds.centerX());
          }
          mTrackBase.getBounds().left = base + halfThumb - mTrackBaseHeight / 8;
          mTrackBase.getBounds().right = base + halfThumb + mTrackBaseHeight / 8;
          mInvalidateRect.inset(-mAddedTouchBounds, -mAddedTouchBounds);
          finalBounds.inset(-mAddedTouchBounds, -mAddedTouchBounds);
          mInvalidateRect.union(finalBounds);
          SeekBarCompat.setHotspotBounds(mRipple, finalBounds.left, finalBounds.top, finalBounds.right, finalBounds.bottom);
          invalidate(mInvalidateRect);
      }
  
  
      private void setHotspot(float x, float y) {
          DrawableCompat.setHotspot(mRipple, x, y);
      }
  
      @Override
      protected boolean verifyDrawable(Drawable who) {
          return who == mThumb || who == mTrack || who == mScrubber || who == mRipple || super.verifyDrawable(who);
      }
  
      private void attemptClaimDrag() {
          ViewParent parent = getParent();
          if (parent != null) {
              parent.requestDisallowInterceptTouchEvent(true);
          }
      }
  
      private Runnable mShowIndicatorRunnable = new Runnable() {
          @Override
          public void run() {
              showFloater();
          }
      };
  
      private void showFloater() {
          if (!isInEditMode()) {
              mThumb.animateToPressed();
              mIndicator.showIndicator(this, mThumb.getBounds());
              notifyBubble(true);
          }
      }
  
      private void hideFloater() {
          removeCallbacks(mShowIndicatorRunnable);
          if (!isInEditMode()) {
              mIndicator.dismiss();
              notifyBubble(false);
          }
      }
  
      private MarkerDrawable.MarkerAnimationListener mFloaterListener = new MarkerDrawable.MarkerAnimationListener() {
          @Override
          public void onClosingComplete() {
              mThumb.animateToNormal();
          }
  
          @Override
          public void onOpeningComplete() {
  
          }
  
      };
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          removeCallbacks(mShowIndicatorRunnable);
          if (!isInEditMode()) {
              mIndicator.dismissComplete();
          }
      }
  
      public boolean isRtl() {
          return (ViewCompat.getLayoutDirection(this) == LAYOUT_DIRECTION_RTL) && mMirrorForRtl;
      }
  
      @Override
      protected Parcelable onSaveInstanceState() {
          Parcelable superState = super.onSaveInstanceState();
          CustomState state = new CustomState(superState);
          state.progress = getProgress();
          state.max = mMax;
          state.min = mMin;
          return state;
      }
  
      @Override
      protected void onRestoreInstanceState(Parcelable state) {
          if (state == null || !state.getClass().equals(CustomState.class)) {
              super.onRestoreInstanceState(state);
              return;
          }
  
          CustomState customState = (CustomState) state;
          setMin(customState.min);
          setMax(customState.max);
          setProgress(customState.progress, false);
          super.onRestoreInstanceState(customState.getSuperState());
      }
  
      static class CustomState extends BaseSavedState {
          private int progress;
          private int max;
          private int min;
  
          public CustomState(Parcel source) {
              super(source);
              progress = source.readInt();
              max = source.readInt();
              min = source.readInt();
          }
  
          public CustomState(Parcelable superState) {
              super(superState);
          }
  
          @Override
          public void writeToParcel(Parcel outcoming, int flags) {
              super.writeToParcel(outcoming, flags);
              outcoming.writeInt(progress);
              outcoming.writeInt(max);
              outcoming.writeInt(min);
          }
  
          public static final Creator<CustomState> CREATOR =
                  new Creator<CustomState>() {
  
                      @Override
                      public CustomState[] newArray(int size) {
                          return new CustomState[size];
                      }
  
                      @Override
                      public CustomState createFromParcel(Parcel incoming) {
                          return new CustomState(incoming);
                      }
                  };
      }
  }
  