自定义view之模拟qq消息拖拽删除效果

原文地址

这个模拟功能的实现主要依靠了PATH和二阶贝塞尔曲线。首先上一张图来简单看一下:

这个模拟功能有以下几个特点:

  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从懵逼到精通——基本操作,这篇文章算是网上流传比较多的一篇。

此处的难点主要是大圆和小圆之间的连接。用一张图简单表示一下:

基本就是这个样子,path的路径就是那个黑色的类似于漏斗一样的东西。此处要计算的角度需要用到三角函数关系式,简单表示一下:


图中标出的两个角度是相等的

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的文章会暂时到这里,下一步准备更新自定义viewgroup的文章。相对于自定义view会稍微简单一点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容