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

写在前头

写消息拖拽效果的文章不少,但是大部分都把自定义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代码

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的代码

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的代码

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的调用代码

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地址

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

(0)

相关推荐

  • Android中贝塞尔曲线的绘制方法示例代码

    贝塞尔曲线,很多人可能不太了解,什么叫做贝塞尔曲线呢?这里先做一下简单介绍:贝塞尔曲线也可以叫做贝济埃曲线或者贝兹曲线,它由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋.一般的矢量图形软件常利用贝塞尔曲线来精确画出曲线. 上面的介绍中,"线段像可伸缩的皮筋"这句话非常关键,但也特别好理解.至于贝塞尔曲线的详细内容大家可以查阅相关资料.        Android提供的贝塞尔曲线绘制接口 在Android开发中,要实现贝塞尔曲线其实还是很简单的,因为Android已经给我们提

  • Android贝塞尔曲线初步学习第三课 Android实现添加至购物车的运动轨迹

    不知上一节高仿QQ未读消息气泡大家还喜欢么,今天继续练习贝赛尔曲线,这一节我们通过贝赛尔曲线和属性动画估值器实现添加至购物车的运动轨迹,效果如下: 1.新建自定义View,重写构造方法,初始化Paint.Path: 2.确定起始点.终止点.控制点坐标,这里我们直接固定: @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh);

  • Android贝塞尔曲线初步学习第一课

    贝塞尔曲线有一阶.二阶.三阶.N阶 一阶就是一条直线,有起点终点,没有控制点,对应方法就是 canvas.drawLine(float startX, float startY, float stopX, float stopY, @NonNull Paint paint) ; 二阶为曲线,有起点终点,一个控制点,对应方法就是 path.quadTo(float x1, float y1, float x2, float y2); 其中x1.y1为控制点坐标, x2.y2为终点坐标,效果如下:

  • Android使用贝塞尔曲线仿QQ聊天消息气泡拖拽效果

    本文实例为大家分享了Android仿QQ聊天消息气泡拖拽效果展示的具体代码,供大家参考,具体内容如下 先画圆,都会吧.代码如下: public class Bezier extends View { private final Paint mGesturePaint = new Paint(); private final Path mPath = new Path(); private float mX1 = 100, mY1 = 150; private float mX2 = 300, m

  • Android贝塞尔曲线实现填充不规则图形并随手指运动

    贝塞尔曲线: 贝塞尔曲线于1962,由法国工程师皮埃尔·贝塞尔所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计.贝塞尔曲线最初由 Paul de Casteljau 于 1959 年运用 de Casteljau 演算法开发,以稳定数值的方法求出贝兹曲线.贝塞尔曲线主要用于二维图形应用程序中的数学曲线,曲线由起始点,终止点(也称锚点)和控制点组成,通过调整控制点,贝塞尔曲线的形状会发生变化. 在此举一个例子,实现贝塞尔曲线,基于以下场景: 上面的图片,我们可以见到一个白色的区域,边缘为弧形,这

  • android中贝塞尔曲线的应用示例

    前言: 贝塞尔曲线又称贝兹曲线,它的主要意义在于无论是直线或曲线都能在数学上予以描述.最初由保罗·德卡斯特里奥(Paul de Casteljau)于1959年运用德卡斯特里奥演算法开发(de Casteljau Algorithm),在1962,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表.目前广泛应用于图形绘制领域来模拟光滑曲线,为计算机矢量图形学奠定了基础.在一些图形处理软件中都能见到贝塞尔曲线,比如CorelDraw中翻译成"贝赛尔工具":而在Firewo

  • Android把商品添加到购物车的动画效果(贝塞尔曲线)

    当我们写商城类的项目的时候,一般都会有加入购物车的功能,加入购物车的时候会有一些抛物线动画,具体代码如下: 实现效果如图: 思路: 确定动画的起终点 在起终点之间使用二次贝塞尔曲线填充起终点之间的点的轨迹 设置属性动画,ValueAnimator插值器,获取中间点的坐标 将执行动画的控件的x.y坐标设为上面得到的中间点坐标 开启属性动画 当动画结束时的操作 难点: PathMeasure的使用 - getLength() - boolean getPosTan(float distance, f

  • android贝塞尔曲线实现波浪效果

    本文实例为大家分享了android贝塞尔曲线实现波浪效果的具体代码,供大家参考,具体内容如下 因为手机录制gif不知道下什么软件好,所以暂时就先忽略效果图了 我在屏幕外多画了1.5个波浪,延伸至屏幕内.然后不断的循环,向右边移动.就有一种波浪的效果. 所以现在只需要画出左边的波长,然后再通过循环添加所有的波长即可. 第一个曲线已经确定了控制点和终点的坐标, 第二条曲线也可以很明显的看出来终点是在x轴的0点坐标,Y轴不变,而控制点是在负的波长的1/4的位置 有了上下曲线以后,其他的就可以直接通过循

  • Android贝塞尔曲线实现手指轨迹

    本文实例为大家分享了Android贝塞尔曲线实现手指轨迹的具体代码,供大家参考,具体内容如下 1.使用贝塞尔曲线前 MyView.java public class MyView extends View { // 实例一个路径对象 private Path mPath = new Path(); public MyView(Context context) { super(context); // TODO Auto-generated constructor stub } public My

  • Android 利用三阶贝塞尔曲线绘制运动轨迹的示例

    本篇文章主要介绍了Android 利用三阶贝塞尔曲线绘制运动轨迹的示例,分享给大家,具体如下: 实现点赞效果,自定义起始点以及运动轨迹 效果图: xml布局: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/rl_root&

随机推荐