当前位置: 首页 > news >正文

炫酷的Android时钟UI控件,隔壁产品都馋哭了

废话不多说,先上效果

效果酷炫,动画丰富,效果爆炸

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方法围绕圆心绘制具有一定角度的文字。需要注意的是,绘制文字时要确保文字中线经过圆盘圆心。

@Override
protected 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计算好对应的数值,实时刷新界面即可。需要注意的是用户的起始落点不能超过圆盘的界限,在单独使用某一个圆盘控件时要考虑边界限制。

@Override
public 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 音视频入门项目

觉得不错,点个在看呗~

相关文章:

  • 面试官:如何监测应用的 FPS ?
  • 一张图概括淘宝直播背后的前端技术 | 赠送多媒体前端手册
  • 活用 Shader,让你的页面更小,更炫,更快
  • 再见!onActivityResult!你好,Activity Results API!
  • 10 个你可能还不知道 VS Code 使用技巧
  • 细数 2020 年官方对 Android 的那些重大更新!
  • 64位系统究竟牛逼在哪里?
  • 如何区分IO密集型、CPU密集型任务?
  • 一起来玩玩WebGL--第一弹
  • 便利贴撕页效果,隔壁产品都馋哭了
  • 没错,华为开始对移动开发下手了!
  • 移动手机病毒编年史(Cabir、Skulls、FakePlayer、HummingBad)
  • 未来五年,做纯应用的开发者如何通过音视频破局?
  • 我看技术人的成长路径
  • 手写一个抖音视频去水印工具,千万别刚一个程序员
  • egg(89)--egg之redis的发布和订阅
  • Fabric架构演变之路
  • javascript 总结(常用工具类的封装)
  • Java教程_软件开发基础
  • java取消线程实例
  • laravel5.5 视图共享数据
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • Magento 1.x 中文订单打印乱码
  • Redux 中间件分析
  • sessionStorage和localStorage
  • Spring-boot 启动时碰到的错误
  • Vue官网教程学习过程中值得记录的一些事情
  • 第三十一到第三十三天:我是精明的小卖家(一)
  • 高度不固定时垂直居中
  • 经典排序算法及其 Java 实现
  • 开发了一款写作软件(OSX,Windows),附带Electron开发指南
  • 面试遇到的一些题
  • 前言-如何学习区块链
  • 如何设计一个微型分布式架构?
  • 设计模式 开闭原则
  • 怎样选择前端框架
  • 正则表达式小结
  • ​业务双活的数据切换思路设计(下)
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • (2)Java 简介
  • (6)STL算法之转换
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (Redis使用系列) Springboot 使用Redis+Session实现Session共享 ,简单的单点登录 五
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (一)VirtualBox安装增强功能
  • (转)iOS字体
  • (转)Scala的“=”符号简介
  • .apk 成为历史!
  • .NET 4.0网络开发入门之旅-- 我在“网” 中央(下)
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .NET 动态调用WebService + WSE + UsernameToken
  • .NET 使用配置文件
  • .net反混淆脱壳工具de4dot的使用
  • .Net接口调试与案例