原标题:一个功能强大的自定义SeekBar
近日,在携程托管亲子园,多段亲子园教师惩罚和推打托管儿童的监控视频在网上流传。监控视频显示,教师除了殴打孩子,还强喂幼儿疑似芥末物。视频在社会引起了巨大关注。同日,携程网CEO发表了一封公开信,称其内心充满了愤怒,并将汲取教训,对孩子进行进一步的体检和心理干预。
作者简介
本篇来自 二转的投稿,主要讲解了一个功能强大的自定义SeekBar控件,希望对大家有所帮助!
二转的博客地址:
http://www.jianshu.com/u/4060186e538c
简述
最近在工作上的需要,自定义了一个漂亮而强大的自定义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'
}
想查看所有版本,请点击下面地址。
https://jcenter.bintray.com/com/zhouyou/signseekbar/
xml
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( newSignSeekBar.OnProgressChangedListener() { @OverridepublicvoidonProgressChanged(SignSeekBar signSeekBar, intprogress, floatprogressFloat,booleanfromUser){ //fromUser 表示是否是用户触发 是否是用户touch事件产生String s = String.format(Locale.CHINA, "onChanged int:%d, float:%.1f", progress, progressFloat); progressText.setText(s); } @OverridepublicvoidgetProgressOnActionUp(SignSeekBar signSeekBar, intprogress, floatprogressFloat){ String s = String.format(Locale.CHINA, "onActionUp int:%d, float:%.1f", progress, progressFloat); progressText.setText(s); } @OverridepublicvoidgetProgressOnFinally(SignSeekBar signSeekBar, intprogress, floatprogressFloat,booleanfromUser){ String s = String.format(Locale.CHINA, "onFinally int:%d, float:%.1f", progress, progressFloat); progressText.setText(s + getContext().getResources().getStringArray(R.array.labels)[progress]); } });
Attributes
支持很多自定义属性设置,具体请看源码。
https://github.com/zhou-you/EasySignSeekBar
实现思路
概况
本库自定义控件主要是用了 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 坐标是否在轨道的矩形方框内,从而判断是否在轨道上。
privatebooleanisTrackTouched(MotionEvent event){ returnisEnabled()&& event.getX()>= getPaddingLeft() && event.getX() <= getMeasuredWidth() - getPaddingRight() && event.getY() >= getPaddingTop() && event.getY() <= getMeasuredHeight() - getPaddingBottom(); }
thumb(滑块)接触有效计算
thumb就是轨道上的圆形滑块,如何判断手指拖动的区域是否在滑块上呢,使用圆的标准方程(x-a)²+(y-b)²=r²来判断
privatebooleanisThumbTouched(MotionEvent event){ if(!isEnabled()) returnfalse; floatmCircleR = isThumbOnDragging ? mThumbRadiusOnDragging : mThumbRadius; floatx = mTrackLength / mDelta * (mProgress - mMin) + mLeft; floaty = 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)组合得出一个新的颜色值。
/** * 计算新的透明度颜色 * *@paramcolor 旧颜色 *@paramratio 透明度系数 */
publicintgetColorWithAlpha(intcolor, floatratio){ intnewColor = 0; intalpha = Math.round(Color.alpha(color) * ratio); intr = Color.red(color); intg = Color.green(color); intb = Color.blue(color); newColor = Color.argb(alpha, r, g, b); returnnewColor; }
thumb(滑块) 最近节点位置计算方法
根据节点个数mSectionCount和两个节点之间的间隔mSectionOffset,与滑块当前位置mThumbCenterX的比较,求出最近一个节点的位置。
//计算最近节点位置,mSectionCount:节点个数,mSectionOffset:两个节点间隔距离,mThumbCenterX:滑块中心点位置
floatx = 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
向前甩一定值后再回到原来位置
完整源码
privatevoidautoAdjustSection(){
inti; //计算最近节点位置,mSectionCount:节点个数,mSectionOffset:两个节点间隔距离,mThumbCenterX:滑块中心点位置floatx = 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.4floatx_ = bigDecimal.setScale( 1, BigDecimal.ROUND_HALF_UP).floatValue(); booleanonSection = x_ == x; // 就在section处,不作valueAnim,优化性能AnimatorSet animatorSet = newAnimatorSet(); 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( newLinearInterpolator()); valueAnim.addUpdateListener( newValueAnimator.AnimatorUpdateListener() { @OverridepublicvoidonAnimationUpdate(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( newAnimatorListenerAdapter() { @OverridepublicvoidonAnimationEnd(Animator animation){ mProgress = (mThumbCenterX - mLeft) * mDelta / mTrackLength + mMin; isThumbOnDragging = false; isTouchToSeekAnimEnd = true; invalidate(); if(mProgressListener != null) { mProgressListener.getProgressOnFinally(SignSeekBar. this, getProgress(), getProgressFloat(), true); } } @OverridepublicvoidonAnimationCancel(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 提示框--三角形边框绘制
单纯的进度提示框实现比较简单,主要是由矩形框+三角形组成,但是加边框绘制的时候比较麻烦一点,需要留出矩形和三角形交接的地方不能画线,这里做了假象交接的地方其实额外绘制了三角形的底边,颜色采用的是矩形库填充的颜色。三角形边框绘制如下:
privatevoiddrawTriangleBoder(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()); floatvalue = 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已经实现了文本绘制换行处理,也支持标签属性,m/s2,μmol/l,μ/l从而实现强大灵活的单位设置。
privatevoidcreateValueTextLayout(){ String value = isShowProgressInFloat ? String.valueOf(getProgressFloat()) : String.valueOf(getProgress()); if(value != null&& unit != null&& !unit.isEmpty()) value += String.format( " %s", unit); Spanned spanned = Html.fromHtml(value); valueTextLayout = newStaticLayout(spanned, valueTextPaint, mSignWidth, Layout.Alignment.ALIGN_CENTER, 1, 0, false); }
圆圈中心绘制文本
圆圈中心绘制文字,高度是比较难控制的,特别是中文,不能简单的通过bounds.height()来获取高度的方式计算,需要先求出baseline这种方式来处理,求baseline的方式是固定的。下面提供一个通用的方法:
/** * 精确画圆圈中心文字(通用方法),其中文字的高度是最难计算适配的,采用此方法,可以完美解决 * *@paramcanvas 画板 *@parampaint 画笔panit *@paramcenterX 圆圈中心X坐标 *@paramcenterY 圆圈中心Y坐标 *@paramradius 半径 *@paramtext 显示的文本 */
privatevoiddrawCircleText(Canvas canvas, Paint paint, floatcenterX, floatcenterY, floatradius, String text){ paint.setTextAlign(Paint.Align.LEFT); Rect bounds = newRect(); paint.getTextBounds(text, 0, text.length(), bounds); Paint.FontMetricsInt fontMetrics = paint.getFontMetricsInt(); floatbaseline = 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然后自己扩展和修改。欢迎大家提出意见和更好的创意。项目地址:
https://github.com/zhou-you/EasySignSeekBar返回搜狐,查看更多
责任编辑: