仿QQ拖動刪除未讀消息個數氣泡

用過手機QQ的應該都看到過,如果有未讀消息,在圖標的右上角會有一個紅色的圓形,裏面有未讀消息的個數,用手指拖動該數字,到一定距離後,鬆開手指,該紅色的圓形會消失,表示這些消息標記爲已讀,如果手指只移動了較小的距離,鬆手會彈回去,先來看下效果圖


左邊的就是手指拖動的數字,右邊的是原始的數字,下面來講下實現過程:

首先明確兩個名詞,固定圓和移動圓,固定圓就是進入頁面時就顯示在界面上的那個數字,如上圖中的數字3,移動圓就是用手指去拖動這個圓時,隨着手指的位置移動的那個圓,也就是上圖中的數字2,在移動的過程中,固定圓的位置不動,但半徑隨着距離拉大不斷變小的,而移動圓的位置隨着手指走,但它的半徑大小是不變的


第一步,首先這裏肯定是用到自定義控件,繼承View即可

public class BounceCircle extends View 


這個自定義類有兩個構造函數

public BounceCircle(Context context, int radius, int circleX, int circleY) {
        super(context);

        this.radius = radius;
        this.circleX = circleX;
        this.circleY = circleY;

        initPaint(context);
    }

    public BounceCircle(Context context, AttributeSet attrs) {
        super(context, attrs);

        initPaint(context);
    }
其中第一個要傳入圓的半徑以及圓心的座標,第二個是系統需要的,下面來看這裏的initPaint方法

private void initPaint(Context context) {
        mContext = context;

        circlePaint = new Paint();
        circlePaint.setColor(Color.RED);
        circlePaint.setAntiAlias(true);

        distanceLimit = Util.dip2px(mContext, distanceLimit);

        textSize = radius;
        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(Util.sp2px(mContext, textSize));
        textFontMetrics = textPaint.getFontMetrics();
        textMove = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2; // drawText從baseline開始,baseline的值爲0,baseline的上面爲負值,baseline的下面爲正值,即這裏ascent爲負值,descent爲正值,比如ascent爲-20,descent爲5,那需要移動的距離就是20 - (20 + 5)/ 2

        path = new Path();
    }

這裏主要是定義了一個Paint和一個TextPaint,其中Paint用來畫圓和圓之間的連接,爲紅色,而TextPaint用來畫數字,爲白色,這裏要注意的一個變量是textMove,之所以用到這個變量,在Android中,setTextAlign可以讓所寫的內容x軸居中,但y軸的居中需要自己處理,drawText從Baseline開始的,而不是從所寫內容的y軸中間開始,baseline上半部的距離爲ascent,下半部分的底的距離爲descent(其實還有一部分內邊距,忽略不計),所以爲了y軸居中,要根據這兩個值算出文字正中間的位置,再移動相應的位置,具體算法註釋中已經說得很清楚了,如果實在理解不了,就記下來,這不是本文的重點


第二步,實現onTouch方法

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (Util.isInCircle(event.getX(), event.getY(), circleX, circleY, radius)) { // 按下的位置必須在圓內才響應後面的操作
                    return true;
                }
                return false;
            case MotionEvent.ACTION_MOVE:
                curX = event.getX();
                curY = event.getY();
                calculateRatio((float) Util.distance(curX, curY, circleX, circleY));
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (ratio > ratioLimit) { // 沒有超出最大移動距離,手擡起時需要讓移動圓回到固定圓的位置
                    shakeAnimation(animationTimes);

                    curX = 0;
                    curY = 0;
                    ratio = 1;
                } else { // 超出最大移動距離
                    needDraw = false;
                    animStart = true;

                    initAnim();

                    // 刪除後的回調
                    if (mFinishListener != null) {
                        mFinishListener.onFinish();
                    }
                }
                break;
        }

        postInvalidate(); // 刷新界面

        return super.onTouchEvent(event);
    }

ACTION_DOWN中,首先我們需要判斷,當前按下的位置是否在固定圓上,比較方法很簡單,判斷按下點的座標到圓心的距離是否大於半徑即可,如果按下位置不在固定圓之內,就直接return false了,也就是不讓自定義控件來處理了,只有按在固定圓上才需要自定義控件繼續處理


在ACTION_MOVE中,不斷獲取當前座標,完後計算縮放比例,這裏的calculateRatio方法定義如下:

/**
     * 計算固定圓縮放的比例
     * @param distance
     * @return
     */
    private void calculateRatio(float distance) {
        ratio = (distanceLimit - distance) / distanceLimit;
    }

參數爲當前手指座標到固定圓圓心的距離,而這裏的distanceLimit是我們規定的一個距離的限值,因爲固定圓和移動圓之間的連接,是隨着距離而不斷變細的,如果細到最後是一條線還沒斷,就沒意義了,這個限值用來計算固定圓縮放的比例,當固定圓縮放到一定比例後,就將固定圓和移動圓之間的連接斷開了


在ACTION_UP和ACTION_CANCEL中,判斷比例是否到達限值,如果到達則播放動畫(就是手指擡起的時候,移動圓消失,有一個爆炸的效果),並且調用回調方法,這個回調方法是提供給使用該自定義控件的人自己發揮的,比如可以修改數據庫,將未讀消息數量置爲0,如果移動距離沒有到限值,鬆手後,要回到固定圓的位置,而且有一個搖晃的動畫,這個動畫用shakeAnimation來實現,參數是抖動的次數

public void shakeAnimation(int counts) {
        // 避免動畫抖動的頻率過大,所以除以2,另外,抖動的方向跟手指滑動的方向要相反
        Animation translateAnimation = new TranslateAnimation((circleX - curX) / 2, 0, (circleY - curY) / 2, 0);
        translateAnimation.setInterpolator(new CycleInterpolator(counts));
        translateAnimation.setDuration(animationTime);
        startAnimation(translateAnimation);
    }
這裏要注意的是抖動的方向是跟移動的方向相反的,因爲這是一個反彈的效果,再就是抖動的範圍我這裏除以2了,避免抖動過大


第三步,實現onDraw方法

上面第二步中,在最後調用了postInvalidate,這用來刷新界面,也就是會執行onDraw方法

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (needDraw) {
            // 畫固定圓
            if (ratio >= ratioLimit) {
                canvas.drawCircle(circleX, circleY, radius * ratio, circlePaint);
            }

            // 畫移動圓和連線
            if (curX != 0 && curY != 0) {
                canvas.drawCircle(curX, curY, radius, circlePaint);
                if (ratio >= ratioLimit) {
                    drawLinePath(canvas);
                }
            }

            // 數字要最後畫,否則會被連線遮掩
            if (curX != 0 && curY != 0) { // 移動圓裏面的數字
                canvas.drawText(message, curX, curY + textMove, textPaint);
            } else { // 只有初始時需要繪製固定圓裏面的數字
                canvas.drawText(message, circleX, circleY + textMove, textPaint);
            }
        }

        if (animStart) { // 動畫進行中
            if (curAnimNumber < animNumber) {
                canvas.drawBitmap(explosionAnim[curAnimNumber], curX - animWidth / 2, curY - animHeight / 2, null);
                curAnimNumber++;
                if (curAnimNumber == 1) { // 第一幀立即執行
                    postInvalidate();
                } else { // 其餘幀每隔固定時間執行
                    postInvalidateDelayed(animInterval);
                }
            } else { // 動畫結束
                animStart = false;
                curAnimNumber = 0;
                recycleBitmap();
            }
        }
    }

在onDraw中,我們要畫的有這麼幾個部分,固定圓,固定圓中的數字,移動圓,移動圓中的數字,還有固定圓和移動圓之間的連接,這裏要注意幾個限定條件,首先,移動距離超過限值時,是不用再繪製固定圓和裏面的數字的,而手指沒有移動時,是不用畫移動圓,裏面數字以及連接的,最後,數字要放在最後面畫,否則會被兩個圓之間的連接所遮掩,這裏我們需要特別關注的是,drawLinePath(canvas);方法,它的實現如下:

/**
     * 畫固定圓和移動圓之間的連線
     * @param canvas
     */
    private void drawLinePath(Canvas canvas) {
        path.reset();

        float distance = (float) Util.distance(circleX, circleY, curX, curY); // 移動圓和固定圓圓心之間的距離
        float sina = (curY - circleY) / distance; // 移動圓圓心和固定圓圓心之間的連線與X軸相交形成的角度的sin值
        float cosa = (circleX - curX) / distance; // 移動圓圓心和固定圓圓心之間的連線與X軸相交形成的角度的cos值

        path.moveTo(circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // A點座標
        path.lineTo(circleX + sina * radius * ratio, circleY + cosa * radius * ratio); // AB連線
        path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, curX + sina * radius, curY + cosa * radius); // 控制點爲兩個圓心的中間點,二階貝塞爾曲線,BC連線
        path.lineTo(curX - sina * radius, curY - cosa * radius); // CD連線
        path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // 控制點也是兩個圓心的中間點,二階貝塞爾曲線,DA連線

        canvas.drawPath(path, circlePaint);
    }

這裏先說明,如果想簡單一點,是不用這樣去畫連接的,完全可以用setStrokeWidth去動態改變畫筆的寬度,這也可以,不過這種連接,沒有我們這裏用多邊形加貝塞爾曲線效果好,下面來說說這個連接怎麼畫:

首先,用一條直線連接固定圓和移動圓的圓心,假設這條直線爲line,完後,在固定圓和移動圓之內,分別畫一條直線和line垂直,完後和圓相交,這樣一來,兩條和line垂直的直線,就和兩個圓有了4個交點,記爲ABCD。

完後我們將這4個點連起來,就會得到一個梯形,這4條邊組成的梯形就是一個Path,我們畫出這個Path,就是我們的移動圓和固定圓之間的連接。我們首先計算移動圓圓心和固定圓圓心之間的距離,完後根據x軸和y軸的差,算出一個正弦和餘弦值,完後根據這個值去算出ABCD四個點的座標,具體座標怎麼算出來的,這裏我就不講了,因爲需要畫圖蠻麻煩,但計算的過程其實挺簡單的,只要學過初中數學,明白正弦和餘弦的意思,拿個紙畫一下,相信馬上可以得出來

另外,這裏還用到了二階貝塞爾曲線,關於貝塞爾曲線的定義,大家自己搜,網上很多,這裏簡單說就是二階的貝塞爾曲線,就是在起點和終點之間有一個控制點,完後根據這個控制點的移動,來畫起點和終點之間的連線,這樣畫出來的連線是有弧度的,效果更好,我們這裏選擇的控制點就是起點和終點之間的中點,取x軸和y軸座標相加的一半


完後這裏還有另外一個動畫,是鬆手後移動圓消失的動畫,這裏用的是幀動畫,一個Bitmap數組,依次播放動畫幀,每幀之間有一個間隔,最後要記得回收Bitmap數組,這個比較簡單,沒有什麼好過多說的


第四步,使用該控件

回到最開始的圖,我們在界面的底部放了一個菜單欄,完後放了兩個自定義的控件,這裏需要注意的是,我一開始將自定義控件是放在佈局xml裏面的,但後來發現這樣一來,我移動它就只能在它本身的大小範圍內,而不能全屏移動,無奈,我改成動態添加了,即在界面加載後,根據我要放的位置,用addView來添加

            int[] position = new int[2];
            messageIcon.getLocationOnScreen(position);

            messageCount = new BounceCircle(this, radius, position[0] + messageIcon.getWidth(), (position[1] - Util.getTopBarHeight(this)));
            messageCount.setNumber("2");
            messageCount.setFinishListener(new BounceCircle.FinishListener() {
                @Override
                public void onFinish() {
                    Toast.makeText(MainActivity.this, "message count dismiss", Toast.LENGTH_LONG).show();
                }
            });
            root.addView(messageCount);

            contactIcon.getLocationOnScreen(position);

            contactCount = new BounceCircle(this, radius, position[0] + contactIcon.getWidth(), (position[1] - Util.getTopBarHeight(this)));
            contactCount.setNumber("3");
            contactCount.setFinishListener(new BounceCircle.FinishListener() {
                @Override
                public void onFinish() {
                    Toast.makeText(MainActivity.this, "contract count dismiss", Toast.LENGTH_LONG).show();
                }
            });
            root.addView(contactCount);

這裏的root是我的根佈局,messageIcon和contactIcon是那個兩個Android的圖標,我相當於,先獲取ImageView圖標的位置,完後再在其右上角addView自定義控件,其實我個人覺得這種方式並不是太好,我想QQ應該是在xml中定義的自定義控件,但這樣弄不知道如何解決移動範圍的問題,如果有人知道還請告知


最後給出完整源碼

MainActivity.java

public class MainActivity extends Activity {
    private RelativeLayout root;
    private BounceCircle messageCount;
    private BounceCircle contactCount;

    private ImageView messageIcon;
    private ImageView contactIcon;
    private int radius = 15; // 圓形半徑

    private boolean init = true;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        root = (RelativeLayout) findViewById(R.id.root);
        messageIcon = (ImageView) findViewById(R.id.message_icon);
        contactIcon = (ImageView) findViewById(R.id.contact_icon);
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        // 只需執行一次,在onWindowFocusChanged方法中才能獲取到控件在屏幕中的座標
        if (init) {
            init = false;

            int[] position = new int[2];
            messageIcon.getLocationOnScreen(position);

            messageCount = new BounceCircle(this, radius, position[0] + messageIcon.getWidth(), (position[1] - Util.getTopBarHeight(this)));
            messageCount.setNumber("2");
            messageCount.setFinishListener(new BounceCircle.FinishListener() {
                @Override
                public void onFinish() {
                    Toast.makeText(MainActivity.this, "message count dismiss", Toast.LENGTH_LONG).show();
                }
            });
            root.addView(messageCount);

            contactIcon.getLocationOnScreen(position);

            contactCount = new BounceCircle(this, radius, position[0] + contactIcon.getWidth(), (position[1] - Util.getTopBarHeight(this)));
            contactCount.setNumber("3");
            contactCount.setFinishListener(new BounceCircle.FinishListener() {
                @Override
                public void onFinish() {
                    Toast.makeText(MainActivity.this, "contract count dismiss", Toast.LENGTH_LONG).show();
                }
            });
            root.addView(contactCount);
        }
    }
}

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:id="@+id/root"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="#CCC"
        >

    <LinearLayout
            android:id="@+id/bottom_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#FFF"
            android:padding="5dp"
            android:layout_alignParentBottom="true"
            >
        <LinearLayout android:layout_width="0dp" android:layout_height="match_parent"
                      android:layout_weight="1" android:gravity="center">
            <RelativeLayout
                    android:layout_width="60dp"
                    android:layout_height="match_parent"
                    >
                <ImageView
                        android:id="@+id/message_icon"
                        android:layout_width="50dp"
                        android:layout_height="50dp"
                        android:layout_centerInParent="true"
                        android:background="@mipmap/ic_launcher"/>
            </RelativeLayout>
        </LinearLayout>
        <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                >
            <ImageView
                    android:id="@+id/contact_icon"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:background="@mipmap/ic_launcher"/>
        </RelativeLayout>
        <RelativeLayout
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                >
            <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:background="@mipmap/ic_launcher"/>
        </RelativeLayout>
    </LinearLayout>
</RelativeLayout>

BounceCircle.java

public class BounceCircle extends View {
    private Context mContext;

    private Paint circlePaint; // 圓形/連線畫筆
    private TextPaint textPaint; // 文字畫筆
    private Paint.FontMetrics textFontMetrics; // 字體
    private Path path;

    private int radius; // 移動圓形半徑
    private float textMove; // 爲了讓文字居中,需要移動的距離

    private float curX; // 當前x座標
    private float curY; // 當前y座標
    private float circleX; // 固定圓的圓心x座標
    private float circleY; // 固定圓的圓心y座標
    private float ratio = 1; // 圓縮放的比例,隨着手指的移動,固定的圓越來越小
    private float ratioLimit = 0.2f; // 固定圓最小的縮放比例,小於該比例時就直接消失
    private int distanceLimit = 100; // 固定圓和移動圓的圓心之間距離的限值,單位DP(配合ratioLimit使用)
    private int textSize; // 字體大小,單位SP

    private int animationTime = 200; // 抖動動畫執行的時間
    private int animationTimes = 1; //  抖動動畫執行次數
    private boolean needDraw = true; // 是否需要執行onDraw方法

    private FinishListener mFinishListener; // 自定義接口,用來回調
    private String message = "1"; // 顯示的數字的初始值

    private Bitmap[] explosionAnim; // 爆炸動畫
    private boolean animStart; // 動畫開始
    private int animNumber = 5; // 動畫幀的個數
    private int curAnimNumber; // 動畫播放的當前幀
    private int animInterval = 200; // 動畫幀之間的間隔
    private int animWidth; // 動畫幀的寬度
    private int animHeight; // 動畫幀的高度

    public BounceCircle(Context context, int radius, int circleX, int circleY) {
        super(context);

        this.radius = radius;
        this.circleX = circleX;
        this.circleY = circleY;

        initPaint(context);
    }

    public BounceCircle(Context context, AttributeSet attrs) {
        super(context, attrs);

        initPaint(context);
    }

    /**
     * 初始化Paint
     * @param context
     */
    private void initPaint(Context context) {
        mContext = context;

        circlePaint = new Paint();
        circlePaint.setColor(Color.RED);
        circlePaint.setAntiAlias(true);

        distanceLimit = Util.dip2px(mContext, distanceLimit);

        textSize = radius;
        textPaint = new TextPaint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setTextSize(Util.sp2px(mContext, textSize));
        textFontMetrics = textPaint.getFontMetrics();
        textMove = -textFontMetrics.ascent - (-textFontMetrics.ascent + textFontMetrics.descent) / 2; // drawText從baseline開始,baseline的值爲0,baseline的上面爲負值,baseline的下面爲正值,即這裏ascent爲負值,descent爲正值,比如ascent爲-20,descent爲5,那需要移動的距離就是20 - (20 + 5)/ 2

        path = new Path();
    }

    /**
     * 初始化爆炸動畫
     */
    private void initAnim() {
        explosionAnim = new Bitmap[animNumber];
        explosionAnim[0] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_one);
        explosionAnim[1] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_two);
        explosionAnim[2] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_three);
        explosionAnim[3] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_four);
        explosionAnim[4] = BitmapFactory.decodeResource(getResources(), R.mipmap.explosion_five);

        // 動畫每幀的長寬都是一樣的,取一個即可
        animWidth = explosionAnim[0].getWidth();
        animHeight = explosionAnim[0].getHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (needDraw) {
            // 畫固定圓
            if (ratio >= ratioLimit) {
                canvas.drawCircle(circleX, circleY, radius * ratio, circlePaint);
            }

            // 畫移動圓和連線
            if (curX != 0 && curY != 0) {
                canvas.drawCircle(curX, curY, radius, circlePaint);
                if (ratio >= ratioLimit) {
                    drawLinePath(canvas);
                }
            }

            // 數字要最後畫,否則會被連線遮掩
            if (curX != 0 && curY != 0) { // 移動圓裏面的數字
                canvas.drawText(message, curX, curY + textMove, textPaint);
            } else { // 只有初始時需要繪製固定圓裏面的數字
                canvas.drawText(message, circleX, circleY + textMove, textPaint);
            }
        }

        if (animStart) { // 動畫進行中
            if (curAnimNumber < animNumber) {
                canvas.drawBitmap(explosionAnim[curAnimNumber], curX - animWidth / 2, curY - animHeight / 2, null);
                curAnimNumber++;
                if (curAnimNumber == 1) { // 第一幀立即執行
                    postInvalidate();
                } else { // 其餘幀每隔固定時間執行
                    postInvalidateDelayed(animInterval);
                }
            } else { // 動畫結束
                animStart = false;
                curAnimNumber = 0;
                recycleBitmap();
            }
        }
    }

    /**
     * 回收Bitmap資源
     */
    private void recycleBitmap() {
        if (explosionAnim != null && explosionAnim.length != 0) {
            for (int i = 0; i < explosionAnim.length; i++) {
                if (explosionAnim[i] != null && !explosionAnim[i].isRecycled()) {
                    explosionAnim[i].recycle();
                    explosionAnim[i] = null;
                }
            }
        }
    }

    /**
     * 畫固定圓和移動圓之間的連線
     * @param canvas
     */
    private void drawLinePath(Canvas canvas) {
        path.reset();

        float distance = (float) Util.distance(circleX, circleY, curX, curY); // 移動圓和固定圓圓心之間的距離
        float sina = (curY - circleY) / distance; // 移動圓圓心和固定圓圓心之間的連線與X軸相交形成的角度的sin值
        float cosa = (circleX - curX) / distance; // 移動圓圓心和固定圓圓心之間的連線與X軸相交形成的角度的cos值

        path.moveTo(circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // A點座標
        path.lineTo(circleX + sina * radius * ratio, circleY + cosa * radius * ratio); // AB連線
        path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, curX + sina * radius, curY + cosa * radius); // 控制點爲兩個圓心的中間點,二階貝塞爾曲線,BC連線
        path.lineTo(curX - sina * radius, curY - cosa * radius); // CD連線
        path.quadTo((circleX + curX) / 2, (circleY + curY) / 2, circleX - sina * radius * ratio, circleY - cosa * radius * ratio); // 控制點也是兩個圓心的中間點,二階貝塞爾曲線,DA連線

        canvas.drawPath(path, circlePaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch(event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (Util.isInCircle(event.getX(), event.getY(), circleX, circleY, radius)) { // 按下的位置必須在圓內才響應後面的操作
                    return true;
                }
                return false;
            case MotionEvent.ACTION_MOVE:
                curX = event.getX();
                curY = event.getY();
                calculateRatio((float) Util.distance(curX, curY, circleX, circleY));
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (ratio > ratioLimit) { // 沒有超出最大移動距離,手擡起時需要讓移動圓回到固定圓的位置
                    shakeAnimation(animationTimes);

                    curX = 0;
                    curY = 0;
                    ratio = 1;
                } else { // 超出最大移動距離
                    needDraw = false;
                    animStart = true;

                    initAnim();

                    // 刪除後的回調
                    if (mFinishListener != null) {
                        mFinishListener.onFinish();
                    }
                }
                break;
        }

        postInvalidate(); // 刷新界面

        return super.onTouchEvent(event);
    }

    /**
     * 計算固定圓縮放的比例
     * @param distance
     * @return
     */
    private void calculateRatio(float distance) {
        ratio = (distanceLimit - distance) / distanceLimit;
    }

    /**
     * 抖動動畫
     * @param counts
     */
    public void shakeAnimation(int counts) {
        // 避免動畫抖動的頻率過大,所以除以2,另外,抖動的方向跟手指滑動的方向要相反
        Animation translateAnimation = new TranslateAnimation((circleX - curX) / 2, 0, (circleY - curY) / 2, 0);
        translateAnimation.setInterpolator(new CycleInterpolator(counts));
        translateAnimation.setDuration(animationTime);
        startAnimation(translateAnimation);
    }

    public interface FinishListener {
        void onFinish();
    }

    public void setFinishListener(FinishListener finishListener) {
        mFinishListener = finishListener;
    }

    /**
     * 設置顯示的數字
     * @param message
     */
    public void setNumber(String message) {
        this.message = message;
    }
}


下載地址:http://download.csdn.net/detail/gesanri/9098837,注意,我是用Android Studio開發的,如果想在Eclipse中運行,請新建目錄並把相關文件拷貝到對應位置,如果自己建的項目包名跟我不一樣,要記得在xml中改成自己的包名,另外Eclipse沒有mipmap,代碼中mipmap要改成drawable,並將動畫資源放入drawable


最新:動態添加自定義控件的問題已解決,參考仿QQ拖動刪除未讀消息個數氣泡之二

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章