自定义View-31 仿QQ消息未读拖拽回弹和爆炸效果

1.要模仿的效果

hnwuq-656lw.gif

2 实现思路

2.1 怎么才能够把一个View拖动到状态栏上面

我们要把这个View放在 WindowManager 上面拖动,原来的View还是在原来位置只是隐藏,
拖动的时候其实是新建了一个View,复制一张图片在WindowManager上面拖动

2.2 回弹就是不断改变拖拽圆的位置

2.3 爆炸效果是一个帧动画

3 代码实现

public class BubbleMessageTouchListener implements View.OnTouchListener, MessageBubbleView.MessageBubbleListener {
    private final Context mContext;
    private final WindowManager mWindowManager;
    private final WindowManager.LayoutParams mParams;
    private View mView;
    private final MessageBubbleView mMessageBubbleView;
    private BubbleDisappearListener mBubbleTouchListener;
    private FrameLayout mFrameLayout;
    private ImageView mBombImage;

    BubbleMessageTouchListener(View view, Context context, BubbleDisappearListener bubbleTouchListener) {
        mView = view;
        this.mContext = context;
        mMessageBubbleView = new MessageBubbleView(view.getContext());
        mBubbleTouchListener = bubbleTouchListener;
        mMessageBubbleView.setMessageBubbleListener(this);
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        mParams = new WindowManager.LayoutParams();
        // 背景要透明
        mParams.format = PixelFormat.TRANSPARENT;

        mFrameLayout = new FrameLayout(context);
        mBombImage = new ImageView(context);
        mFrameLayout.addView(mBombImage);

        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mBombImage.getLayoutParams();
        layoutParams.width = FrameLayout.LayoutParams.WRAP_CONTENT;
        layoutParams.height = FrameLayout.LayoutParams.WRAP_CONTENT;
        mBombImage.setLayoutParams(layoutParams);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 要在WindowManager上面搞一个View ,上一节写好的贝塞尔的View
                mWindowManager.addView(mMessageBubbleView, mParams);
                //获取当前View的bitmap
                int[] location = new int[2];
                mView.getLocationOnScreen(location);
                Bitmap bitmap = getBitmapByView(mView);
                //初始化固定圆
                mMessageBubbleView.setDragBitmap(bitmap);
                mMessageBubbleView.initFixedPoint(location[0] + mView.getWidth() / 2,
                        location[1] + mView.getWidth() / 2 - BubbleUtils.getStatusBarHeight(mContext));
                //按下,隐藏当前的view.
                mView.setVisibility(View.INVISIBLE);
                break;
            case MotionEvent.ACTION_MOVE:
                mMessageBubbleView.updateDragPoint(event.getRawX(), event.getRawY() - BubbleUtils.getStatusBarHeight(mContext));
                break;
            case MotionEvent.ACTION_UP:
                mMessageBubbleView.handleActionUp();
                break;

            default:
                break;
        }
        return true;
    }

    /**
     * 从一个View中获取Bitmap
     *
     * @param view
     * @return
     */
    private Bitmap getBitmapByView(View view) {
        view.buildDrawingCache();
        Bitmap bitmap = view.getDrawingCache();
        return bitmap;
    }

    @Override
    public void restore() {
        mWindowManager.removeView(mMessageBubbleView);
        mView.setVisibility(View.VISIBLE);
    }

    @Override
    public void dismiss(PointF pointF) {
        mWindowManager.removeView(mMessageBubbleView);
        // 要在 mWindowManager 添加一个爆炸动画
        mWindowManager.addView(mFrameLayout,mParams);

        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(mFrameLayout);
                // 通知一下外面该消失
                if(mBubbleTouchListener != null){
                    mBubbleTouchListener.dismiss();
                }
            }
        },getAnimationDrawableTime(drawable));
    }

    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;
    }

    public interface BubbleDisappearListener {
        /**
         * 消失
         */
        void dismiss();
    }
}

    public interface BubbleDisappearListener {
        /**
         * 消失
         */
        void dismiss();
    }
}
public class BubbleUtils {

    /**
     * dip 转换成 px
     *
     * @param dip
     * @param context
     * @return
     */
    public static int dip2px( float dip, Context context) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
    }

    /**
     * 获取状态栏高度
     *
     * @return
     */
    public static int getStatusBarHeight(Context context) {
        //获取status_bar_height资源的ID
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根据资源ID获取响应的尺寸值
            return context.getResources().getDimensionPixelSize(resourceId);
        }
        return dip2px(25, context);
    }

    /**
     * As meaning of method name. 获得两点之间的距离 (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2) 开平方
     * Math.sqrt:开平方 Math.pow(p0.y - p1.y, 2):求一个数的平方
     *
     * @param p0
     * @param p1
     * @return
     */
    public static float getDistanceBetween2Points(PointF p0, PointF p1) {
        float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2)
                + Math.pow(p0.x - p1.x, 2));
        return distance;
    }

    /**
     * Get point between p1 and p2 by percent. 根据百分比获取两点之间的某个点坐标
     *
     * @param p1
     * @param p2
     * @param percent
     * @return
     */
    public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
        return new PointF(evaluateValue(percent, p1.x, p2.x), evaluateValue(
                percent, p1.y, p2.y));
    }

    /**
     * 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
     *
     * @param fraction
     *            = 1
     * @param start
     *            = 10
     * @param end
     *            = 3
     * @return
     */
    public static float evaluateValue(float fraction, Number start, Number end) {
        // start = 10   end = 2
        //fraction = 0.5
        // result = 10 + (-8) * fraction = 6
        return start.floatValue() + (end.floatValue() - start.floatValue())
                * fraction;
    }

    /**
     * Get the point of intersection between circle and line.
     * 获取通过指定圆心,斜率为lineK的直线与圆的交点。
     *
     * @param pMiddle
     *            The circle center point.
     * @param radius
     *            The circle radius.
     * @param lineK
     *            The slope of line which cross the pMiddle.
     * @return
     */
    public static PointF[] getIntersectionPoints(PointF pMiddle, float radius,
                                                 Double lineK) {
        PointF[] points = new PointF[2];

        //高中数学:几何
        float arctan, xOffset = 0, yOffset = 0;
        if (lineK != null) {
            // 计算直角三角形边长
            // 余切函数(弧度)
            arctan = (float) Math.atan(lineK);
            // 正弦函数
            xOffset = (float) (Math.sin(arctan) * radius);
            // 余弦函数
            yOffset = (float) (Math.cos(arctan) * radius);
        } else {
            xOffset = radius;
            yOffset = 0;
        }
        points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
        points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);

        return points;
    }
}

public class MessageBubbleView extends View {

    private static final String TAG = "zsjTAG";
    private PointF mFixedPoint;
    private PointF mDragPoint;
    private int mFixedMaxRadius = 10;
    private int mFixedMinRadius = 5;
    private int mDragRadius = 12;
    private Paint mPaint;
    private PointF mP0;
    private PointF mP1;
    private PointF mP2;
    private PointF mP3;
    private PointF mControlPoint;
    private Bitmap mDragBitmap;

    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);
        mFixedMaxRadius = dip2px(mFixedMaxRadius);
        mFixedMinRadius = dip2px(mFixedMinRadius);
        mDragRadius = dip2px(mDragRadius);
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setDither(true);
        mPaint.setColor(Color.RED);
    }

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

/*    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                float fixedX = event.getX();
                float fixedY = event.getY();
                initFixedPoint(fixedX, fixedY);
                break;
            case MotionEvent.ACTION_MOVE:
                float dragX = event.getX();
                float dragY = event.getY();
                updateDragPoint(dragX, dragY);
                break;
            case MotionEvent.ACTION_UP:

                break;

            default:
                break;
        }
        invalidate();
        return true;
    }*/

    public void updateDragPoint(float dragX, float dragY) {
        if (mDragPoint == null) {
            mDragPoint = new PointF( dragX, dragY);
        }
        mDragPoint.x =  dragX;
        mDragPoint.y = dragY;
        invalidate();
    }


    public void initFixedPoint(float fixedX, float fixedY) {
        if (mFixedPoint == null) {
            mFixedPoint = new PointF();
        }
        mFixedPoint.x = fixedX;
        mFixedPoint.y = fixedY;
        invalidate();
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mFixedPoint == null || mDragPoint == null) {
            return;
        }
        //绘制拖拽圆
        canvas.drawCircle(mDragPoint.x, mDragPoint.y, mDragRadius, mPaint);
        //移动的时候,固定圆缩小.根据拖拽圆和固定圆的距离缩小
        //计算拖拽圆和固定圆的距离
        double dragFixedDistance = getDragFixedDistance(mFixedPoint, mDragPoint);
        float fixedRadius = (float) (mFixedMaxRadius - dragFixedDistance / 14);
        if (fixedRadius > mFixedMinRadius) {
            //绘制固定圆
            canvas.drawCircle(mFixedPoint.x, mFixedPoint.y, fixedRadius, mPaint);
            Path bezierPath = getBezierPath(mFixedPoint, mDragPoint);
            canvas.drawPath(bezierPath, mPaint);
        }
        canvas.drawBitmap(mDragBitmap, mDragPoint.x - mDragBitmap.getWidth() / 2
                , mDragPoint.y - mDragBitmap.getHeight() / 2, null);
    }

    private Path getBezierPath(PointF fixedPoint, PointF dagPoint) {
        double dragFixedDistance = getDragFixedDistance(mFixedPoint, mDragPoint);
        float fixedRadius = (float) (mFixedMaxRadius - dragFixedDistance / 14);
        float dx = Math.abs(fixedPoint.x - dagPoint.x);
        float dy = Math.abs(fixedPoint.y - dagPoint.y);
        float tanA = dy / dx;
        float a = (float) Math.atan(tanA);

        //P0 点
        if (mP0 == null) {
            mP0 = new PointF();
        }
        mP0.x = mDragPoint.x + (int) (mDragRadius * Math.sin(a));
        mP0.y = mDragPoint.y - (int) (mDragRadius * Math.cos(a));

        //P1 点
        if (mP1 == null) {
            mP1 = new PointF();
        }
        mP1.x = mFixedPoint.x + (int) (fixedRadius * Math.sin(a));
        mP1.y = mFixedPoint.y - (int) (fixedRadius * Math.cos(a));


        //P2 点
        if (mP2 == null) {
            mP2 = new PointF();
        }
        mP2.x = mFixedPoint.x - (int) (fixedRadius * Math.sin(a));
        mP2.y = mFixedPoint.y + (int) (fixedRadius * Math.cos(a));


        //P0 点
        if (mP3 == null) {
            mP3 = new PointF();
        }
        mP3.x = mDragPoint.x - (int) (mDragRadius * Math.sin(a));
        mP3.y = mDragPoint.y + (int) (mDragRadius * Math.cos(a));


        //绘制路径
        Path path = new Path();
        path.moveTo(mP0.x, mP0.y);

        //控制点选择固定圆和拖拽圆的中心点
        PointF controlPoint = getControlPoint();
        path.quadTo(controlPoint.x, controlPoint.y, mP1.x, mP1.y);

        path.lineTo(mP2.x, mP2.y);
        path.quadTo(controlPoint.x, controlPoint.y, mP3.x, mP3.y);
        path.close();
        return path;
    }

    private PointF getControlPoint() {
        if (mControlPoint == null) {
            mControlPoint = new PointF();
        }
        mControlPoint.x = (mDragPoint.x + mFixedPoint.x) / 2;
        mControlPoint.y = (mDragPoint.y + mFixedPoint.y) / 2;
        return mControlPoint;
    }

    private double getDragFixedDistance(PointF fixedPoint, PointF dagPoint) {
        return Math.sqrt((dagPoint.x - fixedPoint.x) * (dagPoint.x - fixedPoint.x) + (dagPoint.y - fixedPoint.y) * (dagPoint.y - fixedPoint.y));
    }

    public static void attach(View view, BubbleMessageTouchListener.BubbleDisappearListener bubbleTouchListener) {
        view.setOnTouchListener(new BubbleMessageTouchListener(view, view.getContext(),bubbleTouchListener));
    }

    public void setDragBitmap(Bitmap dragBitmap) {
        mDragBitmap = dragBitmap;
        invalidate();
    }

    public void handleActionUp() {
        double dragFixedDistance = getDragFixedDistance(mFixedPoint, mDragPoint);
        float fixedRadius = (float) (mFixedMaxRadius - dragFixedDistance / 14);
        if (fixedRadius > mFixedMinRadius) {
            //回弹
            // 0 - 1
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(1);
            valueAnimator.setDuration(250);
            final PointF start = new PointF(mFixedPoint.x, mFixedPoint.y);
            final PointF end = new PointF(mDragPoint.x, mDragPoint.y);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float percent = (float) animation.getAnimatedValue();
                    PointF pointF = BubbleUtils.getPointByPercent(end, start, percent);
                    updateDragPoint(pointF.x, pointF.y);
                }
            });
            valueAnimator.setInterpolator(new OvershootInterpolator(3f));
            valueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (mListener != null) {
                        mListener.restore();
                    }
                }
            });
            valueAnimator.start();
        } else {
            //爆炸
            if (mListener != null){
                mListener.dismiss(mDragPoint);
            }
        }
    }


    private MessageBubbleListener mListener;

    public void setMessageBubbleListener(MessageBubbleListener listener) {
        this.mListener = listener;
    }

    public interface MessageBubbleListener {
        // 还原
        public void restore();

        // 消失爆炸
        public void dismiss(PointF pointF);
    }
}

4 仿写效果

gpjvn-6go3t.gif

5 完整代码

messagebubbleview

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生x阅读 15,967评论 3 119
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,387评论 25 707
  • 内存 栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量栈是动态的,存储速度比堆要快,仅次于寄存...
    李波小丑阅读 424评论 0 1
  • 经常会不知道到底写什么,之所以喜欢简书也是因为便捷,无论何时有了思路,拿起手机即可。但我又明白还是要写些什么,因为...
    古灵精蓉儿阅读 559评论 0 1
  • (1)邂逅. 一个四月,诗意的江南水乡小镇。一个狭长的道子里,有一个院子,在那里我碰到了一个做手工植物染的女人。怀...
    青涩的1912阅读 685评论 0 0