这篇文章我们比较了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;