前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android贝塞尔曲线实现消息拖拽消失

Android贝塞尔曲线实现消息拖拽消失

作者头像
砸漏
发布2020-11-04 11:22:49
5960
发布2020-11-04 11:22:49
举报
文章被收录于专栏:恩蓝脚本

写在前头

写消息拖拽效果的文章不少,但是大部分都把自定义View写死了,我们要实现的是传入一个View,每个View都可以实现拖拽消失爆炸的效果,当然我也是站在巨人的肩膀上来学习的。但个人觉得程序员本就应该敢于学习和借鉴。

源码地址:源码Github地址

效果图

分析(用到的知识点):

(1)ValueAnimator (数值生成器) 用于生成数值,可以设置差值器来改变数字的变化幅度。

(2)ObjectAnimator (动画生成器) 用于生成各种属性,布局动画,同样也可以设置差值器来改变效果。

(3)贝塞尔一阶曲线

(4)自定义View的基础知识

(5)WindowManager 使view拖拽能显示在整个屏幕的任何地方,而不是局限于父布局内

具体实现方法

一、首先我们要实现基础效果

基础效果是点击屏幕任意一点能出现消息拖拽的效果,但是此时我们不用管我们拖动的View,只需要完成大致模型。该部分的难点在于贝塞尔一阶曲线的怎么实现。

基础效果图

分析:

(1)点击任意一点画出两个圆,和一个有贝塞尔曲线组成的path路径

(2)随着拖动距离的增加原点的圆半径逐渐缩小,当距离达到一定大以后原点的圆和贝塞尔曲线组成的path不再显示

贝塞尔曲线的画法

首先我们需要求出角a的大小,根据角a来求到A,B,C,D的坐标位子,然后求到控制点E点的坐标,通过Path.quadTo()方法来连接A,B和C,D两条贝塞尔曲线。

各点坐标

A(c1.x+sina*c1半径,c1.y-cina*c1半径) B(c2.x+sina*c2半径,c2.y-cina*c2半径) C(c2.x-sina*c1半径,c2.y+cina*c1半径) D(c1.x-sina*c2半径,c1.y+cina*c2半径) E ((c1.x+c2.x)/2,(c1.y+c2.y)/2)

贝塞尔曲线的path代码

代码语言:javascript
复制
private Path getBezeierPath() {
  double distance = getDistance(mBigCirclePoint,mLittleCirclePoint);
 
  mLittleCircleRadius = (int) (mLittleCircleRadiusMax - distance / 10);
  if (mLittleCircleRadius < mLittleCircleRadiusMin) {
   // 超过一定距离 贝塞尔和固定圆都不要画了
   return null;
  }
 
  Path bezeierPath = new Path();
 
  // 求角 a
  // 求斜率
  float dy = (mBigCirclePoint.y-mLittleCirclePoint.y);
  float dx = (mBigCirclePoint.x-mLittleCirclePoint.x);
  float tanA = dy/dx;
  // 求角a
  double arcTanA = Math.atan(tanA);
 
  // A
  float Ax = (float) (mLittleCirclePoint.x + mLittleCircleRadius*Math.sin(arcTanA));
  float Ay = (float) (mLittleCirclePoint.y - mLittleCircleRadius*Math.cos(arcTanA));
 
  // B
  float Bx = (float) (mBigCirclePoint.x + mBigCircleRadius*Math.sin(arcTanA));
  float By = (float) (mBigCirclePoint.y - mBigCircleRadius*Math.cos(arcTanA));
 
  // C
  float Cx = (float) (mBigCirclePoint.x - mBigCircleRadius*Math.sin(arcTanA));
  float Cy = (float) (mBigCirclePoint.y + mBigCircleRadius*Math.cos(arcTanA));
 
  // D
  float Dx = (float) (mLittleCirclePoint.x - mLittleCircleRadius*Math.sin(arcTanA));
  float Dy = (float) (mLittleCirclePoint.y + mLittleCircleRadius*Math.cos(arcTanA));
 
 
 
  // 拼装 贝塞尔的曲线路径
  bezeierPath.moveTo(Ax,Ay); // 移动
  // 两个点
  PointF controlPoint = getControlPoint();
  // 画了第一条 第一个点(控制点,两个圆心的中心点),终点
  bezeierPath.quadTo(controlPoint.x,controlPoint.y,Bx,By);
 
  // 画第二条
  bezeierPath.lineTo(Cx,Cy); // 链接到
  bezeierPath.quadTo(controlPoint.x,controlPoint.y,Dx,Dy);
  bezeierPath.close();
 
  return bezeierPath;
 }

二、完善代码

这部分我们需要完善所有代码,实现代码的分离,使得所用View都能被拖动,且需要创建一个监听器来监听View是否拖动结束了,结束后调用回调方法以便需要做其他处理。

需要完成的功能:

(1)将传入的View画出来

(2)在手指抬起时判断是爆炸还是回弹

(3)完成回弹和爆炸的代码部分

(4)回弹或者爆炸结束后调用回调通知动画结束

(5)使用WindowManager把自定义拖拽View加进去,隐藏原来得View实现View在任意地方拖动

完整代码部分

(1)自定义View的代码

代码语言:javascript
复制
public class MsgDrafitingView extends View{
private PointF mLittleCirclePoint;
private PointF mBigCirclePoint;
private Paint mPaint;
//大圆半径
private int mBigCircleRadius = 10;
//小圆半径
private int mLittleCircleRadiusMax = 10;
private int mLittleCircleRadiusMin = 2;
private int mLittleCircleRadius;
private Bitmap dragBitmap;
private OnToucnUpListener mOnToucnUpListener;
public MsgDrafitingView(Context context) {
this(context,null);
}
public MsgDrafitingView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MsgDrafitingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mBigCircleRadius = dip2px(mBigCircleRadius);
mLittleCircleRadiusMax = dip2px(mLittleCircleRadiusMax);
mLittleCircleRadiusMin = dip2px(mLittleCircleRadiusMin);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setAntiAlias(true);
mPaint.setDither(true);
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
}
@Override
protected void onDraw(Canvas canvas) {
if (mBigCirclePoint == null || mLittleCirclePoint == null) {
return;
}
//画大圆
canvas.drawCircle(mBigCirclePoint.x, mBigCirclePoint.y, mBigCircleRadius, mPaint);
//获得贝塞尔路径
Path bezeierPath = getBezeierPath();
if (bezeierPath!=null) {
// 小到一定层度就不见了(不画了)
canvas.drawCircle(mLittleCirclePoint.x, mLittleCirclePoint.y, mLittleCircleRadius, mPaint);
// 画贝塞尔曲线
canvas.drawPath(bezeierPath, mPaint);
}
// 画图片
if (dragBitmap != null) {
canvas.drawBitmap(dragBitmap, mBigCirclePoint.x - dragBitmap.getWidth() / 2,
mBigCirclePoint.y - dragBitmap.getHeight() / 2, null);
}
}
private Path getBezeierPath() {
double distance = getDistance(mBigCirclePoint,mLittleCirclePoint);
mLittleCircleRadius = (int) (mLittleCircleRadiusMax - distance / 10);
if (mLittleCircleRadius < mLittleCircleRadiusMin) {
// 超过一定距离 贝塞尔和固定圆都不要画了
return null;
}
Path bezeierPath = new Path();
// 求角 a
// 求斜率
float dy = (mBigCirclePoint.y-mLittleCirclePoint.y);
float dx = (mBigCirclePoint.x-mLittleCirclePoint.x);
float tanA = dy/dx;
// 求角a
double arcTanA = Math.atan(tanA);
// A
float Ax = (float) (mLittleCirclePoint.x + mLittleCircleRadius*Math.sin(arcTanA));
float Ay = (float) (mLittleCirclePoint.y - mLittleCircleRadius*Math.cos(arcTanA));
// B
float Bx = (float) (mBigCirclePoint.x + mBigCircleRadius*Math.sin(arcTanA));
float By = (float) (mBigCirclePoint.y - mBigCircleRadius*Math.cos(arcTanA));
// C
float Cx = (float) (mBigCirclePoint.x - mBigCircleRadius*Math.sin(arcTanA));
float Cy = (float) (mBigCirclePoint.y + mBigCircleRadius*Math.cos(arcTanA));
// D
float Dx = (float) (mLittleCirclePoint.x - mLittleCircleRadius*Math.sin(arcTanA));
float Dy = (float) (mLittleCirclePoint.y + mLittleCircleRadius*Math.cos(arcTanA));
// 拼装 贝塞尔的曲线路径
bezeierPath.moveTo(Ax,Ay); // 移动
// 两个点
PointF controlPoint = getControlPoint();
// 画了第一条 第一个点(控制点,两个圆心的中心点),终点
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Bx,By);
// 画第二条
bezeierPath.lineTo(Cx,Cy); // 链接到
bezeierPath.quadTo(controlPoint.x,controlPoint.y,Dx,Dy);
bezeierPath.close();
return bezeierPath;
}
/**
* 获得控制点距离
*/
public PointF getControlPoint() {
return new PointF((mLittleCirclePoint.x+mBigCirclePoint.x)/2,(mLittleCirclePoint.y+mBigCirclePoint.y)/2);
}
/**
* 获得两点之间的距离
*/
private double getDistance(PointF point1, PointF point2) {
return Math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
}
/**
* 绑定View
*/
public static void attach(View view, MsgDrafitingListener.BubbleDisappearListener disappearListener) {
view.setOnTouchListener(new MsgDrafitingListener(view.getContext(),disappearListener));
}
public void initPoint(float x, float y) {
mBigCirclePoint = new PointF(x,y);
mLittleCirclePoint = new PointF(x,y);
}
public void updatePoint(float x,float y)
{
mBigCirclePoint.x = x;
mBigCirclePoint.y = y;
invalidate();
}
public void setDragBitmap(Bitmap dragBitmap) {
this.dragBitmap = dragBitmap;
}
public void setOnToucnUpListener(OnToucnUpListener listener)
{
mOnToucnUpListener = listener;
}
public interface OnToucnUpListener {
// 还原
void restore();
// 消失爆炸
void dismiss(PointF pointF);
}
/**
* 处理手指抬起后的操作
*/
public void OnTouchUp()
{
if (mLittleCircleRadius   mLittleCircleRadiusMin) {
// 回弹 ValueAnimator 值变化的动画 0 变化到 1
ValueAnimator animator = ObjectAnimator.ofFloat(1);
animator.setDuration(250);
final PointF start = new PointF(mBigCirclePoint.x, mBigCirclePoint.y);
final PointF end = new PointF(mLittleCirclePoint.x, mLittleCirclePoint.y);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float percent = (float) animation.getAnimatedValue();// 0 - 1
PointF pointF = Utils.getPointByPercent(start, end, percent);
//更新位子
updatePoint(pointF.x, pointF.y);
}
});
// 设置一个差值器 在结束的时候回弹
animator.setInterpolator(new OvershootInterpolator(3f));
animator.start();
// 还要通知 TouchListener
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if(mOnToucnUpListener != null){
mOnToucnUpListener.restore();
}
}
});
} else {
// 爆炸
if(mOnToucnUpListener != null){
mOnToucnUpListener.dismiss(mBigCirclePoint);
}
}
}
}

(2)自定义OnTouchListenner的代码

代码语言:javascript
复制
public class MsgDrafitingListener implements View.OnTouchListener {
private WindowManager mWindowManager;
private WindowManager.LayoutParams params;
private MsgDrafitingView mMsgDrafitingView;
private Context context;
// 爆炸动画
private FrameLayout mBombFrame;
private ImageView mBombImage;
private BubbleDisappearListener mDisappearListener;
public MsgDrafitingListener(Context context,BubbleDisappearListener disappearListener)
{
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
params = new WindowManager.LayoutParams();
mMsgDrafitingView = new MsgDrafitingView(context);
//背景透明
params.format = PixelFormat.TRANSPARENT;
this.context = context;
mBombFrame = new FrameLayout(context);
mBombImage = new ImageView(context);
mBombImage.setLayoutParams(new FrameLayout.LayoutParams(Utils.dip2px(30,context),
Utils.dip2px(30,context)));
mBombFrame.addView(mBombImage);
this.mDisappearListener = disappearListener;
}
@Override
public boolean onTouch(final View view, MotionEvent motionEvent) {
switch (motionEvent.getAction())
{
case MotionEvent.ACTION_DOWN:
//隐藏自己
view.setVisibility(View.INVISIBLE);
mWindowManager.addView(mMsgDrafitingView,params);
int[] location = new int[2];
view.getLocationOnScreen(location);
Bitmap bitmap = getBitmapByView(view);
//y轴需要减去状态栏的高度
mMsgDrafitingView.initPoint(location[0] + view.getWidth() / 2,
location[1]+view.getHeight()/2 -Utils.getStatusBarHeight(context));
// 给消息拖拽设置一个Bitmap
mMsgDrafitingView.setDragBitmap(bitmap);
//设置OnTouchUpListener
mMsgDrafitingView.setOnToucnUpListener(new MsgDrafitingView.OnToucnUpListener() {
@Override
public void restore() {
//还原位子
// 把消息的View移除
mWindowManager.removeView(mMsgDrafitingView);
// 把原来的View显示
view.setVisibility(View.VISIBLE);
}
@Override
public void dismiss(PointF pointF) {
//爆炸效果
// 要去执行爆炸动画 (帧动画)
//移除拖拽的view
mWindowManager.removeView(mMsgDrafitingView);
// 要在 mWindowManager 添加一个爆炸动画
mWindowManager.addView(mBombFrame,params);
mBombImage.setBackgroundResource(R.drawable.anim_bubble_pop);
AnimationDrawable drawable = (AnimationDrawable) mBombImage.getBackground();
mBombImage.setX(pointF.x-drawable.getIntrinsicWidth()/2);
mBombImage.setY(pointF.y-drawable.getIntrinsicHeight()/2);
drawable.start();
// 等它执行完之后我要移除掉这个 爆炸动画也就是 mBombFrame
mBombImage.postDelayed(new Runnable() {
@Override
public void run() {
mWindowManager.removeView(mBombFrame);
// 通知一下外面该消失
if(mDisappearListener != null){
mDisappearListener.dismiss(view);
}
}
},getAnimationDrawableTime(drawable));
}
});
break;
case MotionEvent.ACTION_MOVE:
mMsgDrafitingView.updatePoint(motionEvent.getRawX(),
motionEvent.getRawY() - Utils.getStatusBarHeight(context));
break;
case MotionEvent.ACTION_UP:
mMsgDrafitingView.OnTouchUp();
break;
}
return true;
}
private Bitmap getBitmapByView(View view) {
view.buildDrawingCache();
Bitmap bitmap = view.getDrawingCache();
return bitmap;
}
public interface BubbleDisappearListener {
void dismiss(View view);
}
/**
* 获取爆炸动画画的时间
* @param drawable
* @return
*/
private long getAnimationDrawableTime(AnimationDrawable drawable) {
int numberOfFrames = drawable.getNumberOfFrames();
long time = 0;
for (int i=0;i<numberOfFrames;i++){
time += drawable.getDuration(i);
}
return time;
}
}

(3)View的调用代码

代码语言:javascript
复制
public class MsgDrafitingViewActivity extends AppCompatActivity{
private Button mButton;
private TextView mText;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.qq_msg_drafitingview_activity);
mButton = findViewById(R.id.mBtn);
mText = findViewById(R.id.mText);
MsgDrafitingView.attach(mButton, new MsgDrafitingListener.BubbleDisappearListener() {
@Override
public void dismiss(View view) {
}
});
MsgDrafitingView.attach(mText, new MsgDrafitingListener.BubbleDisappearListener() {
@Override
public void dismiss(View view) {
}
});
}
}

源码地址:源码Github地址

以上就是本文的全部内容,希望对大家的学习有所帮助。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-09-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档