效果图:
分析 一 :
1、应用的地方:如未读数据的清除等
2、这个控件要实现哪些功能呢?
1)拖拽超出范围时,断开了,此时我们松手,图标消失
2)拖拽超出范围时,断开了,此时我们把图标移动回去,图标恢复原样
3)拖拽没有超出范围时,此时我们松手,图标弹回去
3、如何实现:
1)我们先画个两个静态的圆圈,一个大的,一个小的
2)绘制中间连接的部分:
3)把静态的数值变成变量
4)不断地修改变量,重绘界面,就能动起来
分析 二 :
1、有两个小球(一个固定圆,一个动圆)
2、然后两个球之间连接(一定区域)
3、固定圆的坐标我们指定,动圆坐标根据我们的鼠标点击移动或者手指触摸移动
还有要实现的效果:
1、单击小球的时候绘制大球,大球可以随手势滑动,小球不动
2、大球和小球之间有一定的范围,在这个范围内,大球任意移动
3、超过范围,只显示大球,返回到范围内松手,恢复原来状态(未读状态)
4、超过范围,只显示大球,不返回到一定的范围内,松手,消失不见(已读状态)
好了,大致的就是这么多了,首先我们先来看下贝塞尔曲线的解释,因为在这里我们中间的连接范围用到了这个知识。我也只是大概的了解了一下,因为我们用的是已经给我们提供好的函数和接口,具体的底层怎么实现的我们就不考究了。
看下图解:(这里面的那两个蓝色的斜线之间的红色是贝塞尔曲线绘制的范围)
首先我们要自定义粘性VIew ,ViscosityView.java
/**
* 自定义粘性view
*/
public class ViscosityView extends View {
private Paint mPaint;
//固定圆,并且初始化
private PointF mFixedCircle = new PointF(150f, 150f);
//固定圆的半径
float mFixedRadius = 14f;
//动圆 并且初始化
private PointF mDragCircle = new PointF(80f, 80f);
//动圆 半径
float mDragRadius = 20f;
//动圆两个焦点的坐标
private PointF[] mDragPoints;
//固定圆的两个焦点坐标
private PointF[] mFixedPoints;
//控制焦点
private PointF mControlPoint;
//获取状态栏的高度
private int mStatusBarHeight;
/**
* 是否断开
*/
private boolean isOutToRange = false;
/**
* 是否消失(是否可见)
*/
private boolean isDisappear = false;
/**
* 两个圆最远的距离
*/
float farestDistance = 100f;
String text = "";
/**
* 设置数字
* @param num
*/
public void setNumber(int num) {
text = String.valueOf(num);
}
/**
* 初始化圆的圆心坐标
* @param x
* @param y
*/
public void initCenter(float x, float y) {
mDragCircle = new PointF(x, y);
mFixedCircle = new PointF(x, y);
mControlPoint = new PointF(x, y);
invalidate();
}
public void setOnDisappearListener(OnDisappearListener mListener) {
this.mListener = mListener;
}
public void setStatusBarHeight(int statusBarHeight) {
this.mStatusBarHeight = statusBarHeight;
}
public OnDisappearListener getOnDisappearListener() {
return mListener;
}
interface OnDisappearListener {
void onDisappear(PointF mDragCenter);
void onReset(boolean isOutOfRange);
}
private OnDisappearListener mListener;
/**
* 清除
*/
private void disappeared() {
isDisappear = true;
invalidate();
if (mListener != null) {
mListener.onDisappear(mDragCircle);
}
}
public ViscosityView(Context context) {
this(context, null);
}
public ViscosityView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViscosityView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 创建画笔
*/
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 设置画笔的颜色
*/
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
//保持当前画布的状态
canvas.save();
//移动画布
canvas.translate(0, -mStatusBarHeight);
//根据两个圆的圆心的距离获取固定圆的半径
float distance = getTempFiexdCircle();
//计算连接部分
//1、获取直线与圆的焦点
float yOffset = mFixedCircle.y - mDragCircle.y;
float xOffset = mFixedCircle.x - mDragCircle.x;
//获取斜率
Double lineK = null;
if (xOffset != 0) {
lineK = (double) yOffset / xOffset;
}
//通过几何工具获取焦点坐标
this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
//2、获取控制点坐标
this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);
if (!isDisappear) {
//画拖拽圆 动圆
//canvas.drawCircle(80f,80f,20f,mPaint);
canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);
if (!isOutToRange) {
//画一个固定圆
//canvas.drawCircle(150f,150f,14f,mPaint);
canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);
//画连接部分 这个是用的那个贝塞尔曲线绘制的连接部分
Path path = new Path();
//跳到某个点1
path.moveTo(mFixedPoints[0].x, mFixedPoints[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, mFixedPoints[1].x, mFixedPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);
}
}
//恢复画布
canvas.restore();
}
/**
* 获取临时的固定圆的半径
* @return
*/
private float getTempFiexdCircle() {
//获取到两个圆心之间的距离
float instance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
//这个是在两个圆之间的实际距离和我们定义的距离之间取得最小值
instance = Math.min(instance, farestDistance);
//0.0f--->1.0f>>>>>1.0f---》0.0f
float percent = instance / farestDistance;
//估值器
return evaluate(percent, mFixedRadius, mFixedRadius * 0.2);
}
/**
* 估值器
* @param fraction
* @param startValue
* @param endValue
* @return
*/
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
/**
* 重写这个方法,让小球动起来
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = 0;
float y = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取到按下的时候的坐标(因为我们已经把画布往上移动了状态栏的高度了,或者是我们在这里做判断)
x = event.getRawX();
y = event.getRawY();
isDisappear = false;
isOutToRange = false;
//更新动圆的坐标
updataDragCircle(x, y);
break;
case MotionEvent.ACTION_MOVE:
//移动的时候获取坐标
x = event.getRawX();
y = event.getRawY();
//更新动圆的坐标
updataDragCircle(x, y);
//处理断开
float distance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
if (distance > farestDistance) { //如果获取到的距离大于我们定义的最大的距离
isOutToRange = true; //断开设置为true
invalidate(); //重绘
return false;
}
break;
case MotionEvent.ACTION_UP:
if (isOutToRange) { //如果是断开
isOutToRange = false; //设置为false
//处理断开
float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
if (d > farestDistance) {
// * a、拖拽超出范围,断开-->松手-->消失
//松手还没有放回去
//isDisappear = true;
disappeared();
//重绘一下
invalidate();
} else {
// * b、拖拽超出范围,断开---->放回去了--->恢复
updataDragCircle(mFixedCircle.x, mFixedCircle.y);
isDisappear = false;
if (mListener != null)
mListener.onReset(isOutToRange);
}
} else {
final PointF tempDragCircle = new PointF(mDragCircle.x, mDragCircle.y);
// * c、拖拽没有超出范围,断开--->恢复
final ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
mAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percent = mAnim.getAnimatedFraction();
PointF p = GeometryUtil.getPointByPercent(tempDragCircle, mFixedCircle, percent);
updataDragCircle(p.x, p.y);
}
});
//差之器
mAnim.setInterpolator(new OvershootInterpolator(4));
mAnim.setDuration(500);
mAnim.start();
}
break;
default:
isOutToRange = false;
break;
}
return true;
}
/**
* 更新拖拽圆的圆心坐标
* @param rawX
* @param rawY
*/
private void updataDragCircle(float rawX, float rawY) {
//更新的坐标
mDragCircle.set(rawX, rawY);
invalidate();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//获取状态栏的高度
mStatusBarHeight = Utils.getStatusBarHeight(this);
}
}
分析下这个ViscosityView.java
创建画笔:
public ViscosityView(Context context) {
this(context, null);
}
public ViscosityView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ViscosityView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
/**
* 创建画笔
*/
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
* 设置画笔的颜色
*/
mPaint.setColor(Color.RED);
}
我们这个时候先来画圆:
private Paint mPaint;
//固定圆,并且初始化
private PointF mFixedCircle = new PointF(150f, 150f);
//固定圆的半径
float mFixedRadius = 14f;
//动圆 并且初始化
private PointF mDragCircle = new PointF(80f, 80f);
//动圆 半径
float mDragRadius = 20f;
//动圆两个焦点的坐标
private PointF[] mDragPoints;
//固定圆的两个焦点坐标
private PointF[] mFixedPoints;
//控制焦点
private PointF mControlPoint;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//获取状态栏的高度
mStatusBarHeight = Utils.getStatusBarHeight(this);
}
重写onDraw()(onDraw()里面写):
//保持当前画布的状态
canvas.save();
//移动画布
canvas.translate(0, -mStatusBarHeight);
在这里我们先把两个圆之间的距离给获取到:
/**
* 获取临时的固定圆的半径
* @return
*/
private float getTempFiexdCircle() {
//获取到两个圆心之间的距离
float instance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
//这个是在两个圆之间的实际距离和我们定义的距离之间取得最小值
instance = Math.min(instance, farestDistance);
//0.0f--->1.0f>>>>>1.0f---》0.0f
float percent = instance / farestDistance;
//估值器
return evaluate(percent, mFixedRadius, mFixedRadius * 0.2);
}
/**
* 估值器
* @param fraction
* @param startValue
* @param endValue
* @return
*/
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
//根据两个圆的圆心的距离获取固定圆的半径
float distance = getTempFiexdCircle();
//计算连接部分
//1、获取直线与圆的焦点
float yOffset = mFixedCircle.y - mDragCircle.y;
float xOffset = mFixedCircle.x - mDragCircle.x;
/**
* 获取斜率
*/
Double lineK = null;
if (xOffset != 0) {
lineK = (double) yOffset / xOffset;
}
//通过几何工具获取焦点坐标
this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
//2、获取控制点坐标
this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);
//绘制动圆
canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);
//画一个固定圆
//canvas.drawCircle(150f,150f,14f,mPaint);
canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);
//canvas.drawCircle(150f,150f,14f,mPaint);
canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);
//画连接部分 这个是用的那个贝塞尔曲线绘制的连接部分
Path path = new Path();
//跳到某个点1
path.moveTo(mFixedPoints[0].x, mFixedPoints[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, mFixedPoints[1].x, mFixedPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);
//恢复画布
canvas.restore();
现在应该可以画出了两个圆还有就是两个圆之间的范围了如图所示:
到目前为止,我们只是画出了轮廓,还是没有让让根据我们的手势动起来,这里我们来实现以下,这个时候我们就要实现一下onTouch()方法了在这里我们来判断按下、移动、抬起的手势。还有就是在这里我们在移动的时候去判断是否是显示,隐藏,断开的了,代码量也不多,在这里我就不多做解释了,代码中我解释详细一下就行了.
/**
* 重写这个方法,让小球动起来
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = 0;
float y = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取到按下的时候的坐标(因为我们已经把画布往上移动了状态栏的高度了,或者是我们在这里做判断)
x = event.getRawX();
y = event.getRawY();
isDisappear = false;
isOutToRange = false;
//更新动圆的坐标
updataDragCircle(x, y);
break;
case MotionEvent.ACTION_MOVE:
//移动的时候获取坐标
x = event.getRawX();
y = event.getRawY();
//更新动圆的坐标
updataDragCircle(x, y);
//处理断开
float distance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
if (distance > farestDistance) { //如果获取到的距离大于我们定义的最大的距离
isOutToRange = true; //断开设置为true
invalidate(); //重绘
return false;
}
break;
case MotionEvent.ACTION_UP:
if (isOutToRange) { //如果是断开
isOutToRange = false; //设置为false
//处理断开
float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);
if (d > farestDistance) {
// * a、拖拽超出范围,断开-->松手-->消失
//松手还没有放回去
//isDisappear = true;
disappeared();
//重绘一下
invalidate();
} else {
// * b、拖拽超出范围,断开---->放回去了--->恢复
updataDragCircle(mFixedCircle.x, mFixedCircle.y);
isDisappear = false;
if (mListener != null)
mListener.onReset(isOutToRange);
}
} else {
final PointF tempDragCircle = new PointF(mDragCircle.x, mDragCircle.y);
// * c、拖拽没有超出范围,断开--->恢复
final ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);
mAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percent = mAnim.getAnimatedFraction();
PointF p = GeometryUtil.getPointByPercent(tempDragCircle, mFixedCircle, percent);
updataDragCircle(p.x, p.y);
}
});
//差之器
mAnim.setInterpolator(new OvershootInterpolator(4));
mAnim.setDuration(500);
mAnim.start();
}
break;
default:
isOutToRange = false;
break;
}
return true;
}
/**
* 更新拖拽圆的圆心坐标
* @param rawX
* @param rawY
*/
private void updataDragCircle(float rawX, float rawY) {
//更新的坐标
mDragCircle.set(rawX, rawY);
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
//保持当前画布的状态
canvas.save();
//移动画布
canvas.translate(0, -mStatusBarHeight);
//根据两个圆的圆心的距离获取固定圆的半径
float distance = getTempFiexdCircle();
//计算连接部分
//1、获取直线与圆的焦点
float yOffset = mFixedCircle.y - mDragCircle.y;
float xOffset = mFixedCircle.x - mDragCircle.x;
//获取斜率
Double lineK = null;
if (xOffset != 0) {
lineK = (double) yOffset / xOffset;
}
//通过几何工具获取焦点坐标
this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);
this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);
//2、获取控制点坐标
this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);
if (!isDisappear) {
//画拖拽圆 动圆
//canvas.drawCircle(80f,80f,20f,mPaint);
canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);
if (!isOutToRange) {
//画一个固定圆
//canvas.drawCircle(150f,150f,14f,mPaint);
canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);
//画连接部分 这个是用的那个贝塞尔曲线绘制的连接部分
Path path = new Path();
//跳到某个点1
path.moveTo(mFixedPoints[0].x, mFixedPoints[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, mFixedPoints[1].x, mFixedPoints[1].y);
path.close();
canvas.drawPath(path, mPaint);
}
}
//恢复画布
canvas.restore();
}
好了,我们来看下实现的动画效果:
工具类:
GeometryUtil.Java工具类:
package vp.zyqj.zz.viscositypoint;
import android.graphics.PointF;
/**
* 几何图形工具
*/
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;
}
}
package vp.zyqj.zz.viscositypoint;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.view.MotionEventCompat;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
public class Utils {
public static Toast mToast;
public static void showToast(Context mContext, String msg) {
if (mToast == null) {
mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT);
}
mToast.setText(msg);
mToast.show();
}
/**
* dip 转换成 px
* @param dip
* @param context
* @return
*/
public static float dip2Dimension(float dip, Context context) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
}
/**
* @param dip
* @param context
* @param complexUnit {@link TypedValue#COMPLEX_UNIT_DIP} {@link TypedValue#COMPLEX_UNIT_SP}}
* @return
*/
public static float toDimension(float dip, Context context, int complexUnit) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(complexUnit, dip, displayMetrics);
}
/** 获取状态栏高度
* @param v
* @return
*/
public static int getStatusBarHeight(View v) {
if (v == null) {
return 0;
}
Rect frame = new Rect();
v.getWindowVisibleDisplayFrame(frame);
return frame.top;
}
public static String getActionName(MotionEvent event) {
String action = "unknow";
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
action = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
break;
case MotionEvent.ACTION_UP:
action = "ACTION_UP";
break;
case MotionEvent.ACTION_CANCEL:
action = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_SCROLL:
action = "ACTION_SCROLL";
break;
case MotionEvent.ACTION_OUTSIDE:
action = "ACTION_SCROLL";
break;
default:
break;
}
return action;
}
}
参考:http://blog.csdn.net/wuyinlei/article/details/50634839 http://www.jb51.net/article/108265.htm http://blog.csdn.net/gesanri/article/details/48490873
http://blog.csdn.net/zhangphil/article/details/49746709 http://www.bubuko.com/infodetail-1092644.html
源码:http://download.csdn.net/detail/lijinweii/9905291