1. 程式人生 > >Android粘性菊花—-粘性LoadingView你所知道的一切

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

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型別

這裡我就不重複招輪子了,懂了原理的人,應該很好理解第二種是怎麼實現的,固定圓的位置更新動態圓的位置都是同一套理論,同時使用這個理論還是實現很多效果,這裡就不論述了,大家有空多嘗試吧。
這裡給出程式碼的下載位置:傳送門