1. 程式人生 > >Android 實現IOS上的水滴效果控制元件

Android 實現IOS上的水滴效果控制元件

看到ios版上QQ重新整理效果像水滴,然後自己也想著去實現這樣的效果,這篇文章暫時沒有介紹下拉重新整理的效果,只是單獨用一個控制元件來實現這樣的水滴效果。
效果圖如下:
這裡寫圖片描述

一、總體思路

1、畫兩個圓形,其中一個就是上面的大圓,還有一個是下面的小圓,大圓和小圓不斷變小,大圓的位置保持不變,小圓的位置不斷向下移動,即圓心不斷下移。
2、畫兩邊的曲線,這時候用到貝塞爾曲線去畫。
3、用屬性動畫實現動態的效果。

二、程式碼實現

1、找出畫曲線的幾個關鍵點。
這裡寫圖片描述
這裡寫圖片描述
其實我是在第一張圖的基礎上,再在上面分別畫兩個圓,就可以得到第二張圖了。關鍵是畫出第一張圖。
(1)在這裡,p1,p2,p3,p4,這4個點分別對應兩個圓的兩邊的點,即p1到p2就是圓的直徑。p3和p4同理,那麼就很容易確定這四個點的座標了。
(2)然後c1和c2是分別控制p1到p3、p2到p4的曲線,是貝塞爾曲線的控制點。它們的橫座標對應的是p3,p4的橫座標(相等),縱座標取兩個圓心距離的一半。這樣畫出這個靜態的圖片就不難了。
(3)畫上下兩個圓進去,就會變成第二張圖的效果。

2、在構造方法中呼叫init()初始化一些基本的變數

private void init(Context context, AttributeSet attrs) {
        drawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG
                | Paint.FILTER_BITMAP_FLAG);
        paint = new Paint();
        paint.setColor(fillColor);
        // 轉換為畫素單位
        bigRadius = dip2px(context, bigRadius);
        smallRadius = dip2px(context, smallRadius);
        distance = dip2px(context, distance);
    }

3、在onDraw()方法中畫水滴效果
要注意的是path需要重新new,
貝塞爾曲線的繪製,用到是path.quadTo這方法。具體可以看程式碼。

@Override
    protected void onDraw(Canvas canvas) {
        // 必須重新new,不然路徑會重複,我之前就是這樣
        path = new Path();
        // 把畫布移到中心
        canvas.translate(width / 2, height / 2);
        // 從canvas層面去除繪製時鋸齒
        canvas.setDrawFilter(drawFilter);
        // 當前的兩個圓心的距離
currentDis = distance * fraction; // 計算當前大圓的半徑 float bigRadius = this.bigRadius - currentDis / bigPercent; float smallRadius = 0; if (currentDis > 5) {// 距離大於5才改變小圓的半徑 smallRadius = this.smallRadius - currentDis / smallPercent; } // 大圓兩邊的兩個點座標 leftX = -bigRadius;// 大圓當前的半徑 leftY = rightY = 0; rightX = bigRadius;// 大圓當前的半徑 // 小圓兩邊的兩個點座標 leftX2 = -smallRadius;// 小圓當前的半徑 leftY2 = rightY2 = currentDis; rightX2 = -leftX2; // 小圓當前的半徑 // 兩個控制點 controlX1 = -smallRadius;// x座標取小圓當前的半徑大小 controlY1 = currentDis / 2;// y座標取兩個圓距離的一半 controlX2 = smallRadius;// x座標取小圓當前的半徑大小 controlY2 = currentDis / 2;// y座標取兩個圓距離的一半 path.moveTo(leftX, leftY); path.lineTo(rightX, rightY); // 用二階貝塞爾曲線畫右邊的曲線,引數的第一個點是右邊的一個控制點 path.quadTo(controlX2, controlY2, rightX2, rightY2); path.lineTo(leftX2, leftY2); // 用二階貝塞爾曲線畫左邊邊的曲線,引數的第一個點是左邊的一個控制點 path.quadTo(controlX1, controlY1, leftX, leftY); // 畫大圓 canvas.drawCircle(0, 0, bigRadius, paint); // 畫小圓 canvas.drawCircle(0, currentDis, smallRadius, paint); // 畫path canvas.drawPath(path, paint); }

4、用屬性動畫,實現動態的效果。

/*** 執行屬性動畫,實現水滴的效果 */
    public void perforAnim() {
        ValueAnimator valAnimator = ObjectAnimator.ofFloat(0, 1);
        valAnimator.addUpdateListener(new AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                fraction = (float) animation.getAnimatedValue();
                postInvalidate();
            }

        });
        valAnimator.setDuration(duration);
        valAnimator.start();
    }

5、重寫onMeasure()方法,處理wrap_content情況。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        /*
         * 處理為wrap_content情況,那麼它的specMode是AT_MOST模式,在這種模式下它的寬/高
         * 等於spectSize,這種情況下view的spectSize是parentSize,而parentSize是
         * 父容器目前可以使用大小,就是父容器當前剩餘的空間大小, 就相當於使用match_parent一樣 的效果,因此我們可以設定一個預設的值
         */
        int widthSpectMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpectSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpectMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpectSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpectMode == MeasureSpec.AT_MOST
                && heightSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width, height);
        } else if (widthSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(width, heightSpectSize);
        } else if (heightSpectMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpectSize, height);
        }
    }

6、其它的一些方法實現。

@Override
    protected void onLayout(boolean changed, int left, int top, int right,
            int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (changed) {
            width = right - left;
            height = bottom - top;
        }
    }
    /**
     * 根據手機的解析度從 dp 的單位 轉成為 px(畫素)
     */
    public int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

7、欄位的定義
private final int fillColor = 0xff999999;// 填充顏色
private Paint paint;
private int width = 100, height = 300;// 預設寬高
/* 兩個圓心的最大距離 /
private int distance = 60;
/* 當前兩個圓心的距離 /
private float currentDis = 0;
private float bigRadius = 20;// 大圓半徑
private float smallRadius = 10;// 小圓半徑
private float controlX1, controlX2, controlY1, controlY2;// 兩個控制點的座標
private float leftX, leftY, rightX, rightY;// 大圓兩邊的兩個點的座標
private float leftX2, leftY2, rightX2, rightY2; // 小圓兩邊的兩個座標
DrawFilter drawFilter;
Path path;
/* 由屬性動畫控制,範圍為0-1 */
float fraction = 0;// 比例值
/* 大圓半徑變化的比例 /
private final int bigPercent = 8;
/* 小圓半徑變化的比例 /
private final int smallPercent = 20;
// 動畫的執行時間
private long duration = 3000;

三、總結

一種動畫效果,應該先分析它的靜態的實現,然後新增動態的效果,這樣就比較容易實現它的動畫效果了。
參考文章