Android 贝塞尔曲线实现QQ拖拽清除效果
纯属好奇心驱动写的一个学习性Demo,效果如下:
vc23tNa1xNK7zPWyu7nm1PK1xFBhdGijrLb41eK49tbQvOTP8sTazeTH+rXE0Ke5+9X9ysfSu8z1sbTI+7b7x/rP36Os1tC85NXiuPZQYXRoysfTycG9zPWxtMj7tvvH+s/fus3Bvcz11rHP39fps8mho7+0z8LNvKO6PGJyIC8+DQo8aW1nIGFsdD0="这里写图片描述" src="http://www.bkjia.com/uploads/allimg/160407/0413013240-1.png" title="\" />
两个带圆弧的线就是由三点确认的一个贝塞尔曲线:
在Android已经有提供画贝塞尔曲线的接口,三个点传进去,效果就出来了。
贝塞尔曲线是用三个或多个点来确定的一条曲线,它在图形图像学中有相当重要的地位,Path中也提供了一些方法来给我们模拟低阶贝赛尔曲线。
本文的主要目的是学习贝塞尔曲线,看一下贝塞尔曲线的一下使用案例。
案例一:
案例二:
案例三:
案例四:
其实还有很多,还有QQ就是上面拖拽清除消息的气泡。看了之后你会好奇是怎么做的吧。
这里不讲背景,只讲贝塞尔曲线的一些效果,先从维基百科上盗图,看看不同数量的点上绘制贝塞尔曲线的效果。
两个点,绘制出来就是直线,也就是所谓的一阶贝塞尔曲线
三个点,二阶贝塞尔曲线:
四个点,三阶贝塞尔曲线:
五个点,四阶贝塞尔曲线:
六个点,五阶贝塞尔曲线:
大家看到上面的图,其实可以这么理解,这也是参考网友的理解。不管是一阶、二阶、还是六阶,他们都至少有两个点,也就是起点和终点。先用橡皮筋把这两个点连接好。而除了起点和终点之外的点都相当于对橡皮筋有吸引力的点,他们因为距离原因的不同,对橡皮筋产生不同程度的吸力,最终达到一个平衡状态,所以橡皮筋往非起始点有所偏移,如下图:
在Android中提供了绘制一阶、二阶、三阶的接口:
一阶接口:
public void lineTo(float x, float y)
二阶接口:
public void quadTo(float x1, float y1, float x2, float y2)
三阶接口:
public void cubicTo(float x1, float y1, float x2, float y2,
float x3, float y3)
关于最上面的QQ拖拽清除效果,就是用的二阶绘制的。
下面来简要讲解一下它的实现原理吧:
第一:那个99+消息是一个ImageView,它是根据OnTouch事件移动的,这个好理解。这是是通过在onTouch方法里面不断的setX()、setY()实现的。
第二:记录起始位置p1(x1,y1),记录手指拖拽位置p2(x2,y2),有了这两点我们就可以绘制直线了,但是我们这里绘制的是带有贝塞尔曲线的Path,所以重要工作是构造Path
第三:构造Path,如下图:
知道了上图两个黑点的坐标,并且知道绿色线的宽度Len,很好求出P1、P2、P3当中的点了,画个坐标系,用三角函数就求出来了。当然这个宽度Len是根据两个黑点之间的距离动态的去计算了,它与两个黑点之间的距离成反比。构造Path的步骤是:
p1-p3-p2之间:贝塞尔曲线
p2-p5之间:直线
p5-p3-p4之间:贝塞尔曲线
p4-p1之间:直线
构造了这样一个Path,然后用实心的画笔就画出效果来了。
当然,要完全达到要求还要加上逻辑控制了,代码如下:
package rander.com.bezier.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.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toast;
import rander.com.bezier.R;
/**
* Created by Rander.C on 16-4-3.
*/
public class QQDragtoClearView extends FrameLayout {
/**
* 最大拖拽长度
*/
private final int DRAG_MAX_LEN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, getResources().getDisplayMetrics());
/**
* 默认的红点半径大小
*/
private final int DEFAULT_RADIUS = 40;
/**
* 消息数量背景,QQ里面起始用的就是一个图片,而不是在红色背景上画一个数量
*/
private ImageView mIvMsgCountBg;
/**
* 拖拽清除的时候的动画视图
*/
private ImageView mIvClearAnim;
/**
* 手触摸的x,y坐标
*/
private float mTouchX;
private float mTouchY;
/**
* 初始的x,y坐标,默认安放的位置
*/
private float mStartX = 300;
private float mStartY = 300;
/**
* 手触摸的坐标和初始点坐标的中间位置
* mCenterX = (mTouchX + mStartX)/2
* mCenterY = (mTouchY + mStartY )/2
* 这个是用来画贝塞尔曲线的一个中转点
*/
private float mCenterX;
private float mCenterY;
private static final int TOUCH_SLOP = 10;
/**
* 画贝塞尔曲线的画笔
*/
private Paint mPaint;
/**
* 画贝塞尔曲线的Path
*/
private Path mPath;
/**
* 是否被点中了
*/
private boolean isTouch;
/**
* 默认的半径大小
*/
private int mRaduis = DEFAULT_RADIUS;
/**
* 判断贝塞尔曲线是否断掉了
*/
private boolean isBroken = false;
public QQDragtoClearView(Context context) {
this(context, null);
}
public QQDragtoClearView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public QQDragtoClearView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.RED);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(60, 60);
mIvClearAnim = new ImageView(getContext());
mIvClearAnim.setLayoutParams(params);
mIvClearAnim.setImageResource(R.drawable.tip_anim);
mIvClearAnim.setVisibility(INVISIBLE);
mIvMsgCountBg = new ImageView(getContext());
mIvMsgCountBg.setLayoutParams(params);
mIvMsgCountBg.setImageResource(R.drawable.skin_tips_newmessage_ninetynine);
mIvMsgCountBg.setVisibility(VISIBLE);
addView(mIvClearAnim);
addView(mIvMsgCountBg);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mTouchX = (int) event.getX();
mTouchY = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//ACTION_DOWN的作用就是判断触摸的点
Rect rect = new Rect();
int[] location = new int[2];
mIvMsgCountBg.getDrawingRect(rect);
mIvMsgCountBg.getLocationOnScreen(location);
rect.left = location[0];
rect.top = location[1];
rect.right = rect.right + location[0];
rect.bottom = rect.bottom + location[1];
if (r