欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

Android 贝塞尔曲线之一行代码实现任意View控件拖拽爆炸消失效果,android贝塞尔,这个效果在QQ中做的很漂

来源: 开发者 投稿于  被查看 18300 次 评论:154

Android 贝塞尔曲线之一行代码实现任意View控件拖拽爆炸消失效果,android贝塞尔,这个效果在QQ中做的很漂


1 简介

在做项目的过程中,有很多都会涉及「消息」这一块的内容,未读消息,会有一个圆形气泡提示未读消息数量,已读就不再显示。这个效果在QQ中做的很漂亮,今天我们在此效果上继续实现「任意控件都可以拖拽消失」。「文末有福利」

图1. 任意控件View拖拽爆炸效果.gif


话不多说直接进入主题,图1就是我们这篇文章要实现的效果,任意View控件都可以实现拖拽爆炸效果。封装后的好处是:拿过来可以直接用,完全实现了效果与特定View分离,一行代码搞定。上代码:

// 通过我们的贝塞尔 View, 绑定任意想要拖拽的控件 view
      MessageBubbleView.bindMessageView(findViewById(R.id.text_view), new OnMessageBubbleTouchListener.OnViewDragDisappearListener() {
          @Override
          public void onDisappear(View originalView) {
              // 该 originalView 就是拖拽消失掉的 View
              Toast.makeText(MainActivity.this, "TextView 控件消失了", Toast.LENGTH_SHORT).show();
          }
      });

不要着急,在实现 1 的效果之前,我们先要实现下 2 简单的消息拖拽效果。

图2. 在任意位置实现消息拖拽效果.gif


2 简单的消息拖拽实现

我们先来分析 2 :在任意位置按下并拖动,会出现两个一大一小实心圆,中间被一类似粘稠物连接,我们索性就称作一个固定圆和一个拖拽圆;

在拖拽圆拖拽的过程中,拖拽圆的大小是不变的,但是位置跟随手指移动;固定圆的圆心是不变的,但是半径是可变的,刚开始拖拽时,固定圆的圆心是最大的,两圆的距离越远,固定圆的半径越小,反之逐渐变大。

有了思路,我们就写代码,按下时先绘制两个圆,并实现拖拽变化

/*
   实现思路:
   1.手指按下的时候,绘制出两个圆(固定圆和拖拽圆)
       固定圆的圆心位置固定,但是半径可发生变化
       拖拽圆的圆心可变,半径固定
   2.手指拖动的时候,不断更新拖拽圆的位置(不断的绘制),
       同时改变固定圆的圆心大小(两个圆越近,固定圆半径越大;两圆越远,固定圆的半径越小;
       两圆距离超过一定值时,固定圆消失不见
*/
public class MessageBubbleView extends View {
   // 两个实心圆--根据点的坐标来绘制圆
   private PointF mDragPoint, mFixationPoint;
   private Paint mPaint;
   private int mDragRadius = 9; // 拖拽圆半径

   // 固定圆最大半径(初始半径)/半径的最小值
   private int mFixationRadiusMax = 7;
   private int mFixationRadiusMin = 3;
   private int mFixationRadius;

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

   public MessageBubbleView(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs, 0);
   }

   public MessageBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       mPaint = new Paint();
       mPaint.setColor(Color.RED);
       mPaint.setAntiAlias(true);
       mPaint.setDither(true);

       mDragRadius = dip2px(mDragRadius);
       mFixationRadiusMax = dip2px(mFixationRadiusMax);
       mFixationRadiusMin = dip2px(mFixationRadiusMin);
   }

   @Override
   public boolean onTouchEvent(MotionEvent event) {
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN:
               // 手指按下的时候,要在当前的位置初始化绘制两个圆
               float downX = event.getX();
               float downY = event.getY();
               initPoint(downX, downY);
               break;
           case MotionEvent.ACTION_MOVE:
               // 在移动的时候,不断的更新位置
               float moveX = event.getX();
               float moveY = event.getY();
               updateDragPointLocation(moveX, moveY);
               break;
       }
       invalidate(); 
       return true;
   }

   private void updateDragPointLocation(float moveX, float moveY) {
       mDragPoint.x = moveX;
       mDragPoint.y = moveY;
   }

   @Override
   protected void onDraw(Canvas canvas) {
       if (mFixationPoint == null || mDragPoint == null) {
           return;
       }

       // 画两个圆: 固定圆有一个初始化大小,而且随着两圆距离的增大而变小,小到一定程度就不见了(不画了)

       // 拖拽圆 半径不变,位置跟随手指移动
       canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);

       double distance = getPointsDistance(mDragPoint, mFixationPoint);

       // 随着拖拽的距离变化,逐渐改变固定圆的半径
       mFixationRadius = (int) (mFixationRadiusMax - distance / 16); // 这个除的值来控制固定圆消失时的距离
       if (mFixationRadius > mFixationRadiusMin) {
           canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
       }
   }

   /**
    * 获取两个点之间的距离(勾股定理)
    */
   private double getPointsDistance(PointF point1, PointF point2) {
       return Math.sqrt((point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y));
   }

   /**
    * 初始化点
    */
   private void initPoint(float downX, float downY) {
       mFixationPoint = new PointF(downX, downY);
       mDragPoint = new PointF(downX, downY);
   }

   private void updateDragPointLocation(float moveX, float moveY) {
       mDragPoint.x = moveX;
       mDragPoint.y = moveY;
   }

   private int dip2px(int dip) {
       return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
   }
}

代码写到这里运行一下,效果如下:

result1.gif


初步效果已经出来了,接下来我们去实现拖动时的贝塞尔曲线效果。

关于贝塞尔曲线的概念知识,这里不是重点,就不再说明,如果不了解请先自行百度,下面上拖拽时的手绘图:

贝塞尔曲线分析.png


这张图就是一些简单的数据计算,不难理解,就是先假设所有条件都已知,计算出我们关心的P0P1P2 P3 点的坐标,然后再想办法求出∠a的值即可;图中红线和蓝线围起来的区域就是我们要实现的粘性区域。下面用代码来实现:

/**
     * 获取贝塞尔曲线路径
     */
    private Path getBesaierPath() {
        double distance = getPointsDistance(mDragPoint, mFixationPoint);

        // 随着拖拽的距离变化,不断改变固定圆的半径
        mFixationRadius = (int) (mFixationRadiusMax - distance / 16);
        if (mFixationRadius < mFixationRadiusMin) {
            // 超过一定距离  贝塞尔曲线和固定圆都不要绘制了
            return null;
        }

        Path besaierPath = new Path();

        // 求角a
        double angleA = Math.atan((mDragPoint.y - mFixationPoint.y) / (mDragPoint.x - mFixationPoint.x));

        float P0x = (float) (mFixationPoint.x + mFixationRadius * Math.sin(angleA));
        float P0y = (float) (mFixationPoint.y - mFixationRadius * Math.cos(angleA));

        float P3x = (float) (mFixationPoint.x - mFixationRadius * Math.sin(angleA));
        float P3y = (float) (mFixationPoint.y + mFixationRadius * Math.cos(angleA));

        float P1x = (float) (mDragPoint.x + mDragRadius * Math.sin(angleA));
        float P1y = (float) (mDragPoint.y - mDragRadius * Math.cos(angleA));

        float P2x = (float) (mDragPoint.x - mDragRadius * Math.sin(angleA));
        float P2y = (float) (mDragPoint.y + mDragRadius * Math.cos(angleA));


        // 拼接 贝塞尔曲线路径
        // 移动到我们的起始点,否则默认从(0,0)开始
        besaierPath.moveTo(P0x, P0y);
        // 求控制点坐标,我们取两圆圆心为控制点(如果取黄金比例0.618是比较好的)
        PointF controlPoint = getControlPoint();
        // 画第一条 前两个参数为控制点坐标  后两个参数为终点坐标
        besaierPath.quadTo(controlPoint.x, controlPoint.y, P1x, P1y);
        besaierPath.lineTo(P2x, P2y);
        besaierPath.quadTo(controlPoint.x, controlPoint.y, P3x, P3y);
        besaierPath.close();

        return besaierPath;
    }

下面就很简单了,在onDraw() 中,在绘制固定圆的同时绘制曲线。代码如下:

// 绘制贝塞尔曲线 如果两圆拖拽到一定距离,固定圆消失的同时不再绘制贝塞尔曲线
        Path besaierPath = getBesaierPath();

        if (besaierPath != null) {
            // 固定圆半径可变 当拖拽在一定距离时才去绘制,超过一定距离就不在绘制
            canvas.drawCircle(mFixationPoint.x, mFixationPoint.y, mFixationRadius, mPaint);
            canvas.drawPath(besaierPath, mPaint);
        }

到这里我们已经实现了「简单的消息拖拽」在任意的位置都可以实现消息拖拽效果了。

3 任意 View 控件拖拽爆炸消失

「重点来了」就 1 效果,先整理下思路:

  1. 如何做到任意View都可以拖动 

  2. 当拖动不超过一定距离时,该View会回弹到原来的位置,还可以继续下一次的拖动

  3. 当拖动超过一定距离时,会有一个爆炸消失的效果

  4. 如何通知用户,你的控件消失了(监听回调)


下面我们一一来解决上面的问题

  1. 我们给 MessageBubbleView  开发一个方法,用于绑定待拖拽View并处理触摸事件

  2. 重写触摸监听

      用户按下时,把原来的控件隐藏,我们通过 WindowManager 添加一个 View,该 View 是被隐藏掉的View的「快照」;

      我们拖动的是这个「快照」,当拖拽超过一定距离时,从 WindowManager 上移除该快照,并实现一个爆炸动画;

      如果用户拖动没有超过该距离值,松手时该快照做回弹动画,动画执行完毕,让真实的View再次显示出来,就可以再次执行拖拽了;


好了,有什么样的想法,就有什么样的行动,我们用代码写出来:

自定义 View 的触摸监听 OnMessageBubbleTouchListener.java 中
 @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 搞一个原始View的快照,并添加WindowManger中
                mWindowManager.addView(mMessageBubbleView, mParams);

                // 初始化贝塞尔View的中心点 也是原始View的中心点
                int[] location = new int[2];
                mOriginalView.getLocationOnScreen(location); //默认获取的是View左上角在屏幕上的坐标(y坐标包含状态栏的高度)
                mMessageBubbleView.initPoint(location[0] + mOriginalView.getWidth() / 2, location[1] + mOriginalView.getHeight() / 2 - getStatusBarHeight(mContext));
                // 这里需要减去状态栏的高度,然后在window上的位置才对 

                // 为什么不设置左上角呢? 拖拽时贝塞尔曲线会连到左上角 不美观

                Bitmap copyBitmap = getCopyBitmapFromView(mOriginalView);
                // 给拖拽的消息View设置一张原始View的快照
                mMessageBubbleView.setDragBitmap(copyBitmap);

                // 已经绘制过后再把原来的隐藏掉
                //mOriginalView.setVisibility(View.INVISIBLE); 

                break;
            case MotionEvent.ACTION_MOVE:
                // 解决一点击View出现闪动的bug
                if (mOriginalView.getVisibility() == View.VISIBLE) {
                    mOriginalView.setVisibility(View.INVISIBLE);
                }
                mMessageBubbleView.updateDragPointLocation(event.getRawX(), event.getRawY() - BubbleUtils.getStatusBarHeight(mContext)); // 同样要减去状态栏高度
                break;
            case MotionEvent.ACTION_UP:
                mMessageBubbleView.handleActionUP();
                break;
        }
        return true;
    }

在触摸监听中同时再定义一个View消失的监听回调,该控件一旦爆炸消失,就会调用该方法。

/**
     * 真正的处理View的消失的监听
     */
    public interface OnViewDragDisappearListener {
        /**
         * 原始View消失的监听
         *
         * @param originalView 原始的View
         */
        void onDisappear(View originalView);
    }

  ……省略代码……

    /**
     * 拖拽的View消失时的监听方法
     *
     * @param pointF
     */
    @Override
    public void onViewDragDisappear(PointF pointF) {
        // 移除消息气泡贝塞尔View,同时添加一个爆炸的View动画
        mWindowManager.removeView(mMessageBubbleView);
        mWindowManager.addView(mBombLayout, mParams);
        mBombView.setBackgroundResource(R.drawable.anim_bubble_bomb);

        AnimationDrawable bombDrawable = (AnimationDrawable) mBombView.getBackground();
        // 矫正爆炸时,位置偏下的问题
        mBombView.setX(pointF.x - bombDrawable.getIntrinsicWidth() / 2);
        mBombView.setY(pointF.y - bombDrawable.getIntrinsicHeight() / 2);
        bombDrawable.start();

        mBombView.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 动画执行完毕,把爆炸布局及时从WindowManager移除
                mWindowManager.removeView(mBombLayout);

                if (mDisappearListener != null) {
                    mDisappearListener.onDisappear(mOriginalView);
                }

            }
        }, getAnimationTotalTime(bombDrawable));
    }

    /**
     * 松手后,拖拽View消失,原来的View重新显示的监听方法
     */
    @Override
    public void onViewDragRestore() {
        mWindowManager.removeView(mMessageBubbleView);
        mOriginalView.setVisibility(View.VISIBLE);
    }


到这里基本也就实现的差不多了,细节代码就不再贴了,可以动手写一写。

点我 我是源码 来获取源码,验证码为「fw6z

这是我在简书写的第一篇文章,如果有错误请指出,同时如果文章对您有帮助,请点个「心」给予支持。

我是标签:   怎么理解贝塞尔曲线? 贝塞尔曲线

感谢 大牛Darren 从他的博客里学到了很多很实用知识,推荐大家学习。



作者:firstxia168

用户评论