自定義View-31 仿QQ消息未讀拖拽回彈和爆炸效果

1.要模仿的效果

hnwuq-656lw.gif

2 實(shí)現(xiàn)思路

2.1 怎么才能夠把一個(gè)View拖動(dòng)到狀態(tài)欄上面

我們要把這個(gè)View放在 WindowManager 上面拖動(dòng),原來(lái)的View還是在原來(lái)位置只是隱藏愈污,
拖動(dòng)的時(shí)候其實(shí)是新建了一個(gè)View耀态,復(fù)制一張圖片在WindowManager上面拖動(dòng)

2.2 回彈就是不斷改變拖拽圓的位置

2.3 爆炸效果是一個(gè)幀動(dòng)畫

3 代碼實(shí)現(xiàn)

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上面搞一個(gè)View ,上一節(jié)寫好的貝塞爾的View
                mWindowManager.addView(mMessageBubbleView, mParams);
                //獲取當(dāng)前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));
                //按下,隱藏當(dāng)前的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;
    }

    /**
     * 從一個(gè)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 添加一個(gè)爆炸動(dòng)畫
        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();
        // 等它執(zhí)行完之后我要移除掉這個(gè) 爆炸動(dòng)畫也就是 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 轉(zhuǎn)換成 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);
    }

    /**
     * 獲取狀態(tài)欄高度
     *
     * @return
     */
    public static int getStatusBarHeight(Context context) {
        //獲取status_bar_height資源的ID
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            //根據(jù)資源ID獲取響應(yīng)的尺寸值
            return context.getResources().getDimensionPixelSize(resourceId);
        }
        return dip2px(25, context);
    }

    /**
     * As meaning of method name. 獲得兩點(diǎn)之間的距離 (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2) 開平方
     * Math.sqrt:開平方 Math.pow(p0.y - p1.y, 2):求一個(gè)數(shù)的平方
     *
     * @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. 根據(jù)百分比獲取兩點(diǎn)之間的某個(gè)點(diǎn)坐標(biāo)
     *
     * @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));
    }

    /**
     * 根據(jù)分度值,計(jì)算從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.
     * 獲取通過(guò)指定圓心,斜率為lineK的直線與圓的交點(diǎn)擎析。
     *
     * @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];

        //高中數(shù)學(xué):幾何
        float arctan, xOffset = 0, yOffset = 0;
        if (lineK != null) {
            // 計(jì)算直角三角形邊長(zhǎng)
            // 余切函數(shù)(弧度)
            arctan = (float) Math.atan(lineK);
            // 正弦函數(shù)
            xOffset = (float) (Math.sin(arctan) * radius);
            // 余弦函數(shù)
            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);
        //移動(dòng)的時(shí)候,固定圓縮小.根據(jù)拖拽圓和固定圓的距離縮小
        //計(jì)算拖拽圓和固定圓的距離
        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 點(diǎn)
        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 點(diǎn)
        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 點(diǎn)
        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 點(diǎn)
        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);

        //控制點(diǎn)選擇固定圓和拖拽圓的中心點(diǎn)
        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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末簿盅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子揍魂,更是在濱河造成了極大的恐慌桨醋,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件现斋,死亡現(xiàn)場(chǎng)離奇詭異喜最,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)庄蹋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門瞬内,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人限书,你說(shuō)我怎么就攤上這事虫蝶。” “怎么了倦西?”我有些...
    開封第一講書人閱讀 165,630評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵能真,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我扰柠,道長(zhǎng)粉铐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評(píng)論 1 295
  • 正文 為了忘掉前任卤档,我火速辦了婚禮蝙泼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘劝枣。我一直安慰自己踱承,他們只是感情好倡缠,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著茎活,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琢唾。 梳的紋絲不亂的頭發(fā)上载荔,一...
    開封第一講書人閱讀 51,718評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音采桃,去河邊找鬼懒熙。 笑死,一個(gè)胖子當(dāng)著我的面吹牛普办,可吹牛的內(nèi)容都是我干的工扎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼衔蹲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼肢娘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起舆驶,我...
    開封第一講書人閱讀 39,345評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤橱健,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后沙廉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拘荡,經(jīng)...
    沈念sama閱讀 45,802評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評(píng)論 3 337
  • 正文 我和宋清朗相戀三年撬陵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了珊皿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,117評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡巨税,死狀恐怖蟋定,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情垢夹,我是刑警寧澤溢吻,帶...
    沈念sama閱讀 35,810評(píng)論 5 346
  • 正文 年R本政府宣布出革,位于F島的核電站姐刁,受9級(jí)特大地震影響定嗓,放射性物質(zhì)發(fā)生泄漏达椰。R本人自食惡果不足惜膝宁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評(píng)論 3 331
  • 文/蒙蒙 一碍论、第九天 我趴在偏房一處隱蔽的房頂上張望透敌。 院中可真熱鬧抗果,春花似錦倡怎、人聲如沸迅耘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)颤专。三九已至纽哥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間栖秕,已是汗流浹背春塌。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留簇捍,地道東北人只壳。 一個(gè)月前我還...
    沈念sama閱讀 48,377評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像暑塑,于是被迫代替她去往敵國(guó)和親吼句。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1事格、通過(guò)CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請(qǐng)求組件 FMDB本地?cái)?shù)據(jù)庫(kù)組件 SD...
    陽(yáng)明先生_X自主閱讀 15,982評(píng)論 3 119
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,180評(píng)論 25 707
  • 內(nèi)存 棧區(qū)(stack):由編譯器自動(dòng)分配釋放惕艳,存放函數(shù)的參數(shù)值,局部變量棧是動(dòng)態(tài)的分蓖,存儲(chǔ)速度比堆要快尔艇,僅次于寄存...
    李波小丑閱讀 430評(píng)論 0 1
  • 經(jīng)常會(huì)不知道到底寫什么,之所以喜歡簡(jiǎn)書也是因?yàn)楸憬菝春祝瑹o(wú)論何時(shí)有了思路终娃,拿起手機(jī)即可。但我又明白還是要寫些什么蒸甜,因?yàn)?..
    古靈精蓉兒閱讀 570評(píng)論 0 1
  • (1)邂逅. 一個(gè)四月棠耕,詩(shī)意的江南水鄉(xiāng)小鎮(zhèn)。一個(gè)狹長(zhǎng)的道子里柠新,有一個(gè)院子窍荧,在那里我碰到了一個(gè)做手工植物染的女人。懷...
    青澀的1912閱讀 693評(píng)論 0 0