专栏首页Android技术分享Android 中心区域选中图表 WheelChart

Android 中心区域选中图表 WheelChart

产品要做一个支持横向滚动中心区域选中惯性滚动停止时回滚到中心位置点击选中处理嵌套滚动的图表需求

效果图如下:

最开始的想法时用MPAndroidChart来做,可用这个库有些细节满足不了产品的需求 如选中的label标签要用选中颜色及回滚功能,然后就很没底,找了很多类似功能的自定义控件的类比,做之前也咨询了一位大佬(在此感谢扔物线大神),觉得薄荷尺子的逻辑和这个需求很类似,就决定用自定义view来实现。

自己以前写过的自定义view都比较简单,自己刚开始做的时候压力挺大的,挺担心自己做不出来影响项目进度的,不过一时也没有好的办法,只能逼着自己去做,主要参考之前仿写薄荷尺子的大神的博客,做了四天下来,总算有点眉目,把demo拿给产品过目也比较满意,这个效果的实现也渐渐领略到开源的魅力,看到自己做出来的效果贼开心贼有成就感,周末打算分享出来,希望能对大家有所帮助,项目中有什么问题请不吝赐教,感激不尽。

目前有些代码可能还不够完善,后续还有一些细节需要优化(如可用折线连接坐标点等),但主体思路已经比较清晰了。

话不多说,效果如下:

源码地址:https://github.com/SilenceBurst/WheelChart

参考博客

之前仿写薄荷尺子的大神 很多代码甚至注释都被我毫不留情的copy过来了 ? : https://blog.csdn.net/totond/article/details/78737990

scoller相关及多点触控相关 请看其系列博客: https://blog.csdn.net/u012422440/article/details/51213348

根据实现的步骤拆分为如下功能点

  1. 自定义属性的设置及使用
  2. draw 绘制图表
  3. 触摸控制并处理多指触控问题(手指拖动图表可移动)
  4. 惯性滚动(根据手指释放时的速度计算图表需要滚动的距离)
  5. 回滚 (up时或者惯性滚动结束 需要回滚到选中位置)
  6. 点击选中 (根据点击的坐标,计算需要选中的下标并选中)
  7. 处理嵌套滚动

1.自定义属性的设置及使用

在attr文件中声明该控件的一些自定义属性,在构造方法中解析,设置控件的属性即可

2. draw 绘制图表

绘制图表其实主要时数学问题,具体坐标的计算就不再赘述了 请教扔物线的时候,我问他会不会有性能问题,他就说了一点,屏幕外不要绘制

我们就只需绘制屏幕上用户看到的内容即可,之前之后的就不用绘制了

但由于如果只绘制屏幕显示区域的话,左右两侧的点需要计算path连接而且在滚动时文字的显示会有突然显示或隐藏的问题,所以把绘制区域加长,左右两侧均多绘制一个label的距离

绘制区域为绿色加红色

我们根据x轴方向当前已滚动的距离getScrollX()计算第一个显示的label下标,再加上控件宽度和一个label距离(右侧多绘制的一个label的距离)计算出最后一个label的下标,只需要绘制两个下标中间即可,其他的就是数学问题了。

多个点的连接使用的贝塞尔曲线,代码参考自:https://www.jianshu.com/p/98088ff77607

3. 触摸控制并处理多指触控问题(手指拖动图表可移动)

触摸控制是根据第一个event点移动的距离,调用view的scrollBy方法滚动view,主要代码如下

//处理滑动 计算现在的event坐标和上一个触摸事件的坐标来计算偏移量 决定scrollBy的多少                                                                                  
@Override                                                                                                                           
public boolean onTouchEvent(MotionEvent event) {                                      
    ...                                             
    switch (event.getAction()) {                                                                                                    
        case MotionEvent.ACTION_DOWN:     
            ...                                                                                                
            //记录首个触控点的id                                                                                                            
            mActivePointerId = event.findPointerIndex(event.getActionIndex());                                                      
            ...                                                                                                                  
            mLastX = event.getX();                                                                              
            ...                                             
            break;                                                                                                                  
        case MotionEvent.ACTION_MOVE:                                                                                               
            if (mActivePointerId == INVALID_ID || event.findPointerIndex(mActivePointerId) == INVALID_ID) {                         
                break;                                                                                                              
            }                                                                                                                       
            //计算首个触控点移动后的坐标                                                                                                         
            float moveX = mLastX - event.getX(mActivePointerId);                                                                    
            if (Math.abs(moveX) > IGNORE_MOVE_OFFSET) {                                                                             
                ...                                                                                                 
                mLastX = event.getX(mActivePointerId);                                                                              
                scrollBy((int) moveX, 0);                                                                                           
            }                                                                                                                       
            break;                                                                                                                  
        case MotionEvent.ACTION_UP:                                                                                             
            mActivePointerId = INVALID_ID;                                                                                          
            mLastX = 0;                                                               
            ...                                               
            break;                                                                                                                  
        case MotionEvent.ACTION_CANCEL:                                                                                             
            mActivePointerId = INVALID_ID;                                                                                          
            mLastX = 0;                     
            ...                                                  
            break;                                                                                                                       
    }                                                                                                                               
    return true;                                                                                                                    
}                               

scrollBy方法内部会调用scrollTo方法,重写了scrollTo方法在里面进行一些选中下标的判断和最小最大滚动位置的拦截

@Override
public void scrollTo(int x, int y) {
    //默认左边缘为x最小值-半个控件的宽度
    if (x < mMinPosition) {
        x = mMinPosition;
    }
    //默认右边缘为x最大值+半个控件的宽度
    if (x > mMaxPosition) {
        x = mMaxPosition;
    }
    if (x != getScrollX()) {
        super.scrollTo(x, y);
    }
    mSelectIndex = scrollX2Index(x);
}

注意 :在move事件中需要根据第一个触控点id计算移动距离,直接调用event.getX()方法,会有多点触控问题(复现步骤:一个手指滑动后,按下第二个手指,第一个手指抬起,view会自动滚动)

因为后面会有点击事件的判断,所以在move时判断如果移动距离小于IGNORE_MOVE_OFFSET = 2.5时,忽略,这样当手机滑动比较慢时,会有部分滑动事件被忽略掉的情况,不过2.5这个值自己滑动时觉得体验还可以,再大的话慢速滑动会有卡顿,太小的话点击事件的判定会过于精确。

4. 惯性滚动

惯性滚动的实现需要用到VelocityTracker计算up事件时的速度,OverScroller处理fling事件

主要思路是,当up事件发生时,判断手指速度,若速度小于最小值,scrollBackToExactPosition()直接将当前选中下标滚动到中心区域;若速度小于最大值按原速度计算否则按最大速度计算,根据此速度 当前x方向偏移量 可scrollTo的最小、最大值调用fling方法,并调用invalidate()方法,invalidate()内部几次回调会调用view的draw方法,在view的draw方法中调用computeScroll()方法,若惯性滚动未结束,调用scrollTo方法将view滚动到该速度应滚动到的位置,再调用postInvalidate(),几次回调又会重新调用view的draw方法,循环调用scrollTo将view再进行滚动 如此实现惯性滚动 直至滚动结束

5. 回滚

这个主要也是数学题,需要回滚的距离过大时,使用OverScroller慢速回滚,若过小则立刻回弹

//触摸事件或惯性滚动结束后 应滚动到中心位置
private void scrollBackToExactPosition() {
    float rightPosition = mSelectIndex * mParent.getXLabelInterval() - (float) getWidth() / 2;
    if (Math.abs(getScrollX() - rightPosition) > IGNORE_OFFSET) {
        int dx = Math.round(rightPosition - getScrollX());
        if (Math.abs(dx) > MIN_SCROLLER_DP) {
            //渐变回弹
            mOverScroller.startScroll(getScrollX(), getScrollY(), dx, 0, 500);
            invalidate();
        } else {
            //立刻回弹
            scrollBy(dx, 0);

6. 点击选中

点击事件的判定:最开始的想法是,判断事件如果是down紧接up即为点击,后来发现这种判定比较苛刻,因为有些点击事件会引起略微的move事件,所以在move事件中判断如果move距离较短,则忽略,这种方法的判定目前没有发现问题,如果大家有好的想法,欢迎讨论。

判定为点击事件后,要根据点击点的坐标位置和当前已滚动的距离,计算出点击点所在的下标,改变需要选中的下标,滚动到指定下标

7. 处理嵌套滚动

由于这个view是横向滚动的,避免被父View拦截事件,我们需要在横向滑动时拦截事件进行处理;在纵向滑动时不作拦截,交由父view AppBarLayout处理

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
           ...
        //记录首个触控点的id
        mActivePointerId = event.findPointerIndex(event.getActionIndex());
        ...
        mLastX = event.getX();
        mLastY = event.getY();
        parent.requestDisallowInterceptTouchEvent(false);//按下时开始让父控件不要处理任何touch事件
        break;
    case MotionEvent.ACTION_MOVE:
        if (mActivePointerId == INVALID_ID || event.findPointerIndex(mActivePointerId) == INVALID_ID) {
            break;
        }
        //计算首个触控点x方向移动距离
        float moveX = mLastX - event.getX(mActivePointerId);
        //计算首个触控点y方向移动距离
        float moveY = mLastY - event.getY(mActivePointerId);
        //判断x方向移动距离大于等于y方向距离 则判断为x轴滚动即滑动图表 反之判断为y轴滚动将事件交由父布局处理
        if (Math.abs(moveX) >= Math.abs(moveY)) {
            if (Math.abs(moveX) > IGNORE_MOVE_OFFSET) {
                parent.requestDisallowInterceptTouchEvent(true);
                mLastX = event.getX(mActivePointerId);
                ...
            }
        } else {
            if (Math.abs(moveY) > IGNORE_MOVE_OFFSET) {
                parent.requestDisallowInterceptTouchEvent(false);
                mLastY = event.getY(mActivePointerId);
            }
        }
        break;
    case MotionEvent.ACTION_UP:
        //判断是单点事件 跳转到指定位置
        ...
        mActivePointerId = INVALID_ID;
        mLastX = 0;
        mLastY = 0;
        parent.requestDisallowInterceptTouchEvent(false);//up或者cancel的时候恢复
        break;
    case MotionEvent.ACTION_CANCEL:
        ...
        mActivePointerId = INVALID_ID;
        mLastX = 0;
        mLastY = 0;
        ...
        parent.requestDisallowInterceptTouchEvent(false);//up或者cancel的时候恢复
        break;
    default:
        mDownAndUp = false;
        break;
}

这个控件的一点一个功能的实现,过程之中问题不断,问题解决又是惊喜,希望自己多些信心,多点努力,收拾行装,又上征程,加油,我们都是追梦人。

最后

针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

  • Android前沿技术大纲

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android 字节跳动算法题:给定ViewGroup打印其内所有的View

    在 Android 下,UI 的布局结构,对标到数据结构中,本质就是一个由 View 和 ViewGroup 组成的多叉树结构。其中 View 只能作为叶子节点...

    Android技术干货分享
  • Android:让你明明白白的使用RecyclerView——SnapHelper详解

    RecyclerView在24.2.0版本中新增了SnapHelper这个辅助类,用于辅助RecyclerView在滚动结束时将Item对齐到某个位置。特别是列...

    Android技术干货分享
  • 冷门干货 你知道Android 中1px的字到底有多高?

    在还原UI的时候我们常会发现一个问题,按照Sketch标注的尺寸去还原设计稿中的文字会产生几个Px的误差,字符上下有些许空白,以致于后期设计审查时频繁微调。

    Android技术干货分享
  • 一看就晕的React事件机制

    腾讯NEXT学位
  • Socket编程(4)TCP粘包问题及解决方案

    Tencent JCoder
  • 私密的搜索引擎搭建

    说明:之前介绍过一个多平台聚合搜索服务Searx,都是以Google等国外搜索为主→传送门,然后这里说的秘迹搜索就是基于Searx二次开发,主要是聚合国内的百度...

    用户1112170
  • 实践: Jenkins Core Api & Job DSL创建项目

    在大规模的Jenkins实践中创建项目也是一个问题,如何通过模板自动化的创建Jenkins项目呢? 可以通过安装Job Dsl插件后,通过 Dsl直接创建项目。...

    泽阳
  • 解读 | 6个问题深度解读CPU漏洞

    近日,谷歌Project Zero安全团队公布了两组CPU特性漏洞,分别命名为Meltdown和Spectre,安恒信息应急响应中心于日前已关注此漏洞并发布漏洞...

    安恒信息
  • NAB SHOW 2018前瞻丨三星将带来VR直播,IBM将为电视广播应用AI技术

    VRPinea
  • Hyper-V无法文件拖拽解决方案~~~这次用一个取巧的方法架设一个FTP来访问某个磁盘,并方便的读写文件

    异常处理汇总-服 务 器 http://www.cnblogs.com/dunitian/p/4522983.html 服务器相关的知识点:http://www...

    逸鹏

扫码关注云+社区

领取腾讯云代金券