Android 自定义优雅的BezierSeekBar 之撸码解析

简介:

0x0 前言

某设计网经常会有很多优秀漂亮的交互设计作品,有一天,偶遇这样的效果,动画流畅,交互自然,于是埋头自己解剖其中的元素,做了个开源控件,十来天有了一百来个star,觉得很受欢迎,今天专门写这潦草几笔,分享案发经过,希望对同行有所帮助。

0x1 准备

效果图

e3a74e144fde762858e3a5571bf0ec2705b16fab

preview 效果分析 曲线部分 716c5c1decc8c8eb5762e4bc1b78eb27023cd015

两个三阶贝塞尔曲线

1、默认状态:直线,首尾标注,默认值标注。
2、手指按下:曲线动画执行,标识小圆规则地放大,值的标注根据曲线波峰相对位置不变,向上同速移动,同时,标注背景渐变加深,动画结束。
3、手指拖动效果:曲线、小圆形、标注三者同时跟随触摸点移动,同时更新标注值。
4、手指离开屏幕:曲线收回动画执行,标识小圆规则缩小到默认状态,选中的值跟随波峰下沉到默认状态,标注背景渐变消失,动画结束。

圆形指示器部分

7d9f4dc233d028a5d7fa3b99c6276d910d051048

缩放效果

拆解状态三部分:默认状态、触摸过程中、触摸后状态。其中默认状态下指示器很小的圆形,距离水平曲线下方一个约定距离,当按下过程中,圆最下方坐标不变,圆直径逐渐增大,圆顶部与曲线的距离不变,直到动画结束。

功能分析

1、控件内元素:标尺、标注用的小圆、选中值,均可配置各自的颜色,
2、可配置值范围,
3、可配置默认值,
4、可实时监听选中的值。
5、可显示单位。

技术分析

曲线部分

通过静态截图可知本控件主要元素为触摸触发的曲线和其伸缩效果。让我们来简单分析一下曲线部分的结构:
//这里展示曲线拆解图
拆解后,触摸部分为六阶贝塞尔曲线,五个基准点,四个控制点,我们将它拆分成两个三阶曲线即可。其中,首尾基准点的Y坐标固定,X坐标随着触摸位置相对移动,剩下的基准点X坐标相对固定,Y坐标根据动画规律升降。再说控制点,为保障默认状态下,曲线部分为水平,首尾两个控制点的Y坐标固定,X坐标相对固定,中间两个控制点Y坐标和中间那个基准点一致,X相对中间基准点固定。
(通过上面的拆解,可以让曲线在默认状态下是一条水平直线,并且在按下状态下,与水平位置、波峰位置,能有比较自然的过度弧形,不至于那么生硬。)

圆形指示器部分

普通圆形,相对自身底部向上变大、向下缩小还原。

动画部分

按下时的动画采用普通的ValueAnimator,匀速LinearInterpolator。另外还有个选中值的背景变化,根据动画进度改变画笔Alpha值即可。
//这里写点动画代码啰。

0x2 代码实现

泡杯茶,挽起袖子开撸!
继承View:


public class BezierSeekBar extends View {
public BezierSeekBar(Context context) {
super(context);
init(context, null);

}

public BezierSeekBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}

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

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public BezierSeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}

private void init(Context context, AttributeSet attrs) {
this.context = context;
}
}

先绘制出曲线效果:


//第一个三阶曲线
bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);

//第二个三阶曲线
bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);

改变其Y坐标,让曲线恢复默认状态:
绘制完整线条:


//line1
bezierPath.reset();
bezierPath.moveTo(0, (float) 2 * height / 3);
bezierPath.lineTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);

//bezier1
bezierPath.moveTo(this.fingerX - circleRadiusMax * 2 * 3, (float) 2 * height / 3);
bezierPath.cubicTo(this.fingerX - circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX - circleRadiusMax * 2 * 1, (float) 2 * height / 3 - bezierHeight, this.fingerX, (float) 2 * height / 3 - bezierHeight);

//bezier2
bezierPath.moveTo(this.fingerX, (float) 2 * height / 3 - bezierHeight);
bezierPath.cubicTo(this.fingerX + circleRadiusMax * 2, (float) 2 * height / 3 - bezierHeight, this.fingerX + circleRadiusMax * 2 * 2, (float) 2 * height / 3, this.fingerX + circleRadiusMax * 2 * 3, (float) 2 * height / 3);

//line2
bezierPath.lineTo(width, (float) 2 * height / 3);
canvas.drawPath(bezierPath, bezierPaint);

添加Touch事件拦截,按下时显示曲线:


@Override
public boolean onTouchEvent(MotionEvent event) {

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
fingerX = event.getX();
if (fingerX < 0F) fingerX = 0F;
if (fingerX > width) fingerX = width;
//触摸介质进入控件,开始动画过渡
this.animatorFingerIn.start();
break;

case MotionEvent.ACTION_MOVE:
fingerX = event.getX();
if (fingerX < 0F) fingerX = 0F;
if (fingerX > width) fingerX = width;
postInvalidate();
break;

case MotionEvent.ACTION_UP:
//触摸介质离开控件,执行动画
this.animatorFingerOut.start();
break;
}
valueSelected = Integer.valueOf(decimalFormat.format(valueMin + (valueMax - valueMin) * fingerX / width));

if (selectedListener != null) {
selectedListener.onSelected(valueSelected);
}
return true;
}

添加动画效果:


this.animatorFingerIn = ValueAnimator.ofFloat(0f, 1f);
this.animatorFingerIn.setDuration(200L);
this.animatorFingerIn.setInterpolator(new LinearInterpolator());

this.animatorFingerOut = ValueAnimator.ofFloat(1f, 0f);
this.animatorFingerOut.setDuration(200L);
this.animatorFingerOut.setInterpolator(new LinearInterpolator());

this.animatorFingerOut.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (float) animation.getAnimatedValue();

animInFinshed = (progress >= 0.15F);
txtSelectedBgPaint.setAlpha((int) (255 * (progress - 0.15F)));
if (progress >= 0.95F) {
textPaint.setColor(colorValueSelected);
} else {
textPaint.setColor(colorValue);
}

bezierHeight = circleRadiusMax * 1.5F * progress;
circleRadius = circleRadiusMin + (circleRadiusMax - circleRadiusMin) * progress;
spaceToLine = circleRadiusMin * 2 * (1F - progress);
postInvalidate();
}
});

绘制圆形指示器,根据动画进度改变其大小:

 canvas.drawCircle(this.fingerX, (float) 2 * height / 3 + spaceToLine + circleRadius, circleRadius, ballPaint);

添加其它辅助元素后,配置通用属性,抛出公共方法:


<declare-styleable name="BezierSeekBar">
//曲线颜色
<attr name="bsBar_color_line" format="reference|color" />
//圆形指示器颜色
<attr name="bsBar_color_ball" format="reference|color" />
//阀值的文本颜色
<attr name="bsBar_color_value" format="reference|color" />
//选中值的文本颜色
<attr name="bsBar_color_value_selected" format="reference|color" />
//选中值的文本颜色背景
<attr name="bsBar_color_bg_selected" format="reference|color" />
//阀值最小
<attr name="bsBar_value_min" format="integer" />
//阀值最大
<attr name="bsBar_value_max" format="integer" />
//默认选中值
<attr name="bsBar_value_selected" format="integer" />
//单位
<attr name="bsBar_unit" format="reference|string" />
</declare-styleable>

private void initAttr(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.BezierSeekBar);

this.colorBall = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_ball, Color.BLACK);
this.colorLine = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_line, Color.BLACK);
this.colorValue = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value, Color.BLACK);
this.colorValueSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_value_selected, Color.WHITE);
this.colorBgSelected = attributes.getColor(R.styleable.BezierSeekBar_bsBar_color_bg_selected, Color.BLACK);
this.valueMin = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_min, 30);
this.valueMax = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_max, 150);
this.valueSelected = attributes.getInteger(R.styleable.BezierSeekBar_bsBar_value_selected, 65);
this.unit = attributes.getString(R.styleable.BezierSeekBar_bsBar_unit) + "";
attributes.recycle();
}
}

最后,测试一下:


<tech.nicesky.bezierseekbar.BezierSeekBar
android:id="@+id/bsBar_test"
app:bsBar_color_ball="@android:color/white"
app:bsBar_color_bg_selected="@android:color/white"
app:bsBar_color_line="@android:color/white"
app:bsBar_color_value="@android:color/white"
app:bsBar_color_value_selected="#ef5350"
app:bsBar_value_min="30"
app:bsBar_value_max="120"
app:bsBar_value_selected="65"
app:bsBar_unit="kg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

完美 :) !


原文发布时间为:2018-11-19
本文作者:fairytale110
本文来自云栖社区合作伙伴“ 安卓巴士Android开发者门户”,了解相关信息可以关注“ 安卓巴士Android开发者门户”。
相关文章
|
3月前
|
数据采集 监控 API
告别手动埋点!Android 无侵入式数据采集方案深度解析
传统的Android应用监控方案需要开发者在代码中手动添加埋点,不仅侵入性强、工作量大,还难以维护。本文深入探讨了基于字节码插桩技术的无侵入式数据采集方案,通过Gradle插件 + AGP API + ASM的技术组合,实现对应用性能、用户行为、网络请求等全方位监控,真正做到零侵入、易集成、高稳定。
598 51
|
8月前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
571 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
8月前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
160 0
|
8月前
|
Android开发 开发者
Android自定义view之利用drawArc方法实现动态效果
本文介绍了如何通过Android自定义View实现动态效果,重点使用`drawArc`方法完成圆弧动画。首先通过`onSizeChanged`进行测量,初始化画笔属性,设置圆弧相关参数。核心思路是不断改变圆弧扫过角度`sweepAngle`,并调用`invalidate()`刷新View以实现动态旋转效果。最后附上完整代码与效果图,帮助开发者快速理解并实践这一动画实现方式。
206 0
|
8月前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
395 65
Android自定义view之网易云推荐歌单界面
|
8月前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
753 84
|
7月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
353 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
8月前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
247 3
|
8月前
|
Android开发 开发者
Android自定义view之围棋动画(化繁为简)
本文介绍了Android自定义View的动画实现,通过两个案例拓展动态效果。第一个案例基于`drawArc`方法实现单次动画,借助布尔值控制动画流程。第二个案例以围棋动画为例,从简单的小球直线运动到双向变速运动,最终实现循环动画效果。代码结构清晰,逻辑简明,展示了如何化繁为简实现复杂动画,帮助读者拓展动态效果设计思路。文末提供完整源码,适合初学者和进阶开发者学习参考。
161 0
Android自定义view之围棋动画(化繁为简)
|
8月前
|
Java Android开发 开发者
Android自定义view之围棋动画
本文详细介绍了在Android中自定义View实现围棋动画的过程。从测量宽高、绘制棋盘背景,到创建固定棋子及动态棋子,最后通过属性动画实现棋子的移动效果。文章还讲解了如何通过自定义属性调整棋子和棋盘的颜色及动画时长,并优化视觉效果,如添加渐变色让白子更明显。最终效果既可作为围棋动画展示,也可用作加载等待动画。代码完整,适合进阶开发者学习参考。
179 0

推荐镜像

更多