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

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值