废话不多说,先上效果效果酷炫,动画丰富,效果爆炸boom~设计思路看腻了市面上各种丑陋难看的时钟控件,是时候整点新活!将现实生活中的摆钟圆形表盘设计、电子手表的数显表盘设计抽象出来,提取出“圆形”、“数显”、“时光流逝感”等词汇,融合这些词汇特征,把特征赋予最终的UI设计......就这样,一个炫酷的UI控件诞生了!拨动时钟圆盘可以调整时钟,伴随时间的流逝,拨动的圆盘还能自动回位,交互逻辑自然顺畅。设置不同的主题色即可体现更多的内涵,“静谧”、“夜晚”、“月夜”、“纯净”等等,控件设计本身的可扩展性非常好。实现方案设计思路清晰明确之后,就要考虑如何实现了。来人,上口号。?没有人?比我?更懂☝️实现类设计从UI图中可以观察到,时钟控件由四个大表盘组成,分别是上下午表盘、小时表盘、分钟表盘、秒钟表盘。在实现思路上首先考虑抽象出圆盘控件父类DiskView,其余表盘均继承自DiskView即可。有了各种各样的表盘,最后再用ViewGroup将其进行组装。而DiskView作为基类,需要承担动画、拖动、点击等交互的逻辑,同时还要具备表盘的公共属性,例如表盘半径radius、表盘旋转角度degree等。
public class DiskView extends View { private static final String TAG = "DiskView"; Context mContext; /** * 圆盘半径 */ int mRadius = 0; /** * 手指第一次按下时的坐标 */ float startX, startY; /** * 当前手指按下点的坐标 */ float curX, curY; /** * 第一次手指按下的点与初始位置形成的夹角 */ int startDegree; /** * 手指按下的点与初始位置形成的夹角 */ int curDegree; /** * 圆盘当前位置相对初始位置的角度,初始位置角度为0度 */ int degree = 0; /** * 手指抬起后是否需要回归原来的状态 */ boolean isNeedReturn = true; ValueAnimator animator;}
UI绘制有了坐标、角度、半径、颜色等属性定义,接下来考虑绘制。绘制采用canvas的图形绘制api,计算好各个图形的位置,赋予对应的颜色。调用rotate方法围绕圆心绘制具有一定角度的文字。需要注意的是,绘制文字时要确保文字中线经过圆盘圆心。@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); //画圆盘 mPaint.setColor(diskColor); canvas.drawCircle(mRadius, mRadius, mRadius, mPaint); //画数字 mPaint.setColor(numColor); Rect bounds = new Rect(); for (int i = 0; i < 60; i++) { if (i == minute) { mPaint.setColor(selectNumColor); } else { mPaint.setColor(numColor); } if (i % 10 != 0) { if (i % 5 == 0) { canvas.drawCircle(mRadius, 2 * mRadius - textHeight * 3 / 2, DisplayUtils.sp2px(mContext, 20) / 4, mPaint); } else { canvas.drawCircle(mRadius, 2 * mRadius - textHeight * 3 / 2, DisplayUtils.sp2px(mContext, 20) / 6, mPaint); } } else { mPaint.getTextBounds(i + "", 0, (i + "").length(), bounds); textHeight = bounds.height(); canvas.drawText(i + "", mRadius - bounds.width() / 2, mRadius * 2 - bounds.height(), mPaint); } canvas.rotate(-6, mRadius, mRadius); }}
交互逻辑控件交互逻辑大部分都在onTouchEvent的回调中进行处理,分别对用户的点击、移动、抬起动作做针对性处理,核心关键在于计算好各个情况的圆盘角度,之后再通过animator计算好对应的数值,实时刷新界面即可。需要注意的是用户的起始落点不能超过圆盘的界限,在单独使用某一个圆盘控件时要考虑边界限制。@Overridepublic boolean onTouchEvent(MotionEvent event) { curX = event.getX(); curY = event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startX = event.getX(); startY = event.getY(); startDegree = computeCurrentAngle(curX, curY); //起始落点不能超过圆盘界限 if (Math.sqrt( (startX - mRadius) * (startX - mRadius) + (startY - mRadius) * (startY - mRadius) ) > mRadius) { startDegree = 0; } break; case MotionEvent.ACTION_MOVE: //起始落点不能超过圆盘界限 if (Math.sqrt( (startX - mRadius) * (startX - mRadius) + (startY - mRadius) * (startY - mRadius) ) > mRadius) { return false; } curDegree = computeCurrentAngle(curX, curY); postInvalidate(); break; case MotionEvent.ACTION_UP: if (Math.sqrt( (startX - mRadius) * (startX - mRadius) + (startY - mRadius) * (startY - mRadius) ) > mRadius) { return false; } int tmpDegree = degree;//手指按下前的圆盘角度 degree = degree + curDegree - startDegree; if (Math.abs(degree) > 360) { degree %= 360; } startDegree = 0; curDegree = 0; startX = 0; startY = 0; //是否需要回位 if (isNeedReturn) { animator = ValueAnimator.ofInt(degree, tmpDegree); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { degree = (int) animation.getAnimatedValue(); postInvalidate(); } }); animator.setDuration(200); animator.setInterpolator(new DecelerateInterpolator()); animator.start(); } break; } return true;}
后记自定义控件开发作为Android开发中的重要一环,如何利用好各个api实现功能是一方面,如何自顶向下进行设计才是重点。在开发之前最关键的事情并不是构思如何实现、如何设计,而是去发掘用户的需求,从需求倒推功能,再从功能角度考虑如何进行设计,最终呈现给用户。技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。
推荐阅读:
音视频面试基础题
OpenGL ES 学习资源分享
开通专辑 | 细数那些年写过的技术文章专辑
NDK 学习进阶免费视频来了
推荐几个堪称教科书级别的 Android 音视频入门项目
觉得不错,点个在看呗~