参考文章:
目录
注:请不要边看边复制代码,以下代码不全。建议缕清整个绘制流程后,到中去看完整代码。写这篇文章的原因是这个github上的代码注释很少,自己也琢磨了很久。为了巩固知识,加深理解,才有了这篇文章。
1. 效果图展示
2. 绘制流程
2.1 onMeasure测量大小
这一步我们直接调用父类的onMeasure(widthMeasureSpec, heightMeasureSpec)
方法就好了,不需要过多的干涉。 代码如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }复制代码
2.2 onLayout 摆放位置
通过图片,我们可知,现在两圆间距
,圆的半径
(半径可以有一个默认值,然后开放自定义属性让外界传入)都已知,但是缺少图片宽度
这个重要参数。
其实,我们可以这么想,这个图片的大小,肯定是不能超过圆的半径的,而且,为了协调,它最好是个正方形。所以,我们可以让外界传入一个比例值,让使用者规定图片的大小要相对于圆的半径多大,这样,图片的宽度就可以确定了,而且也可以确保图片一定在圆圈之内。
so,图片的宽度可以这么计算:
int picWidth = scale * radius ;//scale就是传入的比例复制代码
好了,图片的宽度已知了,那么我们的摆放位置也就可以求了:
- 首先在onSizeChanged保存一些需要重复使用到的数值(比如:间距)
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { spacing = (w - 2 * tabNum * radius) / (tabNum + 1); startX = spacing + radius; startY = h / 2; super.onSizeChanged(w, h, oldw, oldh); }复制代码
这里的startX
是第一个圆的中点x坐标,startY
是第一个圆的中点y坐标。 这里没有直接记录控件的宽和高,因为通过第一个圆来求其他圆的位置会容易点。 2. 在onLayout中确定各个圆的位置
private float scale = 0.5f; @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { tabNum = getChildCount(); for (int i = 0; i < tabNum; i++) { View child = getChildAt(i); child.layout((int) (spacing + (1 - scale * 1) * radius + i * (spacing + 2 * radius)), (int) (startY - scale * radius ), (int) (spacing + (1 + scale * 1 ) * radius + i * (spacing + 2 * radius)), (int) (startY + scale * radius )); } }复制代码
这里如果看 源码的话,会看到,它的计算方法中还会除以一个g2
变量,这个g2
变量等于1.41421
。 他的代码如下:
child.layout((int) (div + (1 - scale * 1 / g2) * radius + i * (div + 2 * radius)), (int) (startY - scale * radius / g2), (int) (div + (1 + scale * 1 / g2) * radius + i * (div + 2 * radius)), (int) (startY + scale * radius / g2));复制代码
div
就是我们的spacing
,也就是两圆间距。 作者这么做的原因未知,希望知道的人留言告知一下。多谢~
2.3 dispatchDraw将圆和图片绘制出来
在测量好大小,确定好位置后,我们就可以拿起我们的画笔,进行绘制图形。
- 初始化画笔
private float radius = 50;public MyDropIndicator(Context context) { super(context); init(); }public MyDropIndicator(Context context, AttributeSet attrs) { super(context, attrs); init(); }private void init() { mPaintCircle = new Paint(); mPaintCircle.setColor(Color.RED); mPaintCircle.setStyle(Paint.Style.STROKE); mPaintCircle.setAntiAlias(true); mPaintCircle.setStrokeWidth(3);}复制代码
dispatchDraw
方法绘制图形
protected void dispatchDraw(Canvas canvas) { tabNum = getChildCount(); for (int i = 0; i < tabNum; i++) { canvas.drawCircle(spacing + radius + i * (spacing + 2 * radius), startY, radius, mPaintCircle); }.....}复制代码
到这一步,基本的界面应该就可以看到了。可以测试一下:
public class TestActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); }}复制代码
复制代码
运行起来的界面效果如下:
2.4 绘制点击图形的波纹效果
点击后的效果如图:
这段动画的原理就是在一段时间内,不断改变圆的半径。 这个效果很容易做,难点应该是如何知道点击的是哪个圆,以及如何确定动画执行的圆的中心点。 那么如何知道是哪个圆呢?
从图中可以看出第一个圆的范围是:
spacing < x < spacing + radius * 2复制代码
依次类推,我们可以得出: 第N个圆的点击范围是:
(N-1)* spacing + radius * 2 < x< N*spacing + radius * 2复制代码
明白原理后,应该我们就可以开始写了。。。
- 初始化画笔
mClickPaint = new Paint();mClickPaint.setColor(Color.YELLOW);mClickPaint.setStyle(Paint.Style.STROKE);mClickPaint.setAntiAlias(true);mClickPaint.setStrokeWidth(radius / 2);复制代码
- onTouchEvent中处理点击事件
public boolean onTouchEvent(MotionEvent event){ float x = event.getX(); if (x > spacing + 2 * radius && x < (spacing + 2 * radius) * tabNum) { int toPos = (int) (x / (spacing + 2 * radius)); if (toPos != currentPos && toPos <= tabNum) { startAniTo(currentPos, toPos); } } else if (x > spacing && x < spacing + 2 * radius) { if (currentPos != 0) { startAniTo(currentPos, 0); } } return super.onTouchEvent(event); }复制代码
- 启动动画
private boolean startAniTo(int currentPos, int toPos) { this.currentPos = currentPos; this.toPos = toPos; if (currentPos == toPos) { return true; } if (animator == null) { animator = ValueAnimator.ofFloat(0, 1.0f); animator.setDuration(1000); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //重点,在draw的时候会根据currentTime进行变换半径 mCurrentTime = (float) animation.getAnimatedValue(); invalidate(); } }); } animator.start(); return true; }复制代码
- 根据currentTime进行绘制不同半径的圆
protected void dispatchDraw(Canvas canvas) { //...省略 if (mCurrentTime > 0 && mCurrentTime <= 0.2) { canvas.drawCircle(spacing + radius + (toPos) * (spacing + 2 * radius), startY, radius * 1.0f * 5 * mCurrentTime, mClickPaint); } //....省略}复制代码
到这一步,点击效果就完成了。。
2.5 利用贝塞尔曲线绘制移动动画
现在,最难的部分来了。 先放一张最后实现的动画效果。
关于三阶贝塞尔曲线的内容,这里不进行展开,请先阅读文章: 三阶贝塞尔曲线的变化规律,用图形来表示,就是如下:
通过观察最终的效果,我们可以发现,其实圆的状态就是以下两种:
分别是状态1,和状态2。(当从左向右运动时)要实现这两种状态,首先要了解如何通过贝塞尔曲线画圆。 用贝塞尔曲线绘制一个圆需要12个点,如图所示。
现在,我们写一个demo来测试一下,当改变点的位置以及改变m的大小,会对圆的形状造成什么影响。 首先我们用三阶贝塞尔曲线绘制出了一个圆,此时:
mc = 0.552284749831;m = 圆的半径(demo里是200) * mc;复制代码
至于为什么是0.551915024494f
,请查看。 此时,我们得到的圆是这样的:
ok,现在我们改变p2的点试试看效果(p2也就是右边的点):
可以看到,我们可以通过修改p2的位置,使得图形更扁。
好了。接下来,我们修改m
的大小:
可以看到,通过修改m的大小,可以改变圆两点之间的弧度,也就是m越大时,两点之间的弧度越扁平。
现在,我们一点点来分析移动过程中图形状态的变化。 首先,我们假设圆从一个点移动到另外一个点用了1s的时间。 现在,
- 当 0 < t < 0.2s的时候,当前的形状是: 可以看到,其实就是把p2点慢慢向右移动,最终移动到半径的两倍的位置; 用代码表示:
if (mCurrentTime > 0 && mCurrentTime <= 0.2) { //画布向右移动,方便进行绘制圆球 canvas.translate(startX,startY); //p2向右移动 p2.setX(radius + 2 * 5 * mCurrentTime * radius / 2); }复制代码
- 当 0.2 < t < 0.5s时, 此时圆开始向右移动,移动的距离是多少呢?答案是:
startX + (t- 0.2f) * distance / 0.7f
。 那为啥是除以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调整一下使椭圆不那么尖; 用代码表示:
if (mCurrentTime > 0.2f && mCurrentTime <= 0.5f){ canvas.translate(startX + (mCurrentTime - 0.2f) * distance / 0.7f,startY); p2.setX(2 * radius); p1.setX(((mCurrentTime - 0.2f) * 0.5f * radius / 0.3f)); p3.setX(((mCurrentTime - 0.2f) * 0.5f * radius / 0.3f)); p2.setMc(mc + (mCurrentTime - 0.2f) * mc /4 / 0.3f); p4.setMc(mc + (mCurrentTime - 0.2f) * mc /4 / 0.3f); }复制代码
- 当0.5 < t < 0.8s时 p1和p3的X坐标继续往右移,mc逐渐重置为原来大小,效果就是圆的最右端固定不变,左边的凸起缩回去。 用代码表示:
if (mCurrentTime > 0.5f && mCurrentTime <= 0.8f){ //开始恢复原始形状 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); }复制代码
- 当0.8 < t < 0.9s时 左边的p4.组点往右平移过头,圆形成凹陷。 用代码表示:
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); } 复制代码
- 当0.9 < t <1s时 这个阶段是处理回弹,p4.组点x逐渐恢复正常。表现为回弹恢复为标准圆。 用代码表示:
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); 复制代码
注意,这里的代码都是默认球是从左向右运动的,明白了从左向右运动的规律后,从右向左其实也就不难了。为了节省代码,只粘贴了从左向右运动的。
OK,至此,本文结束。谢谢观看。