您的位置:首页 > 移动开发 > Android开发

QQ的粘性控件的实现原理

2016-09-06 12:49 316 查看
声明:本文资料来自于传智播客安卓52期

先来张效果图:



话不多说,线上代码,然后逐个分析

自定义粘性控件

public class GooView extends View {

private static final String TAG = "TAG";
private Paint mPaint;

public GooView(Context context) {
this(context, null);
}

public GooView(Context context, AttributeSet attrs) {
this(context, attrs , 0);
}

public GooView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

// 做初始化操作

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.RED);
}

PointF[] mStickPoints = new PointF[]{
new PointF(250f, 250f),
new PointF(250f, 350f)
};
PointF[] mDragPoints = new PointF[]{
new PointF(50f, 250f),
new PointF(50f, 350f)
};
PointF mControlPoint = new PointF(150f, 300f);
PointF mDragCenter = new PointF(80f, 80f);
float mDragRadius = 14f;
PointF mStickCenter = new PointF(150f, 150f);
float mStickRadius = 12f;
private int statusBarHeight;
float farestDistance = 80f;
private boolean isOutofRange;//是否超出界限控制区
private boolean isDisappear;//拖动圆是否存在

@Override
protected void onDraw(Canvas canvas) {

// 计算连接点值, 控制点, 固定圆半径

// 1. 获取固定圆半径(根据两圆圆心距离)
float tempStickRadius = getTempStickRadius();

// 2. 获取直线与圆的交点
float yOffset = mStickCenter.y - mDragCenter.y;
float xOffset = mStickCenter.x - mDragCenter.x;
Double lineK = null;
if(xOffset != 0){
lineK = (double) (yOffset / xOffset);
}
// 通过几何图形工具获取交点坐标
mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);

// 3. 获取控制点坐标
mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);

// 保存画布状态
canvas.save();
canvas.translate(0, -statusBarHeight);

// 画出最大范围(参考用)
mPaint.setStyle(Style.STROKE);
canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
mPaint.setStyle(Style.FILL);

if(!isDisappear){
if(!isOutofRange){
// 3. 画连接部分
Path path = new Path();
// 跳到点1
path.moveTo(mStickPoints[0].x, mStickPoints[0].y);
// 画曲线1 -> 2
path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
// 画直线2 -> 3
path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
// 画曲线3 -> 4
path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);

// 画附着点(参考用)
mPaint.setColor(Color.BLUE);
canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint);
canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint);
canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint);
canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint);
mPaint.setColor(Color.RED);

// 2. 画固定圆
canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint);
}

// 1. 画拖拽圆
canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
}

// 恢复上次的保存状态
canvas.restore();
}

// 获取固定圆半径(根据两圆圆心距离)
private float getTempStickRadius() {
float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);

//      if(distance> farestDistance){
//          distance = farestDistance;
//      }
distance = Math.min(distance, farestDistance);

// 0.0f -> 1.0f
float percent = distance / farestDistance;
Log.d(TAG, "percent: " + percent);

// percent , 100% -> 20%
return evaluate(percent, mStickRadius, mStickRadius * 0.2f);
}

public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
float x;
float y;

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isOutofRange = false;
isDisappear = false;
x = event.getRawX();
y = event.getRawY();
updateDragCenter(x, y);

break;
case MotionEvent.ACTION_MOVE:
x = event.getRawX();
y = event.getRawY();
updateDragCenter(x, y);

// 处理断开事件
float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
if(distance > farestDistance){
isOutofRange = true;
invalidate();
}

break;
case MotionEvent.ACTION_UP:
if(isOutofRange){
float d = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
if(d > farestDistance){
// a. 拖拽超出范围,断开, 松手, 消失
isDisappear = true;
invalidate();
}else {
//b. 拖拽超出范围,断开,放回去了,恢复
updateDragCenter(mStickCenter.x, mStickCenter.y);
}

}else {
//              c. 拖拽没超出范围, 松手,弹回去
final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y);

ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
mAnim.addUpdateListener(new AnimatorUpdateListener() {

@Override
public void onAnimationUpdate(ValueAnimator mAnim) {
// 0.0 -> 1.0f
float percent = mAnim.getAnimatedFraction();
PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent);
updateDragCenter(p.x, p.y);
}
});
mAnim.setInterpolator(new OvershootInterpolator(4));
mAnim.setDuration(500);
mAnim.start();
}

break;

default:
break;
}

return true;
}

/**
* 更新拖拽圆圆心坐标,并重绘界面
* @param x
* @param y
*/
private void updateDragCenter(float x, float y) {
mDragCenter.set(x, y);
invalidate();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

statusBarHeight = Utils.getStatusBarHeight(this);
}

}


几何图形工具类

**
* 几何图形工具
*/
public class GeometryUtil {

/**
* As meaning of method name.
* 获得两点之间的距离
* @param p0
* @param p1
* @return
*/
public static float getDistanceBetween2Points(PointF p0, PointF p1) {
float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
return distance;
}

/**
* Get middle point between p1 and p2.
* 获得两点连线的中点
* @param p1
* @param p2
* @return
*/
public static PointF getMiddlePoint(PointF p1, PointF p2) {
return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
}

/**
* Get point between p1 and p2 by percent.
* 根据百分比获取两点之间的某个点坐标
* @param p1
* @param p2
* @param percent
* @return
*/
public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
return new PointF(evaluateValue(percent, p1.x , p2.x), evaluateValue(percent, p1.y , p2.y));
}

/**
* 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
* @param fraction
* @param start
* @param end
* @return
*/
public static float evaluateValue(float fraction, Number start, Number end){
return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
}

/**
* Get the point of intersection between circle and line.
* 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
*
* @param pMiddle The circle center point.
* @param radius The circle radius.
* @param lineK The slope of line which cross the pMiddle.
* @return
*/
public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
PointF[] points = new PointF[2];

float radian, xOffset = 0, yOffset = 0;
if(lineK != null){
radian= (float) Math.atan(lineK);
xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);
}else {
xOffset = radius;
yOffset = 0;
}
points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);

return points;
}
}


Activity

public class MainActivity extends Activity {
private GooView  gv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gv=new GooView(MainActivity.this)
setContentView(gv);
gv.setGooViewListener(new GooViewListener(){
//重写GooViewListener接口里面的方法
});

}

}




GooView 详解:

这里属于自定义View(继承于View)

extends View (GooView extends View)

重写构造方法

进行测量

重写onDraw方法

重写构造方法并初始化一个画笔(Paint对象)

public GooView(Context context) {
this(context, null);
}

public GooView(Context context, AttributeSet attrs) {
this(context, attrs , 0);
}

public GooView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

// 做初始化操作

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.RED);
}




构造如上所示的静态图坐标

//固定圆的圆上两点坐标
PointF[] mStickPoints = new PointF[]{
new PointF(250f, 250f),
new PointF(250f, 350f)
};
//移动圆的圆上两点坐标
PointF[] mDragPoints = new PointF[]{
new PointF(50f, 250f),
new PointF(50f, 350f)
};
//两圆圆心连线的中点坐标
PointF mControlPoint = new PointF(150f, 300f);
//拖动圆的圆心
PointF mDragCenter = new PointF(80f, 80f);
//拖动圆的半径
float mDragRadius = 14f;
//固定圆的圆心
PointF mStickCenter = new PointF(150f, 150f);
//静态圆的半径
float mStickRadius = 12f;
private int statusBarHeight;
//控制区的半径
float farestDistance = 80f;


重点

onDraw方法( 计算连接点值, 控制点, 固定圆半径)

@Override
protected void onDraw(Canvas canva
eecc
s) {
/*1. 获取固定圆半径(根据两圆圆心距离)
float tempStickRadius = getTempStickRadius();

// 2. 获取直线与圆的交点
float yOffset = mStickCenter.y - mDragCenter.y;
float xOffset = mStickCenter.x - mDragCenter.x;
Double lineK = null;
if(xOffset != 0){
lineK = (double) (yOffset / xOffset);
}
// 通过几何图形工具获取交点坐标
mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);

// 3. 获取控制点坐标
mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);

// 保存画布状态
canvas.save();
canvas.translate(0, -statusBarHeight);

// 画出最大范围(参考用)
mPaint.setStyle(Style.STROKE);
canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
mPaint.setStyle(Style.FILL);

if(!isDisappear){//拖拽圆需要存在
if(!isOutofRange){//如果拖拽未超出界限
// 3. 画连接部分
Path path = new Path();
// 跳到点1
path.moveTo(mStickPoints[0].x, mStickPoints[0].y);
// 画曲线1 -> 2
path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
// 画直线2 -> 3
path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
// 画曲线3 -> 4
path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);

// 画附着点(参考用)
mPaint.setColor(Color.BLUE);
canvas.drawCircle(mDragPoints[0].x, mDragPoints[0].y, 3f, mPaint);
canvas.drawCircle(mDragPoints[1].x, mDragPoints[1].y, 3f, mPaint);
canvas.drawCircle(mStickPoints[0].x, mStickPoints[0].y, 3f, mPaint);
canvas.drawCircle(mStickPoints[1].x, mStickPoints[1].y, 3f, mPaint);
mPaint.setColor(Color.RED);

// 2. 画固定圆
canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint);
}

// 1. 画拖拽圆
canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
}

// 恢复上次的保存状态
canvas.restore();
}


分析:

float tempStickRadius = getTempStickRadius();

作用是根据两个圆心的距离的变化动态设置固定圆的半径,以保证在拖动过程中固定圆的大小发生变化



获取直线与圆的交点:用于构建贝塞尔曲线(即上图中的P0和P2)

float yOffset = mStickCenter.y - mDragCenter.y;
float xOffset = mStickCenter.x - mDragCenter.x;
Double lineK = null;
if(xOffset != 0){
lineK = (double) (yOffset / xOffset);
}
// 通过几何图形工具获取交点坐标
mDragPoints = GeometryUtil.getIntersectionPoints(mDragCenter, mDragRadius, lineK);
mStickPoints = GeometryUtil.getIntersectionPoints(mStickCenter, tempStickRadius, lineK);


获取控制点坐标(即上图中的P1)

mControlPoint = GeometryUtil.getMiddlePoint(mDragCenter, mStickCenter);


画出界定区域圆圈

// 保存画布状态
canvas.save();
canvas.translate(0, -statusBarHeight);

// 画出最大范围(参考用)即控制界限区域的那个圆
mPaint.setStyle(Style.STROKE);
canvas.drawCircle(mStickCenter.x, mStickCenter.y, farestDistance, mPaint);
mPaint.setStyle(Style.FILL);




画由1,2,3,4构成的封闭区关于类Path的使用详解

// 3. 画连接部分
Path path = new Path();
// 跳到点1
path.moveTo(mStickPoints[0].x, mStickPoints[0].y);
// 画曲线1 -> 2
path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);
// 画直线2 -> 3
path.lineTo(mDragPoints[1].x, mDragPoints[1].y);
// 画曲线3 -> 4
path.quadTo(mControlPoint.x, mControlPoint.y, mStickPoints[1].x, mStickPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);


画固定圆和拖拽圆:

// 2. 画固定圆
canvas.drawCircle(mStickCenter.x, mStickCenter.y, tempStickRadius, mPaint);
// 1. 画拖拽圆
canvas.drawCircle(mDragCenter.x, mDragCenter.y, mDragRadius, mPaint);
// 恢复上次的保存状态
canvas.restore();


完成上面的onDraw()函数后,将会得到一个静态效果,如下:



接下来要实现动态变化的效果

动态变化主要通过重写onTouchEvent函数,通过按下,移动,抬起三个事件来改变图形效果

1 。处理按下(MotionEvent.ACTION_DOWN)需要做的事:

根据按下的位置坐标,画出拖动点和由点1,2,3,4构成的封闭图形

case MotionEvent.ACTION_DOWN:
isOutofRange = false;
isDisappear = false;//标记拖动圆为超出界限区域
x = event.getRawX();//getRowX:触摸点相对于屏幕的坐标
y = event.getRawY();
updateDragCenter(x, y); //更新拖动圆(即执行onDraw函数,重绘拖动圆)


2。 处理移动(MotionEvent.ACTION_MOVE:)所需要做的事

移动过程中需要根据固定点和拖拽点的距离来改变固定点的半径,

需要判断固定点和移动点之间的距离是否大于farestDistance(即两点之间的最大限制距离,即控制界限区域的那个圆的半径)。

如果大于:断开两个圆之间的连线(此时通过getTempStickRadius()获取的固定圆的半径为0,所以固定圆将消失);

如果不大于:则动态改变拖拽圆的位置和大小,以及固定圆的大小。

case MotionEvent.ACTION_MOVE:
x = event.getRawX();
y = event.getRawY();
updateDragCenter(x, y);

// 处理断开事件
float distance = GeometryUtil.getDistanceBetween2Points(mDragCenter, mStickCenter);
if(distance > farestDistance){
isOutofRange = true;
invalidate();
}
在这里需要注意的是:当用拖拽出界限区域后又拖拽回到界限区域内时未做任何事情,用户可以在这里加一个else执行执行自己的代码


3 。处理手指抬起时需要做的事情

判断拖拽是否超出界限

超出界限:判断两个圆的圆心距离(在拖拽已经超出界限区域还要判断圆心距离,是因为在移动动作中,当用户移出界限区后又移回界限区时isOutofRange没有改变,但圆心距会小于farestDistance)

如果Distance>farestDistance:拖拽超出范围,断开, 松手, 消失

如果小于:拖拽超出范围,断开,放回去了,恢复

没有超出界限区: 松手,弹回去

c. 拖拽没超出范围, 松手,弹回去

final PointF tempDragCenter = new PointF(mDragCenter.x, mDragCenter.y);
//值动画
ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
mAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator mAnim) {
// 0.0 -> 1.0f
//获取动画执行的百分比(0.0 -> 1.0f)
float percent = mAnim.getAnimatedFraction();
//根据百分比获取两点之间的某个点坐标
PointF p = GeometryUtil.getPointByPercent(tempDragCenter, mStickCenter, percent);
updateDragCenter(p.x, p.y);
}
});
mAnim.setInterpolator(new OvershootInterpolator(4));
mAnim.setDuration(500);
mAnim.start();
}


辅助函数:

/**
* 更新拖拽圆圆心坐标,并重绘界面
* @param x
* @param y
*/
private void updateDragCenter(float x, float y) {
mDragCenter.set(x, y);
invalidate();//会调用onDraw()函数
}
//view的大小发生改变时被系统回调用
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);

statusBarHeight = Utils.getStatusBarHeight(this);
}


对整个自定义粘性控件的总结:

第一步:先自定义描绘控件所需要的参数常量

第二步:先通过onDraw绘画出一个想要的静态view效果图

第三步:通过重写onTouchEvent方法。分析清楚按下,移动,抬起时所需要改变的效果,在这三个动作发生时改变相应的参数并调用invalidate();方法,对view进行重绘

自定义控件暴露事件回调的方法:

1.声明一个接口:

interface GooViewListener{
// 相应动作对应的回调方法(在view的onTouchEvent的不同情况下被调用)
public void fun1();
public void fun2();
..........
}


2.定义一个 GooViewListener接口变量:

public GooViewListener gvlistener;


3.为GooViewListener定义一个监听方法:

public void setGooViewListener(GooViewListener gvlistener){
this.gvlistener=gvlistener;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息