自定義view之模擬qq訊息拖拽刪除效果

saka發表於2017-03-23

github地址

這個模擬功能的實現主要依靠了PATH和二階貝塞爾曲線。首先上一張圖來簡單看一下:

自定義view之模擬qq訊息拖拽刪除效果

這個模擬功能有以下幾個特點:

  1. 在開始的時候點選圓以外的區域不會觸發拖動事件
  2. 點選圓的時候可以拖拽,此時會有一個拉伸效果,連線大圓和小圓
  3. 拉伸到一定距離(自己設定)以後兩個圓會斷開,此時即使再拖拽進距離之內的時候也不會再產生已經斷開的連線
  4. 在距離之內鬆手的時候會回彈會原位置,並伴有一個彈跳動畫

介紹了這麼多,看過我前邊文章的朋友應該會有一個基本思路。

暴露介面

這個模擬功能共分為三部分,一個是那個小圓,固定的位置,一個是那個大圓,可以移動,還有一部分就是中間的連線部分,會跟隨大圓一起延伸。

首先看一下都有哪些介面可以呼叫:

 public void setMinR(float minR) {
        this.minR = minR;
    }

    public void setMaxR(float maxR) {
        this.maxR = maxR;
    }

    public void setBrokeDistance(float distance) {
        this.brokeDistance = distance;
    }複製程式碼

第一個setMinR是設定小圓的半徑,第二個setMaxR是設定大圓的半徑,第三個setBrokeDistance是設定斷開的距離,也就是小圓和大圓的圓心之間的最大連線距離。

初始化

public Buble(Context context) {
        super(context);
        init();
    }

    public Buble(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }複製程式碼

簡單的看一下初始化方法。

private void init() {
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.GREEN);
        paint.setAntiAlias(true);
    }複製程式碼

其實只有一個畫筆,這裡可以為各個區域分別設定畫筆。

繪製圖形

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawOriginalCircle(canvas);
        if (!canBroke) {
            drawMoveCircle(canvas);
            drawBCurve(canvas);
        }
    }複製程式碼

這三個方法相對簡單,drawOriginalCircle是畫中心的小圓,然後canBroke是一個開關,控制是否執行畫移動的圓和畫弧線。

 private void drawOriginalCircle(Canvas canvas) {
        canvas.drawCircle(getWidth() / 2, getHeight() / 2, minR, paint);
    }

    private void drawMoveCircle(Canvas canvas) {
        canvas.drawCircle(moveX, moveY, maxR, paint);
    }

    private void drawBCurve(Canvas canvas) {
        canvas.drawPath(path, paint);
    }複製程式碼

注意,moveX, moveY和path都是變化的,所以在他們的值發生改變以後千萬不要忘記invalidate。

path的連線

關於path的文章網上一大堆。
Path從懵逼到精通——基本操作,這篇文章算是網上流傳比較多的一篇。

此處的難點主要是大圓和小圓之間的連線。用一張圖簡單表示一下:

自定義view之模擬qq訊息拖拽刪除效果

基本就是這個樣子,path的路徑就是那個黑色的類似於漏斗一樣的東西。此處要計算的角度需要用到三角函式關係式,簡單表示一下:

自定義view之模擬qq訊息拖拽刪除效果

圖中標出的兩個角度是相等的

double angle = Math.atan((offsetX - minCircleX) / (offsetY - minCircleY));複製程式碼

求出這個角度(offsetX是移動圓心的座標)。

這樣就可以算出四個點的座標了。

private void setPath(float offsetX, float offsetY) {
        float minCircleX = (float) getWidth() / 2;
        float minCircleY = (float) getHeight() / 2;
        double angle = Math.atan((offsetX - minCircleX) / (offsetY - minCircleY));
        float x1 = (float) (minCircleX + Math.cos(angle) * minR);
        float y1 = (float) (minCircleY - Math.sin(angle) * minR);
        float x2 = (float) (offsetX + Math.cos(angle) * maxR);
        float y2 = (float) (offsetY - Math.sin(angle) * maxR);
        float x3 = (float) (offsetX - Math.cos(angle) * maxR);
        float y3 = (float) (offsetY + Math.sin(angle) * maxR);
        float x4 = (float) (minCircleX - Math.cos(angle) * minR);
        float y4 = (float) (minCircleY + Math.sin(angle) * minR);
        float centerX = minCircleX + (offsetX - minCircleX) / 2;
        float centerY = minCircleY + (offsetY - minCircleY) / 2;
        path.reset();
        path.moveTo(minCircleX, minCircleY);
        path.lineTo(x1, y1);
        path.quadTo(centerX, centerY, x2, y2);
        path.lineTo(x3, y3);
        path.quadTo(centerX, centerY, x4, y4);
        path.lineTo(minCircleX, minCircleY);
        path.close();
    }複製程式碼

注意quadTo的四個引數的意義,前兩個是你的錨點的座標,後兩個是你要移動到那個點的位置的座標。

觸控事件

這個直接上程式碼來實現思路吧,沒什麼好講的。

 switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                this.canBroke = false;
                moveX = event.getX();
                moveY = event.getY();
                touchArea = !setCanBroke(moveX, moveY, maxR);
                break;
            case MotionEvent.ACTION_MOVE:
                if (touchArea) {
                    moveX = event.getX();
                    moveY = event.getY();
                    if (setCanBroke(moveX, moveY, brokeDistance)) {
                        touchArea = false;
                        this.canBroke = true;
                    } else {
                        setPath(moveX, moveY);
                    }
                    invalidate();
                }
                break;
            case MotionEvent.ACTION_UP:
                Log.d("aaa", "actionUp" + touchArea);
                if (touchArea) {
                    resetCircle(event.getX(), event.getY());
                }
                break;
        }
        return true;複製程式碼

這裡主要說明一下這個setCanBroke:

 private boolean setCanBroke(float offsetX, float offsetY, float brokeDistance) {
        float minCircleX = (float) getWidth() / 2;
        float minCircleY = (float) getHeight() / 2;
        return (offsetX - minCircleX) * (offsetX - minCircleX) +
                (offsetY - minCircleY) * (offsetY - minCircleY) > brokeDistance * brokeDistance;
    }複製程式碼

這個表示是否超出了最大移動距離,超出則返回真,未超出則返回假。同時在touchArea的設定中它也用用到了,主要是判斷點選區域是否在圓圈上。

最後還要講一下這個resetCicle,這個是一個屬性動畫來控制返回原點的彈性動畫:

private void resetCircle(float x, float y) {
        valueAnimatorX = ValueAnimator.ofFloat(x, (float) getWidth() / 2);
        valueAnimatorY = ValueAnimator.ofFloat(y, (float) getHeight() / 2);
        valueAnimatorX.removeAllUpdateListeners();
        valueAnimatorY.removeAllUpdateListeners();
        valueAnimatorX.setInterpolator(new BounceInterpolator());
        valueAnimatorY.setInterpolator(new BounceInterpolator());
        valueAnimatorX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                tempX = (float) animation.getAnimatedValue();
                moveX = tempX;
            }
        });
        valueAnimatorY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                tempY = (float) animation.getAnimatedValue();
                moveY = tempY;
                setPath(tempX, tempY);
                postInvalidate();
            }
        });
        set.playTogether(valueAnimatorX, valueAnimatorY);
        set.start();
    }複製程式碼

其中的插值器是BounceInterpolator,類似於小球彈跳的動畫,在我前邊的文章中有介紹。

最後來看一下不會斷開的效果,相當有意思:

自定義view之模擬qq訊息拖拽刪除效果

關於自定義view的文章會暫時到這裡,下一步準備更新自定義viewgroup的文章。相對於自定義view會稍微簡單一點。

相關文章