您的位置:首页 > 移动开发 > Android开发

Android粘性菊花—-粘性LoadingView你所知道的一切

2015-11-17 01:14 519 查看

Android粘性菊花—-粘性loadingView你所知道的一切

前沿

今天先看看我们要做的效果图。



我们需要做的就是这样的一个带有粘性的loading控件,可以看到里面有两种方式可以切换,一种是直线粘性loading另外一种是菊花形状的粘性控件。

准备知识

要做这样的一个效果我们主要需要了解以下几个方面的知识。

如求两个圆的共切线

贝塞尔曲线的画法

这里我将详细解释并一步一步的分享我们的LINE这总状态下是如果画出来的,其实CIRCLE这种状态无非也就是把我们的圆的初始化位置改变了,其他的没有什么变化。

原理讲解

我们先来看一下这一张图。



把我们的屏幕分成这么宽的几个部分,然后摆放我们的圆,首先是画5个静态的圆在我们的中央,然后我们的一个动态圆在两侧留出位置,那么我们的屏幕宽度。

width = 2r*2+2R*5+6*2d

绘制好了我们几个静态圆就事实更新我们的动态圆就是我们的小圆的位置就行了,这个时候就有这种效果了。



然后我们在根据它是否相交做个放大的效果。



下面就是难点了,也是这次的重点。

贝塞尔曲线

我们先给出一个定义,贝塞尔曲线其实就是给定任意个点,通过这任意个点,可以画出一条光滑的曲线。

这里我们先看一个解释。



我们有三个起始点A、B、C 现在我在AB上取一点A1在BC上取一点B1使得AA1:AB=BB1:BC链接A1B1在上面取一点D,使得A1D:A1B1=AA1:AB=BB1:BC,这样经过所有路线的点D最终绘制出来的线,叫做贝塞尔曲线(这里举得例子是二阶贝塞尔),二阶贝塞尔就需要确定两个点,最终的图就是。



那么同理四阶的贝尔曲线最后的样子就是


理解了贝塞尔曲线,那么接下来我们就要做我们的粘贴性部分了,如下图所示。



A,D,B,C这几个点就是我们要计算的切点,当我们知道O,F点的坐标和小圆和大圆的半径的时候,我们要求这几个点,所需要求的其实就是我途中标出来的OffestY,和offsetX大圆小圆同时都需要求,但是方法都相同,所以根据几何知识我们可以得到offsetY=DF×sin∠b,offsetx=DF×cos∠b,而∠b又可以通过tan∠b=OX/XF得到。所以这个时候我们的offsetY和offsetx就求出来了,我们就只需要绘制贝赛尔曲线就行了。

实际操作

我们新建一个工程,继承一个View,取名叫loadingView.



然后在类里面写一个内部类,用来记录我们的圆位置。

class Circle
{
public Circle(int mRaduis, float mx, float my)
{
super();
this.mRaduis = mRaduis;
this.mx = mx;
this.my = my;
}

int mRaduis;
float mx;
float my;
}


然后就可以声明我们的变量。

public class LoadingView extends View{

Path mPath = new Path(); //封闭空间的path
Paint mPaint = new Paint();//画笔
Circle mStaticCircles[]; //静态圆
Circle mDynamCircle;//动态圆
int mCircleSpace = 20;
private int mCirclesRadius = 20; //静态半径
private int mDynamCirlcRadius = (int) (mCirclesRadius *0.5); //动态半径
private boolean mIsAnimationRunning = false;
...}


然后我们写一个初始化的方法,取名叫init

private void init()
{
int wid = getMeasuredWidth();
int height = getMeasuredHeight();
mStaticCircles = new Circle[5];
// 求出间距
mCircleSpace = (wid - mCirclesRadius * (mStaticCircles.length + 2) * 2) / ((mStaticCircles.length + 2) * 2);
mDynamCircle = new Circle(mDynamCirlcRadius, mDynamCirlcRadius + mCircleSpace, height / 2);
for (int i = 0; i < mStaticCircles.length; i++)
{
// 计算的时候把 i+1
mStaticCircles[i] = new Circle(mCirclesRadius, ((i + 2) * 2 - 1) * (mCirclesRadius + mCircleSpace), height / 2);
}
}


但是我们需要在哪里去调用我们这个方法呢?显然在构造的时候调用是不行的,那么我们就重写一下onMeasure方法。把我们的初始化赋值放在里面。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
init();
}


初始化成功了那么就应该画我们的圆形了。我们同时也写一个方法来绘制我们的圆形,取名字叫drawCircle

private void drawCircle(Canvas canvas)
{
for (Circle circles : mStaticCircles)
{

canvas.drawCircle(circles.mx, circles.my, circles.mRaduis, mPaint);
}
if (mIsAnimationRunning)
canvas.drawCircle(mDynamCircle.mx, mDynamCircle.my, mDynamCircle.mRaduis, mPaint);
}


里面实现也很简单就是绘制我们的圆。既然圆形都绘制了那么就需要绘制我们的贝塞尔曲线了。我们取名一个函数叫drawBersal用这个函数来绘制我们的贝塞尔区域让这个控件变得有粘性起来。

private void drawBersal(Canvas canvas)
{
for (int i = 0; i < mStaticCircles.length; i++)
{
// 两个圆之间的距离
int length = (int) Math.sqrt((mStaticCircles[i].mx - mDynamCircle.mx) * (mStaticCircles[i].mx - mDynamCircle.mx) + (mStaticCircles[i].my - mDynamCircle.my)
* (mStaticCircles[i].my - mDynamCircle.my));

// 距离阀值
int corssLength = mDynamCircle.mRaduis + mStaticCircles[i].mRaduis + mCircleSpace;
if (length < corssLength)
{

// 计算两个圆相切的点
float offsetX = (float) (mDynamCirlcRadius * Math.sin(Math.atan((mStaticCircles[i].my - mDynamCircle.my) / (mStaticCircles[i].mx - mDynamCircle.mx))));
float offsetY = (float) (mDynamCirlcRadius * Math.cos(Math.atan((mStaticCircles[i].my - mDynamCircle.my) / (mStaticCircles[i].mx - mDynamCircle.mx))));

int startX = (int) (mDynamCircle.mx + offsetX);
int startY = (int) (mDynamCircle.my - offsetY);

int endX = (int) (mDynamCircle.mx - offsetX);
int endY = (int) (mDynamCircle.my + offsetY);

float offsetstaticX = (float) (mStaticCircles[i].mRaduis * Math.sin(Math.atan((mStaticCircles[i].my - mDynamCircle.my) / (mStaticCircles[i].mx - mDynamCircle.mx))));
float offsetstaticY = (float) (mStaticCircles[i].mRaduis * Math.cos(Math.atan((mStaticCircles[i].my - mDynamCircle.my) / (mStaticCircles[i].mx - mDynamCircle.mx))));

int startStaticX = (int) (mStaticCircles[i].mx + offsetstaticX);
int startStaticY = (int) (mStaticCircles[i].my - offsetstaticY);

int endStaticX = (int) (mStaticCircles[i].mx - offsetstaticX);
int endStaticY = (int) (mStaticCircles[i].my + offsetstaticY);

int anchorX = (int) ((mStaticCircles[i].mx + mDynamCircle.mx) / 2);
int anchorY = (int) ((mStaticCircles[i].my + mDynamCircle.my) / 2);

mPath.reset();

mPath.moveTo(startX, startY);
mPath.quadTo(anchorX, anchorY, startStaticX, startStaticY);
mPath.lineTo(endStaticX, endStaticY);
mPath.quadTo(anchorX, anchorY, endX, endY);
mPath.lineTo(startX, startY);
canvas.drawPath(mPath, mPaint);
return;
}

}


我们看到我们其实是绘制了两条贝塞尔曲线通过path去封闭,贝塞尔曲线都共用了一个点那就是我取的两个圆的中点,这里我简单说一下贝塞尔封闭区域的绘制过程,还是使用上面讲解的那个图,我们先把原点移动到了A,然后确定D点和两个圆心中点,有了封闭图形的半边,然后调用LineTo绘制直线到C点同样的再一次调用贝塞尔函数绘制到B点,最后LinTo绘制到A点封口,通过path绘制封闭的区域,这个时候我们的贝塞尔封闭区域就画好了,但是请注意这里我写了判断是两个圆圆心的距离小于某个值才去做粘性的这个绘制。

既然绘制都写好了那么就应该上屏了,重写我们的onDraw方法。

@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
drawCircle(canvas);
drawBersal(canvas);
}


最后一步就是让我们的loading动起来,这里我们直接使用一个ValueAnimator来计算我们的值就好了。我们对外写一个startAnimation方法。

ValueAnimator animator = null;
public void startAnimation()
{
if (animator != null)
{
animator.removeAllListeners();
animator.cancel();
animator = null;
init();
}

animator = ValueAnimator.ofInt((int) mDynamCircle.mx, (mCircleSpace + mCirclesRadius) * (2 * (mStaticCircles.length + 2) - 1));

animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new AnimatorUpdateListener()
{

@Override
public void onAnimationUpdate(ValueAnimator animation)
{
int vaule = (int) animation.getAnimatedValue();

mDynamCircle.mx = vaule;
caclScale();
postInvalidate();
}
});
animator.addListener(new AnimatorListener()
{

@Override
public void onAnimationStart(Animator paramAnimator)
{
mIsAnimationRunning = true;
}

@Override
public void onAnimationRepeat(Animator paramAnimator)
{

}

@Override
public void onAnimationEnd(Animator paramAnimator)
{
mIsAnimationRunning = false;
}

@Override
public void onAnimationCancel(Animator paramAnimator)
{
mIsAnimationRunning = false;
}
});
animator.setRepeatMode(Animation.REVERSE);
animator.setRepeatCount(Animation.INFINITE);
animator.setDuration(3000);
animator.start();
}


这样通过属性动画直接帮我们计算我们的值,我们直接去刷新界面就行了,但是注意到我有个caclScale的方法这个方法是来判断当前是否需要缩放。

private void caclScale()
{
for (int i = 0; i < mStaticCircles.length; i++)
{
// 两个圆之间的距离
int length = (int) Math.sqrt((mStaticCircles[i].mx - mDynamCircle.mx) * (mStaticCircles[i].mx - mDynamCircle.mx) + (mStaticCircles[i].my - mDynamCircle.my)
* (mStaticCircles[i].my - mDynamCircle.my));
// 交叉的距离
int corssLength = mDynamCircle.mRaduis + mStaticCircles[i].mRaduis + mCircleSpace / 2;
if (length < corssLength)
{// 已经开始交叉
float scale = 1.0f * length / corssLength;
// mDynamCircle.mRaduis = (int) (mRadius/2f * (1 + scale));
mStaticCircles[i].mRaduis = (int) (mCirclesRadius * (1 + mDynamCircle.mRaduis * 1f / mCirclesRadius * (1 - scale)));
//              return;
}
else
{
mStaticCircles[i].mRaduis = mCirclesRadius;
mDynamCircle.mRaduis = mDynamCirlcRadius;
}

}
}


到这里基本上LINE这种状态的loading也写完了。相信大家也应该理解得差不多了。

CIRCLE类型

这里我就不重复招轮子了,懂了原理的人,应该很好理解第二种是怎么实现的,固定圆的位置更新动态圆的位置都是同一套理论,同时使用这个理论还是实现很多效果,这里就不论述了,大家有空多尝试吧。

这里给出代码的下载位置:传送门
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 控件 粘性