`
shuai1234
  • 浏览: 972454 次
  • 性别: Icon_minigender_1
  • 来自: 山西
社区版块
存档分类
最新评论

Android drawPath实现QQ拖拽泡泡

 
阅读更多

这两天学习了使用Path绘制贝塞尔曲线相关,然后自己动手做了一个类似QQ未读消息可拖拽的小气泡,效果图如下:

这里写图片描述

接下来一步一步的实现整个过程。

基本原理

其实就是使用Path绘制三点的二次方贝塞尔曲线来完成那个妖娆的曲线的。然后根据触摸点不断绘制对应的圆形,根据距离的改变改变原始固定圆形的半径大小。最后就是松手后返回或者爆裂的实现。

Path介绍:

顾名思义,就是一个路径的意思,Path里面有很多的方法,本次设计主要用到的相关方法有

moveTo() 移动Path到一个指定的点 
quadTo() 绘制二次贝塞尔曲线,接收两个点,第一个是控制弧度的点,第二个是终点。 
lineTo() 就是连线 
close() 闭合Path路径, 
reset() 重置Path的相关设置

  • Path入门热身:

    path.reset();
    path.moveTo(200, 200);
    //第一个坐标是对应的控制的坐标,第二个坐标是终点坐标
    path.quadTo(400, 250, 600, 200);
    
    canvas.drawPath(path, paint);
    canvas.translate(0, 200);
    //调用close,就会首尾闭合连接
    path.close();
    canvas.drawPath(path, paint);
    

    记得不要在onDraw方法中new Path或者 Paint哟!

Path

具体实现拆分:

其实整个过程就是绘制了两个贝塞尔二次曲线的的闭合Path路径,然后在上面添加两个圆形。

原理图1 
原理图2

  • 闭合的Path 路径实现从左上点画二次贝塞尔曲线到左下点,左下点连线到右下点,右下点二次贝塞尔曲线到右上点,最后闭合一下!!

  • 相关坐标的确定 
    这是这次里面的难点之一,因为涉及到了数学里面的一个sin,cos,tan等等,我其实也忘完了,然后又脑补了一下,废话不多说,直接上图!! 
    旋转过程的角标

为什么自己要亲自去画一下呢,因为画了你才知道,在360旋转的过程中,角标体系是有两套的,如果就使用一套来画的话,就画出现在旋转的过程中曲线重叠在一起的情况!

问题已经抛出来了,接下来直接看看代码实现!

角度确定

根据贴出来的原理图可以知道,我们可以使用起始圆心坐标和拖拽的圆心坐标,根据反正切函数来得到具体的弧度。

int dy = Math.abs(CIRCLEY - startY);
int dx = Math.abs(CIRCLEX - startX);
 angle = Math.atan(dy * 1.0 / dx);

ok,这里的startX,Y就是移动过程中的坐标。angle就是得到的对应的弧度(角度)。

相关Path绘制

前面已经提到在旋转的过程中有两套坐标体系,一开始我也很纠结这个坐标体系要怎么确定,后面又恍然大悟,其实相当于就是一三象限正比例增长,二四象限,反比例增长。

 flag = (startY - CIRCLEY  ) * (startX- CIRCLEX ) <= 0;
 //增加一个flag,用于判断使用哪种坐标体系。

最最重要的来了,绘制相关的Path路径!

 path.reset();
 if (flag) {
     //第一个点
 path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));

 path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));

path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
path.close();
canvas.drawPath(path, paint);
 } else {
     //第一个点
     path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));

     path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
     path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));

     path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
     path.close();
     canvas.drawPath(path, paint);
 }

这里的代码就是把图片上相关的数学公式Java化而已!

到这里,其实主要的工作就完成的差不多了! 
接下来,设置paint 为填充的效果,最后再画两个圆

 paint.setStyle(Paint.Style.FILL)
 canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的
 canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的

就可以绘制出想要的效果了!

这里不得不再说说onTouch的处理!

  case MotionEvent.ACTION_DOWN://有事件先拦截再说!!
            getParent().requestDisallowInterceptTouchEvent(true);
            CurrentState = STATE_IDLE;
            animSetXY.cancel();
            startX = (int) ev.getX();
            startY = (int) ev.getRawY();
            break;

处理一下事件分发的坑!

测量和布局

这样基本过得去了,但是我们的布局什么的还没有处理,math_parent是万万没法使用到具体项目当中去的! 
测量的时候,如果发现不是精准模式,那么都手动去计算出需要的宽度和高度。

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
    int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
    if (modeWidth == MeasureSpec.UNSPECIFIED || modeWidth == MeasureSpec.AT_MOST) {
        widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);
    }
    if (modeHeight == MeasureSpec.UNSPECIFIED || modeHeight == MeasureSpec.AT_MOST) {
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_RADIO * 2, MeasureSpec.EXACTLY);
    }
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

然后在布局变化时,获取相关坐标,确定初始圆心坐标:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    CIRCLEX = (int) ((w) * 0.5 + 0.5);
    CIRCLEY = (int) ((h) * 0.5 + 0.5);
}

然后清单文件里面就可以这样配置了:

 <com.lovejjfg.circle.DragBubbleView
    android:id="@+id/dbv"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"/>

这样之后,又会出现一个问题,那就是wrap_content 之后,这个View能绘制的区域只有自身那么大了,拖拽了都看不见了!这个坑怎么办呢,其实很简单,父布局加上Android:clipChildren="false" 的属性! 
这个坑也算是解决了!!

相关状态的确定

我们是不希望它可以无限的拖拽的,就是有一个拖拽的最远距离,还有就是放手后的返回,爆裂。那么对应的,这里需要确定几种状态:

  private final static int STATE_IDLE = 1;//静止的状态
    private final static int STATE_DRAG_NORMAL = 2;//正在拖拽的状态
    private final static int STATE_DRAG_BREAK = 3;//断裂后的拖拽状态
    private final static int STATE_UP_BREAK = 4;//放手后的爆裂的状态
    private final static int STATE_UP_BACK = 5;//放手后的没有断裂的返回的状态
    private final static int STATE_UP_DRAG_BREAK_BACK = 6;//拖拽断裂又返回的状态
    private int CurrentState = STATE_IDLE;

private int MIN_RADIO = (int) (ORIGIN_RADIO * 0.4);//最小半径
    private int MAXDISTANCE = (int) (MIN_RADIO * 13);//最远的拖拽距离

确定好这些之后,在move的时候,就要去做相关判断了:

  case MotionEvent.ACTION_MOVE://移动的时候
            startX = (int) ev.getX();
            startY = (int) ev.getY();

            updatePath();
            invalidate();
            break;

private void updatePath() {
    int dy = Math.abs(CIRCLEY - startY);
    int dx = Math.abs(CIRCLEX - startX);

    double dis = Math.sqrt(dy * dy + dx * dx);
    if (dis <= MAXDISTANCE) {//增加的情况,原始半径减小
        if (CurrentState == STATE_DRAG_BREAK || CurrentState == STATE_UP_DRAG_BREAK_BACK) {
            CurrentState = STATE_UP_DRAG_BREAK_BACK;
        } else {
            CurrentState = STATE_DRAG_NORMAL;
        }
        ORIGIN_RADIO = (int) (DEFAULT_RADIO - (dis / MAXDISTANCE) * (DEFAULT_RADIO - MIN_RADIO));
        Log.e(TAG, "distance: " + (int) ((1 - dis / MAXDISTANCE) * MIN_RADIO));
        Log.i(TAG, "distance: " + ORIGIN_RADIO);
    } else {
        CurrentState = STATE_DRAG_BREAK;
    }
//        distance = dis;
    flag = (startY - CIRCLEY) * (startX - CIRCLEX) <= 0;
    Log.i("TAG", "updatePath: " + flag);
    angle = Math.atan(dy * 1.0 / dx);
}

updatePath() 的方法之前已经看过部分了,这次的就是完整的。 
这里做的事就是根据拖拽的距离更改相关的状态,并根据百分比来修改原始圆形的半径大小。还有就是之前介绍的确定相关的弧度!

最后放手的时候:

   case MotionEvent.ACTION_UP:
            if (CurrentState == STATE_DRAG_NORMAL) {
                CurrentState = STATE_UP_BACK;
                valueX.setIntValues(startX, CIRCLEX);
                valueY.setIntValues(startY, CIRCLEY);
                animSetXY.start();
            } else if (CurrentState == STATE_DRAG_BREAK) {
                CurrentState = STATE_UP_BREAK;
                invalidate();
            } else {
                CurrentState = STATE_UP_DRAG_BREAK_BACK;
                valueX.setIntValues(startX, CIRCLEX);
                valueY.setIntValues(startY, CIRCLEY);
                animSetXY.start();
            }
            break;

自动返回这里使用到的 ValueAnimator

 animSetXY = new AnimatorSet();

    valueX = ValueAnimator.ofInt(startX, CIRCLEX);
    valueY = ValueAnimator.ofInt(startY, CIRCLEY);
    animSetXY.playTogether(valueX, valueY);
    valueX.setDuration(500);
    valueY.setDuration(500);
    valueX.setInterpolator(new OvershootInterpolator());
    valueY.setInterpolator(new OvershootInterpolator());
    valueX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            startX = (int) animation.getAnimatedValue();
            Log.e(TAG, "onAnimationUpdate-startX: " + startX);
            invalidate();
        }

    });
    valueY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            startY = (int) animation.getAnimatedValue();
            Log.e(TAG, "onAnimationUpdate-startY: " + startY);
            invalidate();

        }
    });

最后在看看完整的onDraw方法吧!

 @Override
protected void onDraw(Canvas canvas) {
    switch (CurrentState) {
        case STATE_IDLE://空闲状态,就画默认的圆
            if (showCircle) {
                canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的
            }
            break;
        case STATE_UP_BACK://执行返回的动画
        case STATE_DRAG_NORMAL://拖拽状态 画贝塞尔曲线和两个圆
            path.reset();
            if (flag) {
                //第一个点
                path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));

                path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));
                path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));

                path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));
                path.close();
                canvas.drawPath(path, paint);
            } else {
                //第一个点
                path.moveTo((float) (CIRCLEX - Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY + Math.cos(angle) * ORIGIN_RADIO));

                path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (startX - Math.sin(angle) * DRAG_RADIO), (float) (startY + Math.cos(angle) * DRAG_RADIO));
                path.lineTo((float) (startX + Math.sin(angle) * DRAG_RADIO), (float) (startY - Math.cos(angle) * DRAG_RADIO));

                path.quadTo((float) ((startX + CIRCLEX) * 0.5), (float) ((startY + CIRCLEY) * 0.5), (float) (CIRCLEX + Math.sin(angle) * ORIGIN_RADIO), (float) (CIRCLEY - Math.cos(angle) * ORIGIN_RADIO));
                path.close();
                canvas.drawPath(path, paint);
            }
            if (showCircle) {
                canvas.drawCircle(CIRCLEX, CIRCLEY, ORIGIN_RADIO, paint);//默认的
                canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
            }
            break;

        case STATE_DRAG_BREAK://拖拽到了上限,画拖拽的圆:
        case STATE_UP_DRAG_BREAK_BACK:
            if (showCircle) {
                canvas.drawCircle(startX == 0 ? CIRCLEX : startX, startY == 0 ? CIRCLEY : startY, DRAG_RADIO, paint);//拖拽的
            }
            break;

        case STATE_UP_BREAK://画出爆裂的效果
            canvas.drawCircle(startX - 25, startY - 25, 10, circlePaint);
            canvas.drawCircle(startX + 25, startY + 25, 10, circlePaint);
            canvas.drawCircle(startX, startY - 25, 10, circlePaint);
            canvas.drawCircle(startX, startY, 18, circlePaint);
            canvas.drawCircle(startX - 25, startY, 10, circlePaint);
            break;

    }


}

到这里,成品就出来了!!

总结:

1、确定默认圆形的坐标; 
2、根据move的情况,实时获取最新的坐标,根据移动的距离(确定出角度),更新相关的状态,画出相关的Path路径。超出上限,不再画Path路径。 
3、松手时,根据相关的状态,要么带Path路径执行动画返回,要么不带Path路径直接返回,要么直接爆裂!

相关源码请移步Github,喜欢就请粉一个吧,有问题欢迎留言或者issue。。

分享到:
评论

相关推荐

    Android 双指拖动和双指缩放图片

    在Android开发中,实现双指拖动和双指缩放图片是常见的手势识别应用场景,尤其在图片查看器或画板类应用中极为常见。本文将深入探讨如何在Android平台上实现这一功能,以及如何将绘制的线条与背景图片进行正片叠底的...

    android手机应用源码自定义泡泡效果源码.rar

    在Android开发中,自定义视图是提升用户体验和实现独特设计的重要手段。本知识点主要探讨的是如何在Android手机应用中创建一个自定义的泡泡效果,这通常用于消息提示或者浮动通知。通过分析提供的"android手机应用...

    android简单的画图板实现代码

    这个"android简单的画图板实现代码"就是一个适合初学者的示例项目,旨在帮助开发者了解如何在Android环境中实现画图功能。我们将深入探讨这个项目中的关键知识点。 首先,我们需要了解Android图形绘制的基础。在...

    android 地图实现六边形

    本篇将详细介绍如何在Android平台上实现六边形布局,并提供一个实际的示例——new-map-demo。 首先,我们要理解Android中的布局系统主要基于矩形,如LinearLayout、RelativeLayout和ConstraintLayout等。然而,为了...

    android实现屏幕画笔工具

    在Android平台上实现一个屏幕画笔工具,涉及到许多关键的技术点,包括图形绘制、触摸事件处理、用户交互设计等。下面将详细阐述这些知识点。 首先,Android的图形绘制主要依赖于Canvas和Paint对象。Canvas提供了...

    android实现简单的手写笔迹效果

    在Android平台上,实现手写笔迹效果是一项常见的需求,尤其在教育、绘画或者签名应用中。这个"drawDemo"项目可能包含了一个简单的示例,教你如何在Android应用中创建一个可交互的手写画板。下面将详细介绍实现这一...

    Android 完美实现图片圆角和圆形

    本知识点将深入探讨如何在Android应用中完美实现图片的圆角和圆形效果。 首先,我们来看如何实现图片的圆角效果。Android提供了一些内置的方式,但它们可能无法满足所有需求,因此开发者通常会选择自定义View来实现...

    android,Canvas制作一个可拖动改变任意形状的四边形,并填充颜

    接下来,我们需要实现拖动功能。这涉及到对触摸事件的处理。在自定义View中,我们通常会重写`onTouchEvent()`方法来监听用户的触摸操作。当用户按下屏幕时,记录下触点的坐标,然后在移动时更新四边形的顶点。最后,...

    android 各种类型的头像实现

    本示例项目“android 各种类型的头像实现”专注于利用SVG(Scalable Vector Graphics)技术来创建可自定义形状的头像视图。SVG是一种基于XML的矢量图像格式,它允许无限缩放而不会损失画质,非常适合在移动设备上...

    android 画笔画板功能效果的实现.zip

    本项目标题"android 画笔画板功能效果的实现.zip"揭示了其核心内容:实现了一个能够模拟钢笔和水彩笔效果的画板,并且具备清除画布功能。下面我们将详细探讨这些知识点。 1. **Canvas与Paint类**: Android中的`...

    Android实现游戏人物移动的例子

    在Android中,我们通常使用Canvas对象进行绘图,它提供了各种绘制图形的方法,如drawRect(), drawBitmap(), drawPath()等。在这个例子中,"切图"是指将游戏人物的不同动作分解成多个图像(帧),然后根据游戏逻辑在...

    Android代码-自定义泡泡效果源码.zip

    "Android代码-自定义泡泡效果源码.zip" 提供的是一份实现自定义气泡效果的源代码,这种效果通常用于消息提示或者指示器等场景,能够使用户界面更加生动有趣。以下是对这个自定义泡泡效果的详细解析: 1. **自定义...

    Android 源码实现雪花飘落效果

    在Android平台上,实现动态效果,如雪花飘落,是一种常见的增强用户体验的方式。本文将深入探讨如何使用Java语言在Android环境中实现这一视觉特效。首先,我们要理解Android开发的基础,包括Activity、View以及绘图...

    在android上实现模仿windows桌面的鼠标残影效果

    在Android平台上实现Windows桌面风格的鼠标残影效果,主要涉及到对Android系统图形处理的理解以及对动画的掌握。这种效果在Windows系统中是通过鼠标指针的轨迹缓存来实现的,使得指针移动时留下一个短暂的动态阴影,...

    android 抖音点赞功能

    总的来说,实现类似抖音的点赞动画效果需要对Android的自定义View、动画系统以及触摸事件处理有深入的理解。通过巧妙地组合和定制这些组件,我们可以创造出与抖音类似的、引人入胜的点赞交互体验。在实际开发中,应...

    Android-Android实现气泡布局

    本文将深入探讨如何在Android中实现气泡布局,包括其基本原理、自定义View的创建以及在实际项目中的应用。 首先,气泡布局的基本特征是它具有一个圆形或近似圆形的头部,通常包含一个图标或文字,然后有一条指向...

    Android:自定义View实现随滑动由箭头变对勾的指示按钮

    在Android开发中,自定义View是一项重要的技能,它允许开发者根据...这个“MagicButton”不仅展示了自定义View的基本原理,还涉及到触摸事件处理、动画效果实现等多个知识点,是Android开发中非常实用的一个实践案例。

    Android 使用Kotlin来实现任务完成提醒效果

    在Android开发中,使用Kotlin实现任务完成提醒效果是一个常见的需求,这涉及到用户界面的交互设计和通知系统。本文将详细解析如何利用Kotlin来创建一个具有提醒功能的应用,同时结合"贝塞尔曲线"和"自定义View"的...

    android实现圆弧(或直线)进度框 可包裹任意layout或单个imageview

    研究一番,熟悉了这个demo的实现原理: 其实就是用canvas.drawPath方法根据当前进度动态计算绘制四条边,实现一个进度条效果。 于是我决定将这个项目的绘制稍作修改,用四个半圆弧绘制四个角,然后用再用四条直线...

    Android代码-Android使用SurfaceView实现墨迹天气的风车效果.zip

    通过以上步骤,你就可以在Android应用中实现一个类似墨迹天气的风车动画效果。`WindmillDemo`应该是这个项目的主要示例代码,你可以详细阅读并学习其中的实现细节。同时,`JavaApk源码说明.txt`文件可能包含了关于...

Global site tag (gtag.js) - Google Analytics