您的位置:首页>科学 >Android qq消息气泡实现效果,BezierDemo源码解析-实现qq消息气泡拖拽消失的效果

Android qq消息气泡实现效果,BezierDemo源码解析-实现qq消息气泡拖拽消失的效果

2023-09-29 06:58

这篇文章我们比较了DraggableFlagView和BezierDemo项目的区别,并提到我们要做源码代码分析其中之一,那么我们来分析一下Bezier Demo的源码,因为这个项目的源码是最简单的,可以更直接的分析出核心的东西。但效果比DraggableFlagView要好。我会尽量详细,让更多的初学者满意。本文主要分析拉伸效果的实现。

源代码结构

Bezier演示只有两个java文件

www.webguidecorpuschristi.com是程序接口,www.webguidecorpuschristi.com是实现粘附和拉伸效果的类。

MainActivity.javapackage github.chenupt.bezier;

导入android.app.Activity;

导入android.os.Bundle;

公共类 MainActivity 扩展了 Activity {

@覆盖

protected void onCreate(Bundle savingInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

}

activity_main.xml

xmlns:tools="http://www.webguidecorpuschristi.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

android:方向=“垂直”

工具:context =“.MainActivity”>

android:layout_width="match_parent"

android:layout_height="match_parent"

android:background="@android:颜色/透明"/>

这里有一个问题:为什么BezierView控件的layout_width和layout_height是match_parent。这是因为这段代码很粗糙,哈哈。

好吧,从上面的活动中可以看出,所有的功能都是由BezierView控件实现的,所以我们直接转向www.webguidecorpuschristi.com

先粘贴代码包github.chenupt.bezier;

导入 android.content.Context;

导入android.graphics.Canvas;

导入android.graphics.Color;

导入android.graphics.Paint;

导入android.graphics.Path;

导入 android.graphics.PorterDuff;

导入 android.graphics.Rect;

导入 android.graphics.drawable.AnimationDrawable;

导入android.util.AttributeSet;

导入 android.view.MotionEvent;

导入android.view.View;

导入android.view.ViewGroup;

导入android.widget.FrameLayout;

导入android.widget.ImageView;

/**

* 由 [email protected] 于 2014 年 11 月 20 日创建。

* 描述:自定义布局绘制贝塞尔曲线

*/

公共类 BezierView 扩展了 FrameLayout {

//默认定点圆半径

公共静态最终浮动DEFAULT_RADIUS = 20;

私家漆油漆;

私有路径路径;

//手势坐标

浮点数 x = 300;

浮点 y = 300;

//锚点坐标

浮动锚X = 200;

浮动锚Y = 300;

//起点坐标

浮动startX = 100;

浮动起始Y = 100;

//定点圆半径

浮动半径 = DEFAULT_RADIUS;

//判断动画是否已经开始

boolean isAnimStart;

//判断是否开始拖动

布尔值 isTouch;

ImageView 探索ImageView;

ImageViewtipImageView;

公共BezierView(上下文上下文){

超级(上下文);

init();

}

public BezierView(上下文上下文,属性集属性){

超级(上下文,属性);

init();

}

public 紫色(Context context, AttributeSet attrs, int defStyleAttr) {

super(上下文,attrs,defStyleAttr);

init();

}

私有 void init(){

路径=新Path();

paint = new Paint();

paint.setAntiAlias(true);

paint.setStyle(Paint.Style.FILL_AND_STROKE);

paint.setStrokeWidth(2);

paint.setColor(www.webguidecorpuschristi.com);

LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);

exploredImageView = new ImageView(getContext());

exploredImageView.setLayoutParams(params);

exploredImageView.setImageResource(R.drawable.tip_anim);

exploredImageView.setVisibility(View.INVISIBLE);

tipImageView = new ImageView(getContext());

tipImageView.setLayoutParams(params);

tipImageView.setImageResource(www.webguidecorpuschristi.com_tips_newmessage_ninetynine);

addView(tipImageView);

addView(exploredImageView);

}

@覆盖

protected void onLayout(booleanchanged,intleft,inttop,intright,intbottom){

exploredImageView.setX(startX - exploredImageView.getWidth()/2);

exploredImageView.setY(startY - exploredImageView.getHeight()/2);

tipImageView.setX(startX - tipImageView.getWidth()/2);

tipImageView.setY(startY - tipImageView.getHeight()/2);

super.onLayout(已更改,左,上,右,下);

}

私有无效计算(){

浮动距离=(浮动)Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));

半径=-距离/15+DEFAULT_RADIUS;

if(半径

isAnimStart = true;

exploredImageView.setVisibility(View.VISIBLE);

exploredImageView.setImageResource(R.drawable.tip_anim);

((AnimationDrawable) exploredImageView.getDrawable()).stop();

((AnimationDrawable) exploredImageView.getDrawable()).start();

tipImageView.setVisibility(View.GONE);

}

//根据角度算出四边形的四个点

浮点偏移X =(浮点)(半径*Math.sin(Math.atan((y - startY)/(x - startX))));

浮点偏移Y =(浮点)(半径*Math.cos(Math.atan((y - startY)/(x - startX))));

浮动x1=startX-offsetX;

浮点y1=startY+offsetY;

浮点数 x2 = x - 偏移量 X;

浮点 y2 = y + 偏移量 Y;

浮点数 x3 = x + offsetX;

float y3 = y - offsetY;

浮动x4=startX+offsetX;

float y4 = startY - offsetY;

path.reset();

path.moveTo(x1, y1);

path.quadTo(anchorX,anchorY,x2,y2);

path.lineTo(x3, y3);

path.quadTo(anchorX,anchorY,x4,y4);

path.lineTo(x1, y1);

//更改图标的位置

tipImageView.setX(x - tipImageView.getWidth()/2);

tipImageView.setY(y - tipImageView.getHeight()/2);

}

@覆盖

受保护的 void onDraw(画布画布){

if(isAnimStart || !isTouch){

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

}其他{

计算();

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

canvas.drawPath(路径,绘画);

canvas.drawCircle(startX, startY, 半径, 绘制);

canvas.drawCircle(x, y, 半径, 油漆);

}

super.onDraw(canvas);

}

@覆盖

public boolean onTouchEvent(MotionEvent event){

if(event.getAction() == MotionEvent.ACTION_DOWN){

//判断触摸点是否在tipImageView中

矩形 矩形 = 新矩形();

int[]位置=新int[2];

tipImageView.getDrawingRect(矩形);

tipImageView.getLocationOnScreen(位置);

rect.left=位置[0];

www.webguidecorpuschristi.com = 位置[1];

rect.right = rect.right + 位置[0];

矩形.底部=矩形.底部+位置[1];

if(rect.contains((int)event.getRawX(),(int)event.getRawY())){

isTouch = true;

}

}else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){

isTouch = false;

tipImageView.setX(startX - tipImageView.getWidth()/2);

tipImageView.setY(startY - tipImageView.getHeight()/2);

}

无效();

if(isAnimStart){

返回 super.onTouchEvent(event);

}

anchorX =  (event.getX() + startX)/2;

anchorY =  (event.getY() + startY)/2;

x =  event.getX();

y =  event.getY();

返回true;

}

}

该控件是一个自定义的FrameLayout,这样不用自定义view,是为了能够直接添加显示消息数量的图片。

关于成员变量的那部分注释已经比较清楚了,我直接看看

init()方法

在init方法中,首先初始化画笔油漆。此涂料用于绘制附着力拉伸效果。然后在paint初始化代码下面的FrameLayout中添加两张图片:exploredImageView和tipImageView。 exploredImageView是拉开后显示的气泡,tipImageView是数字提示。这两个ImageView只是为了辅助模仿QQ,但不是我们要讨论的。核。

onLayout()方法

不重要,省略。

calculate()方法

这是一种根据手指拖动位置计算各个坐标的方法。同时这里也根据坐标点来定义路径:path.reset();

path.moveTo(x1, y1);

path.quadTo(anchorX,anchorY,x2,y2);

path.lineTo(x3, y3);

path.quadTo(anchorX,anchorY,x4,y4);

path.lineTo(x1, y1);

此代码是粘合拉伸效果的核心。过了一段时间,我们做的各种实验就在这里修改了。

onDraw()方法@Override

protected void onDraw(Canvas canvas){

if(isAnimStart || !isTouch){

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

}其他{

计算();

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

canvas.drawPath(路径,绘画);

canvas.drawCircle(startX, startY, 半径, 绘制);

canvas.drawCircle(x,y,半径,油漆);

}

super.onDraw(canvas);

}

该方法调用上面的calculate方法,然后根据计算出的值绘制路径和圆。

onTouchEvent()方法

该方法会根据触摸点的位置变化记录必要的位置信息,以便通过calculate()方法进行计算,同时在需要时发送绘图请求。

一步步分解

如果到这里就结束了,你肯定会不满意——“我还是不明白贝塞尔曲线是如何应用到上面的。”为了彻底理解,我们将做一些分解代码的实验。

首先我们找到onDraw方法,@Override

protected void onDraw(Canvas canvas){

if(isAnimStart || !isTouch){

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

}其他{

计算();

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

canvas.drawPath(路径,绘画);

canvas.drawCircle(startX, startY, 半径, 绘制);

canvas.drawCircle(x,y,半径,油漆);

}

super.onDraw(canvas);

}

in if(isAnimStart || !isTouch){

里的代码是拉下来后的效果,不用担心。

主要看else中的代码

首先调用calculate()方法,然后canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);

去掉这个也没关系。

然后用贝塞尔曲线绘制一条闭合路径:canvas.drawPath(path,paint);

然后我在两端画了圆圈。

为了更直观的看到效果,我们将原来的

//默认定点圆半径

公共静态最终浮动DEFAULT_RADIUS = 20;

更改为

//默认定点圆半径

公共静态最终浮动DEFAULT_RADIUS = 150;

这样拉大了可以更清楚的看到拉伸的过程,而且长时间拉伸也不会断裂。断裂临界点由以下代码确定:

计算方法中

浮动距离 = (浮动) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));

半径 = -距离/15+DEFAULT_RADIUS;

如果(半径 < 9){

isAnimStart = true;

更改后获得的效果如下:

看我,我把屏幕拉出了一半。

但是还是很难看出曲线是怎么画的。这是因为画笔绘画的绘制类型是填充模式。我们将其更改为行模式:

将 init() 方法更改为

private void init(){

路径 = new Path();

paint = new Paint();

paint.setAntiAlias(true);

paint.setStyle(Paint.Style.STROKE);

paint.setStrokeWidth(2);

paint.setColor(www.webguidecorpuschristi.com);

......

所以我们可以看到线条是如何组合在一起的:

可以看出,它确实是由两个圆和一条闭合路径组成的。那张数码图片有点烦人,咱们想办法把它去掉吧

在calculate()方法适当位置添加tipImageView.setVisibility(View.GONE);

我是在第三行左右加上的,只要能保证执行就可以了。我不敢说加在这里是最合适的,我只是想把它去掉。

以下是拆下后来回拉伸后的变形图:

有点猥琐。 。 。 。

现在我们也删除这两个圆圈。这两个圆仅根据两点之间的距离改变下半径(第二个点也改变圆点的坐标)。贝塞尔曲线位于中间。我们来看看包含贝塞尔曲线的路径。

去除圆圈只需注释掉ondraw方法的相关代码即可:

5 {IMG_5:Ahr0Chm6ly9pbwctymxvzy5JC2RUAW1NLMNUL2LTZ19JB252ZXJ0LZYTEYYJQ0Y2FIMTQXMZMZMTEXMDU3YTJILNBUZW ==/}

以下是标注后的效果:

这是我们的路。

回到构建这条路径的代码,在calculate方法中:path.reset();

path.moveTo(x1, y1);

path.quadTo(anchorX,anchorY,x2,y2);

path.lineTo(x3, y3);

path.quadTo(anchorX,anchorY,x4,y4);

path.lineTo(x1, y1);

lineTo方法是绘制直线,quadTo方法是绘制贝塞尔曲线。准确的说,就是画一条二阶贝塞尔曲线。为了看到路径的顺序,我们分别定义

(x1,y1)是点A

(x2,y2)是点B

(x3, y3) 是点 C

(x4, y4) 是点 D

(anchorX,anchorY)为X点,即二阶贝塞尔曲线的控制点。这里有两条二阶贝塞尔曲线,都是同一个控制点。

同时在画布上标记这些点的字母。具体方法是调用canvas.drawText。具体代码修改我就不贴出来了。

各点显示位置有偏差(特别是X点)。这是因为canvas.drawText的参数需要根据字符的大小进行调整。我这样做并不是为了简单,但你应该知道这些点。 A、B、C、D 的实际位置很容易识别,但 X 应该在中间。

有了上图,就很容易理解这段代码路径了。moveTo(x1, y1);

path.quadTo(anchorX,anchorY,x2,y2);

path.lineTo(x3, y3);

path.quadTo(anchorX,anchorY,x4,y4);

path.lineTo(x1, y1);

拉伸的附着效果主要取决于quadTo绘制的两条贝塞尔曲线。这两条曲线以它们之间的中间位置作为控制点,使曲线以同一弧度向内弯曲。当两端圆之间的距离越来越长时,两条曲线的控制点和端点的位置也会发生变化(需要根据距离计算端点和控制点的位置),形成橡皮筋的粘合效果。

各坐标点的计算

现在的最后一个问题是如何找到这些变化点。

首先我们需要记录手指运动过程中,触摸点的变化情况,在demo中是使用(x,y)来代表这个触摸点,然后根据(startX,startY)(这个点是写死的)计算出控制点的坐标(anchorX,anchorY)

代码如下@Override

public boolean onTouchEvent(MotionEvent event) {

if(event.getAction() == MotionEvent.ACTION_DOWN){

// 判断触摸点是否在tipImageView中

Rect rect = new Rect();

int[] location = new int[2];

tipImageView.getDrawingRect(rect);

tipImageView.getLocationOnScreen(location);

rect.left = location[0];

www.webguidecorpuschristi.com = location[1];

rect.right = rect.right + location[0];

rect.bottom = rect.bottom + location[1];

if (rect.contains((int)event.getRawX(), (int)event.getRawY())){

isTouch = true;

}

}else if(event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL){

isTouch = false;

tipImageView.setX(startX - tipImageView.getWidth()/2);

tipImageView.setY(startY - tipImageView.getHeight()/2);

}

invalidate();

if(isAnimStart){

return super.onTouchEvent(event);

}

anchorX =  (event.getX() + startX)/2;

anchorY =  (event.getY() + startY)/2;

x =  event.getX();

y =  event.getY();

return true;

}

其中if和else代码块中的的代码和粘连效果无关,这些代码是关于气泡的ImageView显示与消失的。

主要就是下面的代码invalidate();

if(isAnimStart){

return super.onTouchEvent(event);

}

anchorX =  (event.getX() + startX)/2;

anchorY =  (event.getY() + startY)/2;

x =  event.getX();

y =  event.getY();

可以看出在onTouchEvent中,主要工作是记录,坐标点的计算还是在calculate()方法里(不过这里也简单的计算了控制点的坐标(anchorX,anchorY),其实这也可以放到calculate里面)。另外

invalidate()方法我觉得还是放在最后比较好。不过没什么大碍,也就是落后一个点而已,你根本感觉不到。

而calculate()方法里面对坐标的计算也很简单,没几行代码,结合上面的几幅图应该很容易解出来。这里就不再赘述了。

其实整篇文章可以用一句话来概括:粘连效果的关键是由同一个控制点(中间点)“拖住”两条贝塞尔曲线。

最后做一点补充,为了将橡皮的效果做的更逼真,这个demo中还动态的改变了两端圆点的半径,当然这也会导致其他点也做相应的改变float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));

radius = -distance/15+DEFAULT_RADIUS;