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

时间:2021-05-20

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

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

  • 在开始的时候点击圆以外的区域不会触发拖动事件
  • 点击圆的时候可以拖拽,此时会有一个拉伸效果,连接大圆和小圆
  • 拉伸到一定距离(自己设定)以后两个圆会断开,此时即使再拖拽进距离之内的时候也不会再产生已经断开的连接
  • 在距离之内松手的时候会回弹会原位置,并伴有一个弹跳动画
  • 介绍了这么多,看过我前边文章的朋友应该会有一个基本思路。

    暴露接口

    这个模拟功能共分为三部分,一个是那个小圆,固定的位置,一个是那个大圆,可以移动,还有一部分就是中间的连接部分,会跟随大圆一起延伸。

    首先看一下都有哪些接口可以调用:

    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的路径就是那个黑色的类似于漏斗一样的东西。此处要计算的角度需要用到三角函数关系式,简单表示一下:

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

    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会稍微简单一点。

    demo下载地址:PathApplication_jb51.rar

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

    声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

    相关文章