Android Path进阶之Path路径实现QQ拖拽气泡效果_path 气泡_Android Coder的博客-程序员宅基地

技术标签: # UI  Path进阶  气泡拖拽爆炸  QQ拖拽气泡  

先上效果图

先将这个过程分状态(阶段):

    1.静止状态,小球的默认状态,由红色的气泡和中间的文本组成;

    2.连接状态(拖拽半径短),拖拽过程中由随着手指一定的气泡和中间不断缩小的小气泡组成;

    3.分离状态(拖拽半径长),中间的小气泡消失,连接的中间线也消失;

    4.回弹或隐藏状态,当从连接状态松手,移动的气泡会回弹到中心,从分离状态松手,移动的气泡爆炸,整个页面隐藏。

1.实现思路

自定义一个DragBubbleView继承View,静止、连接和分离状态都是需要绘制一个大的可动气泡,只是坐标不同,连接状态时,首先随着手指的移动,改变可动气泡的坐标,同时在中心绘制固定气泡,固定气泡的圆心随拖拽半径的变大二变小,大气泡和小气泡中间连接处是两条二阶贝赛尔曲线,贝塞尔曲线可以参考上一小节总结 Android Path进阶之Path常用API,第一条二阶贝塞尔曲线的两个数据点分别是A和B,第二条二阶贝塞尔曲线的两个数据点分别是C和D,两条贝塞尔曲线的控制点都是大气泡和小气泡的圆心连线的中点G,然后两条贝塞尔曲线和两条直线BC和DA闭和,外加大圆和小圆的部分,3部分刚好形成连接状态时的图形绘制,分离状态只需要绘制大气泡,爆炸时使用5张连续的图片顺序播放形成爆炸效果。

2.初始化

对过程进行了分解之后开始实现,首先确定需要使用到的变量,并会其初始化。一只画笔绘制可动气泡中心文本,一只画笔绘制两个气泡和中心不规则图形,一只画笔绘制爆炸时效果,一个路径(Path)用来记录两个气泡中间连接不规则的图形,然后在构造方法中对画笔和路径进行初始化,在onSizeChange中对坐标点初始化。

public class DragBubbleView extends View {

    private static final int BUBBLE_STATE_DEFAULT = 0;//气泡默认状态--静止
    private static final int BUBBLE_STATE_CONNECT = 1;//气泡相连
    private final int BUBBLE_STATE_APART = 2;//气泡分离
    private final int BUBBLE_STATE_DISMISS = 3;//气泡消失
    private float mBubbleRadius = 60;//气泡半径
    private int mBubbleColor = Color.RED;//气泡颜色
    private String mTextStr = "99+";//气泡消息文字
    private int mTextColor = Color.WHITE;//气泡消息文字颜色
    private float mTextSize = 60;//气泡消息文字大小
    private float mBubFixedRadius = mBubbleRadius;//不动气泡的半径
    private float mBubMovableRadius = mBubbleRadius;//可动气泡的半径
    private PointF mBubFixedCenter;//不动气泡的圆心
    private PointF mBubMovableCenter;//可动气泡的圆心
    private Paint mBubblePaint;//气泡的画笔
    private Path mBezierPath;//贝塞尔曲线path
    private Paint mTextPaint;//文本绘制画笔
    private Rect mTextRect;//文本绘制区域
    private Paint mBurstPaint;//气泡爆炸消失画笔
    private Rect mBurstRect;//爆炸绘制区域
    private int mBubbleState = BUBBLE_STATE_DEFAULT;//气泡状态标志
    private float mDist;//两气泡圆心距离
    private float mMaxDist = 4 * mBubbleRadius;//气泡相连状态最大圆心距离
    private float MOVE_OFFSET = 2 * mBubbleRadius;//手指触摸偏移量
    private Bitmap[] mBurstBitmapsArray;//气泡爆炸的bitmap数组
    private boolean mIsBurstAnimStart = false;//是否在执行气泡爆炸动画
    private int mCurDrawableIndex;//当前气泡爆炸图片index

    //气泡爆炸的图片id数组
    private int[] mBurstDrawablesArray = {
            R.drawable.burst_1,
            R.drawable.burst_2,
            R.drawable.burst_3,
            R.drawable.burst_4,
            R.drawable.burst_5
    };

    public DragBubbleView(Context context) {
        this(context, null);
    }

    public DragBubbleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化气泡画笔
        mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBubblePaint.setColor(mBubbleColor);
        mBubblePaint.setStyle(Paint.Style.FILL);

        //初始化文本绘制画笔
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextRect = new Rect();

        //初始化爆炸画笔
        mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBurstPaint.setFilterBitmap(true);

        //初始化贝赛尔曲线path
        mBezierPath = new Path();

        mBurstRect = new Rect();

        //将气泡爆炸的drawable转为bitmap
        mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
        for (int i = 0; i < mBurstDrawablesArray.length; i++) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
            mBurstBitmapsArray[i] = bitmap;
        }

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        //不动气泡圆心
        if (mBubFixedCenter == null) {
            mBubFixedCenter = new PointF(w / 2, h / 2);
        } else {
            mBubFixedCenter.set(w / 2, h / 2);
        }

        //可动气泡圆心
        if (mBubMovableCenter == null) {
            mBubMovableCenter = new PointF(w / 2, h / 2);
        } else {
            mBubMovableCenter.set(w / 2, h / 2);
        }
    }
}

3.气泡状态改变与触摸事件监听

气泡的各种状态的改变都是通过手指在屏幕的状态决定的,因此需要对触摸事件进行监听。

1.DOWN事件。气泡的默认状态是静止状态,在Down事件时判断下是不是手指的坐标是否在大气泡的半径内,如果在,则将气泡状态改为连接状态,如果不是则不作处理,即保持原来的默认静止状态

2.MOVE事件。Move事件先判断如果气泡是隐藏状态则不作处理,如果不是,求出固定气泡圆心和移动坐标之间的距离,如果该距离小于我们定的临界值,则认为气泡还处于连接状态,如果该距离大于我们定的临界值,则将气泡的状态改为分离状态,同时设置移动气泡的坐标为移动的坐标,并改变固定气泡的半径,最后别忘了调用invalidate方法进行重绘。

3.UP事件。UP事件时我们只需要判断下气泡当前的状态。如果当前气泡是连接状态,我们需要将移动气泡重置到中心,并将气泡状态设置为默认状态。如果当前气泡的状态为断开状态,我们只需要执行爆炸动画,并将气泡状态设置为默认状态。气泡重置动画和爆炸动画后面讲。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mBubbleState != BUBBLE_STATE_DISMISS) {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    Log.d(TAG, "mDist: " + (mDist < mBubbleRadius + MOVE_OFFSET));
                    if (mDist < mBubbleRadius + MOVE_OFFSET) {
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    } else {
                        mBubbleState = BUBBLE_STATE_DEFAULT;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "mBubbleState: " + mBubbleState);
                if (mBubbleState != BUBBLE_STATE_DISMISS && mBubbleState != BUBBLE_STATE_DEFAULT) {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    Log.d(TAG, "mDist: " + mDist);
                    mBubMovableCenter.set(event.getX(), event.getY());//设置移动气泡的坐标
                    mBubFixedRadius = mBubbleRadius - mDist / 8;//缩小固定气泡的半径
                    if (mDist < mMaxDist + MOVE_OFFSET) {
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    } else {
                        mBubbleState = BUBBLE_STATE_APART;//将气泡变为断开状态
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                if (mBubbleState == BUBBLE_STATE_CONNECT) {
                    startBubbleResetAnim();//回弹效果
                } else if (mBubbleState == BUBBLE_STATE_APART) {
                    startBubbleBurstAnim();//爆炸动画
                }
                break;
        }
        return true;
    }

4.绘制

绘制是在ondraw方法中进行的,上面已经对气泡的每种状态进行了设置。接下来就是根据不同的状态绘制不同的状态。

1.默认静止状态、连接状态和分离状态大气泡,因为这三种状态都有一个大气泡,因此可以放一起绘制,因此只要不是隐藏状态,都需要绘制。

        //一个气泡加消息数据
        if (mBubbleState != BUBBLE_STATE_DISMISS) {
            canvas.drawCircle(mBubMovableCenter.x, mBubMovableCenter.y, mBubMovableRadius, mBubblePaint);
            mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
            canvas.drawText(mTextStr, mBubMovableCenter.x - mTextRect.width() / 2, mBubMovableCenter.y + mTextRect.height() / 2, mTextPaint);
        }

2.连接状态小圆和不规则图形的绘制。连接状态时移动气泡已经在上面绘制了,还剩下小圆的绘制和不规则图形的绘制,小圆的绘制简单,直接绘制。不规则图形的绘制主要是求控制点的坐标和A、B、C、D四点的坐标,然后利用Path形成闭和路径,然后绘制Path。

        if (mBubbleState == BUBBLE_STATE_CONNECT) {

            //绘制不动圆
            canvas.drawCircle(mBubFixedCenter.x, mBubFixedCenter.y, mBubFixedRadius, mBubblePaint);

            //绘制贝塞尔曲线

            //控制点坐标
            float ctrlX = (mBubFixedCenter.x + mBubMovableCenter.x) / 2;
            float ctrlY = (mBubFixedCenter.y + mBubMovableCenter.y) / 2;

            float sinTheta = (mBubMovableCenter.y - mBubFixedCenter.y) / mDist;
            float cosTheta = (mBubMovableCenter.x - mBubFixedCenter.x) / mDist;

            //B
            float iBubMovableStartX = mBubMovableCenter.x + sinTheta * mBubMovableRadius;
            float iBubMovableStartY = mBubMovableCenter.y - cosTheta * mBubMovableRadius;

            //A
            float iBubFixedEndX = mBubFixedCenter.x + mBubFixedRadius * sinTheta;
            float iBubFixedEndY = mBubFixedCenter.y - mBubFixedRadius * cosTheta;

            //D
            float iBubFixedStartX = mBubFixedCenter.x - mBubFixedRadius * sinTheta;
            float iBubFixedStartY = mBubFixedCenter.y + mBubFixedRadius * cosTheta;
            //C
            float iBubMovableEndX = mBubMovableCenter.x - mBubMovableRadius * sinTheta;
            float iBubMovableEndY = mBubMovableCenter.y + mBubMovableRadius * cosTheta;

            mBezierPath.reset();
            mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY);
            mBezierPath.quadTo(ctrlX, ctrlY, iBubMovableEndX, iBubMovableEndY);//二阶贝塞尔曲线
            mBezierPath.lineTo(iBubMovableStartX, iBubMovableStartY);//一阶贝赛尔曲线
            mBezierPath.quadTo(ctrlX, ctrlY, iBubFixedEndX, iBubFixedEndY);//二阶贝塞尔曲线
            mBezierPath.close();//闭和曲线

            canvas.drawPath(mBezierPath, mBubblePaint);//绘制两圆中间的区域

        }

3.重置回弹动画。UP事件后可能会执行重置回弹动画或者爆炸效果,先来说说回弹动画,回弹动画,实际上是移动气泡的坐标从UP之后的坐标回到固定气泡的过程,因此可以采用属性动画实现,并且在动画结束前会有回弹效果,因此我们对属性动画设置差值器设置为OvershootInterpolator,该方法在UP事件时被调用。

    private void startBubbleResetAnim() {
        ValueAnimator animator = ValueAnimator.ofObject(new PointFEvaluator(), mBubMovableCenter, mBubFixedCenter);
        animator.setDuration(300);
        animator.setInterpolator(new OvershootInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBubMovableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mBubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        animator.start();
    }

4.爆炸效果。在断开状态松手,会实现一个爆炸效果,爆炸效果实际上是5张静图的顺序播放,这里也采用属性动画实现,动画的变化值为int,大小从0变化到爆炸数组的length。

    private void startBubbleBurstAnim() {
        mBubbleState = BUBBLE_STATE_DISMISS;
        ValueAnimator animator = ValueAnimator.ofInt(0, mBurstBitmapsArray.length);
        animator.setDuration(1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurDrawableIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

值变了之后还要重绘,因此ondraw方法中也需要做相应的处理

        if (mBubbleState == BUBBLE_STATE_DISMISS && mCurDrawableIndex < mBurstBitmapsArray.length) {
            mBurstRect.set((int) (mBubMovableCenter.x - mBubMovableRadius),
                    (int) (mBubMovableCenter.y - mBubMovableRadius),
                    (int) (mBubMovableCenter.x + mBubMovableRadius),
                    (int) (mBubMovableCenter.y + mBubMovableRadius)
            );
            canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null, mBurstRect, mBurstPaint);
        }

好了,讲完了,核心其实在连接状态的绘制,完整代码:

public class DragBubbleView extends View {

    private static final int BUBBLE_STATE_DEFAULT = 0;//气泡默认状态--静止
    private static final int BUBBLE_STATE_CONNECT = 1;//气泡相连
    private static final String TAG = "DragBubbleView";
    private final int BUBBLE_STATE_APART = 2;//气泡分离
    private final int BUBBLE_STATE_DISMISS = 3;//气泡消失
    private float mBubbleRadius = 60;//气泡半径
    private int mBubbleColor = Color.RED;//气泡颜色
    private String mTextStr = "99+";//气泡消息文字
    private int mTextColor = Color.WHITE;//气泡消息文字颜色
    private float mTextSize = 60;//气泡消息文字大小
    private float mBubFixedRadius = mBubbleRadius;//不动气泡的半径
    private float mBubMovableRadius = mBubbleRadius;//可动气泡的半径
    private PointF mBubFixedCenter;//不动气泡的圆心
    private PointF mBubMovableCenter;//可动气泡的圆心
    private Paint mBubblePaint;//气泡的画笔
    private Path mBezierPath;//贝塞尔曲线path
    private Paint mTextPaint;//文本绘制画笔
    private Rect mTextRect;//文本绘制区域
    private Paint mBurstPaint;//气泡爆炸消失画笔
    private Rect mBurstRect;//爆炸绘制区域
    private int mBubbleState = BUBBLE_STATE_DEFAULT;//气泡状态标志
    private float mDist;//两气泡圆心距离
    private float mMaxDist = 4 * mBubbleRadius;//气泡相连状态最大圆心距离
    private float MOVE_OFFSET = 2 * mBubbleRadius;//手指触摸偏移量
    private Bitmap[] mBurstBitmapsArray;//气泡爆炸的bitmap数组
    private boolean mIsBurstAnimStart = false;//是否在执行气泡爆炸动画
    private int mCurDrawableIndex;//当前气泡爆炸图片index

    //气泡爆炸的图片id数组
    private int[] mBurstDrawablesArray = {
            R.drawable.burst_1,
            R.drawable.burst_2,
            R.drawable.burst_3,
            R.drawable.burst_4,
            R.drawable.burst_5
    };

    public DragBubbleView(Context context) {
        this(context, null);
    }

    public DragBubbleView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化气泡画笔
        mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBubblePaint.setColor(mBubbleColor);
        mBubblePaint.setStyle(Paint.Style.FILL);

        //初始化文本绘制画笔
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextRect = new Rect();

        //初始化爆炸画笔
        mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBurstPaint.setFilterBitmap(true);

        //初始化贝赛尔曲线path
        mBezierPath = new Path();

        mBurstRect = new Rect();

        //将气泡爆炸的drawable转为bitmap
        mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length];
        for (int i = 0; i < mBurstDrawablesArray.length; i++) {
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
            mBurstBitmapsArray[i] = bitmap;
        }

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        //不动气泡圆心
        if (mBubFixedCenter == null) {
            mBubFixedCenter = new PointF(w / 2, h / 2);
        } else {
            mBubFixedCenter.set(w / 2, h / 2);
        }

        //可动气泡圆心
        if (mBubMovableCenter == null) {
            mBubMovableCenter = new PointF(w / 2, h / 2);
        } else {
            mBubMovableCenter.set(w / 2, h / 2);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //1,静止状态,一个气泡加消息数据
        //2, 连接状态,一个气泡加消息数据,贝塞尔曲线,本身位置上气泡,大小可变化
        //3,分离状态,一个气泡加消息数据
        //4,消失状态,爆炸效果
        Log.d(TAG, "onDraw mBubbleState: " + mBubbleState);
        if (mBubbleState == BUBBLE_STATE_CONNECT) {

            //绘制不动圆
            canvas.drawCircle(mBubFixedCenter.x, mBubFixedCenter.y, mBubFixedRadius, mBubblePaint);

            //绘制贝塞尔曲线

            //控制点坐标
            float ctrlX = (mBubFixedCenter.x + mBubMovableCenter.x) / 2;
            float ctrlY = (mBubFixedCenter.y + mBubMovableCenter.y) / 2;

            float sinTheta = (mBubMovableCenter.y - mBubFixedCenter.y) / mDist;
            float cosTheta = (mBubMovableCenter.x - mBubFixedCenter.x) / mDist;

            //B
            float iBubMovableStartX = mBubMovableCenter.x + sinTheta * mBubMovableRadius;
            float iBubMovableStartY = mBubMovableCenter.y - cosTheta * mBubMovableRadius;

            //A
            float iBubFixedEndX = mBubFixedCenter.x + mBubFixedRadius * sinTheta;
            float iBubFixedEndY = mBubFixedCenter.y - mBubFixedRadius * cosTheta;

            //D
            float iBubFixedStartX = mBubFixedCenter.x - mBubFixedRadius * sinTheta;
            float iBubFixedStartY = mBubFixedCenter.y + mBubFixedRadius * cosTheta;
            //C
            float iBubMovableEndX = mBubMovableCenter.x - mBubMovableRadius * sinTheta;
            float iBubMovableEndY = mBubMovableCenter.y + mBubMovableRadius * cosTheta;

            mBezierPath.reset();
            mBezierPath.moveTo(iBubFixedStartX, iBubFixedStartY);
            mBezierPath.quadTo(ctrlX, ctrlY, iBubMovableEndX, iBubMovableEndY);//二阶贝塞尔曲线
            mBezierPath.lineTo(iBubMovableStartX, iBubMovableStartY);//一阶贝赛尔曲线
            mBezierPath.quadTo(ctrlX, ctrlY, iBubFixedEndX, iBubFixedEndY);//二阶贝塞尔曲线
            mBezierPath.close();//闭和曲线

            canvas.drawPath(mBezierPath, mBubblePaint);//绘制两圆中间的区域

        }

        //一个气泡加消息数据
        if (mBubbleState != BUBBLE_STATE_DISMISS) {
            canvas.drawCircle(mBubMovableCenter.x, mBubMovableCenter.y, mBubMovableRadius, mBubblePaint);
            mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect);
            canvas.drawText(mTextStr, mBubMovableCenter.x - mTextRect.width() / 2, mBubMovableCenter.y + mTextRect.height() / 2, mTextPaint);
        }

        //爆炸效果
        if (mBubbleState == BUBBLE_STATE_DISMISS && mCurDrawableIndex < mBurstBitmapsArray.length) {
            mBurstRect.set((int) (mBubMovableCenter.x - mBubMovableRadius),
                    (int) (mBubMovableCenter.y - mBubMovableRadius),
                    (int) (mBubMovableCenter.x + mBubMovableRadius),
                    (int) (mBubMovableCenter.y + mBubMovableRadius)
            );
            canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null, mBurstRect, mBurstPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mBubbleState != BUBBLE_STATE_DISMISS) {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    Log.d(TAG, "mDist: " + (mDist < mBubbleRadius + MOVE_OFFSET));
                    if (mDist < mBubbleRadius + MOVE_OFFSET) {
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    } else {
                        mBubbleState = BUBBLE_STATE_DEFAULT;
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "mBubbleState: " + mBubbleState);
                if (mBubbleState != BUBBLE_STATE_DISMISS && mBubbleState != BUBBLE_STATE_DEFAULT) {
                    mDist = (float) Math.hypot(event.getX() - mBubFixedCenter.x, event.getY() - mBubFixedCenter.y);
                    Log.d(TAG, "mDist: " + mDist);
                    mBubMovableCenter.set(event.getX(), event.getY());//设置移动气泡的坐标
                    mBubFixedRadius = mBubbleRadius - mDist / 8;//缩小固定气泡的半径
                    if (mDist < mMaxDist + MOVE_OFFSET) {
                        mBubbleState = BUBBLE_STATE_CONNECT;
                    } else {
                        mBubbleState = BUBBLE_STATE_APART;//将气泡变为断开状态
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                if (mBubbleState == BUBBLE_STATE_CONNECT) {
                    startBubbleResetAnim();//回弹效果
                } else if (mBubbleState == BUBBLE_STATE_APART) {
                    startBubbleBurstAnim();//爆炸动画
                }
                break;
        }
        return true;
    }

    //回弹动画
    private void startBubbleResetAnim() {
        ValueAnimator animator = ValueAnimator.ofObject(new PointFEvaluator(), mBubMovableCenter, mBubFixedCenter);
        animator.setDuration(300);
        animator.setInterpolator(new OvershootInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mBubMovableCenter = (PointF) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mBubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        animator.start();
    }

    //爆炸动画
    private void startBubbleBurstAnim() {
        mBubbleState = BUBBLE_STATE_DISMISS;
        ValueAnimator animator = ValueAnimator.ofInt(0, mBurstBitmapsArray.length * 2);
        animator.setDuration(1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mCurDrawableIndex = (int) animation.getAnimatedValue();
                invalidate();
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mBubMovableCenter.set(mBubFixedCenter.x, mBubFixedCenter.y);
                mBubbleState = BUBBLE_STATE_DEFAULT;
            }
        });
        animator.start();
    }

}

Demo:https://github.com/987570437/PaintDemo

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zb987570437/article/details/100673252

智能推荐

EBS API:附件批量导入_Acenol的博客-程序员宅基地

附件相关表附件主要视图:fnd_attached_docs_form_vl附件主要表:fnd_attached_documents附件表:fnd_documents附件数据类型表:fnd_document_datatypes附件短文本表:fnd_documents_short_text附件长文本表:fnd_documents_long_text附件文件表:fnd_lobsRelat...

Java中的ContentPane_weixin_30515513的博客-程序员宅基地

getContentPanepublic Container getContentPane()返回此窗体的 contentPane 对象API格式是这样写的,我觉像是除去边框,保存里面的内容!转载于:https://www.cnblogs.com/WillAndInsist/archive/2010/03/17/1687940.html...

WinDbg神断点_weixin_30521649的博客-程序员宅基地

https://blogs.msdn.microsoft.com/alejacma/2007/10/31/cryptoapi-tracer-script/我得多少年才能学会这种写法。转载于:https://www.cnblogs.com/suanguade/p/6119805.html

iOS开发——基础篇——assign,copy,retain之间的区别以及weak和strong的区别_weixin_30566111的博客-程序员宅基地

@property (nonatomic, assign) NSString *title;什么是assign,copy,retain之间的区别?assign: 简单赋值,不更改索引计数(Reference Counting)。copy: 建立一个索引计数为1的对象,然后释放旧对象retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1...

iOS-UIWebView加载HTMLString图片显示超过屏幕宽度,导致webView可以左右滑动处的理方法..._weixin_30480583的博客-程序员宅基地

修改webview图片适配的代码,是使用js去修改图片适配屏幕宽度:// 网络请求加载的数据,进行字典转模型NSDictionary *dict = [result objectForKey:@"data"];HQNewsDetailModel *model = [HQNewsDetailModel mj_objectWithKeyValues:dict];/**...

Linux递归删除文件_weixin_30414245的博客-程序员宅基地

后台目前每个模块生成的目标文件都放在make目录下面,逐个去删除很麻烦,需要在src这一级目录一次性删除所有*.o文件;find . -name "*.o" -exec rm {} \;命令说明:可以上面的rm换成其他命令如ls;转载于:https://www.cnblogs.com/skiing886/p/7608601.html...

随便推点

SVN 教程_weixin_30492601的博客-程序员宅基地

SAE自2011-7-10日起,将全面支持SVN代码部署,用户不仅可以通过任何SVN客户端部署代码,而且SAE现有的代码部署方式也已经对接应用的SVN仓库,即使不使用SVN客户端部署代码,也保证了代码版本。 如:通过在线代码编辑器(http://sdk.tools.sinaapp.com)等方式对代码的修改、部署等操作也会像svn commit一样,产生一个新版本。通过SVN客户...

C#线程优先级浅析_1361976860的博客-程序员宅基地

 C#线程优先级的必要性:如果在应用程序中有多个线程在运行,但一些线程比另一些线程重要,该怎么办在这种情况下,可以在一个进程中为不同的线程指定不同的优先级。一般情况下,如果有优先级较高的线程在工作,就不会给优先级较低的线程分配任何时间片,其优点是可以保证给接收用户输入的线程指定较高的优先级。在大多数的时间内,这个线程什么也不做,而其他线程则执行它们的任务。但是,如果用户输入了信息,这个线程就立即获...

centos6.4安装java_CentOS6.4 64位系统安装jdk_研究生欧阳同学呀的博客-程序员宅基地

1. CentOS操作安装好了以后,系统自带了openJDK,先查看相关的安装信息:$rpm -qa | grep javatzdata-java-2013b-1.el6.noarchjava-1.6.0-openjdk-1.6.0.0-1.61.1.11.11.el6_4.x86_64java-1.7.0-openjdk-1.7.0.19-2.3.9.1.el6_4.x86_642. 可以用ja...

【Oracle】RAC添加新节点_weixin_30755709的博客-程序员宅基地

RAC添加节点:环境:OS:OEL5.6RAC:10.2.0.1.0原有rac1,rac2两个节点。如今要添加rac3节点:操作过程:改动三个节点上的/etc/hosts文件192.168.90.2rac1192.168.90.5rac2192.168.90.6rac3192.168.91.3rac1-priv192.168....

一些常用的WebService._Claire_ljy的博客-程序员宅基地

方便学习之用...天气预报Web服务,数据来源于中国气象局EndpointDiscoWSDLIP地址来源搜索 WEB 服务(是目前最完整的IP地址数据)EndpointDiscoWSDL随机英文、数字和中文简体字 WEB 服务EndpointDiscoWSDL中国邮政编码 &lt;-&gt; ...

linux 内核GPIO 模拟 I2C_cainiao_learn的博客-程序员宅基地

转载地址:http://wenku.baidu.com/view/015c8549c850ad02df804105.html一、应用背景在许多情况下,我们并没有足够的I2C总线,本文主在介绍如何利用Linux内核中的i2c-gpio模块,利用2条GPIO线模拟i2c总线,并挂载设备二、思路先通过对i2c-gpio所定义的结构体初始化(包括初始化i2c的2条线,频