QQ红点拖拽效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mingyunxiaohai/article/details/88916825

今天来做一下QQ列表上的红点拖拽效果

思路:首先我们得给小圆点定义一些状态,默认状态,手指点上去的状态,手指一动时的状态,手指松开时的状态。在onTouchEvent方法中更新状态值,最后在onDraw中根据不同的状态值绘制圆和path。思路很简单,就是绘制的时候我们需要把中学时候学的几何数学拿来用一下啦。

先定义一些成员变量,把状态啊,画笔啊,半径啊,原点等都初始化好然后在开始,具体可以到最下面点击源码查看

先看onDraw方法

首先,只要不是爆炸状态,我们都需要绘制移动的圆点和上面的数字。

       //只要不是爆炸的情况都要绘制圆和字
        if (mState != BUBBLE_STATE_BLAST) {
            canvas.drawCircle(mMovePoint.x, mMovePoint.y, mMoveRadius, mBubblePaint);
            mTextPaint.getTextBounds(mText, 0, mText.length(), mTextRect);
            canvas.drawText(mText, mMovePoint.x - mTextRect.width() / 2, mMovePoint.y + mTextRect.height() / 2, mTextPaint);
        }

然后就是当我们手指点到圆上开始拖拽的的状态,这时候我们需要绘制一个静止的圆和一个移动的圆,当两个圆小于一定的距离的时候,我们需要在他们之间绘制一个黏性的效果,其实就是绘制两条二街贝塞尔曲线。

绘制贝塞尔曲线的时候,需要求曲线的起始点,结束点和控制点的坐标,这时候会用到中学几何数学的小知识

计算角度 在直角三角形中,非直角的sin值等于对边长比斜边长.使用勾股定理计算即可。
sinA=对边/斜边 cosB=邻边/斜边 tanA=对边/邻边

看着下面的图绘制会更清晰
在这里插入图片描述

   //链接状态绘制静止的圆和赛贝尔曲线
        if (mState == BUBBLE_STATE_CLICK) {
            //绘制静止的圆
            canvas.drawCircle(mQuitPoint.x, mQuitPoint.y, mQuitRadius, mBubblePaint);
            //绘制贝塞尔曲线
            //找到控制点
            float controlX = (mMovePoint.x + mQuitPoint.x) / 2;
            float controlY = (mMovePoint.y + mQuitPoint.y) / 2;
            //计算角度 在直角三角形中,非直角的sin值等于对边长比斜边长.使用勾股定理计算即可
            //sinA=对边/斜边  cosB=邻边/斜边   tanA=对边/邻边
            float sinThet = (mMovePoint.y - mQuitPoint.y) / mDist;
            float cosThet = (mMovePoint.x - mQuitPoint.x) / mDist;

            //A点
            float ax = mQuitPoint.x - mQuitRadius * sinThet;
            float ay = mQuitPoint.y + mQuitRadius * cosThet;
            //B点
            float bx = mMovePoint.x - mMoveRadius * sinThet;
            float by = mMovePoint.y + mMoveRadius * cosThet;
            //C点
            float cx = mMovePoint.x + mMoveRadius * sinThet;
            float cy = mMovePoint.y - mMoveRadius * cosThet;
            //D点
            float dx = mQuitPoint.x + mQuitRadius * sinThet;
            float dy = mQuitPoint.y - mQuitRadius * cosThet;

            //设置path的路径
            mBezierPath.reset();
            mBezierPath.moveTo(ax, ay);
            mBezierPath.quadTo(controlX, controlY, bx, by);

            mBezierPath.lineTo(cx, cy);
            mBezierPath.quadTo(controlX, controlY, dx, dy);
            mBezierPath.close();
            canvas.drawPath(mBezierPath, mBubblePaint);
        }

找到各个点之后就简单了,使用path的api将各个点连接起来,最后绘制就ok。

最后是爆炸状态,我们在初始化的时候定义一个有5张爆炸小图片的bitmap数组,使用属性动画控制数组的下标,然后循环绘制这几张图片。

  //爆炸状态绘制爆炸图片
        if (mState == BUBBLE_STATE_BLAST && mBlastIndex < mBlastDrawablesArray.length) {
            mBlastRect.left = mMovePoint.x - mMoveRadius;
            mBlastRect.top = mMovePoint.y - mMoveRadius;
            mBlastRect.right = mMovePoint.x + mMoveRadius;
            mBlastRect.bottom = mMovePoint.y + mMoveRadius;
            canvas.drawBitmap(mBlastBitmapsArray[mBlastIndex], null, mBlastRect, mBlastPaint);
        }

OK 下面是onTouchEvent中,我们需要根据手指的各种事件来切换当前的状态

public boolean onTouchEvent(MotionEvent event) {
        float x = event.getRawX();
        float y = event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //勾股定理算出点击位置和静止圆的圆心距离
                mDist = (float) Math.hypot(x - mQuitPoint.x, y - mQuitPoint.y);
                if (mState == BUBBLE_STATE_DEFAULT) {
                    //如果手指点击到了圆上或者圆的附近
                    if (mDist < mMoveRadius + MOVE_OFFSET) {
                        mState = BUBBLE_STATE_CLICK;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (mState != BUBBLE_STATE_DEFAULT) {
                    //勾股定理算出点击位置和静止圆的圆心距离,也就是手指一动的距离
                    mDist = (float) Math.hypot(x - mQuitPoint.x, y - mQuitPoint.y);
                    mMovePoint.x = event.getRawX();
                    mMovePoint.y = event.getRawY();
                    //如果手指点击到了圆上或者圆的附近
                    if (mState == BUBBLE_STATE_CLICK) {
                        //手指一动的距离小于我们定义的一个最大的距离,就绘制贝塞尔曲线,反之就是分离状态
                        if (mDist < mMaxDist - MOVE_OFFSET) {
                            mQuitRadius = (mMoveRadius - mDist / 8);
                        } else {
                            mState = BUBBLE_STATE_BREAK;
                        }
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                //如果还没断开直接返回原状
                if (mState == BUBBLE_STATE_CLICK) {
                    //执行回弹动画
                    startBackAnim();
                }
                //断开了
                else if (mState == BUBBLE_STATE_BREAK) {
                    //如果断开了,小球的位置移动到距离2倍移动小球的距离以内也返回原状
                    if (mDist < mMoveRadius * 2) {
                        //执行回弹动画
                        startBackAnim();
                    } else {
                        mState = BUBBLE_STATE_BLAST;
                        //执行爆炸动画
                        startBlastAnim();
                    }
                }
                break;
            default:
        }
        return true;
    }

DOWN事件,如果我们的手指点击到圆上或者圆的附近(附近使用一个偏移量MOVE_OFFSET来定义),就把状态改成点击连接的状态

MOVE事件,判断手指移动动的距离小于我们定义的一个最大的距离,就绘制贝塞尔曲线和静止的圆,反之就定义为分离状态。

UP事件,如果静止圆和移动圆还没断开直接返回原状,执行回弹动画,如果已经断开了,在判断如果移动的圆这时候移动到了原来的点的一定范围内,就还需要回到原点,执行回弹动画,反之就执行爆炸动画。

//爆炸动画
private void startBlastAnim() {
        ValueAnimator animator = ValueAnimator.ofInt(0, 5);
        animator.setDuration(500);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBlastIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if(mOnExecuteFinishListener!=null){
                    mOnExecuteFinishListener.onFinish(EXECUTE_STATE_BLAST);
                }
            }
        });
        animator.start();
    }

    //回弹动画
    private void startBackAnim() {
        PointF start = new PointF(mMovePoint.x, mMovePoint.y);
        PointF end = new PointF(mQuitPoint.x, mQuitPoint.y);
        //系统的PointFEvaluator只能支持21以上的,编译不通过。所以自己弄了一个把它代码抄过来就行啦
        ValueAnimator animator = ValueAnimator.ofObject(new MyPointFEvaluator(), start, end);
        animator.setDuration(200);
        animator.setInterpolator(new OvershootInterpolator(5f));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mMovePoint = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                mState = BUBBLE_STATE_DEFAULT;
                if(mOnExecuteFinishListener!=null){
                    mOnExecuteFinishListener.onFinish(EXECUTE_STATE_BACK);
                }
            }
        });
        animator.start();
    }

最后就是两个简单的属性动画,爆炸动画用来控制我们的bitmap数组的index值。回弹动画来控制两个点的移动,使用系统默认的插值器OvershootInterpolator(运动到终点后,冲过终点后再回弹)。

这里的PointFEvaluator这个估值器系统有提供,但是只支持5.0以上,所以自定了一个PointFEvaluator,把系统的源码抄一下即可。

OK到这里效果就出来了可以看下图
在这里插入图片描述
效果出来了,那怎么用呢,现在是在我们自定义的view中可以全屏拖动绘制,但是如果把这个veiw放到列表中怎么办呢,只能在列表的那一条区域中拖拽吗,当然不符合我们的预期

思路就是,当我们点击列表中的红点的时候,通过当前的Window对象拿到我们的跟布局DecorView,然后把我们自定义的view放到跟布局中,把当前的textview设置隐藏,然后在我们的自定义view的手指点击的地方开始绘制圆就可以了

这里需要注意onTouchEvent获取坐标使用event.getRawX()和event.getRawY(),不能使用event.getX()和event.getY()了,因为我们现在的自定义veiw和点击的textveiw不在一个布局中。getRawX()是相对于屏幕来说的而getX()是相对于父布局来说的。

所以我们在textvew的onTouchListener中,找到DecorView,然后把我们的自定义view放进去,最后把事件传递到我们的自定义view中就好了。

public class QQViewListenter implements View.OnTouchListener , QQBubbleView.OnExecuteFinishListener {

    private Context mContext;

    private ViewGroup mViewGroup;
    private QQBubbleView mQQBubbleView;
    private View currentClickView;
    public QQViewListenter(Context context) {
        mContext = context;
        Window window = ((Activity) context).getWindow();
        mViewGroup = (ViewGroup) window.getDecorView();
        mQQBubbleView = new QQBubbleView(context);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        currentClickView = v;
        Log.e("onTouch","x--"+event.getRawX()+"y--"+event.getRawY()+"--event"+event.getAction());
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            if(mViewGroup!=null){
                mViewGroup.addView(mQQBubbleView);
            }
            ViewParent parent = v.getParent();
            if (parent == null) {
                return false;
            }
            if(v instanceof TextView){
                String text = ((TextView) v).getText().toString();
                mQQBubbleView.setText(text);
            }
            //防止父容器消费事件
            parent.requestDisallowInterceptTouchEvent(true);
            int width = v.getWidth();
            mQQBubbleView.setCenter(event.getRawX(),event.getRawY(),width/2);
            mQQBubbleView.setOnDismissListener(this);
            currentClickView.setVisibility(View.INVISIBLE);
        }
        //事件传递
        mQQBubbleView.onTouchEvent(event);
        return true;
    }

    @Override
    public void onFinish(int type) {
        if(mViewGroup!=null&&mQQBubbleView!=null){
            mViewGroup.removeView(mQQBubbleView);
        }
        if(type == EXECUTE_STATE_BACK){
            currentClickView.setVisibility(View.VISIBLE);
        }else {
            currentClickView.setVisibility(View.GONE);
        }
    }
}

OnExecuteFinishListener是我们自顶一个view中定义的一个接口,用来监听手指抬起之后的的状态结果,因为完事后我们需要移除我们添加到DecorView中的我们自己的veiw。

使用的时候,我们在Adapter中的textveiw设置setOnTouchListener监听传入我们上面写的QQViewListenter即可。

最终效果:
在这里插入图片描述
源码位置

猜你喜欢

转载自blog.csdn.net/mingyunxiaohai/article/details/88916825
今日推荐