EasySignSeekBar一个漂亮而强大的自定义view

github地址:https://github.com/zhou-you/EasySignSeekBar

简述

最近在工作上的需要,自定义了一个漂亮而强大的自定义view,但不仅仅只是一个SeekBar而已哦,一定要耐心看完。刚开始是不愿意自己去写的,这东西太浪费时间,UI这东西不一定是个技术活,但一定是个细活。浏览了很多自定义控件,都没有符合需要的,最终只能自己开撸。实现了效果后想着看能不能也方便他人,如果其他人有类似的效果,修改下属性配置就可以直接使用,于是就分享出来,取名:EasySignSeekBar 。

EasySignSeekBar

本库主要提供一个漂亮而强大的自定义SeekBar,进度变化由提示牌 (sign)展示,具有强大的属性设置,支持设置section(节点)、mark(标记)、track(轨迹)、thumb(拖动块)、progress(进度)、sign(提示框)等功能

主要功能

  • 强大的track(轨迹)和second track (选中轨迹)的最小值、最大值、轨迹粗细,颜色等设置;
  • 灵活的数字显示支持整数和小数;
  • 支持设置进度单位,例如 10s,15km/h、平方等;
  • 支持手柄拖动块thumb半径、颜色、阴影、透明度等;
  • 支持节点个数、文字大小、颜色设置;
  • 支持自动滚动最近区段标记节点;
  • 支持指示牌宽高、颜色、圆角半径、三角arrow指示、border边框、跟随thumb移动等;
  • 支持设置拖动进度监听回掉;
  • ......

效果预览

因GIF图压缩的原因动画看起来有些不流程和模糊。


用法介绍

build.gradle设置

dependencies {
 compile 'com.zhouyou:signseekbar:1.0.1'
}

想查看所有版本,请点击下面地址。

xml

<com.zhouyou.view.seekbar.SignSeekBar
        android:id="@+id/seek_bar"
        android:layout_width="match_parent"
        android:layout_height="16dp"
        app:ssb_section_text_position="bottom_sides"
        app:ssb_show_progress_in_float="false"
        app:ssb_show_section_mark="false"
        app:ssb_show_section_text="true"
        app:ssb_show_sign="true"
        app:ssb_show_thumb_text="false"
        app:ssb_sign_arrow_height="5dp"
        app:ssb_sign_arrow_width="10dp"
        app:ssb_sign_border_color="@color/color_red"
        app:ssb_sign_border_size="1dp"
        app:ssb_sign_color="@color/color_gray"
        app:ssb_sign_show_border="true"/>

java

signSeekBar.getConfigBuilder()
                .min(0)
                .max(4)
                .progress(2)
                .sectionCount(4)
                .trackColor(ContextCompat.getColor(getContext(), R.color.color_gray))
                .secondTrackColor(ContextCompat.getColor(getContext(), R.color.color_blue))
                .thumbColor(ContextCompat.getColor(getContext(), R.color.color_blue))
                .sectionTextColor(ContextCompat.getColor(getContext(), R.color.colorPrimary))
                .sectionTextSize(16)
                .thumbTextColor(ContextCompat.getColor(getContext(), R.color.color_red))
                .thumbTextSize(18)
                .signColor(ContextCompat.getColor(getContext(), R.color.color_green))
                .signTextSize(18)
                .autoAdjustSectionMark()
                .sectionTextPosition(SignSeekBar.TextPosition.BELOW_SECTION_MARK)
                .build();

回调

设置回调可以监听进度变化情况。

signSeekBar.setOnProgressChangedListener(new SignSeekBar.OnProgressChangedListener() {
            @Override
            public void onProgressChanged(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
            //fromUser 表示是否是用户触发 是否是用户touch事件产生
                String s = String.format(Locale.CHINA, "onChanged int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s);
            }

            @Override
            public void getProgressOnActionUp(SignSeekBar signSeekBar, int progress, float progressFloat) {
                String s = String.format(Locale.CHINA, "onActionUp int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s);
            }

            @Override
            public void getProgressOnFinally(SignSeekBar signSeekBar, int progress, float progressFloat,boolean fromUser) {
                String s = String.format(Locale.CHINA, "onFinally int:%d, float:%.1f", progress, progressFloat);
                progressText.setText(s + getContext().getResources().getStringArray(R.array.labels)[progress]);
            }
        });

Attributes

支持很多自定义属性设置,具体请看源码

主要实现思路介绍

概况

本库自定义控件主要是用了Canvas相关的drawXXX系列方法、一些简单的算法和动画来完成的。比如拖动轨迹、滑块thumb拖动、放大、自动滚动最近节点、指示牌、区段节点标记、进度单位显示等。接下来会讲解下主要的实现思路,对于自定义View的其它基本流程,属性获取和设置、onMeasure的重写等都不重点介绍,想了解完整流程请看源码

track(轨道)绘制

画轨道比较简单,主要实现方式就是画两条不同颜色的线条(其实画的是一条分为左右两部分,衔接的地方是有个thumb遮挡着),主要是要求出滑动thumb的中心点mThumbCenterX,mThumbCenterX的计算非常重要,本库的很多计算都是围绕mThumbCenterX,mThumbCenterX是通过onTouchEvent事件MotionEvent
根据down、move事件实时计算出中心点x坐标。

        // 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);

track(轨道)接触有效计算

MotionEvent的getX()和getY()获得的永远是相对view的触摸位置坐标,getRawX()和getRawY()获得的是相对屏幕的位置,轨道计算用的是getX,getY 相对于容器的位置坐标x,y,计算x,y坐标是否在轨道的矩形方框内,从而判断是否在轨道上。

    private boolean isTrackTouched(MotionEvent event) {
        return isEnabled() && event.getX() >= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight()
                && event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom();
    }

thumb(滑块)接触有效计算

thumb就是轨道上的圆形滑块,如何判断手指拖动的区域是否在滑块上呢,使用圆的标准方程(x-a)²+(y-b)²=r²来判断

    private boolean isThumbTouched(MotionEvent event) {
        if (!isEnabled())
            return false;
        float mCircleR = isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius;
        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 + mCircleR) * (mLeft + mCircleR);
    }

thumb(滑块)透明度实现

滑块的透明度,是将滑块的颜色值进行计算加上alpha,求出一个新的颜色值,主要是使用Color这个工具类的方法,大家经常用到的是Color的parseColor(@Size(min=1) String colorString)方法,库中主要用的是Color的另一些方法alpha(int color)、red(int color)、green(int color)、blue(int color)方法分别求出argb值,求出的透明度经过计算修改后,再用 Color.argb(alpha, r, g, b)组合得出一个新的颜色值。

    /**
     * 计算新的透明度颜色
     *
     * @param color 旧颜色
     * @param ratio 透明度系数
     */
    public int getColorWithAlpha(int color, float ratio) {
        int newColor = 0;
        int alpha = Math.round(Color.alpha(color) * ratio);
        int r = Color.red(color);
        int g = Color.green(color);
        int b = Color.blue(color);
        newColor = Color.argb(alpha, r, g, b);
        return newColor;
    }

thumb(滑块) 最近节点位置计算方法

根据节点个数mSectionCount和两个节点之间的间隔mSectionOffset,与滑块当前位置mThumbCenterX的比较,求出最近一个节点的位置。

 //计算最近节点位置,mSectionCount:节点个数,mSectionOffset:两个节点间隔距离,mThumbCenterX:滑块中心点位置
        float x = 0;
        for (i = 0; i <= mSectionCount; i++) {
            x = i * mSectionOffset + mLeft;
            if (x <= mThumbCenterX && mThumbCenterX - x <= mSectionOffset) {
                break;
            }
        }

thumb(滑块) 滚动最近节点动画效果实现

滑块自动滚动到最近节点增加了动画移动效果,使用ValueAnimator实现动画,Property Animation提供了Animator.AnimatorListener和Animator.AnimatorUpdateListener两个监听器用于动画在播放过程中的重要动画事件。其中AnimatorUpdateListener监听中onAnimationUpdate() 方法,动画每播放一帧时调用,在动画过程中,可侦听此事件来获取并使用 ValueAnimator 计算出来的属性值。利用传入事件的 ValueAnimator 对象,调用其 getAnimatedValue() 方法即可获取当前的属性值,就是修改后滑块的位置mThumbCenterX。此动画还配合有Interpolator,动画播放采用LinearInterpolator线性插值的方式执行动画。插值器它定义了动画变化过程中的属性变化规则,它根据时间比例因子计算出一个插值因子,用于设定目标对象的动画执行是否为线性变化、非线性变化或先加速后减速等等。Android系统本身内置了一些通用的Interpolator(插值器),如下:

类或接口名 说明
AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
LinearInterpolator 以常量速率改变
OvershootInterpolator 向前甩一定值后再回到原来位置

完整源码:

    private void autoAdjustSection() {
        int i;
        //计算最近节点位置,mSectionCount:节点个数,mSectionOffset:两个节点间隔距离,mThumbCenterX:滑块中心点位置
        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);
        //BigDecimal setScale保留1位小数,四舍五入,2.35变成2.4
        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;
                    invalidate();

                    if (mProgressListener != null) {
                        mProgressListener.onProgressChanged(SignSeekBar.this, getProgress(), getProgressFloat(),true);
                    }
                }
            });
        }
        if (!onSection) {
            animatorSet.setDuration(mAnimDuration).playTogether(valueAnim);
        }
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
                isThumbOnDragging = false;
                isTouchToSeekAnimEnd = true;
                invalidate();
                if (mProgressListener != null) {
                    mProgressListener.getProgressOnFinally(SignSeekBar.this, getProgress(), getProgressFloat(),true);
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin;
                isThumbOnDragging = false;
                isTouchToSeekAnimEnd = true;
                invalidate();
            }
        });
        animatorSet.start();
    }

采用BigDecimal处理小数

代码中的小数采用BigDecimal来处理,只介绍setScale相关方法,其它更多方法可以自己去学习,这里只是抛砖引玉。
BigDecimal.setScale()方法用于格式化小数点
setScale(1)表示保留一位小数,默认用四舍五入方式
setScale(1,BigDecimal.ROUND_DOWN)直接删除多余的小数位,如2.35会变成2.3
setScale(1,BigDecimal.ROUND_UP)进位处理,2.35变成2.4
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35变成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35变成2.3,如果是5则向下舍

Sign 提示框--三角形边框绘制

单纯的进度提示框实现比较简单,主要是由矩形框+三角形组成,但是加边框绘制的时候比较麻烦一点,需要留出矩形和三角形交接的地方不能画线,这里做了假象交接的地方其实额外绘制了三角形的底边,颜色采用的是矩形库填充的颜色。三角形边框绘制如下:

private void drawTriangleBoder(Canvas canvas, Point point1, Point point2, Point point3, Paint paint) {
        triangleboderPath.reset();
        triangleboderPath.moveTo(point1.x, point1.y);
        triangleboderPath.lineTo(point2.x, point2.y);
        paint.setColor(signPaint.getColor());
        float value = mSignBorderSize / 6;
        paint.setStrokeWidth(mSignBorderSize + 1f);
        canvas.drawPath(triangleboderPath, paint);
        triangleboderPath.reset();
        paint.setStrokeWidth(mSignBorderSize);
        triangleboderPath.moveTo(point1.x - value, point1.y - value);
        triangleboderPath.lineTo(point3.x, point3.y);
        triangleboderPath.lineTo(point2.x + value, point2.y - value);
        paint.setColor(mSignBorderColor);
        canvas.drawPath(triangleboderPath, paint);
    }

Sign 提示框--进度单位unit实现方式

进度单位很多需求也是需要的,不是单纯的用canvas.drawText来绘制。这里采用的是StaticLayout。使用Canvas的drawText绘制文本是不会自动换行的,即使一个很长很长的字符串,drawText也只显示一行,超出部分被隐藏在屏幕之外。可以逐个计算每个字符的宽度,通过一定的算法将字符串分割成多个部分,然后分别调用drawText一部分一部分的显示, 但是这种显示效率会很低。StaticLayout是android中处理文字换行的一个工具类,StaticLayout已经实现了文本绘制换行处理,也支持标签属性<small>,m/s<sup>2</sup>,μmol/l,μ/l从而实现强大灵活的单位设置。

 private void createValueTextLayout() {
        String value = isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress());
        if (value != null && unit != null && !unit.isEmpty())
            value += String.format(" <small>%s</small>", unit);
        Spanned spanned = Html.fromHtml(value);
        valueTextLayout = new StaticLayout(spanned, valueTextPaint, mSignWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, false);
    }

圆圈中心绘制文本

圆圈中心绘制文字,高度是比较难控制的,特别是中文,不能简单的通过bounds.height()来获取高度的方式计算,需要先求出baseline这种方式来处理,求baseline的方式是固定的。下面提供一个通用的方法:

 /**
     * 精确画圆圈中心文字(通用方法),其中文字的高度是最难计算适配的,采用此方法,可以完美解决
     *
     * @param canvas  画板
     * @param paint   画笔panit
     * @param centerX 圆圈中心X坐标
     * @param centerY 圆圈中心Y坐标
     * @param radius  半径
     * @param text    显示的文本
     */
    private void drawCircleText(Canvas canvas, Paint paint, float centerX, float centerY, float radius, String text) {
        paint.setTextAlign(Paint.Align.LEFT);
        Rect bounds = new Rect();
        paint.getTextBounds(text, 0, text.length(), bounds);
        Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt();
        float baseline = centerY - radius + (2 * radius - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;
        canvas.drawText(text, centerX - radius + radius - bounds.width() / 2, baseline, paint);
    }

总结

欢迎大家Star of Fork,使用Gradle依赖很方便,也可以clone来试着按自己的想法修改,后期想法是再进行优化,直接继承此view然后自己扩展和修改。欢迎大家提出意见和更好的创意。

关于我

联系方式

本群旨在为使用我github项目的人提供方便,如果遇到问题欢迎在群里提问。

欢迎加入QQ交流群:581235049

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,065评论 4 62
  • 一个人一辈子可以拥有多少个星期一?命定人意,尽不相同。日子各有各的劫数,各有各的算法。有人活在活泼...
    文小雅阅读 212评论 0 0
  • 今年先定个小目标,开个首脑班吧。 2016年曾在网络上很盛行的一句话“先定一个能达到的小目标,比方说先挣它一...
    懒虫读书阅读 649评论 0 2
  • 小时候就盼望着过节,走亲戚拜节。盼望着好吃的礼品零食!记得有次过节,那时还很小,外公来我家里带来了一堆礼品,其中就...
    曉非阅读 156评论 0 0
  • 准备工作 //www.greatytc.com/p/1ac62d93b962 Observer的创建 Ob...
    谈小龙阅读 777评论 0 0