前言:世人总是恐惧失败,但失败了也大不从头再来
相关系列文章:
Android自定义控件三部曲文章索引:http://blog.csdn.net/harvic880925/article/details/50995268
前几篇给大家讲了有关绘图的知识,这篇我们稍微停一下,来看下手机QQ中拖动删除的效果是如何实现的;
这篇涉及到的知识有:
- saveLayer图层相关知识
- Path的贝赛尔曲线
- 手势监听
- animationlist逐帧动画
本篇的效果图如下:
这里有三个效果点:
1、拉长效果的实现
2、拉的不够长时返回初始状态
3、拉的够长后显示爆炸消除效果
####一、拉伸效果实现
####1、实现原理
一上来先给大家讲本篇最难的部分,这点理解了,后面就轻松了
本节先实现一个圆圈的拉伸效果,效果图如下:
看起来是不是挺好玩的,跟拉弹弓一样,这里主要有两个效果组成:
- 新加一个跟圆圈跟手指位置移动的圆
- 两个圆之间的连线填充用贝赛尔曲线
拼接过程如下图:
从上面的拼接图中可以看出,整个拉伸效果是由两个圆和中间的贝赛尔曲线连线所组成的矩形所组成的。
下面部分将涉及贝赛尔曲线,不理解的同学先看这篇文章《自定义控件三部曲之绘图篇(六)——Path之贝赛尔曲线和手势轨迹、水波纹效果》
在贝赛尔曲线部分我们已经讲了,贝赛尔曲线关键地在于控件点的坐标如何动态的确定,我们已经说过贝赛尔曲线的控制点我们可以借助PhtotoShop的钢笔工具来找;
那我们就来借助钢笔工具来找一下,如下图:
我们单独拿出来最终的结果图来看一下:
P0,P1是两个圆的切线的交点(切点),Q0是二阶贝赛尔曲线的控制点。从图中大概可以看出Q0的位置应该在两个圆心连线的中点。
在知道两个圆心点位置以后,Q0点的坐标很容易求得,但是P0,P1的坐标要怎么来求得现在的当务之急了。
先给大家画个图来看求下图中P0点的坐标
这里演示的是圆形向右下拉的过程(为什么选择向右下拉为例来计算坐标我们后面会讲),左上角的圆形是初始圆形(圆心坐标是x0,yo),右下角的圆形是拖动后的圆形(圆心坐标是x1,y1);
首先,在这个图中有四个切点P0,P1,P2,P3;这四个切点的坐标就是我们所要求的。我们这里以求P0为例来演示下求坐标的过程。
先看P0所在位置所形成的三角形,所在初始圆形的坐标是(x0,y0)
我们单独把这个三角形拿出来,这里可以很明显的可以看出P0的坐标是:
x = x0 + r * sina;
y = y0 - r * cosa;
由于屏幕坐标系是X轴向右为正,Y轴向下为正。所以P0的X坐标是比圆形x0坐标大的,所以要加上r * sina;而P0的Y坐标是在圆形y0坐标的上方,比y0小,所以要减去r * cosa;
用同样的方法可以求出P1,P2,P3的坐标公式:
//P1
x = x1 + r * sina;
y = y1 - r * cosa;
//P2
x = x1 - r * sina;
y = y1 + r * cosa;
//P3
x = x0 - r * sina;
y = y0 + r * cosa;
那么问题来了,角度a的值是多少呢?
我们再回过头来看一下我们的高清无码大图:
tan(a) = dy/dx;
所以a = arctan(dy/dx);
这样角度a的值就求到了,自然sina和cosa也就得到了。
####2、代码实现
下面我们就来看一下如何用代码来实现这个手拖动的过程;
注意:这篇博客并不是要制造出来一个通用组件,而是主要为了讲解拖动消除的原理,后面我们会逐渐对这篇文章进行扩充,最终将产出一个通用控件!慢慢来吧
(1)、新建类及初始化
由于我们这篇是讲解基本原理,所以我们新建一个类派生自FramLayout,然后在这个类中做绘图等等操作。
public class RedPointView extends FrameLayout {
private PointF mStartPoint, mCurPoint;
private int mRadius = 20;
private Paint mPaint;
private Path mPath;
public RedPointView(Context context) {
super(context);
initView();
}
public RedPointView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public RedPointView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
private void initView() {
mStartPoint = new PointF(100, 100);
mCurPoint = new PointF();
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
}
}
我们新建了一个RedPointView类派生自FramLayout,然后添加了一个初始化函数:
private void initView() {
mStartPoint = new PointF(100, 100);
mCurPoint = new PointF();
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.FILL);
mPath = new Path();
}
首先是两个点坐标,分别表示两个圆的圆心位置。mStartPoint表示起始圆心位置,mCurPoint是当前手指的位置,也就是移动的圆心位置。然后是初始化Paint和Path;
(2)、圆随着手指移动
这部分的效果图如下:当手指移动时新画一个圆在随着手指移动
所以我们要先定义一个变量表示当前用户的手指是不是下按状态,如果是下按状态就根据当前手指的位置多画一个圆;完整代码如下:
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawCircle(mStartPoint.x, mStartPoint.y, mRadius, mPaint);
if (mTouch) {
canvas.drawCircle(mCurPoint.x, mCurPoint.y, mRadius, mPaint);
}
canvas.restore();
super.dispatchDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
mTouch = true;
}
break;
case MotionEvent.ACTION_UP: {
mTouch = false;
}
}
mCurPoint.set(event.getX(), event.getY());
postInvalidate();
return true;
}
我们先来看看对onTouchEvent的拦截过程,在onTouchEvent中,在手指下按时将mTouch赋值为true,在手机抬起时赋值为false;
然后将当前手指的位置传给mCurPoint保存,然后调用postInvalidate()强制重绘;最后return true表示当前消息到此为止,就不再往父控件传了。
以前我们讲过postInvalidate()和invadite()的区别,这里再简单说一下:invadite()必须在主线程中调用,而postInvalidate()内部是由Handler的消息机制实现的,所以在任何线程都可以调用,但实时性没有invadite()强。所以一般为了保险起见,一般是使用postInvalidate()来刷新界面。
然后是dispatchDraw函数,在《自定义控件三部曲之绘图篇(十三)——Canvas与图层(一)》中已经讲过onDraw、dispatchDraw的区别:
由于我们这里是继承自FrameLayout所以是重写dispatchDraw()函数来进行重绘
我们来看看dispatchDraw中实现代码,这里可谓是有难度:
protected void dispatchDraw(Canvas canvas) {
canvas.saveLayer(new RectF(0, 0, getWidth(), getHeight()), mPaint, Canvas.ALL_SAVE_FLAG);
canvas.drawCircle(mStartPoint.x, mStartPoint.y, mRadius, mPaint);
if (mTouch) {
canvas.drawCircle(mCurPoint.x, mCurPoint.y, mRadius, mPaint);
}
canvas.restore();
super.dispatchDraw(canvas);
}
- super.dispatchDraw(canvas)操作的位置问题
首先是super.dispatchDraw(canvas)放的位置很重要,我们有时把它写在绘图操作的最上方,有时把它写在所有绘图操作的最下方,关于这两个位置是有很大差别的,有关位置的问题,下面我们会再讲,这里放在哪里都不会有影响。 canvas.saveLayer()
与canvas.restore()
是Canvas的绘图操作,以前已经有详细讲过了,在这里也不是一两句能讲的完的,不理解的同学先把这两篇文章看完再回来看:
《自定义控件三部曲之绘图篇(十三)——Canvas与图层(一)》、《自定义控件三部曲之绘图篇(十四)——Canvas与图层(二)》- 最后是画初始圆和移动圆的位置
canvas.drawCircle(mStartPoint.x, mStartPoint.y, mRadius, mPaint);
if (mTouch) {
canvas.drawCircle(mCurPoint.x, mCurPoint.y, mRadius, mPaint);
}
这里主要是根据当前手指是不是在移动来判断是不是画出随手指移动的圆。代码难度不大就不再细讲了。
到这里,我们就实现了两个圆的显示了,最关键的部分来了——下面就是要看如何利用贝赛尔曲线把这两个圆连接起来。
(3)、贝赛尔曲线连接两个圆
首先,我们先看如何把路径给计算出来的:
//圆半径
private int mRadius = 20;
private void calculatePath() {
float x = mCurPoint.x;
float y = mCurPoint.y;
float startX = mStartPoint.x;
float startY = mStartPoint.y;
// 根据角度算出四边形的四个点
float dx = x - startX;
float dy = y - startY;
double a = Math.atan(dy / dx);
float offsetX = (float) (mRadius * Math.sin(a));
float offsetY = (float) (mRadius * Math.cos(a));
// 根据角度算出四边形的四个点
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;
float anchorX = (startX + x) / 2;
float anchorY = (startY + y) / 2;
mPath.reset();
mPath.moveTo(x1, y1);
mPath.quadTo(anchorX, anchorY, x2, y2);
mPath.lineTo(x3, y3);
mPath.quadTo(anchorX, anchorY, x4, y4);
mPath.lineTo(x1, y1);
}
先来看这段:
float x = mCurPoint.x;
float y = mCurPoint.y;
float startX = mStartPoint.x;
float startY = mStartPoint.y;
float dx = x - startX;
float dy = y - startY;
double a = Math.atan(dy / dx);
float offsetX = (float) (mRadius * Math.sin(a));
float offsetY = (float) (mRadius * Math.cos(a));
这里就是根据两个圆心坐标来计算出dx,dy,然后利用double a = Math.atan(dy / dx)得到夹角a的值,然后求得mRadius * Math.sin(a) 和 mRadius * Math.cos(a)的值;
然后利用我们开篇中得到的公式计算出P0,P1,P2,P3四个切点的坐标:
float x1 = startX + offsetX;
float y1 = startY - offsetY;
float x2 = x + offsetX;
float y2 = y - offsetY;
float x3 = x - offsetX;
float