42

QQ红点拖拽效果

 5 years ago
source link: https://blog.csdn.net/mingyunxiaohai/article/details/88916825?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

版权声明:本文为博主原创文章,未经博主允许不得转载。 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=对边/邻边

看着下面的图绘制会更清晰

uqYBN3j.png!web
//链接状态绘制静止的圆和赛贝尔曲线
        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到这里效果就出来了可以看下图

iM3iuuj.gif

效果出来了,那怎么用呢,现在是在我们自定义的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即可。

最终效果:

2eUVRvi.gif源码位置

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK