自定义View之炫酷的水滴ViewPageIndicator

2023-10-23 07:38

本文主要是介绍自定义View之炫酷的水滴ViewPageIndicator,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布 

        去年在是某个Android群了看到有人发了一个设计图,觉得很好。想自己实现一下,到上网搜了一些资料,比如参考,这位兄弟已经把如何绘制一个弹性的圆写的很详细了,在此对他表示感谢。不过他没有完整实现这个自定义控件,所以还是自己动手实现一个,但是我觉得效果和原设计还有差距,一直没写博客。这几天抽时间把里面的效果在改了改,顺便也把博客写了。

源码地址放在最后,先上效果图:

下面开始分析写得思路,先来个方法截图:



        用贝塞尔曲线绘制一个圆需要12个点,如上图所示。然后在绘制时用mPath.cubicTo()依次连接,canvas.drawPath(mPath, mPaint)就能绘制一个完整的圆了,弹性圆就是在此基础上调整p的参数。比如{p2,p3,p4},增加X坐标,会使圆向右凸起。

代码中XPoint为x相同的一组点:p2,p3,p4和p8,p9,p10,YPoint 同理。代码中的mc对应图中的M,绘制圆时这个值是固定的,理论参考:How to create circle with Bézier curves?。p1={p5,p6,p7}.,p3={p11,p0,p1},p2={p2,p3,p4},p4={p8,p9,p10},radius为圆半径。

private XPoint p2, p4;
private YPoint p1, p3;
private void resetP() {p1.setY(radius);
    p1.setX(0);
    p1.setMc(mc);

    p3.setY(-radius);
    p3.setX(0);
    p3.setMc(mc);

    p2.setY(0);
    p2.setX(radius);
    p2.setMc(mc);

    p4.setY(0);
    p4.setX(-radius);
    p4.setMc(mc);
}
resetP()在完成选项切换时都需要调用一下,重置绘制的圆形形状,不然有时候会绘制不规则的圆,造成这个的原因是view刷新频率是有限的,有些临界状态直接就跳过了,导致参数没跟着变化就绘制了图像。

下面根据两种切换viewpager的方式分析:

第一种情况,点击indicator切换:

在onTouchEvent计算将要切换的位置,调用startAniTo(int currentPos, int toPos),  animator监听setTouchAble(!animating)是禁止动画未结束用户又去手动滑动viewpager切换。

private boolean startAniTo(int currentPos, int toPos) {this.currentPos = currentPos;
    this.toPos = toPos;
    if (currentPos == toPos)return true;
    startColor = roundColors[(this.currentPos) % 4];
    endColor = roundColors[(toPos) % 4];
    resetP();
    startX = div + radius + (this.currentPos) * (div + 2 * radius);
    distance = (toPos - this.currentPos) * (2 * radius + div) + (toPos > currentPos ? -radius : radius);
    if (animator == null) {animator = ValueAnimator.ofFloat(0, 1.0f);
        animator.setDuration(duration);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Override
            public void onAnimationUpdate(ValueAnimator animation) {mCurrentTime = (float) animation.getAnimatedValue();
                invalidate();
            }});
        animator.addListener(new Animator.AnimatorListener() {@Override
            public void onAnimationStart(Animator animation) {animating = true;
                setTouchAble(!animating);
            }@Override
            public void onAnimationEnd(Animator animation) {goo();
                animating = false;
                setTouchAble(!animating);
            }@Override
            public void onAnimationCancel(Animator animation) {goo();
                animating = false;
                setTouchAble(!animating);
            }@Override
            public void onAnimationRepeat(Animator animation) {}});
    }animator.start();
    if (mViewPager != null) {mViewPager.setCurrentItem(toPos);
    }return true;
}

下面是dispatchDraw方法,为了更简单看懂,我就截取position从左向右的情况;处理临界情况很重要,没处理好你会发现绘制出来的是什么鬼!

@Override
protected void dispatchDraw(Canvas canvas) {canvas.save();
    mPath.reset();
    tabNum = getChildCount();
    for (int i = 0; i < tabNum; i++) {canvas.drawCircle(div + radius + i * (div + 2 * radius), startY, radius, mPaintCircle);
    }if (mCurrentTime == 0) {resetP();
        canvas.drawCircle(div + radius + (currentPos) * (div + 2 * radius), startY, 0, mClickPaint);
        mPaint.setColor(startColor);
        canvas.translate(startX, startY);
        p2.setX(radius);
    }if (mCurrentTime > 0 && mCurrentTime <= 0.2) {if (animating)canvas.drawCircle(div + radius + (toPos) * (div + 2 * radius), startY, radius * 1.0f * 5 * mCurrentTime, mClickPaint);
        canvas.translate(startX, startY);
        p2.setX(radius + 2 * 5 * mCurrentTime * radius / 2);
    } else if (mCurrentTime > 0.2 && mCurrentTime <= 0.5) {canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);
        p2.setX(2 * radius);
        p1.setX(0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);
        p3.setX(0.5f * radius * (mCurrentTime - 0.2f) / 0.3f);
        p2.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);
        p4.setMc(mc + (mCurrentTime - 0.2f) * mc / 4 / 0.3f);
    } else if (mCurrentTime > 0.5 && mCurrentTime <= 0.8) {canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);
        p1.setX(0.5f * radius + 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
        p3.setX(0.5f * radius + 0.5f * radius * (mCurrentTime - 0.5f) / 0.3f);
        p2.setMc(1.25f * mc - 0.25f * mc * (mCurrentTime - 0.5f) / 0.3f);
        p4.setMc(1.25f * mc - 0.25f * mc * (mCurrentTime - 0.5f) / 0.3f);
    } else if (mCurrentTime > 0.8 && mCurrentTime <= 0.9) {p2.setMc(mc);
        p4.setMc(mc);
        canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);
        p4.setX(-radius + 1.6f * radius * (mCurrentTime - 0.8f) / 0.1f);
    } else if (mCurrentTime > 0.9 && mCurrentTime < 1) {p1.setX(radius);
        p3.setX(radius);
        canvas.translate(startX + distance, startY);
        p4.setX(0.6f * radius - 0.6f * radius * (mCurrentTime - 0.9f) / 0.1f);
    }if (mCurrentTime == 1) {lastCurrentTime = 0;
        mPaint.setColor(endColor);
        p1.setX(radius);
        p3.setX(radius);
        canvas.translate(startX + distance, startY);
        p4.setX(0);
        currentPos = toPos;
        resetP();
        canvas.translate(radius, 0);
    }mPath.moveTo(p1.x, p1.y);
    mPath.cubicTo(p1.right.x, p1.right.y, p2.bottom.x, p2.bottom.y, p2.x, p2.y);
    mPath.cubicTo(p2.top.x, p2.top.y, p3.right.x, p3.right.y, p3.x, p3.y);
    mPath.cubicTo(p3.left.x, p3.left.y, p4.top.x, p4.top.y, p4.x, p4.y);
    mPath.cubicTo(p4.bottom.x, p4.bottom.y, p1.left.x, p1.left.y, p1.x, p1.y);
    if (mCurrentTime > 0 && mCurrentTime < 1)mPaint.setColor(getCurrentColor(mCurrentTime, startColor, endColor));
    canvas.drawPath(mPath, mPaint);
    canvas.restore();
    super.dispatchDraw(canvas);
}
mCurrentTime 是动画变化时刷新的值,从0到1,根据这个值重绘时计算圆的坐标。我将mCurrentTime 分为下列几种状态:
mCurrentTime == 0: 
        这个状态就是根据position绘制正常的圆。
mCurrentTime > 0 && mCurrentTime <= 0.2:
        这个此时圆向右凸起,但是原本的canvas.translate和上个状态不变,所以是圆停止在当前位置并且慢慢凸起的效果。
mCurrentTime > 0.2 && mCurrentTime <= 0.5:
        这时圆开始平移,canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f, startY);那为啥是除以0.7呢?因为0到0.2没平移,0.2到0.9平移完成,0.9到1处理回弹。平移时间只有0.9-0.2=0.7,这段时间要完成一个distance的距离的平移。同时之前圆向右凸起时,p2组的点x坐标总共增加了一个radius(这个决定凸起程度)。现在要把它弄回对称椭圆,所以p1组和p3组的点要右移半个radius,同时mc调整一下使椭圆不那么尖;
mCurrentTime > 0.5 && mCurrentTime <= 0.8:
        p1和p3的X坐标继续往右移,mc逐渐重置为原来大小,效果就是圆的最右端固定不变,左边的凸起缩回去,
mCurrentTime > 0.8 && mCurrentTime <= 0.9:
        左边的p4.组点往右平移过头,圆形成凹陷,
mCurrentTime > 0.9 && mCurrentTime < 1:
        这个阶段是处理回弹,p4.组点x逐渐恢复正常。表现为回弹恢复为标准圆。
mCurrentTime == 1:
        position此时真实改变了,重置为正常的圆。
        以上的每个阶段在进入下个阶段时,都需要重置一下p坐标,因为view刷新频率是有限的,有些结束的临界状态值直接就跳过了,导致参数没跟着变化就绘制了图像。


第二种情况,拖动viewpager切换:

viewPager.addOnPageChangeListener,在onPageScrolled中调用 updateDrop(position, positionOffset, positionOffsetPixels),更新位置。这里需要注意的是点击indicator也会回调, 若不进行判断会造成重复的移动,所以之前在动画开启的监听时设置boolean animating值。

private void updateDrop(int position, float positionOffset, int positionOffsetPixels) {if (animator != null)animator.cancel();
    if ((position + positionOffset) - currentPos > 0)direction = true;
    else if ((position + positionOffset) - currentPos < 0)direction = false;
    if (direction)toPos = currentPos + 1;
    else
        toPos = currentPos - 1;
    startColor = roundColors[(currentPos) % 4];
    endColor = roundColors[(currentPos + (direction ? 1 : -1)) % 4];
    startX = div + radius + (currentPos) * (div + 2 * radius);
    distance = direction ? ((2 * radius + div) + (direction ? -radius : radius)) : (-(2 * radius + div) + (direction ? -radius : radius));
    mCurrentTime = position + positionOffset - (int) (position + positionOffset);
    if (!direction)mCurrentTime = 1 - mCurrentTime;
    if (Math.abs(lastCurrentTime - mCurrentTime) > 0.2) {//突变时根据接近0或1更改为0或1;
        if (lastCurrentTime < 0.1)mCurrentTime = 0;
        else if (lastCurrentTime > 0.9)mCurrentTime = 1;
    }lastCurrentTime = mCurrentTime;
    invalidate();
}
        这里我用mCurrentTime = position + positionOffset - (int) (position + positionOffset);然而这样计算是有问题的,比如向左滑动,它是从0到0.9几,然后突变为0,为了这个判断添加了一个lastCurrentTime ,根据接近接近0或1更改为0或1。

        总结一下,需要注意的是mCurrentTime 状态的划分、临界状态的处理、以及在合适的位置重置p坐标,在写的过程几次碰到绘制的图像莫名其妙,这是p的坐标问题,查找原因一般也是状态没重置。

       写在最后:昨天投稿了,今天收到郭神的回复,会帮我发布文章,但是由于稿件的积压,需要等上一段时间,还是很开心,感谢郭神。

源码地址:https://github.com/Ulez/DropIndicator


这篇关于自定义View之炫酷的水滴ViewPageIndicator的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/266466

相关文章

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

Oracle type (自定义类型的使用)

oracle - type   type定义: oracle中自定义数据类型 oracle中有基本的数据类型,如number,varchar2,date,numeric,float....但有时候我们需要特殊的格式, 如将name定义为(firstname,lastname)的形式,我们想把这个作为一个表的一列看待,这时候就要我们自己定义一个数据类型 格式 :create or repla

MVC(Model-View-Controller)和MVVM(Model-View-ViewModel)

1、MVC MVC(Model-View-Controller) 是一种常用的架构模式,用于分离应用程序的逻辑、数据和展示。它通过三个核心组件(模型、视图和控制器)将应用程序的业务逻辑与用户界面隔离,促进代码的可维护性、可扩展性和模块化。在 MVC 模式中,各组件可以与多种设计模式结合使用,以增强灵活性和可维护性。以下是 MVC 各组件与常见设计模式的关系和作用: 1. Model(模型)

HTML5自定义属性对象Dataset

原文转自HTML5自定义属性对象Dataset简介 一、html5 自定义属性介绍 之前翻译的“你必须知道的28个HTML5特征、窍门和技术”一文中对于HTML5中自定义合法属性data-已经做过些介绍,就是在HTML5中我们可以使用data-前缀设置我们需要的自定义属性,来进行一些数据的存放,例如我们要在一个文字按钮上存放相对应的id: <a href="javascript:" d

一步一步将PlantUML类图导出为自定义格式的XMI文件

一步一步将PlantUML类图导出为自定义格式的XMI文件 说明: 首次发表日期:2024-09-08PlantUML官网: https://plantuml.com/zh/PlantUML命令行文档: https://plantuml.com/zh/command-line#6a26f548831e6a8cPlantUML XMI文档: https://plantuml.com/zh/xmi

argodb自定义函数读取hdfs文件的注意点,避免FileSystem已关闭异常

一、问题描述 一位同学反馈,他写的argo存过中调用了一个自定义函数,函数会加载hdfs上的一个文件,但有些节点会报FileSystem closed异常,同时有时任务会成功,有时会失败。 二、问题分析 argodb的计算引擎是基于spark的定制化引擎,对于自定义函数的调用跟hive on spark的是一致的。udf要通过反射生成实例,然后迭代调用evaluate。通过代码分析,udf在

鸿蒙开发中实现自定义弹窗 (CustomDialog)

效果图 #思路 创建带有 @CustomDialog 修饰的组件 ,并且在组件内部定义controller: CustomDialogController 实例化CustomDialogController,加载组件,open()-> 打开对话框 , close() -> 关闭对话框 #定义弹窗 (CustomDialog)是什么? CustomDialog是自定义弹窗,可用于广告、中