有时候,意外也许就会造成一个不经意间的成功。

【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】

【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】

【注意:本文章前两节尽是吐槽,要看代码,实现方案什么的,请直接看第三节】

重要的话要说三遍。。。

咳咳,,,咱们不是专业写手,就不要那么装文艺了,还是逗比点好。 不如咱们先上个图?

咳咳,请忽略我竖屏录制了啦。。。。还有,请忽略为啥那条线会在屏幕边边走,在下不拘束它的自由←_←

##起因 事情的起源是这样滴,因为某种需求,咱们需要撸一个这样子的控件(为了不泄露设计图,咱们就拿MPAndroidChart的图展示吧,反正需求都一样):

拿到设计图,第一想法,这有多难,直接上MP库呗,于是把库放到MethodsCount一查,哭了。。。2K多个方法欸,2K欸!!!!2K!!!!

遂放弃,,,还是自己开干吧

看到曲线什么的,第一时间**贝塞尔曲线**走起~ 于是,最为一个面向搜索引擎编程的程序员,当然谷歌一下贝塞尔。。。

随便搜搜,于是就看到CSDN的一篇文章文章点我。

啊~好细致,好赞啊!!!可惜在下没法短时间内理解啊TAT。然而,按照我平时的经验,还是撸个初步的东西出来吧。。。

OMG....这神马啊,这尖尖,都快能戳死人了好吗。。。。 于是,选择战略性撤退,休息一晚再开干。

##意外 第二天,毫无疑问的继续一脸蒙逼。。。 这时候,一位老朋友叫我帮他抠个图,是的,你没看错,抠图。。。。如果有看过我的一起撸个朋友圈系列文章的人,或许会知道,在下也会AE这个视频后期软件。。。

抠就抠吧。。。。但!!! 意外就这么来了。。。。抠图的时候,为了边缘平滑,我经常调节锚点,使曲线更加的平滑,然后居然让我发现了一个规律0.0,大致原理如下吧:

如图,如果多看几遍,也许你会发现,当两个控制点的x位置在前后两个坐标内,而y分别与前后两个坐标平齐的时候,转折点的衔接最为平滑,否则妥妥的出现尖尖(嗯。。。我还特地用鼠标绕了几圈标出尖尖位置)。

妈蛋,得来毫不费功夫啊。。。。真的想抱着我朋友亲几口,可惜在下不搞基- -

##实现

既然找到了突破口,那妥妥的开干啊。

于是兴冲冲的继承View,开始我们的伟业:

public class TestView extends View {// 最大值private final float maxValue = 100f;// 测试数据private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,24f, 26f, 58f };//private float[] testDatas = { 60f, 55f, 57f, 50f ,56f,70f};//private float[] testDatas = { 60f, 55f};// 点记录private List<Point> datas;private final int num = 12;// 路径private Path clicPath;// 渐变填充private Paint mPaint;// 辅助性画笔private Paint controllPaintA;private Paint controllPaintB;private Path linePath;private PathMeasure mPathMeasure;private float[] mCurrentPosition = new float[2];private float[] mPrePosition = new float[2];LinearGradient mGradient;int width;int height;int offSet;...构造器初始化以上的东西
复制代码

我们定义了一个最大值,和一组测试数据。这个最大值的作用是用来计算当前数据在屏幕的y位置,比如这样:最大值100,我们的数值15,但我们的屏幕是720*1280,那么当然不可以只画15像素了,这怎么看得到嘛,我们的y位置判定为:

屏幕高度*(1-(15/100))

为什么要用1减去百分比,因为原点不在左下角而在左上角,所以我们需要减掉。

接下来到measure初始化我们的点。

 @Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();offSet = width / testDatas.length;if (datas.size() == 0) {for (int i = 0; i < testDatas.length; i++) {float ratio = testDatas[i] / maxValue;Point point;if (i == 0) {point = new Point(0, (int) (height * (1 - ratio)));}else if (i == testDatas.length - 1) {point = new Point(width, (int) (height * (1 - ratio)));}else {point = new Point(i * offSet, (int) (height * (1 - ratio)));}datas.add(point);}}if (mGradient == null) {mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),Shader.TileMode.CLAMP);mPaint.setShader(mGradient);}}
复制代码

其中我们的offSet是偏移量,其作用是使点在屏幕上的x位置是均分的,然后初始化一个线性渐变。

这时候我们的点是这样的(为了更方便查看,我们设定为横屏并给上线条):

点和点之间的x偏移都是一致的(最后一个除外)

然后我们在onDraw开始绘制():

 @Overrideprotected void onDraw(Canvas canvas) {clicPath.reset();super.onDraw(canvas);//clicPath.moveTo(datas.get(0).x, datas.get(0).y);for (int i = 0; i < datas.size() - 1; i++) {Point startPoint = datas.get(i);Point endPoint = datas.get(i + 1);if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);int controllA_X = (startPoint.x + endPoint.x) >>1;int controllA_Y = startPoint.y;int controllB_X = (startPoint.x + endPoint.x) >>1;int controllB_Y = endPoint.y;clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);// 控制点展示canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);//控制点展示canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);}clicPath.lineTo(datas.get(datas.size() - 1).x, height);clicPath.lineTo(datas.get(0).x, height);clicPath.lineTo(datas.get(0).x, datas.get(0).y);canvas.drawPath(clicPath, mPaint);}
复制代码

这里解析一下: 当i==0,也就是画第一个点的时候,我们需要把画笔移到我们第一个点的位置,否则永远都会从0,0开始,以后就不需要移动了,因为画完一条线后,画笔位置会停留在最后一个点。

我们可以看到两个控制点的坐标,跟我们上面AE展示出来的是一样的,x位置都是取两个点的中间,y则是分别跟两边平齐,这样的曲线最为圆滑

clicPath.cubicTo这个方法,前面4个参数分别代表着控制点1的xy,控制点2的xy,最后一个参数则是结束点的xy,在下一次循环到来之时,最后一个参数则会作为下一次绘制的起点。

最后别忘了在循环外面将path封闭起来,我们不可以直接用path.close(),因为close方法是最后一个点与第一个点直接连一条直线的,但我们需要填充曲线下方。

为了方便展示,我们添加了参考点以及将线条设置为stroke,先不填充:

可以看到,我们的控制点都很好的分布在两点之间,曲线看起来十分平滑。

为了更清晰,我们将测试数据减少一点:

private float[] testDatas = { 60f, 30f, 57f, 41f ,88f,70f};
复制代码

现在看起来更加的清晰,然后我们填充一下并取消掉辅助线条和辅助点。

现在初步达到我们的效果了。。

然而,程序员的冤家产品却说:哎,这太单调了,给个动画呗。。。。

妈蛋!!!!!

不过骂完还是得干啊-T-

于是这次我们需要借助PathMeasure这个类

这个类通常用于将某个path转换为一个具体的position,更多情况下是用作路径动画。

还记得我们之前定义的变量里面有些什么吗:

    private PathMeasure mPathMeasure;private float[] mCurrentPosition = new float[2];private float[] mPrePosition = new float[2];
复制代码

根据命名,也很清楚是干啥的。

接下来继续开工:

首先定义一个公用方法给外部调用:

public void startAnima(long duration) {}
复制代码

我们通过这个方法来绘制线条

然后我们利用ValueAnimator来动态获取我们path的坐标

 public void startAnima(long duration) {if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());valueAnimator.setDuration(duration);// 减速插值器valueAnimator.setInterpolator(new DecelerateInterpolator());valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (Float) animation.getAnimatedValue();// 获取当前点坐标封装到mCurrentPositionmPathMeasure.getPosTan(value, mCurrentPosition, null);invalidate();if (value == mPathMeasure.getLength()) animaFirst = true;}});valueAnimator.start();}
复制代码

为了防止onDraw里面多次绘制,我们定义一个animaFirst。

然后补充我们的onDraw方法:

  @Overrideprotected void onDraw(Canvas canvas) {...if (animaFirst) {linePath.moveTo(datas.get(0).x, datas.get(0).y);mPrePosition[0] = datas.get(0).x;mPrePosition[1] = datas.get(0).y;animaFirst = false;}else {int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);int controllA_Y = (int) mPrePosition[1];int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) /2);int controllB_Y = (int) mCurrentPosition[1];linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],mCurrentPosition[1]);mPrePosition[0] = mCurrentPosition[0];mPrePosition[1] = mCurrentPosition[1];}canvas.drawPath(linePath, controllPaintA);}
复制代码

如果动画刚启动,我们就把点移到第一个点的位置,同时记录 如果动画已经启动了,我们就重复前面的步骤画出贝塞尔,当然,你也可以直接lineTo,然后将当前点付给前一个点。

最后,我们在onDetachedFromWindow清掉各种信息,毕竟那啥,内存还是挺珍贵的对吧-V-

   @Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();datas.clear();clicPath=null;controllPaintA=null;controllPaintB=null;mPathMeasure=null;}
复制代码

最终效果图(未修复到屏幕边边继续画的问题。。。,以及貌似有些地方有点偏差):

【附】所有代码(可以直接copy使用,因为是测试demo,所以并没有封装什么的,同时measure那里也没有指定wrap_content时的大小,大家可以自行封装或修复或扩展哈哈-V-):

/*** Created by 大灯泡 on 2016/2/29.*/
public class TestView extends View {// 最大值private final float maxValue = 100f;// 测试数据//private float[] testDatas = { 55f, 38f, 50f, 44f, 31f, 22f, 9f, 19f, 50f, 78f, 62f, 51f, 45f, 66f, 79f, 50f, 33f,//        24f, 26f, 58f };private float[] testDatas = { 60f, 30f, 57f, 41f, 88f, 70f };//private float[] testDatas = { 60f, 55f};// 点记录private List<Point> datas;// 路径private Path clicPath;// 渐变填充private Paint mPaint;// 辅助性画笔private Paint controllPaintA;private Paint controllPaintB;private Path linePath;private PathMeasure mPathMeasure;private float[] mCurrentPosition = new float[2];private float[] mPrePosition = new float[2];LinearGradient mGradient;int width;int height;int offSet;public TestView(Context context) {this(context, null);}public TestView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public TestView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);clicPath = new Path();linePath = new Path();datas = new ArrayList<>();mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//mPaint.setStyle(Paint.Style.STROKE);controllPaintA = new Paint(Paint.ANTI_ALIAS_FLAG);controllPaintA.setStyle(Paint.Style.STROKE);controllPaintA.setStrokeWidth(5);controllPaintA.setColor(0xffff0000);controllPaintB = new Paint(Paint.ANTI_ALIAS_FLAG);controllPaintB.setStyle(Paint.Style.STROKE);controllPaintB.setColor(0xff00ff00);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();height = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();offSet = width / testDatas.length;if (datas.size() == 0) {for (int i = 0; i < testDatas.length; i++) {float ratio = testDatas[i] / maxValue;Point point;if (i == 0) {point = new Point(0, (int) (height * (1 - ratio)));}else if (i == testDatas.length - 1) {point = new Point(width, (int) (height * (1 - ratio)));}else {point = new Point(i * offSet, (int) (height * (1 - ratio)));}datas.add(point);}}if (mGradient == null) {mGradient = new LinearGradient(getMeasuredWidth() >> 1, getMeasuredHeight() >> 1, getMeasuredWidth() >> 1,getMeasuredHeight(), Color.parseColor("#e0cab3"), Color.parseColor("#ffffff"),Shader.TileMode.CLAMP);mPaint.setShader(mGradient);}}private boolean animaFirst = true;@Overrideprotected void onDraw(Canvas canvas) {clicPath.reset();super.onDraw(canvas);for (int i = 0; i < datas.size() - 1; i++) {Point startPoint = datas.get(i);Point endPoint = datas.get(i + 1);if (i == 0) clicPath.moveTo(startPoint.x, startPoint.y);int controllA_X = (startPoint.x + endPoint.x) >> 1;int controllA_Y = startPoint.y;int controllB_X = (startPoint.x + endPoint.x) >> 1;int controllB_Y = endPoint.y;clicPath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, endPoint.x, endPoint.y);/**辅助点和线**///canvas.drawCircle(controllA_X,controllA_Y,5,controllPaintA);//canvas.drawCircle(controllB_X,controllB_Y,5,controllPaintB);//canvas.drawCircle(startPoint.x,startPoint.y,5,mPaint);//canvas.drawLine(startPoint.x,startPoint.y,controllA_X,controllA_Y,mPaint);//canvas.drawLine(endPoint.x,endPoint.y,controllB_X,controllB_Y,mPaint);}clicPath.lineTo(datas.get(datas.size() - 1).x, height);clicPath.lineTo(datas.get(0).x, height);clicPath.lineTo(datas.get(0).x, datas.get(0).y);canvas.drawPath(clicPath, mPaint);if (animaFirst) {linePath.moveTo(datas.get(0).x, datas.get(0).y);mPrePosition[0] = datas.get(0).x;mPrePosition[1] = datas.get(0).y;animaFirst = false;}else {int controllA_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);int controllA_Y = (int) mPrePosition[1];int controllB_X = (int) ((mPrePosition[0] + mCurrentPosition[0]) / 2);int controllB_Y = (int) mCurrentPosition[1];linePath.cubicTo(controllA_X, controllA_Y, controllB_X, controllB_Y, mCurrentPosition[0],mCurrentPosition[1]);mPrePosition[0] = mCurrentPosition[0];mPrePosition[1] = mCurrentPosition[1];}canvas.drawPath(linePath, controllPaintA);}public void startAnima(long duration) {if (mPathMeasure == null) mPathMeasure = new PathMeasure(clicPath, true);ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());valueAnimator.setDuration(duration);// 减速插值器valueAnimator.setInterpolator(new DecelerateInterpolator());valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float value = (Float) animation.getAnimatedValue();// 获取当前点坐标封装到mCurrentPositionmPathMeasure.getPosTan(value, mCurrentPosition, null);Log.d("curX",""+mCurrentPosition[0]);invalidate();if (value == mPathMeasure.getLength())animaFirst = true;}});valueAnimator.start();}@Overrideprotected void onDetachedFromWindow() {super.onDetachedFromWindow();datas.clear();clicPath = null;controllPaintA = null;controllPaintB = null;mPathMeasure = null;}
}
复制代码

记一次意外的自定义控件相关推荐

  1. 【事故】记一次意外把公司项目放到GitHub并被fork,如何使用DMCA下架政策保障隐私

    前言

  2. 记:一次意外JTAG使用引发对于STM32内核的了解

    文章目录 0x01 数据异常? 0x02 排查过程 0x03 究其原因 下面有空更新,内部解锁FLASH操作.出差期间发文,很累了,暂时先写到这里 更多 0x01 数据异常? 在一次偶然的情况下,大佬 ...

  3. 记:一次单板的意外串口异常解决(下)——PC端

    0x00前言 写下这篇文章主要目的是总结一下最近调试的一块单板遇到的,关于串口方面的一些问题.本文将会分为上下两篇,这篇为下篇,主要讲述的是: 串口接在PC端时出现意外的接收数据错误 阅读本文,您可能 ...

  4. Yosimite 系统 “发生意外错误(错误代码-50)” (记一次macbook pro(mid2012) 自主维修排错经历)...

    电脑型号: Macbook Pro(Mid 2012)   A1278 问题描述: 上周,电脑偶尔弹出提示框"发生意外错误(错误代码-50)",弹出这个提示之后硬盘好像变成只读模式 ...

  5. 看书要仔细----自定义控件库摸索记

    日前在Windows Phone上做了一个小练习,用模板定义了一个控件,觉得这控件使用效果还不错,想把它做成一个控件库,留着给其他项目用. 没想到一个小小的控件库居然花了几天的时间才搞定.下面请看这个 ...

  6. 挨踢人生路--记我的10年18家工作经历 - 后记

    挨踢人生路--记我的10年18家工作经历 - 前言 挨踢人生路--记我的10年18家工作经历 - 从大学说起――不得不说的一些事情 挨踢人生路--记我的10年18家工作经历 - 第1家公司,在老家的工 ...

  7. [转载] 杜拉拉升职记——02 单相思与性骚扰

    来源:李可. 杜拉拉升职记(第三版). 西安: 陕西师范大学出版社, 2010, 5. 02  单相思与性骚扰的区别 拉拉注意到,DB所有经理办公室沿走道的这一面,都是用大块的玻璃来做间隔墙. 拉拉问 ...

  8. android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  9. Java练习 SDUT-2737_小鑫の日常系列故事(六)——奇遇记

    小鑫の日常系列故事(六)--奇遇记 Time Limit: 1000 ms Memory Limit: 65536 KiB Problem Description 今天,小鑫在山上玩的时候,意外被推下 ...

最新文章

  1. 如何进行网站主题模型优化_如何进行网站关键词优化
  2. 密码太多记不住?SSO帮你轻松访问VDI及外部资源
  3. 在.Net中使用log4Net
  4. 个人觉得非常好用的mysql客户端工具的HeidiSQL
  5. Python爬虫教程之Scrapy 框架
  6. 微信悄悄更新:朋友圈不喜欢的评论可以删除了
  7. a.hashCode() 有什么用?与 a.equals(b)有什么关系?
  8. Python模块受欢迎排行榜Top200
  9. WARCannon:高速低功耗网络爬虫
  10. Ansible tower 3.7.0-4自动化运维管理安装方法
  11. 计算机械效率的公式四种,物理计算公式.doc
  12. 计算机应用发展史的第四代,1.1.1 计算机的概念及其发展史
  13. md文件转换成word文档
  14. cdh5.9运行mapreduce uber任务报java.lang.RuntimeException: native snappy library not available错误
  15. linux内存占用过高怎么解决,centos7内存占用过高处理方法
  16. 微信传文件又慢又限制大小?试试这3个免费在线传文件工具!
  17. 【WEB安全】PHP靶场实战分析——DVWA
  18. 国民技术MCU产品类别介绍——速记分类
  19. 贾跃亭所持乐视网股权触及平仓线 部分面临被司法拍卖
  20. html页面加载有时没有网样式,网站css样式不加载是什么原因?

热门文章

  1. CHM文件不能正确显示
  2. C++ 20的悲叹,未出世就被群嘲“劝退”
  3. Neo4j-Cypher语言语法
  4. nginx反向代理监听非80端口造成的端口丢失解决方案
  5. DOM之城市二级联动
  6. AndroidMainifest标签说明2——lt;activitygt;
  7. 结合EM快速解决复杂的配置问题
  8. HDOJ 2009 求数列的和
  9. 黄淮学院计算机录取线,黄淮学院录取投档线
  10. 华为-yolo系列详解