仿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拖动删除未读消息个数气泡之二

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以通过使用Ant Design Vue的Upload组件和Dragger组件实现图片上传并拖拽位置。具体代码示例如下: ``` <template> <div> <a-upload :action="uploadUrl" :before-upload="beforeUpload" :on-success="onSuccess" > <a-icon type="upload" /> Click to Upload </a-upload> <a-dragger :action="uploadUrl" :before-upload="beforeUpload" :on-success="onSuccess" > <p class="ant-upload-drag-icon"> <a-icon type="inbox" /> </p> <p class="ant-upload-text">Click or drag file to this area to upload</p> <p class="ant-upload-hint">Support for a single or bulk upload.</p> </a-dragger> <img :src="imageUrl" width="200" height="200" style="margin-top: 20px;"> </div> </template> <script> export default { data() { return { imageUrl: '', uploadUrl: 'your-upload-server-url' }; }, methods: { beforeUpload(file) { // 在这里可以对文件进行校验,例如大小、类型等 console.log('before upload', file); }, onSuccess(response, file, fileList) { // 上传成功后的回调函数 console.log('upload success', response, file, fileList); this.imageUrl = URL.createObjectURL(file.raw); } } }; </script> ``` 在这段代码中,我们使用了`a-upload`组件和`a-dragger`组件来展示图片上传的界面。`a-upload`组件可以通过单击上传按钮或者拖拽到上传区域来完成文件上传;`a-dragger`组件则是将上传区域改为了一个拖拽框,视频等其他多格式的文件也能方便的拖拽上传。同时,我们在`beforeUpload`函数中可以对文件进行校验,并在`onSuccess`函数中处理上传成功后的事件。最后设置`imageUrl`的值为上传图片的预览链接即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值