/* * Copyright 2015 Hippo Seven * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.hippo.widget; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.v4.graphics.drawable.DrawableCompat; import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.AbsoluteLayout; import android.widget.PopupWindow; import com.hippo.nimingban.R; import com.hippo.yorozuya.AnimationUtils; import com.hippo.yorozuya.LayoutUtils; import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.SimpleHandler; public class Slider extends View { private static final char[] CHARACTERS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; private static final int BUBBLE_WIDTH = 26; private static final int BUBBLE_HEIGHT = 32; private Context mContext; private Paint mPaint; private Paint mBgPaint; private final RectF mLeftRectF = new RectF(); private final RectF mRightRectF = new RectF(); private PopupWindow mPopup; private BubbleView mBubble; private int mStart; private int mEnd; private int mProgress; private float mPercent; private int mDrawProgress; private float mDrawPercent; private int mTargetProgress; private float mThickness; private float mRadius; private float mCharWidth; private float mCharHeight; private int mBubbleWidth; private int mBubbleHeight; private int mBubbleMinWidth; private int mBubbleMinHeight; private int mPopupX; private int mPopupY; private int mPopupWidth; private final int[] mTemp = new int[2]; private boolean mReverse = false; private boolean mShowBubble; private float mDrawBubbleScale = 0f; private ValueAnimator mProgressAnimation; private ValueAnimator mBubbleScaleAnimation; private OnSetProgressListener mListener; private CheckForShowBubble mCheckForShowBubble; public Slider(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public Slider(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @SuppressWarnings("deprecation") private void init(Context context, AttributeSet attrs) { mContext = context; mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setTextAlign(Paint.Align.CENTER); Resources resources = context.getResources(); mBubbleMinWidth = resources.getDimensionPixelOffset(R.dimen.slider_bubble_width); mBubbleMinHeight = resources.getDimensionPixelOffset(R.dimen.slider_bubble_height); mBubble = new BubbleView(context, textPaint); mBubble.setScaleX(0.0f); mBubble.setScaleY(0.0f); AbsoluteLayout absoluteLayout = new AbsoluteLayout(context); absoluteLayout.addView(mBubble); absoluteLayout.setBackgroundDrawable(null); mPopup = new PopupWindow(absoluteLayout); mPopup.setOutsideTouchable(false); mPopup.setTouchable(false); mPopup.setFocusable(false); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Slider); textPaint.setColor(a.getColor(R.styleable.Slider_textColor, Color.WHITE)); textPaint.setTextSize(a.getDimensionPixelSize(R.styleable.Slider_textSize, 12)); updateTextSize(); setRange(a.getInteger(R.styleable.Slider_start, 0), a.getInteger(R.styleable.Slider_end, 0)); setProgress(a.getInteger(R.styleable.Slider_slider_progress, 0)); mThickness = a.getDimension(R.styleable.Slider_thickness, 2); mRadius = a.getDimension(R.styleable.Slider_radius, 6); setColor(a.getColor(R.styleable.Slider_color, Color.BLACK)); mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBgPaint.setColor(a.getBoolean(R.styleable.Slider_dark, false) ? 0x4dffffff : 0x42000000); a.recycle(); mProgressAnimation = new ValueAnimator(); mProgressAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); mProgressAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(@NonNull ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); mDrawPercent = value; mDrawProgress = Math.round(MathUtils.lerp((float) mStart, mEnd, value)); updateBubblePosition(); mBubble.setProgress(mDrawProgress); invalidate(); } }); mBubbleScaleAnimation = new ValueAnimator(); mBubbleScaleAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(@NonNull ValueAnimator animation) { float value = (Float) animation.getAnimatedValue(); mDrawBubbleScale = value; mBubble.setScaleX(value); mBubble.setScaleY(value); invalidate(); } }); } private void updateTextSize() { int length = CHARACTERS.length; float[] widths = new float[length]; mPaint.getTextWidths(CHARACTERS, 0, length, widths); mCharWidth = 0.0f; for (float f : widths) { mCharWidth = Math.max(mCharWidth, f); } Paint.FontMetrics fm = mPaint.getFontMetrics(); mCharHeight = fm.bottom - fm.top; } private void updateBubbleSize() { int oldWidth = mBubbleWidth; int oldHeight = mBubbleHeight; mBubbleWidth = (int) Math.max(mBubbleMinWidth, Integer.toString(mEnd).length() * mCharWidth + LayoutUtils.dp2pix(mContext, 8)); mBubbleHeight = (int) Math.max(mBubbleMinHeight, mCharHeight + LayoutUtils.dp2pix(mContext, 8)); if (oldWidth != mBubbleWidth && oldHeight != mBubbleHeight) { //noinspection deprecation AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) mBubble.getLayoutParams(); lp.width = mBubbleWidth; lp.height = mBubbleHeight; mBubble.setLayoutParams(lp); } } private void updatePopup() { int width = getWidth(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); getLocationInWindow(mTemp); mPopupWidth = (int) (width - mRadius - mRadius + mBubbleWidth); int popupHeight = mBubbleHeight; mPopupX = (int) (mTemp[0] + mRadius - (mBubbleWidth / 2)); mPopupY = (int) (mTemp[1] - popupHeight + paddingTop + ((getHeight() - paddingTop - paddingBottom) / 2) - mRadius -LayoutUtils.dp2pix(mContext, 2)); mPopup.update(mPopupX, mPopupY, mPopupWidth, popupHeight, false); } private void updateBubblePosition() { float x = ((mPopupWidth - mBubbleWidth) * (mReverse ? (1.0f - mDrawPercent) : mDrawPercent)); mBubble.setX(x); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); updatePopup(); updateBubblePosition(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mPopup.showAtLocation(this, Gravity.TOP | Gravity.LEFT, mPopupX, mPopupY); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mPopup.dismiss(); } private void startProgressAnimation(float percent) { float currentValue; if (mProgressAnimation.isRunning()) { mProgressAnimation.setCurrentPlayTime(mProgressAnimation.getCurrentPlayTime()); Object value = mProgressAnimation.getAnimatedValue(); if (value instanceof Float) { currentValue = (float) value; } else { currentValue = mDrawPercent; } } else { currentValue = mDrawPercent; } mProgressAnimation.cancel(); mProgressAnimation.setFloatValues(currentValue, percent); mProgressAnimation.setDuration(Math.min(500, (long) (50 * getWidth() * Math.abs(currentValue - percent)))); mProgressAnimation.start(); } private void startShowBubbleAnimation() { mBubbleScaleAnimation.cancel(); mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 1.0f); mBubbleScaleAnimation.setInterpolator(AnimationUtils.FAST_SLOW_INTERPOLATOR); mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 1.0f))); mBubbleScaleAnimation.start(); } private void startHideBubbleAnimation() { mBubbleScaleAnimation.cancel(); mBubbleScaleAnimation.setFloatValues(mDrawBubbleScale, 0.0f); mBubbleScaleAnimation.setInterpolator(AnimationUtils.SLOW_FAST_INTERPOLATOR); mBubbleScaleAnimation.setDuration((long) (300 * Math.abs(mDrawBubbleScale - 0.0f))); mBubbleScaleAnimation.start(); } public void setColor(int color) { mPaint.setColor(color); mBubble.setColor(color); invalidate(); } public void setRange(int start, int end) { mStart = start; mEnd = end; setProgress(mProgress); updateBubbleSize(); } public void setProgress(int progress) { progress = MathUtils.clamp(progress, mStart, mEnd); int oldProgress = mProgress; if (mProgress != progress) { mProgress = progress; mPercent = MathUtils.delerp(mStart, mEnd, mProgress); mTargetProgress = progress; if (mProgressAnimation == null) { // For init mDrawPercent = mPercent; mDrawProgress = mProgress; updateBubblePosition(); mBubble.setProgress(mDrawProgress); } else { startProgressAnimation(mPercent); } invalidate(); } if (mListener != null) { mListener.onSetProgress(this, progress, oldProgress, false, true); } } public int getProgress() { return mProgress; } public void setReverse(boolean reverse) { if (mReverse != reverse) { mReverse = reverse; invalidate(); } } public void setOnSetProgressListener(OnSetProgressListener listener) { mListener = listener; } @Override protected void onDraw(@NonNull Canvas canvas) { int width = getWidth(); int height = getHeight(); if (width < LayoutUtils.dp2pix(mContext, 24)) { canvas.drawRect(0, 0, width, getHeight(), mPaint); } else { int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); float thickness = mThickness; float radius = mRadius; float halfThickness = thickness / 2; int saved = canvas.save(); canvas.translate(0, paddingTop + ((height - paddingTop - paddingBottom) / 2)); float currentX = paddingLeft + radius + (width - radius - radius - paddingLeft - paddingRight) * (mReverse ? (1.0f - mDrawPercent) : mDrawPercent); mLeftRectF.set(paddingLeft + radius, -halfThickness, currentX, halfThickness); mRightRectF.set(currentX, -halfThickness, width - paddingRight - radius, halfThickness); // Draw bar if (mReverse) { canvas.drawRect(mRightRectF, mPaint); canvas.drawRect(mLeftRectF, mBgPaint); } else { canvas.drawRect(mLeftRectF, mPaint); canvas.drawRect(mRightRectF, mBgPaint); } // Draw controller float scale = 1.0f - mDrawBubbleScale; if (scale != 0.0f) { canvas.scale(scale, scale, currentX, 0); canvas.drawCircle(currentX, 0, radius, mPaint); } canvas.restoreToCount(saved); } } private void setShowBubble(boolean showBubble) { if (mShowBubble != showBubble) { mShowBubble = showBubble; if (showBubble) { startShowBubbleAnimation(); } else { startHideBubbleAnimation(); } } } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mListener != null) { if (action == MotionEvent.ACTION_DOWN) { mListener.onFingerDown(); } else if (action == MotionEvent.ACTION_UP) { mListener.onFingerUp(); } } int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); float radius = mRadius; float x = event.getX(); int progress = Math.round(MathUtils.lerp((float) mStart, (float) mEnd, MathUtils.clamp((mReverse ? (getWidth() - paddingLeft - radius - x) : (x - radius - paddingLeft)) / (getWidth() - radius - radius - paddingLeft - paddingRight), 0.0f, 1.0f))); float percent = MathUtils.delerp(mStart, mEnd, progress); // ACTION_CANCEL not changed if (action == MotionEvent.ACTION_CANCEL) { progress = mProgress; percent = mPercent; } if (mTargetProgress != progress) { mTargetProgress = progress; startProgressAnimation(percent); if (mListener != null) { mListener.onSetProgress(this, progress, mProgress, true, false); } } if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { SimpleHandler.getInstance().removeCallbacks(mCheckForShowBubble); setShowBubble(false); } else if (action == MotionEvent.ACTION_DOWN) { if (mCheckForShowBubble == null) { mCheckForShowBubble = new CheckForShowBubble(); } SimpleHandler.getInstance().postDelayed(mCheckForShowBubble, ViewConfiguration.getTapTimeout()); } if (action == MotionEvent.ACTION_UP) { int oldProgress = mProgress; if (mProgress != progress) { mProgress = progress; mPercent = mDrawPercent; } if (mListener != null) { mListener.onSetProgress(this, progress, oldProgress, true, true); } } break; } return true; } @SuppressLint("ViewConstructor") private static class BubbleView extends AppCompatImageView { private static final float TEXT_CENTER = (float) BUBBLE_WIDTH / 2.0f / BUBBLE_HEIGHT; private final Drawable mDrawable; private final Paint mTextPaint; private String mProgressStr = ""; private final Rect mRect = new Rect(); @SuppressWarnings("deprecation") public BubbleView(Context context, Paint paint) { super(context); setImageResource(R.drawable.v_slider_bubble); mDrawable = DrawableCompat.wrap(getDrawable()); setImageDrawable(mDrawable); mTextPaint = paint; } public void setColor(int color) { DrawableCompat.setTint(mDrawable, color); } public void setProgress(int progress) { String str = Integer.toString(progress); if (!str.equals(mProgressStr)) { mProgressStr = str; mTextPaint.getTextBounds(str, 0, str.length(), mRect); invalidate(); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); setPivotX(w / 2); setPivotY(h); } @Override protected void onDraw(@NonNull Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); int x = width / 2; int y = (int) ((height * TEXT_CENTER) + (mRect.height() / 2)); canvas.drawText(mProgressStr, x, y, mTextPaint); } } public interface OnSetProgressListener { void onSetProgress(Slider slider, int newProgress, int oldProgress, boolean byUser, boolean confirm); void onFingerDown(); void onFingerUp(); } private final class CheckForShowBubble implements Runnable { @Override public void run() { setShowBubble(true); } } }