首页 > 系统 > Android > 正文

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

2019-10-21 21:26:31
字体:
来源:转载
供稿:网友

写在前头

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

源码地址:源码Github地址

效果图

Android,贝塞尔曲线,消息拖拽

 分析(用到的知识点): 

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

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

(3)贝塞尔一阶曲线

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

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

具体实现方法

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

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

基础效果图

Android,贝塞尔曲线,消息拖拽

 分析:

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

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

贝塞尔曲线的画法

Android,贝塞尔曲线,消息拖拽

首先我们需要求出角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地址

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


注:相关教程知识阅读请移步到Android开发频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表