Android-自定义垂直拽托进度条

示例.jpeg

样式类似iOS控制中心的音量、亮度控制。

源码

  • 自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="VerticalSeekBar">
        <!-- 背景进度颜色 -->
        <attr name="vsb_bg" format="color" />
        <!-- 已有进度颜色 -->
        <attr name="vsb_progress_bg" format="color" />
        <!-- 已有进度的蒙版颜色 -->
        <attr name="vsb_progress_mask_bg" format="color" />
        <!-- 蒙版是否开启 -->
        <attr name="vsb_has_progress_mask_enable" format="boolean" />
        <!-- 进度条圆角半径 -->
        <attr name="vsb_bg_radius" format="integer|float|dimension" />
        <!-- 当前进度值 -->
        <attr name="vsb_progress" format="float" />
        <!-- 最小进度值 -->
        <attr name="vsb_min_progress" format="float" />
        <!-- 最大进度值 -->
        <attr name="vsb_max_progress" format="float" />
        <!-- 保留高度,可以让最小值处于的高度不在最底部,值为dp -->
        <attr name="vsb_retain_progress_height" format="integer|float|dimension"/>
        <!-- 是否debug模式,debug模式会把当前进度绘制到View上,方便调试 -->
        <attr name="vsb_is_debug" format="boolean"/>
    </declare-styleable>
</resources>
  • 代码实现
/**
 * 垂直拽托进度条
 */
public class VerticalSeekBar extends View {
    /**
     * View默认最小宽度
     */
    private int mDefaultWidth;
    /**
     * View默认最小高度
     */
    private int mDefaultHeight;
    /**
     * 控件宽
     */
    private int mViewWidth;
    /**
     * 控件高
     */
    private int mViewHeight;
    /**
     * 背景颜色
     */
    private int mBgColor;
    /**
     * 进度背景颜色
     */
    private int mProgressBgColor;
    /**
     * 进度蒙版颜色
     */
    private int mProgressMaskBgColor;
    /**
     * 进度蒙版是否开启
     */
    private boolean mProgressMaskEnable;
    /**
     * 进度条的圆角半径
     */
    private int mBgRadius;
    /**
     * 当前进度
     */
    private float mProgress;
    /**
     * 最小进度值
     */
    private float mMin;
    /**
     * 最大进度值
     */
    private float mMax;
    /**
     * 最小保留高度,可以让进度条不拖到底
     */
    private int mRetainProgressHeight;
    /**
     * 是否debug调试
     */
    private boolean mIsDebug;
    /**
     * 背景画笔
     */
    private Paint mBgPaint;
    /**
     * 进度画笔
     */
    private Paint mProgressPaint;
    /**
     * 进度蒙版画笔
     */
    private Paint mProgressMarkBgPaint;
    /**
     * Debug信息画笔
     */
    private Paint mDebugPaint;
    /**
     * 进度更新监听
     */
    private VerticalSeekBar.OnProgressUpdateListener mOnProgressUpdateListener;

    public VerticalSeekBar(Context context) {
        this(context, null);
    }

    public VerticalSeekBar(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VerticalSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        initAttr(context, attrs, defStyleAttr);
        //取消硬件加速
        setLayerType(LAYER_TYPE_SOFTWARE, null);
        //背景画笔
        mBgPaint = new Paint();
        mBgPaint.setAntiAlias(true);
        mBgPaint.setColor(mBgColor);
        mBgPaint.setStyle(Paint.Style.FILL);
        //进度画笔
        mProgressPaint = new Paint();
        mProgressPaint.setColor(mProgressBgColor);
        mProgressPaint.setAntiAlias(true);
        mProgressPaint.setStyle(Paint.Style.FILL);
        //进度蒙版画笔
        mProgressMarkBgPaint = new Paint();
        mProgressMarkBgPaint.setColor(mProgressMaskBgColor);
        mProgressMarkBgPaint.setAntiAlias(true);
        mProgressMarkBgPaint.setStyle(Paint.Style.FILL);
        //Debug信息画笔
        mDebugPaint = new Paint();
        mDebugPaint.setColor(Color.parseColor("#FF0000"));
        mDebugPaint.setTextSize(sp2px(context, 12f));
        mDebugPaint.setTextAlign(Paint.Align.CENTER);
        mDebugPaint.setAntiAlias(true);
        mDebugPaint.setStyle(Paint.Style.FILL);
        //计算默认宽、高
        mDefaultWidth = dip2px(context, 36f);
        mDefaultHeight = dip2px(context, 114f);
    }

    private void initAttr(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        int defaultBgColor = Color.parseColor("#EDF0FA");
        int defaultProgressBgColor = Color.parseColor("#6D79FE");
        int defaultProgressMaskBgColor = Color.parseColor("#33000000");
        int defaultBgRadius = dip2px(context, 8f);
        float defaultProgress = 0;
        float defaultMinProgress = 0;
        int defaultMaxProgress = 100;
        //默认不设置保留高度
        int defaultRetainProgressHeight = 0;
        if (attrs != null) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.VerticalSeekBar, defStyleAttr, 0);
            //进度背景颜色
            mBgColor = array.getColor(R.styleable.VerticalSeekBar_vsb_bg, defaultBgColor);
            //已有进度的背景颜色
            mProgressBgColor = array.getColor(R.styleable.VerticalSeekBar_vsb_progress_bg, defaultProgressBgColor);
            //已有进度的蒙版颜色
            mProgressMaskBgColor = array.getColor(R.styleable.VerticalSeekBar_vsb_progress_mask_bg, defaultProgressMaskBgColor);
            //是否开启蒙版
            mProgressMaskEnable = array.getBoolean(R.styleable.VerticalSeekBar_vsb_has_progress_mask_enable, false);
            //进度条的圆角
            mBgRadius = array.getDimensionPixelSize(R.styleable.VerticalSeekBar_vsb_bg_radius, defaultBgRadius);
            //当前进度值
            mProgress = array.getFloat(R.styleable.VerticalSeekBar_vsb_progress, defaultProgress);
            //最小进度值
            mMin = array.getFloat(R.styleable.VerticalSeekBar_vsb_min_progress, defaultMinProgress);
            //最大进度值
            mMax = array.getFloat(R.styleable.VerticalSeekBar_vsb_max_progress, defaultMaxProgress);
            //保留高度
            mRetainProgressHeight = array.getDimensionPixelSize(R.styleable.VerticalSeekBar_vsb_retain_progress_height, defaultRetainProgressHeight);
            //是否Debug调试
            mIsDebug = array.getBoolean(R.styleable.VerticalSeekBar_vsb_is_debug, false);
            array.recycle();
        } else {
            mBgColor = defaultBgColor;
            mProgressBgColor = defaultProgressBgColor;
            mProgressMaskBgColor = defaultProgressMaskBgColor;
            mProgressMaskEnable = false;
            mProgress = defaultProgress;
            mMin = defaultMinProgress;
            mMax = defaultMaxProgress;
            mRetainProgressHeight = defaultRetainProgressHeight;
            mIsDebug = false;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //裁切圆角
        clipRound(canvas);
        //画背景
        drawBg(canvas);
        //画进度条
        drawProgress(canvas);
        if (mProgressMaskEnable) {
            //画进度条蒙版
            drawProgressMark(canvas);
        }
        if (mIsDebug) {
            //画Debug信息
            drawDebugInfo(canvas);
        }
    }

    //------------ getFrameXxx()方法都是处理padding ------------

    private float getFrameLeft() {
        return getPaddingStart();
    }

    private float getFrameRight() {
        return mViewWidth - getPaddingEnd();
    }

    private float getFrameTop() {
        return getPaddingTop();
    }

    private float getFrameBottom() {
        return mViewHeight - getPaddingBottom();
    }

    //------------ getFrameXxx()方法都是处理padding ------------

    /**
     * 裁剪圆角
     */
    private void clipRound(Canvas canvas) {
        Path path = new Path();
        RectF roundRect = new RectF(getFrameLeft(), getFrameTop(), getFrameRight(), getFrameBottom());
        path.addRoundRect(roundRect, mBgRadius, mBgRadius, Path.Direction.CW);
        canvas.clipPath(path);
    }

    /**
     * 画背景
     */
    private void drawBg(Canvas canvas) {
        canvas.drawRect(new RectF(getFrameLeft(), getFrameTop(),
                        getFrameRight(), getFrameBottom()),
                mBgPaint);
    }

    /**
     * 画进度
     */
    private void drawProgress(Canvas canvas) {
        drawProgressByPaint(canvas, mProgressPaint);
    }

    /**
     * 画进度蒙版
     */
    private void drawProgressMark(Canvas canvas) {
        drawProgressByPaint(canvas, mProgressMarkBgPaint);
    }

    /**
     * 画Debug信息
     */
    private void drawDebugInfo(Canvas canvas) {
        float halfWidth = (mViewWidth - getPaddingStart() - getPaddingEnd()) / 2f;
        float halfHeight = (mViewHeight - getPaddingTop() - getPaddingBottom()) / 2f;
        //计算baseline
        Paint.FontMetrics fontMetrics = mDebugPaint.getFontMetrics();
        float distance = ((fontMetrics.bottom - fontMetrics.top) / 2f) - fontMetrics.bottom;
        float baseline = halfHeight + distance;
        //获取当前进度值,并只取1位小数值
        float progressRatio = getProgressRatio();
        String text = floatValueRetainFormat(1, progressRatio) + "";
        canvas.drawText(
                text,
                halfWidth,
                baseline,
                mDebugPaint
        );
    }

    /**
     * 画进度,传画笔,因为进度背景和蒙版其实是一样的,只是用的画笔不同
     */
    private void drawProgressByPaint(Canvas canvas, Paint paint) {
        float contentHeight = mViewHeight - getPaddingTop() - getPaddingBottom();
        float progressRatio = getProgressRatio();
        //计算进度条可用进度,总高度 - 设置的保留高度
        float usableHeight = contentHeight - mRetainProgressHeight;
        //计算出当前进度的top值
        float top;
        if (progressRatio == 0) {
            top = usableHeight;
        } else {
            top = usableHeight * (1 - progressRatio);
        }
        //画进度矩形
        RectF rect = new RectF(getFrameLeft(),
                top,
                getFrameRight(),
                getFrameBottom());
        canvas.drawRect(rect, paint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(handleMeasure(widthMeasureSpec, true),
                handleMeasure(heightMeasureSpec, false));
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        //拦截Down事件,然后让父类不进行拦截
        if (action == MotionEvent.ACTION_DOWN) {
            getParent().requestDisallowInterceptTouchEvent(true);
            if (mOnProgressUpdateListener != null) {
                mOnProgressUpdateListener.onStartTrackingTouch(this);
            }
            return true;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        int contentHeight = mViewHeight - getPaddingTop() - getPaddingBottom();
        if (action == MotionEvent.ACTION_DOWN) {
            return true;
        } else if (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_UP) {
            //Move或Up的时候,计算拽托进度
            float endY = event.getY();
            //限制拉到顶
            if (endY < 0) {
                endY = 0;
            }
            //限制拉到底
            if (endY > contentHeight) {
                endY = contentHeight;
            }
            //计算触摸点和高度的差值
            float distanceY = Math.abs(contentHeight - endY);
            float ratio = distanceY / contentHeight;
            //计算百分比应该有的进度:进度 = 总进度 * 进度百分比值
            float progress = mMax * ratio;
            setProgress(progress, true);
            if (action == MotionEvent.ACTION_UP) {
                if (mOnProgressUpdateListener != null) {
                    mOnProgressUpdateListener.onStopTrackingTouch(this);
                }
            }
            return true;
        }
        return super.onTouchEvent(event);
    }

    /**
     * 处理MeasureSpec
     */
    private int handleMeasure(int measureSpec, boolean isWidth) {
        int result;
        if (isWidth) {
            result = mDefaultWidth;
        } else {
            result = mDefaultHeight;
        }
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;
        } else {
            //处理wrap_content的情况
            if (specMode == MeasureSpec.AT_MOST) {
                result = Math.min(result, specSize);
            }
        }
        return result;
    }

    /**
     * 设置进度背景颜色
     */
    public void setBgColor(int bgColor) {
        //没有变化,不重绘
        if (bgColor == mBgColor) {
            return;
        }
        this.mBgColor = bgColor;
        mBgPaint.setColor(bgColor);
        invalidate();
    }

    /**
     * 设置已有进度的背景颜色
     */
    public void setProgressBgColor(int progressBgColor) {
        //没有变化,不重绘
        if (progressBgColor == mProgressBgColor) {
            return;
        }
        this.mProgressBgColor = progressBgColor;
        mProgressPaint.setColor(progressBgColor);
        invalidate();
    }

    /**
     * 设置进度
     */
    public void setProgress(float progress) {
        setProgress(progress, false);
    }

    /**
     * 设置进度
     *
     * @param fromUser 是否是用户触摸发生的改变
     */
    public void setProgress(float progress, boolean fromUser) {
        //忽略相同进度的设置
        if (mProgress == progress) {
            return;
        }
//        if (progress > mMin && progress < mMax) {
//        }
        this.mProgress = progress;
        invalidate();
        if (mOnProgressUpdateListener != null) {
            mOnProgressUpdateListener.onProgressUpdate(this, progress, fromUser);
        }
    }

    /**
     * 获取当前进度
     */
    public float getProgress() {
        return mProgress;
    }

    /**
     * 设置进度最小值
     */
    public void setMin(float min) {
        this.mMin = min;
        invalidate();
    }

    /**
     * 获取最小进度
     */
    public float getMin() {
        return mMin;
    }

    /**
     * 设置进度最大值
     */
    public void setMax(float max) {
        this.mMax = max;
        invalidate();
    }

    /**
     * 获取最大进度
     */
    public float getMax() {
        return mMax;
    }

    /**
     * 是否开启进度蒙版
     */
    public void setProgressMaskEnable(boolean enable) {
        this.mProgressMaskEnable = enable;
        invalidate();
    }

    /**
     * 设置进度最小保留高度
     *
     * @param retainProgressHeight 像素值
     */
    public void setRetainProgressHeight(int retainProgressHeight) {
        mRetainProgressHeight = retainProgressHeight;
    }

    /**
     * 是否调试模式
     */
    public boolean isDebug() {
        return mIsDebug;
    }

    /**
     * 设置是否调试模式
     */
    public void setDebug(boolean debug) {
        mIsDebug = debug;
        invalidate();
    }

    public interface OnProgressUpdateListener {
        /**
         * 按下时回调
         */
        void onStartTrackingTouch(VerticalSeekBar seekBar);

        /**
         * 进度更新时回调
         *
         * @param progress 当前进度
         * @param fromUser 是否是用户改变的
         */
        void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser);

        /**
         * 松手时回调
         */
        void onStopTrackingTouch(VerticalSeekBar seekBar);
    }

    public static class SimpleProgressUpdateListener implements OnProgressUpdateListener {
        @Override
        public void onStartTrackingTouch(VerticalSeekBar seekBar) {
        }

        @Override
        public void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser) {
        }

        @Override
        public void onStopTrackingTouch(VerticalSeekBar seekBar) {
        }
    }

    public void setOnProgressUpdateListener(
            VerticalSeekBar.OnProgressUpdateListener onProgressUpdateListener) {
        mOnProgressUpdateListener = onProgressUpdateListener;
    }

    /**
     * 获取当前进度值比值
     */
    public float getProgressRatio() {
        return mProgress / (mMax * 1.0f);
    }

    /**
     * Float值保留几位小数,返回字符串
     *
     * @param digits 保留几位
     * @param value  要格式化的值
     */
    private String floatValueRetainFormat(int digits, float value) {
        StringBuilder builder = new StringBuilder("0.");
        for (int i = 0; i < digits; i++) {
            builder.append("#");
        }
        DecimalFormat format = new DecimalFormat(builder.toString());
        //结果补0
        StringBuilder resultBuilder = new StringBuilder(format.format(value));
        //如果格式化后是整数,则添加一个.0,例如1,就是1.0,2是2.0
        if (!resultBuilder.toString().contains(".")) {
            resultBuilder.append(".");
            for (int i = 0; i < digits; i++) {
                resultBuilder.append("0");
            }
        }
        return resultBuilder.toString();
    }

    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }

    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}

使用

  • 布局xml
<com.psyone.brainmusic.view.wiget.VerticalSeekBar
            android:id="@+id/volume_seek_bar"
            android:layout_width="36dp"
            android:layout_height="114dp"
            app:vsb_min_progress="1"
            app:vsb_max_progress="100"
            app:vsb_bg="#EDF0FA"
            app:vsb_progress_bg="#3DDA9B"
            app:vsb_bg_radius="8dp" />
  • Java代码
//查找控件
VerticalSeekBar volumeSeekBar = findViewById(R.id.volume_seek_bar);
//设置进度背景颜色
volumeSeekBar.setBgColor(Color.parseColor("#00FF00"));
//设置已有进度的背景颜色
volumeSeekBar.setProgressBgColor(Color.parseColor("#FF0000"));
//设置当前进度
volumeSeekBar.setProgress(0.5f);
//设置最小值
volumeSeekBar.setMin(0f);
//设置最大值
volumeSeekBar.setMax(1f);
//设置进度监听
volumeSeekBar.setOnProgressUpdateListener(new VerticalSeekBar.SimpleProgressUpdateListener() {
    @Override
    public void onProgressUpdate(VerticalSeekBar seekBar, float progress, boolean fromUser) {
        super.onProgressUpdate(seekBar, progress, fromUser);
        //进度更新回调
    }

    @Override
    public void onStopTrackingTouch(VerticalSeekBar seekBar) {
        super.onStopTrackingTouch(seekBar);
        //手势触摸结束回调
    }
});
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269