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

原文地址

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

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

  1. 在開始的時候點擊圓以外的區(qū)域不會觸發(fā)拖動事件
  2. 點擊圓的時候可以拖拽,此時會有一個拉伸效果,連接大圓和小圓
  3. 拉伸到一定距離(自己設定)以后兩個圓會斷開,此時即使再拖拽進距離之內(nèi)的時候也不會再產(chǎn)生已經(jīng)斷開的連接
  4. 在距離之內(nèi)松手的時候會回彈會原位置,并伴有一個彈跳動畫

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

暴露接口

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

首先看一下都有哪些接口可以調(diào)用:

 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);
    }

其實只有一個畫筆,這里可以為各個區(qū)域分別設置畫筆。

繪制圖形

protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawOriginalCircle(canvas);
        if (!canBroke) {
            drawMoveCircle(canvas);
            drawBCurve(canvas);
        }
    }

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

 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都是變化的,所以在他們的值發(fā)生改變以后千萬不要忘記invalidate。

path的連接

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

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

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


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

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的四個參數(shù)的意義,前兩個是你的錨點的坐標,后兩個是你要移動到那個點的位置的坐標。

觸摸事件

這個直接上代碼來實現(xiàn)思路吧,沒什么好講的。

 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的設定中它也用用到了,主要是判斷點擊區(qū)域是否在圓圈上。

最后還要講一下這個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的文章會暫時到這里,下一步準備更新自定義viewgroup的文章。相對于自定義view會稍微簡單一點。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容