Android自定義View之仿QQ未讀消息拖拽效果

1、前言

用QQ的時候,發現未讀消息拖拽效果蠻有意思,就模仿了一下。java

2、效果圖

具體效果以下:android

效果圖具備如下特性:git

  • 小圓點拖拽是有範圍的
  • 在拖拽範圍進行拖拽後釋放小圓點會進行回彈後回到初始位置
  • 拖拽的時候,中心的圓會慢慢變小,拖拽的圓大小不變,中間鏈接的部分愈來愈長而且越來細,直至消失
  • 若是超出定義的拖拽範圍後進行釋放會有爆炸的效果而且消失

3、分析

1.組成

先分析這個視圖的組成:github

  • 中心的小圓:一個固定的圓
  • 跟着手指移動的小圓:一個拖拽的圓
  • 兩個圓的鏈接部分
  • 兩條直線(兩個圓的直徑),用來鏈接兩條貝塞爾曲線,造成封閉圖形

2.繪製

  • 中心的小圓和拖拽的小圓 繪製小圓相對比較簡單,直接調用canvas.drawCircle便可,定點中心圓的圓心是固定的,拖拽圓的圓形是手指觸摸屏幕的座標。
  • 兩個圓之間鏈接的部分 中間鏈接的部分實際上是兩條二階貝塞爾曲線,具體分析以下:

貝塞爾曲線效果圖

簡單分析

那麼(p1-p3)這條線能夠用如下僞碼來繪製:canvas

Path rPathLeft = new Path();
rPathLeft.moveTo(P1.x,P1.y);
rPathLeft.quadTo(M.x,M.y,P3.x,P3.y);
複製代碼

同理(P2-P4)這條線能夠用如下僞代碼來繪製:app

Path rPathRight = new Path();
rPathRight.moveTo(P2.x,P2.y);
rPathRight.quadTo(M.x,M.y,P4.x,P4.y);
複製代碼
  • 兩條直線 兩條直線就是分別是(P1-P2)和(P3-P4)
rPathLeft.lineTo(P4.x,P4.y)
rPathRight.lineTo(P2.x,P2.y)
複製代碼

繪製以上兩條貝塞爾曲線和直線須要五個點:P1,P2,P3,P4,M,其中P1,P2,P3,P4是圓的切點,如今只知道兩個圓的中心圓點O1和O2,那麼怎麼根據這兩個點來求其他四個圓的切點呢?先分析:ide

簡單分析二

根據上圖可得知:函數

tan(a) = y / x佈局

a = arctan(y / x)post

P3.x = X1-r2*sina

P3.y = Y1-r2*cosa

P1.x = X0-r1*sina

P1.y = X0-r1*cosa

同理P2,P4也能夠求出。

2.1.靜態實現

下面先靜態實現繪製圖像,直接繼承幀佈局(FrameLayout)

public class RedPointView extends FrameLayout{
    //畫筆
    private Paint rPaint,tPaint;
    //半徑
    private int rRadius;
    //固定圓的圓心
    PointF tCenterPointF = new PointF(300,400);
    //拖拽圓的圓心
    PointF tDragPointF = new PointF(200,550);
    //固定圓的半徑
    private float tCenterRadius = 30;
    //拖拽圓的半徑
    private float tDragRadius = 30;
    //線條
    private Path rPath;

    //一個參數的構造函數,調用兩個參數的構造函數
    public RedPointView(Context context) {
        this(context,null);
    }
    //兩個參數的構造函數,調用三個參數的構造函數
    public RedPointView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }
    //三個參數的構造函數
    public RedPointView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    //初始化小圓
    private void init(){
        //初始化Paint對象
        rPaint = new Paint();
        //設置顏色 紅色
        rPaint.setColor(Color.RED);
        //設置抗鋸齒
        rPaint.setAntiAlias(true);
        //設置填充
        rPaint.setStyle(Paint.Style.FILL);

        tPaint = new Paint();
        //設置顏色 紅色
        tPaint.setColor(Color.RED);
        //設置抗鋸齒
        tPaint.setAntiAlias(true);
        //半徑 25
        rRadius = 25;
    }

    //繪製本身孩子方法
    //ViewGroup上繪製東西的時候每每重寫的是dispatchDraw()方法而不是onDraw()方法
    protected void dispatchDraw(Canvas canvas){
        super.dispatchDraw(canvas);

        //先繪製固定圓
        canvas.drawCircle(tCenterPointF.x,tCenterPointF.y,tCenterRadius,rPaint);
        //再繪製拖拽圓
        canvas.drawCircle(tDragPointF.x,tDragPointF.y,tDragRadius,rPaint);

        float x = tCenterPointF.x - tDragPointF.x;
        float y = tDragPointF.y - tCenterPointF.y;
        //求a的角度
        double a = Math.atan(y / x);

        //中心圓的p1 x座標偏移
        float offsetX1 = (float) (tCenterRadius * Math.sin(a));
        float offsetY1= (float) (tCenterRadius * Math.cos(a));

        //拖拽圓的p2 x座標偏移
        float offsetX2 = (float) (tDragRadius * Math.sin(a));
        float offsetY2= (float) (tDragRadius * Math.cos(a));

        //p1的座標
        float p1_x = tCenterPointF.x - offsetX1;
        float p1_y = tCenterPointF.y - offsetY1;


        //p2的座標
        float p2_x = tCenterPointF.x + offsetX1;
        float p2_y = tCenterPointF.y + offsetY1;
        
        //p3的座標
        float p3_x = tDragPointF.x - offsetX2;
        float p3_y = tDragPointF.y - offsetY2;

        //p4的座標
        float p4_x = tDragPointF.x + offsetX2;
        float p4_y = tDragPointF.y + offsetY2;
        //控制點M的座標
        float controll_x = (tCenterPointF.x + tDragPointF.x) / 2;
        float controll_y = (tDragPointF.y + tCenterPointF.y) / 2;
        //建立Path來繪製路徑
        rPath = new Path();
        //繪製路徑方向:P1->P3->P4->P1
        rPath.reset();
        rPath.moveTo(p1_x,p1_y);
        rPath.quadTo(controll_x,controll_y,p3_x,p3_y);
        rPath.lineTo(p4_x,p4_y);
        rPath.quadTo(controll_x,controll_y,p2_x,p2_y);
        rPath.lineTo(p1_x,p1_y);
        rPath.close();
        canvas.drawPath(rPath,tPaint);

    }

}
複製代碼

佈局文件直接ConstraintLayout嵌套這個自定義View便可:

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.knight.qq_redpoint.MainActivity">

    <com.knight.qq_redpoint.RedPointView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

</android.support.constraint.ConstraintLayout>
複製代碼

效果圖以下:

靜態效果圖

2.2.動態實現

靜態效果繪製出來了,那麼繼續往下走,實現動態效果,實現動態無非是拖拽圓的切點和貝塞爾曲線的控制點在變化,而拖拽圓的圓心實際上是觸摸屏幕的座標,那麼其切點和控制點根據上一個步驟的公式來求出,下面直接在觸摸方法onTouchEvent來處理:

public boolean onTouchEvent(MotionEvent event){
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //event.getRawX:表示的是觸摸點距離屏幕左邊界的距離 
                //event.getRawY:表示的是觸摸點距離屏幕上邊界的距離
                //event.getX()取相對於你觸摸的view的左邊的偏移(X座標)
                //event.getY()取相對於你觸摸的view的頂邊的偏移(Y座標)
                float originalDragX = event.getX();
                float originalDragy = event.getY();
                updateDragPoint(originalDragX,originalDragy);
                break;
            case MotionEvent.ACTION_MOVE:
                float overDragX = event.getX();
                float overDragy = event.getY();
                //移動的時候不斷更新拖拽圓的位置
                updateDragPoint(overDragX,overDragy);
                break;
        }
        return true;

    }
    
        //更新拖拽圓的圓心座標
    private void updateDragPoint(float x,float y){
        tDragPointF.set(x,y);
        postInvalidate();

    }
複製代碼

效果圖以下:

動態效果圖

2.2.1 中心圓半徑變化

仔細觀察效果,發現隨着拖拽距離的增長,中心圓的半徑是愈來愈小的 好像有那麼一點點感受了,可是遠遠還不夠。那麼咱們能夠定一個規則,拖拽距離和中心圓之間的關係,而且設置拖拽最大距離:

//中心的最小半徑
    private float minRadius = 8;
    //默認拖拽最大距離
    private float maxDistance = 160//計算拖動過程當中中心圓的半徑
    private float changeCenterRadius() {
        float mDistance_x = tDragPointF.x - tCenterPointF.x;
        float mDistance_y = tDragPointF.y - tCenterPointF.y;
        //兩個圓之間的距離
        float mDistance = (float) Math.sqrt(Math.pow(mDistance_x, 2) + Math.pow(mDistance_y, 2));
        //計算中心圓的半徑 這裏用拖拽圓默認的半徑去減距離變化的長度(這裏能夠本身定義變化的半徑)
        float r = tDragRadius - minRadius * (mDistance / maxDistance);
        //計算出半徑若是小於最小的半徑 就賦值最小半徑
        if (r < minRadius) {
            r = minRadius;
        }
        return r;


    }


複製代碼

最後在onDraw方法裏,添加計算變化中心圓的半徑便可:

//繪製方法
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();
        //繪製固定中心圓
        tCenterRadius = changeCenterRadius();
        canvas.drawCircle(tCenterPointF.x, tCenterPointF.y, tCenterRadius, rPaint);
        ....
    }
複製代碼

效果圖以下:

中心圓變化

2.2.2 距離限制

下面增長拖拽距離限制,當拖拽距離大於給定的距離時,中心圓就會消失,邏輯很簡單,也就是在onTouchEvent裏的ACTION_MOVE,計算兩個圓的拖拽距離,若是超出給定的拖拽距離,就不繪製貝塞爾曲線和中心固定圓:

//標識 拖拽距離是否大於規定的拖拽範圍
    private boolean isOut;

    //標識 若是超出拖拽範圍
    private boolean isOverStep;
    //繪製方法
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.save();

        if(!isOut){
            //繪製固定中心圓
            tCenterRadius = changeCenterRadius();
            canvas.drawCircle(tCenterPointF.x, tCenterPointF.y, tCenterRadius, rPaint);
            .....
        }
        //一旦超出給定的拖拽距離 就繪製拖拽圓
        if(!isOverStep){
            canvas.drawCircle(tDragPointF.x,tDragPointF.y,tDragRadius,rPaint);
        }
    }
    
    //重寫onTouchEvent方法
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            ........
            case MotionEvent.ACTION_MOVE:
                float overDragX = event.getX();
                float overDragy = event.getY();
                //移動的時候不斷更新拖拽圓的位置
                updateDragPoint(overDragX, overDragy);
                float tDragDistance = getDistanceTwo(tCenterPointF,tDragPointF);
                //判斷若是拖拽距離大於給定距離時
                if(tDragDistance > maxDistance){
                    isOut = true;
                }else{
                    //這裏要注意 不能賦值isOut爲false 由於一旦超出給定的拖拽距離就沒辦法恢復了
                    isOverStep = false;
                }
                break;
        }
        return true;

    }
    
    //計算兩個圓之間的距離
    private float getDistanceTwo(PointF tCenterPointF,PointF tDragPointF){
        return (float) Math.sqrt(Math.pow(tCenterPointF.x - tDragPointF.x,2) + Math.pow(tCenterPointF.y - tDragPointF.y,2));
    }

複製代碼

效果圖以下:

拖拽超出範圍
上面只作了超出拖拽範圍的效果,下面添加沒有超出拖拽範圍效果,鬆開手後拖拽圓會回彈原來的位置,那就要在 MotionEvent.ACTION_UP作處理:

//重寫onTouchEvent方法
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            ....
            case MotionEvent.ACTION_UP:
                getDistanceTwo(tCenterPointF,tDragPointF);
                //這裏要判斷
                if(!isOut){
                    //沒有超出
                    kickBack();
                }
                postInvalidate();
                break;
        }
        return true;

    }
    
   /** * 拖拽圓回彈動畫 * */
    private void kickBack() {
        final PointF initPoint = new PointF(tDragPointF.x,
                tDragPointF.y);
        final PointF finishPoint = new PointF(tCenterPointF.x,
                tCenterPointF.y);
        //值從0平滑過渡1
        ValueAnimator animator = ValueAnimator.ofFloat(0.0f,1.0f);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //獲取動畫執行進度
                float rFraction = animation.getAnimatedFraction();
                //更新拖拽圓的圓心
                PointF updateDragPoint = getPoint(
                        initPoint, finishPoint, rFraction);
                updateDragPoint(updateDragPoint.x, updateDragPoint.y);
            }
        });
        //設置動畫插值器
        animator.setInterpolator(new OvershootInterpolator(3.0f));
        //動畫時間
        animator.setDuration(500);
        animator.start();
    }


    /** * * 根據百分比獲取兩點之間的某個點座標 * @param initPoint 初識圓 * @param finishPoint 最終圓 * @param percent 百分比 * @return * */
    public PointF getPoint(PointF initPoint, PointF finishPoint, float percent) {
        return new PointF(getValue(initPoint.x , finishPoint.x,percent), getValue(initPoint.y , finishPoint.y,percent));
    }

    /** * 獲取分度值 * @param start * @param finish * @param fraction * @return */
    public float getValue(Number start, Number finish,float fraction){
        return start.floatValue() + (finish.floatValue() - start.floatValue()) * fraction;
    }
複製代碼

效果圖:

回彈效果圖

2.2.3 增長爆炸效果

當拖拽圓超出拖拽範圍後,會有一個爆炸效果後並消失,下面添加爆炸效果:

//初始化爆炸圖片
    private int[] explodeImgae = new int[]{
        R.mipmap.explode_1,
        R.mipmap.explode_2,
        R.mipmap.explode_3,
        R.mipmap.explode_4,
        R.mipmap.explode_5
    };
    //爆炸ImageView
    private ImageView explodeImage;
複製代碼

並在init初始化方法裏添加對這爆炸圖像:

//添加爆炸圖像
        explodeImage = new ImageView(getContext());
        //設置佈局參數
        LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        explodeImage.setLayoutParams(lp);
        explodeImage.setImageResource(R.mipmap.explode_1);
        //一開始不顯示
        explodeImage.setVisibility(View.INVISIBLE);
        //增長到viewGroup中
        addView(explodeImage);
複製代碼

並實現播放動畫方法:

/** * * 超過拖拽範圍外顯示爆炸效果 * */
    private void showExplodeImage(){
        //屬性動畫
        ValueAnimator animator = ValueAnimator.ofInt(0,explodeImgaes.length - 1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //不斷更新圖像變化
                explodeImage.setBackgroundResource(explodeImgaes[(int) animation.getAnimatedValue()]);
            }
        });
        //爲動畫添加監聽
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                //結束了 把圖像設置不可見狀態
                explodeImage.setVisibility(View.GONE);
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                //開始時 設置爲可見
                explodeImage.setVisibility(View.VISIBLE);
            }

            @Override
            public void onAnimationPause(Animator animation) {
                super.onAnimationPause(animation);
            }

            @Override
            public void onAnimationResume(Animator animation) {
                super.onAnimationResume(animation);
            }
        });
        //時間
        animator.setDuration(600);
        //播放一次
        animator.setRepeatMode(ValueAnimator.RESTART);
        //差值器
        animator.setInterpolator(new OvershootInterpolator());
        animator.start();
    }

複製代碼

MotionEvent.ACTION_UP裏:

case MotionEvent.ACTION_UP:
                getDistanceTwo(tCenterPointF,tDragPointF);
                //這裏要判斷
                if(!isOut){
                    //沒有超出
                    kickBack();
                }
                if(isOut){
                    //擡起標識
                    isOverandUp = true;
                    //讓爆炸圖片在原點中央
                    explodeImage.setX(event.getX() - tDragRadius);
                    explodeImage.setY(event.getY() - tDragRadius);
                    //若是中心圓和拖拽圓大於拖拽距離 就播放爆炸
                    if(getDistanceTwo(tCenterPointF,tDragPointF) > maxDistance){
                        showExplodeImage();
                    }
                    //這裏是若是拖拽圓和中心圓距離已經超出拖拽距離 而後又把拖拽圓移動與中心圓大於30 仍是會爆炸
                    if(getDistanceTwo(tCenterPointF,tDragPointF) >=30){
                        showExplodeImage();
                    }

                }
                postInvalidate();
                break;
複製代碼

dispatchView超出拖拽距離到小於恢復中心圓的距離邏輯:

if(isOut){
            //若是一開始超出拖拽範圍 後面又移動拖拽圓與中心圓的距離少於30,就恢復中心圓位置
            if(getDistanceTwo(tCenterPointF,tDragPointF) < 30 && isOverandUp){
                canvas.drawCircle(tCenterPointF.x, tCenterPointF.y, tCenterRadius, rPaint);
                isOut = false;
                isOverandUp = false;
            }


        }


        //一旦超出給定的拖拽距離 就繪製拖拽圓
        if(!isOverStep){
            //若是超出而且擡起
            if(!isOverandUp && isOut){
                canvas.drawCircle(tDragPointF.x,tDragPointF.y,tDragRadius,rPaint);
            }

        }
複製代碼

效果圖以下:

效果圖一

效果圖二

4、添加到ListView

1.添加到WindowManager

上面所實現的效果還遠遠不夠,怎麼像QQ那樣,在ListView或者Recycleview裏小圓點能自由在屏幕內拖拽呢?由於view只能在它的父控件內繪製,因此也只能在本身的列表內移動,那怎麼能在全屏拖拽呢?只能藉助WindowManager,也就是當將要拖拽的圓點添加到windowManager,而且設置觸摸監聽,自定義拖拽view從繼承ViewGroup變爲繼承View:

public class BetterRedPointView extends View{ 

    //WindowManager 對象
    private WindowManager windowManager;
    //拖拽view的寬
    private int dragViewWidth;
    //拖拽view的高
    private int dragViewHeight;
    //WindowManager 佈局參數
    private WindowManager.LayoutParams params;
    //狀態監聽
    private DragViewStatusListener dragViewStatusListener;
    //佈局參數
    params = new WindowManager.LayoutParams();
    //背景透明
    params.format = PixelFormat.TRANSLUCENT;
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    //以左上角爲基準
    params.gravity = Gravity.TOP | Gravity.LEFT;
    
}
複製代碼

構造函數將拖拽的viewWindowManager對象傳進來,並初始化一些參數:

//構造函數
    public BetterRedPointView(Context context, View dragView, WindowManager windowManager){
        super(context);
        this.context = context;
        this.dragView = dragView;
        this.windowManager = windowManager;
        init();

    }
    
   //初始化小圓
    private void init() {
        //手動測量
        dragView.measure(1,1);
        dragViewWidth = dragView.getMeasuredWidth() / 2;
        dragViewHeight = dragView.getMeasuredHeight() / 2;

        tDragRadius = dragViewHeight;
        //中心圓的半徑
        tCenterRadius = SystemUtil.dp2px(context,8);
        //最大拖拽距離
        maxDistance = SystemUtil.dp2px(context,80);
        //最小半徑
        minRadius = SystemUtil.dp2px(context,3);

        //佈局參數
        params = new WindowManager.LayoutParams();
        //背景透明
        params.format = PixelFormat.TRANSLUCENT;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        //以左上角爲基準
        params.gravity = Gravity.TOP | Gravity.LEFT;

    }
複製代碼

2.更新拖拽view的位置

在上面例子中更新拖拽圓updateDragPoint的方法,也一樣經過WindowManager.updateViewLayout來更新拖拽view的的位置:

/** * 更新拖拽圓心座標 * @param x * @param y */
    private void updateDragPoint(float x, float y) {
        tDragPointF.set(x, y);
        changeManagerView(x,y);
        postInvalidate();

    }
    
    /** * 從新繪製拖拽圓的佈局 * @param x x座標 * @param y y座標 */
    private void changeManagerView(float x,float y){
        params.x = (int)(x - dragViewWidth);
        params.y = (int)(y - dragViewHeight - statusBarHeight);
        windowManager.updateViewLayout(dragView,params);
    }
複製代碼

3.添加狀態監聽

增長拖拽圓和中心圓的拖拽狀況監聽:

public interface DragViewStatusListener {

    /** * 在拖拽範圍外移動 * * @param dragPoint */
    void outDragMove(PointF dragPoint);

    /** * 在拖拽範圍外移動 * 產生爆炸效果 * */
    void outDragMoveUp(PointF dragPoint);

    /** * 在拖拽範圍內移動 * * @param dragPoint */
    void inDragUp(PointF dragPoint);


    /** * 當移出拖拽範圍 後拖拽到範圍內 恢復中心圓 * */
    void recoverCenterPoint(PointF centerPoint);


}

複製代碼

在對應觸發的狀況下實現監聽回調,如爆炸的動畫:

case MotionEvent.ACTION_UP:
                getDistanceTwo(tCenterPointF,tDragPointF);
                //這裏要判斷
                if(!isOut){
                    //沒有超出
                    kickBack();
                }
                if(isOut){
                    //擡起標識
                    isOverandUp = true;
                    //讓爆炸圖片在原點中央
                    //explodeImage.setX(event.getX() - tDragRadius);
                    //explodeImage.setY(event.getY() - tDragRadius);
                    //若是中心圓和拖拽圓大於拖拽距離 就播放爆炸
                    if(getDistanceTwo(tCenterPointF,tDragPointF) > maxDistance){
                        //這裏監聽作爆炸效果
                        if(dragViewStatusListener != null){
                             dragViewStatusListener.outDragMoveUp(tDragPointF);
                        }
                    }
                    //這裏是若是拖拽圓和中心圓距離已經超出拖拽距離 而後又把拖拽圓移動與中心圓大於30 仍是會爆炸
                    if(getDistanceTwo(tCenterPointF,tDragPointF) >=30){
                        if(dragViewStatusListener != null){
                           dragViewStatusListener.outDragMoveUp(tDragPointF);
                        }
                    }
                }
複製代碼

4.新建中間橋樑

新建一個類,主要用來輔助,主要用來建立拖拽自定義view和建立WindowManager對象初始化數據,而且做出各類狀況下(在範圍內拖拽,範圍外拖拽)的邏輯和爆炸邏輯,主要代碼以下:

public class BetterRedPointViewControl implements View.OnTouchListener,DragViewStatusListener{


    //上下文
    private Context context;
    //拖拽佈局的id
    private int mDragViewId;
    //WindowManager 對象
    private WindowManager windowManager;
    //佈局參數
    private WindowManager.LayoutParams params;

    private BetterRedPointView betterRedPointView;
    //被拖拽的View
    private View dragView;
    //要顯示的View
    private View showView;

    //狀態欄高度
    private int statusHeight;
    //最大拖拽距離
    private float maxDistance = 560;
    //中心圓的半徑
    private float tCenterRadius = 30;
    //小圓最小半徑
    private float minRadius = 8;

    
    //構造函數
    public BetterRedPointViewControl(Context context,View showView,int mDragViewId,DragStatusListener dragStatusListener){
        this.context = context;
        this.showView = showView;
        this.mDragViewId = mDragViewId;
        this.dragStatusListener = dragStatusListener;
        //設置監聽 執行本身的觸摸事件
        showView.setOnTouchListener(this);
        params = new WindowManager.LayoutParams();
        params.format = PixelFormat.TRANSLUCENT;

    }
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = MotionEventCompat.getActionMasked(event);
        if(action == MotionEvent.ACTION_DOWN){

            ViewParent parent = v.getParent();
            if(parent == null){
                return false;
            }

            parent.requestDisallowInterceptTouchEvent(true);
            statusHeight = SystemUtil.getStatusBarHeight(showView);
            showView.setVisibility(View.INVISIBLE);
            dragView = LayoutInflater.from(context).inflate(mDragViewId,null,false);
            //獲取文本內容
            getText();
            windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            //每當觸摸的時候就建立拖拽的小圓
            betterRedPointView = new BetterRedPointView(context,dragView,windowManager);
            //初始化數據
            init();
            //設置監聽回調
            betterRedPointView.setDragViewStatusListener(this);
            //添加到窗體進行顯示
            windowManager.addView(betterRedPointView,params);
            windowManager.addView(dragView,params);

        }
        betterRedPointView.onTouchEvent(event);
        return true;
    }

    @Override
    public void outDragMove(PointF dragPoint) {

    }

    @Override
    public void outDragMoveUp(PointF dragPoint) {
      removeView();
      showExplodeImage(dragPoint);
      dragStatusListener.outScope();
    }

    @Override
    public void inDragUp(PointF dragPoint) {
      removeView();
      dragStatusListener.inScope();
    }

    @Override
    public void recoverCenterPoint(PointF centerPoint) {
      removeView();
      dragStatusListener.inScope();
    }


    /** * 初始化數據 * */
    private void init(){
        //計算小圓在屏幕中的座標
        int[] points = new int[2];
        showView.getLocationInWindow(points);
        int x = points[0] + showView.getWidth() / 2;
        int y = points[1] + showView.getHeight() / 2;
        betterRedPointView.setStatusBarHeight(statusHeight);
// betterRedPointView.setMaxDistance(maxDistance);
// betterRedPointView.setCenterRadius(tCenterRadius);
// betterRedPointView.setMinRadius(minRadius);
        betterRedPointView.setCenterDragPoint(x,y);
    }
    /** * 獲取文本內容 * */
    private void getText(){
        if(showView instanceof TextView && dragView instanceof TextView){
            ((TextView)dragView).setText((((TextView) showView).getText().toString()));

        }
    }

    /** * 移出view對象 * */
    private void removeView(){
        if (windowManager != null && betterRedPointView.getParent() != null && dragView.getParent() != null) {
            windowManager.removeView(betterRedPointView);
            windowManager.removeView(dragView);
        }
    }
}
複製代碼

5.調用

在Recycleview內執行調用便可:

public class RecycleviewAdapter extends RecyclerView.Adapter<ItemHolder> {
    /** * 須要刪除的view的position 用於更新rv操做 */
    ArrayList<Integer> needRemoveList =new ArrayList<Integer>();

    private Context mContext;
    public RecycleviewAdapter(Context mContext){
        this.mContext = mContext;
    }



    @NonNull
    @Override
    public ItemHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        //加載佈局文件
        View itemView = LayoutInflater.from(mContext).inflate(R.layout.item,null);
        return new ItemHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ItemHolder itemHolder, final int i) {
        itemHolder.tv_dragView.setText(String.valueOf(i));

        Glide.with(mContext).load(R.mipmap.iv_image).apply(RequestOptions.bitmapTransform(new CircleCrop()).override(200,200)).into(itemHolder.iv_head);
        //是否隱藏要拖拽的view
        if(needRemoveList.contains(i)){
            itemHolder.tv_dragView.setVisibility(View.GONE);
        }
        else {
            itemHolder.tv_dragView.setVisibility(View.VISIBLE);
            itemHolder.tv_dragView.setText(String.valueOf(i));
        }
        //一個是拖拽的view 一個是拖拽的view佈局
        new BetterRedPointViewControl(mContext, itemHolder.tv_dragView, R.layout.includeview, new BetterRedPointViewControl.DragStatusListener() {
            /** * 在範圍內 * */
            @Override
            public void inScope() {
                notifyDataSetChanged();
            }

            /** * 在範圍外 * */
            @Override
            public void outScope() {
                needRemoveList.add(i);
                notifyDataSetChanged();

            }
        });
    }

    @Override
    public int getItemCount() {
        return 100;
    }
}
複製代碼

6.最終效果

效果圖以下:

最終效果圖

5、項目例子

github地址:github.com/KnightAndro…

相關文章
相關標籤/搜索