BezierDemo源代码解析-实现qq消息气泡拖拽消失的效果
这篇文章中我们比較了DraggableFlagView和BezierDemo两个项目的差别,提到将对当中一个做源代码分析,那么我们就来分析BezierDemo的源代码吧。由于这个项目的源代码最简单。能够更直接的去分析核心的东西。可是效果还是DraggableFlagView好些。我尽量讲的具体些,满足很多其它的刚開始学习的人。
这篇文章主要分析拉伸效果的实现。
源代码结构
BezierDemo仅仅有两个java文件
当中MainActivity.java是程序界面,而BezierView.java是实现了粘连拉伸效果的类。
MainActivity.java
package github.chenupt.bezier; import android.app.Activity; import android.os.Bundle; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <github.chenupt.bezier.BezierView android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/transparent" /> </LinearLayout>
这里有个疑问:为啥BezierView控件的layout_width和layout_height为match_parent。 这是由于这个代码非常粗糙,哈哈。
好了,从上面的activity能够看出。全部的功能都是BezierView控件实现的,因此我们直接转向BezierView.java
先贴代码
package github.chenupt.bezier; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.AnimationDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; /** * Created by chenupt@gmail.com on 11/20/14. * Description : custom layout to draw bezier */ public class BezierView extends FrameLayout { // 默认定点圆半径 public static final float DEFAULT_RADIUS = 20; private Paint paint; private Path path; // 手势坐标 float x = 300; float y = 300; // 锚点坐标 float anchorX = 200; float anchorY = 300; // 起点坐标 float startX = 100; float startY = 100; // 定点圆半径 float radius = DEFAULT_RADIUS; // 推断动画是否開始 boolean isAnimStart; // 推断是否開始拖动 boolean isTouch; ImageView exploredImageView; ImageView tipImageView; public BezierView(Context context) { super(context); init(); } public BezierView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public BezierView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ path = new Path(); paint = new Paint(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setStrokeWidth(2); paint.setColor(Color.RED); 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(R.drawable.skin_tips_newmessage_ninetynine); addView(tipImageView); addView(exploredImageView); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 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(changed, left, top, right, bottom); } private void calculate(){ float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2)); radius = -distance/15+DEFAULT_RADIUS; if(radius < 9){ isAnimStart = true; exploredImageView.setVisibility(View.VISIBLE); exploredImageView.setImageResource(R.drawable.tip_anim); ((AnimationDrawable) exploredImageView.getDrawable()).stop(); ((AnimationDrawable) exploredImageView.getDrawable()).start(); tipImageView.setVisibility(View.GONE); } // 依据角度算出四边形的四个点 float offsetX = (float) (radius*Math.sin(Math.atan((y - startY) / (x - startX)))); float offsetY = (float) (radius*Math.cos(Math.atan((y - startY) / (x - startX)))); float x1 = startX - offsetX; float y1 = startY + offsetY; float x2 = x - offsetX; float y2 = y + offsetY; float x3 = x + offsetX; float y3 = y - offsetY; float 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); } @Override protected void onDraw(Canvas canvas){ if(isAnimStart || !isTouch){ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); }else{ calculate(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); canvas.drawPath(path, paint); canvas.drawCircle(startX, startY, radius, paint); canvas.drawCircle(x, y, radius, paint); } super.onDraw(canvas); } @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]; rect.top = 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; } }
该控件是一个自己定义的FrameLayout,之所以不用自己定义view,是为了能直接加入显示消息数目的图片。
关于成员变量的那部分凝视已经比較清楚了。我直接看看
init()方法
在init方法中首先初始化了画笔paint,这个paint就是绘制粘连拉伸效果的。然后paint初始化代码以下为FrameLayout加入了两个图片:exploredImageView和tipImageView。exploredImageView是在拉断之后显示的气泡,而tipImageView是数字提示。这两个ImageView都仅仅是为了辅助模仿qq。但不是我们要讨论的核心。
onLayout()方法
非重点,略。
calculate()方法
这是依据手指拖动位置计算各坐标的的方法,同一时候还在这里依据坐标点将path路径也定义了:
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); }else{ calculate(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); canvas.drawPath(path, paint); // canvas.drawCircle(startX, startY, radius, paint); // canvas.drawCircle(x, y, radius, paint); } super.onDraw(canvas); }
这种方法调用了上面的calculate方法。然后依据计算出的值绘制path和圆圈。
onTouchEvent()方法
这种方法将依据触摸点的位置变化记录必要的位置信息。供calculate()方法计算,同一时候在必要的地方发送绘制请求。
一步一步分解
假设说到这里就结束。你肯定不惬意-“我还是没明确贝塞尔曲线是怎样应用到里面的呢”。为了彻底明确我们将做几个分解代码的实验。
首先我们找到onDraw方法,
@Override protected void onDraw(Canvas canvas){ if(isAnimStart || !isTouch){ canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); }else{ calculate(); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY); canvas.drawPath(path, paint); canvas.drawCircle(startX, startY, radius, paint); canvas.drawCircle(x, y, radius, paint); } super.onDraw(canvas); }
在
if(isAnimStart || !isTouch){
中的代码是拉断之后的效果,不去管他。
主要看else中的代码
首先调用了calculate()方法,然后调用了
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.OVERLAY);
这个去掉也无所谓。
接着绘制了一条带有贝塞尔曲线的封闭路径:
canvas.drawPath(path, paint);
然后分别绘制了两端的圆圈。
为了更直观的看出效果。我们将原本
// 默认定点圆半径
public static final float DEFAULT_RADIUS = 20;
改成
// 默认定点圆半径
public static final float DEFAULT_RADIUS = 150;
这样大点会更清楚的看到拉伸过程,并且拉非常长也不会断,拉断的临界点是以下代码决定的:
calculate方法中
float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2));
radius = -distance/15+DEFAULT_RADIUS;
if(radius < 9){
isAnimStart = true;
更改之后得到的效果例如以下:
你看我都拉了半边屏幕。
可是这样仍然难以看到曲线是怎样绘制的,这是由于画笔paint的绘制类型是填充模式的,我们改成线条模式:
将init()方法改成
private void init(){
path = new Path();
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);
......
这样我们就能看到线条是怎样组合的了:
能够看出的确是两个圆圈和一条封闭的路径组成的。那个数字图片有点碍眼,我们想办法去掉
在calculate()方法的适当位置加上
tipImageView.setVisibility(View.GONE);
我是加在第三行左右,总之能保证会被运行即可。
我不敢说加在这里最合适。我仅仅是单纯的想去掉它而已。
以下是去掉之后来回拉伸的变换图:
有点猥琐。。。。
如今我们将两个圆圈也去掉吧,这两个圆圈不过依据两点之间距离的大小改变了下半径而已(第二个点也改变了圆点坐标)。
贝塞尔曲线在中间那部分,让我们看看包括了贝塞尔曲线的path路径的真面目。
去掉圆圈仅仅需将ondraw方法的相关代码凝视掉:
以下是凝视之后的效果:
这就是我们的path了。
回到构建这个path的代码,在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方法就是绘制贝塞尔曲线。准确的说,是绘制二阶贝塞尔曲线。为了能看出path的先后顺序,我们分别定义
(x1, y1)为A点
(x2, y2)为B点
(x3, y3)为C点
(x4, y4)为D点
(anchorX, anchorY)为X点,这是二阶贝塞尔曲线的控制点。这里有两条二阶贝塞尔曲线。都是同一个控制点。
同一时候在canvas中将这几个点的字母标注出来,详细的做法是调用canvas.drawText,改动详细的代码我就不发了。
每一个点的显示位置有所偏差(尤其是X点),这是由于canvas.drawText的參数须要依据字符的大小做调整,我为了简便。没有去做,可是这些点你应该知道他们的实际位置,A,B,C。D非常好辨认,可是X应该是在中间才对。
有了上面那幅图对于这段代码就好理解了
path.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]; rect.top = 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()方法里面对坐标的计算也非常简单,没几行代码,结合上面的几幅图应该非常easy解出来。这里就不再赘述了。
事实上整篇文章能够用一句话来概括:粘连效果的关键是由同一个控制点(中间点)“拖住”两条贝塞尔曲线。
最后做一点补充。为了将橡皮的效果做的更逼真。这个demo中还动态的改变了两端圆点的半径,当然这也会导致其它点也做对应的改变
float distance = (float) Math.sqrt(Math.pow(y-startY, 2) + Math.pow(x-startX, 2)); radius = -distance/15+DEFAULT_RADIUS;
原文
http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0311/2577.html