先上个效果:
源码可以看这里
想试试效果的话可以直接通过gradle引入:
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
compile 'com.github.nanyi5452:viewpagerDotIndicator:4c0a7bcf83'
然后布局里面加入就可以了,注意父布局属性里要加上 android:clipChildren=”false” 这样view才能在范围外绘制。
<com.nanyi545.www.bounceindicatorlib.BounceIndicator
android:id="@+id/indicator"
android:layout_margin="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
可以调用 mBounceIndicator.addCount(); 方法来变化计数器数字。
下面介绍下我实现的思路。
第一步
画出拖拽点和固定点之间的平滑曲线。
简单起见,我用了下面的这个方案。
1. 连接两圆圆心,做线A
2. 分别过两圆圆心,做A的垂线,交于圆周
3. 以在同一侧的两个圆周交点为终点,以线A中点为控制点,做贝塞尔曲线
这只是一种简单的平滑方案,个人认为另外一种平滑曲线更加好看一点也稍微麻烦一点,可以参考这里。
这个计算过程放在重写的onTouchEvent方法里面,根据手指触点计算出这个平滑曲线的路径,以供draw方法绘制。
第二步
实现松手之后的回弹效果。
逻辑上很简单,如果拖拽的距离>某固定值,起点的圆弹向松手点,并且爆炸消失(爆炸消失如何实现最后介绍);如果拖拽的距离<某固定值,松手点的数字弹回到起点。在第一步的基础上只要知道回弹过程中的起点终点的位置就行了,而这两个位置可以利用sdk中的Scroller类来轻松算出。
以松手点的数字弹回到起点的效果为例,稍微讲解下核心代码:
1 初始化Scroller,第二个参数OvershootInterpolator顾名思义,是让回弹的时候稍微弹过头一点。
toStartScroller=new Scroller(getContext(),new OvershootInterpolator(2));
2 在onTouchEvent里判断当手指抬起的时候(MotionEvent.ACTION_UP),调用startScroll方法。
float dx = stickX - mLastX;
float dy = stickY - mLastY;
if (toStartScroller.isFinished())
toStartScroller.startScroll((int)mLastX, (int)mLastY, (int)dx, (int)dy, 300);
(int)mLastX 和 (int)mLastY 表示从这个位置开始, (int)dx 和 (int)dy 分别表示水平/垂直的移动距离,最后一个参数表示移动过程的时长。
3 单单这样使用Scroller不会有任何视觉效果,Scroller类只是计算了位置,需要根据计算的位置自己来更新view。基本的套路就重写computeScroll方法,在computeScroll里更新view。
if (toStartScroller.computeScrollOffset()) {
mLastX=toStartScroller.getCurrX();
mLastY=toStartScroller.getCurrY();
endX=mLastX;
endY=mLastY;
if (needToDrawConnectingPath()) initConnectingPath();
invalidate();
}
这其中getCurrX()、getCurrY()就是获取计算的位置。 initConnectingPath()方法就是第一步里说的计算平滑曲线的函数。
起点的圆弹向松手点的效果也是一样的套路,可以参考源码。
第三步:
实现最后的爆炸消失效果。
爆炸消失的效果,我实现的方法应该和qq的不太一样。我只是简单粗暴地在手指抬起位置画了15个小圆圈向随机方向移动,同时线性增加透明度。
private float[] disolveXseed=new float[15];
private float[] disolveYseed=new float[15];
private float[] disolveX=new float[15];
private float[] disolveY=new float[15];
这其中 disolveSeed 纯粹就是 -0.5到0.5之间的随机数,disolveX disolveY 是爆炸过程中小圆圈的圆心位置(相对于手指触摸点位置)。
private void initDisolve(){
for (int ii=0;ii<disolveXseed.length;ii++){
disolveXseed[ii]= (float) Math.random()-0.5f;
disolveYseed[ii]= (float) Math.random()-0.5f;
}
}
disolveController也是一个Scroller,来控制爆炸消失的进度,我只需要一个标量来控制进度就可以,所以x的起始值终值都是0
disolveController.startScroll(0,0,0,MAX_DISOLVE_STAGE,1000);
最后是老套路了,在重写的computeScroll方法里面加上获取爆炸进度,根据爆炸进度来计算各个小圆圈的圆心位置,以及透明度(爆炸刚开始时不透明,爆炸完成时候完全透明)。 这里MAX_DISOLVE_STAGE*2*(2*endR)什么乱起八糟的其实就是一个常数,而progress是线性变化的,这样看上去的效果就是15个小圆圈同时远离手指抬起点。
if (disolveController.computeScrollOffset()){
int alpha=MAX_DISOLVE_STAGE-disolveController.getCurrY();
int progress=disolveController.getCurrY();
disolvePaint.setAlpha(alpha);
for (int ii=0;ii<disolveX.length;ii++){
disolveX[ii]= disolveXseed[ii]*progress/MAX_DISOLVE_STAGE*2*(2*endR);
disolveY[ii]= disolveYseed[ii]*progress/MAX_DISOLVE_STAGE*2*(2*endR);
}
invalidate();
}