1. 程式人生 > >QQ小紅點終極版 DragPointView

QQ小紅點終極版 DragPointView

傳送門

前言

之前寫了自定義View之QQ小紅點(一),還沒有看過的可以去大概瞅一眼。我再大概囉嗦一下,上篇文章主要介紹了小紅點的實現原理(貝塞爾曲線)以及相關的程式碼實現。時隔兩週,今天我帶大家還把之前寫的demo封裝成一個簡單易用,具有一定的程度的可定製的開源控制元件。為了避免遺落某些點,下面咱們再來回顧一下QQ上的紅點效果。

需求分析

下面咱們一起看看QQ上的紅點效果是什麼樣的,這個時候可以把自己當成產品經理,仔細琢磨,仔細研究。最重要的是把細節都摳出來,然後反饋給開發。哈哈,開玩笑啦~

展示

很顯然,它需要展示文字並且是陣列(對於文字而言,數字文字無異)。它的背景可以是不同的顏色(紅色,淺藍色,甚至其他),在文字長度變化時背景能夠自動適應,例如:展示1的時候它是個原,而展示66的時候它是兩個半圓夾著矩形(表述不好,自行幻想~)

移位

當我們點住紅點不放,到處走走,會發現它是隨著手指移動的。看起來好像沒什麼麻煩的哈,無非就是監聽觸控事件,設定新的位置或者translatX/Y值。好,這個時候再回想一下上篇文章demo的實現。你會發現,demo中並沒有這個東東,demo中的“動”的效果只是單純的重繪。OK,這裡咱們只是先分析分析,客觀先往下看

控制元件層次

倔強的小紅點無論你怎麼拖拽,它都是在頂層,並不會被某個東西覆蓋或者遮擋。甚至,它竟然能跑到狀態列的位置,自由自在的玩耍,忍了。說的這,我想有些同學可能已經有想法了

恢復回彈

小紅點在某個範圍內來回拖拽都會有貝塞爾拉伸部分,但是超過閥值之後就再也沒有貝塞爾拉伸部分。在某個地方釋放觸控事件,控制元件都會執行冒泡動畫並且消失,除了在初始位置的某個小範圍內控制元件釋放的話控制元件會恢復到初始位置(注意此處的小範圍與上面提到的“某個範圍”不同等)

連帶效果

介面上紅點分為兩類:第一類是單個聊天會話未讀訊息,拖動消失後不會影響其他紅點;第二類是會話未讀數總和,拖拽消失後會連帶所有會話的紅點(順序執行消失動畫後隱藏)

程式碼實現

自定義View屬性定義

    <declare-styleable name="DragPointView">

        <!--最大可拖拽距離-->
        <attr name="maxDragLength" format="dimension"/>
        <!--中心圓形半徑-->
        <attr
name="centerCircleRadius" format="dimension"/>
<!--拖拽圓形半徑--> <attr name="dragCircleRadius" format="dimension"/> <!--中心圓形變化最小比例--> <attr name="centerMinRatio" format="float"/> <!--恢復動畫時長--> <attr name="recoveryAnimDuration" format="integer"/> <!--回彈係數--> <attr name="recoveryAnimBounce" format="float"/> <!--貝塞爾部分顏色--> <attr name="colorStretching" format="color"/> <!--標記--> <attr name="sign" format="string"/> <!--清理標記--> <attr name="clearSign" format="string"/> <!--是否可拖拽--> <attr name="canDrag" format="boolean"/> </declare-styleable>

首先,需要展示文字。有兩個方案,第一個的方案就是自己實現文字的展示,這個方式需要注意的是文字的居中展示。第二個方案就是直接繼承TextView,在TextView基礎上實現功能。那麼工作量來看,顯然我們是直接繼承TextView,讓TextView幫我們完成文字相關的展示工作。大家可以發現很多有文字的原生控制元件都是繼承於TextView的。還有一個好處就是,我們可以為它設定background,通過shape我們即可以實現我們想要的背景效果

public abstract class AbsDragPointView extends TextView{ ... }

背景效果

接著,咱們處理移位效果。其實很簡單,在之前的基礎上轉變一下思想,將之前dragCircle的變化換成centerCircle的變化。之前demo的實現是centerCircle處於初始位置不變,而dragCenter隨著手指移動,這個時候跟咱們的需求就相反了,咱們需要控制元件本身跟著移動(也就是dragCenter),還有就是此處監聽事件的位置,不再以getX為計算資料,而是採用getRaw。getX獲取的是事件位置在控制元件中的x值,而getRaw是基於整個螢幕

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!canDrag || ClearViewHelper.getInstance().isClearSigning(sign)
                || (mRecoveryAnim != null && mRecoveryAnim.isRunning())
                || (mRemoveAnim != null && mRemoveAnim.isRunning())) {
            return super.onTouchEvent(event);
        }
        if (mRecoveryAnim == null || !mRecoveryAnim.isRunning()) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if(getParent() != null)
                    getParent().requestDisallowInterceptTouchEvent(true);
                    downX = event.getRawX();
                    downY = event.getRawY();
                    isInCircle = true;
                    postInvalidate();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = (int) event.getRawX() - downX;
                    float dy = (int) event.getRawY() - downY;
                    mCenterCircle.x = mWidthHalf - dx;
                    mCenterCircle.y = mHeightHalf - dy;
                    mDistanceCircles = MathUtils.getDistance(mCenterCircle, mDragCircle);
                    mIsDragOut = mIsDragOut ? mIsDragOut : mDistanceCircles > mMaxDragLength;
                    setX(origX + dx);
                    setY(origY + dy);
                    postInvalidate();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    getParent().requestDisallowInterceptTouchEvent(false);
                    upX = getX();
                    upY = getY();
                    upAndCancelEvent();
                    break;
            }
        }
        return true;
    }

控制元件層次,什麼意思呢?咱們的整個佈局,最終形成了一個view tree就像多叉樹一樣。每個控制元件位於某個層次就決定了它的展示範圍,如果我們的控制元件處於某個父容器中,那麼它的最大可顯空間也就是父容器的空間。但是,咱們的需求是在全螢幕的任何位置都是可顯的。

同樣,這裡我也想到了兩個方案。第一個方案就是設定父容器設定clipChildren屬性為true,這個方案缺陷很多,因為我實踐過了。主要的問題:1.該控制元件的直接或者間接關係的所有父容器都需要設定 2.在ListView或者RecyclerView等列表控制元件中即時設定的clipChildren屬性,控制元件的可顯範圍也只是其自身及以上的位置 3.無法拖拽到ActionBar,ToolBar以及狀態列位置。可想而知,這個idea pass了。

第二個方案將View新增到Window上,哇塞,沒有任何問題。但是實現上可能就要繞一些了,為什麼?你想啊,通過什麼方式將控制元件新增到window上呢?

直接把原來的View移除,然後新增?那麼什麼時候做這個操作呢?答案是:任何觸控事件觸發的時候,而且在事件正發生呢,你把人家移走了。那事件怎麼繼續呢?所以換個方式pass

OK,我說我的實現方式。佈局上的控制元件與window新增的控制元件是兩個控制元件,並且他們繼承於同一個父類AbsDragPointView,該類是個抽象類,定義了兩個控制元件共同的成員變數(其實就是那麼自定義View的屬性)以及幾個抽象方法,這樣處理的原因是為了抽象這個兩個控制元件統一規範的行為方式。

public abstract class AbsDragPointView extends TextView{

    protected float mCenterRadius;
    protected float mDragRadius;
    protected float mCenterMinRatio;
    protected float mRecoveryAnimBounce;
    protected int mMaxDragLength;
    protected int colorStretching;
    protected int mRecoveryAnimDuration;
    protected String sign;
    protected String clearSign;
    protected boolean canDrag;

    protected PointViewAnimObject mRemoveAnim;
    protected Interpolator mRecoveryAnimInterpolator;
    protected OnPointDragListener mOnPointDragListener;
    protected AbsDragPointView mNextRemoveView;

    public AbsDragPointView(Context context) {
        super(context);
    }

    public AbsDragPointView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AbsDragPointView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public PointViewAnimObject getRemoveAnim(){
        return mRemoveAnim;
    }

    public AbsDragPointView setRemoveAnim(PointViewAnimObject removeAnim){
        this.mRemoveAnim = removeAnim;
        return this;
    }

    public AbsDragPointView setRemoveAnim(Animator mRemoveAnim) {
        this.mRemoveAnim = new PointViewAnimObject(mRemoveAnim,this);
        return this;
    }

    public AbsDragPointView setRemoveAnim(AnimationDrawable mRemoveAnim) {
        this.mRemoveAnim = new PointViewAnimObject(mRemoveAnim,this);
        return this;
    }

    public OnPointDragListener getOnPointDragListener() {
        return mOnPointDragListener;
    }

    public String getClearSign() {
        return clearSign;
    }

    public AbsDragPointView setClearSign(String clearSign) {
        this.clearSign = clearSign;
        return this;
    }

    public float getCenterRadius() {
        return mCenterRadius;
    }

    public AbsDragPointView setCenterRadius(float mCenterRadius) {
        this.mCenterRadius = mCenterRadius;
        postInvalidate();
        return this;
    }

    public float getDragRadius() {
        return mDragRadius;
    }

    public AbsDragPointView setDragRadius(float mDragRadius) {
        this.mDragRadius = mDragRadius;
        postInvalidate();
        return this;
    }

    public int getMaxDragLength() {
        return mMaxDragLength;
    }

    public AbsDragPointView setMaxDragLength(int mMaxDragLength) {
        this.mMaxDragLength = mMaxDragLength;
        return this;
    }

    public float getCenterMinRatio() {
        return mCenterMinRatio;
    }

    public AbsDragPointView setCenterMinRatio(float mCenterMinRatio) {
        this.mCenterMinRatio = mCenterMinRatio;
        postInvalidate();
        return this;
    }

    public int getRecoveryAnimDuration() {
        return mRecoveryAnimDuration;
    }

    public AbsDragPointView setRecoveryAnimDuration(int mRecoveryAnimDuration) {
        this.mRecoveryAnimDuration = mRecoveryAnimDuration;
        return this;
    }

    public float getRecoveryAnimBounce() {
        return mRecoveryAnimBounce;
    }

    public AbsDragPointView setRecoveryAnimBounce(float mRecoveryAnimBounce) {
        this.mRecoveryAnimBounce = mRecoveryAnimBounce;
        return this;
    }

    public int getColorStretching() {
        return colorStretching;
    }

    public AbsDragPointView setColorStretching(int colorStretching) {
        this.colorStretching = colorStretching;
        postInvalidate();
        return this;
    }

    public String getSign() {
        return sign;
    }

    public AbsDragPointView setSign(String sign) {
        this.sign = sign;
        return this;
    }

    public void setRecoveryAnimInterpolator(Interpolator mRecoveryAnimInterpolator) {
        this.mRecoveryAnimInterpolator = mRecoveryAnimInterpolator;
    }

    public Interpolator getRecoveryAnimInterpolator() {
        return mRecoveryAnimInterpolator;
    }

    public void clearRemoveAnim() {
        this.mRemoveAnim = null;
    }

    public AbsDragPointView setOnPointDragListener(OnPointDragListener onDragListener) {
        this.mOnPointDragListener = onDragListener;
        return this;
    }

    public boolean isCanDrag() {
        return canDrag;
    }

    public AbsDragPointView setCanDrag(boolean canDrag) {
        this.canDrag = canDrag;
        return this;
    }

    public AbsDragPointView getNextRemoveView() {
        return mNextRemoveView;
    }

    public void setNextRemoveView(AbsDragPointView mNextRemoveView) {
        this.mNextRemoveView = mNextRemoveView;
    }

    public abstract void reset();
    public abstract void startRemove();


}

接著咱們定義兩個View,DragPointViewDragPointViewWindow。前者是需要在真正在佈局中展示的,它的作用也只是展示以及接收咱們需要的自定義屬性,也就是說它除了接收屬性外,其他的與TextView沒有任何差別

public class DragPointView extends AbsDragPointView {

    public static final float DEFAULT_CENTER_MIN_RATIO = 0.5f;
    public static final int DEFAULT_RECOVERY_ANIM_DURATION = 200;

    private DragViewHelper dragViewHelper;

    public DragPointView(Context context) {
        super(context);
        init();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        int flowMaxRadius = Math.min(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
        mCenterRadius = mCenterRadius == 0 ? flowMaxRadius : Math.min(mCenterRadius, flowMaxRadius);
        mDragRadius = mDragRadius == 0 ? flowMaxRadius : Math.min(mDragRadius, flowMaxRadius);
        mMaxDragLength = mMaxDragLength == 0 ? flowMaxRadius * 10 : mMaxDragLength;
    }

    public DragPointView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragPointView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragPointView, defStyleAttr, 0);
        mMaxDragLength = array.getDimensionPixelSize(R.styleable.
                DragPointView_maxDragLength, MathUtils.dip2px(context, 0));
        mCenterRadius = array.getDimensionPixelSize(R.styleable.DragPointView_centerCircleRadius, 0);
        mDragRadius = array.getDimensionPixelSize(R.styleable.DragPointView_centerCircleRadius, 0);
        mCenterMinRatio = array.getFloat(R.styleable.DragPointView_centerMinRatio, DEFAULT_CENTER_MIN_RATIO);
        mRecoveryAnimDuration = array.getInt(R.styleable.
                DragPointView_recoveryAnimDuration, DEFAULT_RECOVERY_ANIM_DURATION);
        colorStretching = array.getColor(R.styleable.DragPointView_colorStretching, 0);
        mRecoveryAnimBounce = array.getFloat(R.styleable.DragPointView_recoveryAnimBounce, 0f);
        sign = array.getString(R.styleable.DragPointView_sign);
        clearSign = array.getString(R.styleable.DragPointView_clearSign);
        canDrag = array.getBoolean(R.styleable.DragPointView_canDrag, true);
        init();
    }

    @Override
    public void startRemove() {
        dragViewHelper.startRemove();
    }

    private void init() {
        dragViewHelper = new DragViewHelper(getContext(),this);
    }

    @Override
    public void reset() {

    }

}

控制元件狀態監聽介面

public interface OnPointDragListener {
    void onRemoveStart(AbsDragPointView view);

    void onRemoveEnd(AbsDragPointView view);

    void onRecovery(AbsDragPointView view);
}

然後再看DragPointViewWindow,這個同樣是繼承了AbsDragPointView父類,因此它具有那麼需要的自定義View屬性變數。並且額外增加了紅點的所有邏輯實現,與上篇文章的思想大致一樣,只不過將drag與center兩個變換了而已。此處通過setX/setY進行移位,當然你也可以通過setTranslateX/Y實現,因為他們倆本質是一樣的

/**
* Sets the visual x position of this view, in pixels. This is equivalent to setting the
* {@link #setTranslationX(float) translationX} property to be the difference between
* the x value passed in and the current {@link #getLeft() left} property.
*
* @param x The visual x position of this view, in pixels.
*/
public void setX(float x) {
setTranslationX(x - mLeft);
}

class DragPointViewWindow extends AbsDragPointView implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {

    private DragPointView origView;
    private Bitmap origBitmap;
    private Paint mPaint;
    private Path mPath;
    protected int mWidthHalf, mHeightHalf;
    private float mRatioRadius;
    private int mMaxRadiusTrebling;
    private boolean isInCircle;
    private float downX, downY;
    private PointF[] mDragTangentPoint;
    private PointF[] mCenterTangentPoint;
    private PointF mCenterCircle;
    private PointF mCenterCircleCopy;
    private PointF mDragCircle;
    private PointF mDragCircleCopy;
    private double mDistanceCircles;
    private PointF mControlPoint;
    private boolean mIsDragOut;
    private ValueAnimator mRecoveryAnim;
    private float origX, origY, upX, upY;

    public void setOrigBitmap(Bitmap origBitmap) {
        this.origBitmap = origBitmap;
    }

    public String getClearSign() {
        return clearSign;
    }

    public DragPointViewWindow setClearSign(String clearSign) {
        this.clearSign = clearSign;
        return this;
    }

    public DragPointViewWindow setCenterRadius(float mCenterRadius) {
        this.mCenterRadius = mCenterRadius;
        return this;
    }

    public DragPointViewWindow setDragRadius(float mDragRadius) {
        this.mDragRadius = mDragRadius;
        return this;
    }

    public DragPointViewWindow(Context context) {
        super(context);
        init();
    }

    public DragPointViewWindow(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragPointViewWindow(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterCircle.x = mDragCircle.x = mWidthHalf = getMeasuredWidth() / 2;
        mCenterCircle.y = mDragCircle.y = mHeightHalf = getMeasuredHeight() / 2;
        int flowMaxRadius = Math.min(mWidthHalf, mHeightHalf);
        mMaxRadiusTrebling = flowMaxRadius * 3;
        origX = getX();
        origY = getY();
    }

    private void init() {
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(18f);
        mPaint.setColor(colorStretching);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mDragTangentPoint = new PointF[2];
        mCenterTangentPoint = new PointF[2];
        mControlPoint = new PointF();
        mCenterCircle = new PointF();
        mCenterCircleCopy = new PointF();
        mDragCircle = new PointF();
        mDragCircleCopy = new PointF();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (getBackground() != null)
            return;
        drawCenterCircle(canvas);
        if (isInCircle) {
            drawBezierLine(canvas);
            drawOriginBitmap(canvas);
        }
    }

    private void drawOriginBitmap(Canvas canvas) {
        if (origBitmap != null && !origBitmap.isRecycled())
            canvas.drawBitmap(origBitmap, 0, 0, mPaint);
    }

    private void drawCenterCircle(Canvas canvas) {
        if (mIsDragOut || !isInCircle) return;
        mPaint.setColor(colorStretching);
        mRatioRadius = Math.min(mCenterRadius, Math.min(mWidthHalf, mHeightHalf));
        if (isInCircle && Math.abs(mCenterMinRatio) < 1.f) {
            mRatioRadius = (float) (Math.max((mMaxDragLength - mDistanceCircles) * 1.f / mMaxDragLength, Math.abs(mCenterMinRatio)) * mCenterRadius);
            mRatioRadius = Math.min(mRatioRadius, Math.min(mWidthHalf, mHeightHalf));
        }
        canvas.drawCircle(mCenterCircle.x, mCenterCircle.y, mRatioRadius, mPaint);
    }

    public void setOrigView(DragPointView origView) {
        this.origView = origView;
    }

    private void drawBezierLine(Canvas canvas) {
        if (mIsDragOut) return;
        mPaint.setColor(colorStretching);
        float dx = mDragCircle.x - mCenterCircle.x;
        float dy = mDragCircle.y - mCenterCircle.y;
        // 控制點
        mControlPoint.set((mDragCircle.x + mCenterCircle.x) / 2,
                (mDragCircle.y + mCenterCircle.y) / 2);
        // 四個切點
        if (dx != 0) {
            float k1 = dy / dx;
            float k2 = -1 / k1;
            mDragTangentPoint = MathUtils.getIntersectionPoints(
                    mDragCircle.x, mDragCircle.y, mDragRadius, (double) k2);
            mCenterTangentPoint = MathUtils.getIntersectionPoints(
                    mCenterCircle.x, mCenterCircle.y, mRatioRadius, (double) k2);
        } else {
            mDragTangentPoint = MathUtils.getIntersectionPoints(
                    mDragCircle.x, mDragCircle.y, mDragRadius, (double) 0);
            mCenterTangentPoint = MathUtils.getIntersectionPoints(
                    mCenterCircle.x, mCenterCircle.y, mRatioRadius, (double) 0);
        }
        // 路徑構建
        mPath.reset();
        mPath.moveTo(mCenterTangentPoint[0].x, mCenterTangentPoint[0].y);
        mPath.quadTo(mControlPoint.x, mControlPoint.y, mDragTangentPoint[0].x, mDragTangentPoint[0].y);
        mPath.lineTo(mDragTangentPoint[1].x, mDragTangentPoint[1].y);
        mPath.quadTo(mControlPoint.x, mControlPoint.y,
                mCenterTangentPoint[1].x, mCenterTangentPoint[1].y);
        mPath.close();
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!canDrag || ClearViewHelper.getInstance().isClearSigning(sign)
                || (mRecoveryAnim != null && mRecoveryAnim.isRunning())
                || (mRemoveAnim != null && mRemoveAnim.isRunning())) {
            return super.onTouchEvent(event);
        }
        if (mRecoveryAnim == null || !mRecoveryAnim.isRunning()) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    if(getParent() != null)
                    getParent().requestDisallowInterceptTouchEvent(true);
                    downX = event.getRawX();
                    downY = event.getRawY();
                    isInCircle = true;
                    postInvalidate();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float dx = (int) event.getRawX() - downX;
                    float dy = (int) event.getRawY() - downY;
                    mCenterCircle.x = mWidthHalf - dx;
                    mCenterCircle.y = mHeightHalf - dy;
                    mDistanceCircles = MathUtils.getDistance(mCenterCircle, mDragCircle);
                    mIsDragOut = mIsDragOut ? mIsDragOut : mDistanceCircles > mMaxDragLength;
                    setX(origX + dx);
                    setY(origY + dy);
                    postInvalidate();
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    getParent().requestDisallowInterceptTouchEvent(false);
                    upX = getX();
                    upY = getY();
                    upAndCancelEvent();
                    break;
            }
        }
        return true;
    }

    private void upAndCancelEvent() {
        if (isInCircle && mDistanceCircles == 0) {
            reset();
            if (mOnPointDragListener != null) {
                mOnPointDragListener.onRecovery(this);
            }
        } else if (!mIsDragOut) {
            mCenterCircleCopy.set(mCenterCircle.x, mCenterCircle.y);
            mDragCircleCopy.set(mDragCircle.x, mDragCircle.y);
            if (mRecoveryAnim == null) {
                mRecoveryAnim = ValueAnimator.ofFloat(1.f, -Math.abs(mRecoveryAnimBounce));
                mRecoveryAnim.setDuration(mRecoveryAnimDuration);
                mRecoveryAnim.addUpdateListener(this);
                mRecoveryAnim.addListener(this);
            }
            if (mRecoveryAnimInterpolator != null)
                mRecoveryAnim.setInterpolator(mRecoveryAnimInterpolator);
            mRecoveryAnim.start();
        } else {
            if (mDistanceCircles <= mMaxRadiusTrebling) {
                reset();
                if (mOnPointDragListener != null) {
                    mOnPointDragListener.onRecovery(this);
                }
            } else if (!TextUtils.isEmpty(clearSign)) {
                ClearViewHelper.getInstance().clearPointViewBySign(origView, clearSign);
            } else {
                startRemove();
            }
        }
    }

    @Override
    public void startRemove() {
        if (mRemoveAnim == null) {
            setVisibility(GONE);
            if (mNextRemoveView != null)
                mNextRemoveView.startRemove();
            if (mOnPointDragListener != null) {
                mOnPointDragListener.onRemoveStart(this);
                mOnPointDragListener.onRemoveEnd(this);
            }
        } else {
            mRemoveAnim.start(mOnPointDragListener);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (mRecoveryAnim != null && mRecoveryAnim.isRunning()) {
            mRecoveryAnim.cancel();
        }
        if (mRemoveAnim != null) {
            mRemoveAnim.cancel();
        }
    }

    @Override
    public void reset() {
        mIsDragOut = false;
        isInCircle = false;
        mDragCircle.x = mCenterCircle.x = mWidthHalf;
        mDragCircle.y = mCenterCircle.y = mHeightHalf;
        mDistanceCircles = 0;
        setTranslationX(0);
        setTranslationY(0);
        origX = getX();
        origY = getY();
        postInvalidate();
    }

    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        float value = (float) valueAnimator.getAnimatedValue();
        float dx = (origX - upX);
        float dy = (origY - upY);
        mCenterCircle.x = dx * value + mWidthHalf;
        mCenterCircle.y = dy * value + mHeightHalf;
        setX(upX + dx * (1 - value));
        setY(upY + dy * (1 - value));
        postInvalidate();
    }

    @Override
    public void onAnimationStart(Animator animator) {

    }

    @Override
    public void onAnimationEnd(Animator animator) {
        reset();
        if (mOnPointDragListener != null) {
            mOnPointDragListener.onRecovery(this);
        }
    }

    @Override
    public void onAnimationCancel(Animator animator) {

    }

    @Override
    public void onAnimationRepeat(Animator animator) {

    }
}

這個時候就應該體現這兩個View的關聯了,很顯然,DragPointView是展示在佈局中的。而DragPointViewWindow是咱們用來在window上代替DragPointView實現效果的,而且上述的DragPointView實現相信大家也看見了,其實就是一個軀殼。這裡我定義了一個helper類用來關聯這兩個控制元件的互動邏輯,每個DragPointView都會例項化一個helper類

構造器中將DragPointView存起來,並且設定了setOnTouchListener。大家不知道有沒有過這樣一個疑問?為什麼控制元件有onTouchEvent方法了,還要這個OnTouchListener幹嘛?很簡單,其實就是為了對外開放,這樣我們可以通過這個監聽去接收控制元件的觸控事件,而不是一味的繼承。在監聽中,我們在down事件時將DragPointView隱藏起來,接著DragPointViewWindow例項化並且把DragPointView的屬性一併進行復制,最後把事件直接傳遞給DragPointViewWindow

private Context context;
    private FrameLayout container;
    private DragPointView originView;
    private DragPointViewWindow windowView;
    private OnPointDragListener onPointDragListener;
    private Runnable animRunnable;

    private WindowManager windowManager;
    private WindowManager.LayoutParams windowParams;
    private FrameLayout.LayoutParams layoutParams;

    public DragViewHelper(Context context, final DragPointView originView) {
        this.context = context;
        this.originView = originView;
        this.originView.setOnTouchListener(this);
        animRunnable = new Runnable() {
            @Override
            public void run() {
                windowView.startRemove();
            }
        };
    }

    public void addViewToWindow() {
        if (windowManager == null) {
            windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        }
        if (windowView == null) {
            createWindowView();
        }
        if (windowParams == null ||
                layoutParams == null) {
            initParams();
        }
        if (container == null) {
            container = new FrameLayout(context);
            container.setClipChildren(false);
            container.setClipToPadding(false);
            windowView.setLayoutParams(layoutParams);
            container.addView(windowView, layoutParams);
        }
        int[] ps = new int[2];
        originView.getLocationInWindow(ps);
        layoutParams.setMargins(ps[0], ps[1], 0, 0);
        layoutParams.width = originView.getWidth();
        layoutParams.height = originView.getHeight();
        windowView.setOrigView(originView);
        originView.setDrawingCacheEnabled(true);
        Bitmap bitmap = Bitmap.createBitmap(originView.getDrawingCache());
        originView.setDrawingCacheEnabled(false);
        windowView.setOrigBitmap(bitmap);
        onPointDragListener = originView.getOnPointDragListener();
        windowView.setVisibility(View.VISIBLE);
        if(container.getParent() != null)
            windowManager.removeView(container);
        windowManager.addView(container, windowParams);
        originView.setVisibility(View.INVISIBLE);
    }

    private void createWindowView() {
        windowView = new DragPointViewWindow(context);
        windowView.setCanDrag(originView.isCanDrag());
        windowView.setCenterMinRatio(originView.getCenterMinRatio());
        windowView.setCenterRadius(originView.getCenterRadius());
        windowView.setColorStretching(originView.getColorStretching());
        windowView.setDragRadius(originView.getDragRadius());
        windowView.setClearSign(originView.getClearSign());
        windowView.setSign(originView.getSign());
        windowView.setMaxDragLength(originView.getMaxDragLength());
        windowView.setRecoveryAnimBounce(originView.getRecoveryAnimBounce());
        windowView.setRecoveryAnimDuration(originView.getRecoveryAnimDuration());
        windowView.setRecoveryAnimInterpolator(originView.getRecoveryAnimInterpolator());
        if (originView.getRemoveAnim() != null)
            windowView.setRemoveAnim(originView.getRemoveAnim().setView(windowView));
        windowView.setOnPointDragListener(this);
    }

當然,我們還需要接收DragPointView的狀態監聽,在相應狀態產生的時候做相應的動作

    @Override
    public void onRemoveStart(AbsDragPointView view) {
        if (onPointDragListener != null) {
            onPointDragListener.onRemoveStart(originView);
        }
    }

    @Override
    public void onRemoveEnd(AbsDragPointView view) {
        if (windowManager != null && container != null) {
            windowManager.removeView(container);
        }
        if (onPointDragListener != null) {
            onPointDragListener.onRemoveEnd(originView);
        }
        if (originView != null) {
            originView.setVisibility(View.GONE);
        }
    }

    @Override
    public void onRecovery(AbsDragPointView view) {
        if (windowManager != null && container != null) {
            windowManager.removeView(container);
        }
        if (originView != null) {
            originView.setVisibility(View.VISIBLE);
        }
        if (onPointDragListener != null) {
            onPointDragListener.onRecovery(originView);
        }
    }

在寫Helper的時候有個小問題,就是windowParams.type = WindowManager.LayoutParams.TYPE_TOAST;為什麼用TYPE_TOAST而不是其他的呢?這裡涉及到android 6.0 SYSTEM_ALERT_WINDOW 許可權驗證的問題,使用TYPE_TOAST可以巧妙避開校驗

OK,最後來說說連帶效果的實現。首先,上述中我定義的兩個屬性:sign與clearSign。sign作為某個控制元件的特殊標記,標記所屬類別。而clearSign標記當自身清除時候要連帶清除哪個類別的控制元件。這裡我同樣寫了clear helper類來實現

public class ClearViewHelper {

    private void ClearViewHelper(){}

    public static ClearViewHelper getInstance(){
        return ClearViewHelperHolder.clearViewHelper;
    }

    private SparseArray<Boolean> clearSigning = new SparseArray<>();

    public void clearPointViewBySign(AbsDragPointView dragPointView, String clearSign) {
        List<AbsDragPointView> list = new ArrayList<>();
        list.add(dragPointView);
        getAllPointViewVisible(dragPointView.getRootView(), list, clearSign);
        if (list.contains(dragPointView))
            list.remove(dragPointView);
        list.add(0, dragPointView);
        for (int i = 0; i < list.size() - 1; i++) {
            list.get(i).setNextRemoveView(list.get(i + 1));
        }
        clearSigning.put(clearSign.hashCode(), true);
        list.get(0).startRemove();
    }

    public void clearSignOver(String clearSign) {
        if (TextUtils.isEmpty(clearSign)) return;
        clearSigning.put(clearSign.hashCode(), false);
    }

    public boolean isClearSigning(String clearSign) {
        if (TextUtils.isEmpty(clearSign)) return false;
        Boolean clear = clearSigning.get(clearSign.hashCode());
        return clear == null ? false : clear.booleanValue();
    }

    private void getAllPointViewVisible(View view, List<AbsDragPointView> list, String clearSign) {
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
                View child = ((ViewGroup) view).getChildAt(i);
                getAllPointViewVisible(child, list, clearSign);
            }
        } else if (view instanceof AbsDragPointView) {
            AbsDragPointView v = (AbsDragPointView) view;
            if (v.getVisibility() == View.VISIBLE
                    && clearSign.equals(v.getSign())
                    && !list.contains(view))
                list.add((AbsDragPointView) view);
        }
    }

    private static class ClearViewHelperHolder{
        public static ClearViewHelper clearViewHelper = new ClearViewHelper();
    }

}

ClearViewHelper 是個單例實現,在DragPointViewWindow中UP/CANCEL事件產生後,呼叫upAndCancelEvent()方法

    private void upAndCancelEvent() {
        if (isInCircle && mDistanceCircles == 0) {
            reset();
            if (mOnPointDragListener != null) {
                mOnPointDragListener.onRecovery(this);
            }
        } else if (!mIsDragOut) {
            mCenterCircleCopy.set(mCenterCircle.x, mCenterCircle.y);
            mDragCircleCopy.set(mDragCircle.x, mDragCircle.y);
            if (mRecoveryAnim == null) {
                mRecoveryAnim = ValueAnimator.ofFloat(1.f, -Math.abs(mRecoveryAnimBounce));
                mRecoveryAnim.setDuration(mRecoveryAnimDuration);
                mRecoveryAnim.addUpdateListener(this);
                mRecoveryAnim.addListener(this);
            }
            if (mRecoveryAnimInterpolator != null)
                mRecoveryAnim.setInterpolator(mRecoveryAnimInterpolator);
            mRecoveryAnim.start();
        } else {
            if (mDistanceCircles <= mMaxRadiusTrebling) {
                reset();
                if (mOnPointDragListener != null) {