【android UI学习】QQ未读消息粘性动画

mac2025-10-05  2

方法简介

先看一下效果, 这里是模仿qq未读消息,清空消息动画效果,主要也是前面讲解了贝塞尔曲线的运用实战

 

下面我们来计算一下其中各个点的坐标位置

 

AB,CD这两条线是通过贝塞尔曲线绘制得出的,

绘制AB线,我们需要得到A,B,Anchor三个点的坐标绘制CD线,我们需要得到C,D,Anchor三个点的坐标

得到AB,CD两条线以后,我们就可以通过path方式,画出来了,分别对应代码中的

// 计算控制点坐标,两个圆心的中点 int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2); int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2); //∠θ float cosTheta = (mBubMoveableCenter.x-mBubStillCenter.x) / mDist; float sinTheta = (mBubStillCenter.y-mBubMoveableCenter.y) / mDist; //A float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta; float iBubStillStartY = mBubStillCenter.y - mBubStillRadius * cosTheta; //D float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta; float iBubStillEndY = mBubStillCenter.y + mBubStillRadius * cosTheta; //C float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta; float iBubMoveableStartY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta; //B float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta; float iBubMoveableEndY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;

 代码讲解

拖动中分为4个状态,根据不同的状态,绘制不同的画面

/** * 气泡默认状态--静止 */ private final int BUBBLE_STATE_DEFAUL = 0; /** * 气泡相连 */ private final int BUBBLE_STATE_CONNECT = 1; /** * 气泡分离 */ private final int BUBBLE_STATE_APART = 2; /** * 气泡消失 */ private final int BUBBLE_STATE_DISMISS = 3;

 处理onTouchEvent事件

    处理ACTION_DOWN事件,计算两个圆圆心距离,当圆形距离小于制定距离以内,设置气泡为连接状态

case MotionEvent.ACTION_DOWN: { //不是消失状态 if (mBubbleState != BUBBLE_STATE_DISMISS) { //两个圆心之间的距离 mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y); if (mDist < mBubbleRadius + MOVE_OFFSET) { // 加上MOVE_OFFSET是为了方便拖拽 mBubbleState = BUBBLE_STATE_CONNECT; } else { mBubbleState = BUBBLE_STATE_DEFAUL; } } }

 处理ACTION_MOVE事件,将拖动的X,Y作为拖动圆的圆心,同时修改不动圆的半径,并不断绘制

case MotionEvent.ACTION_MOVE: { if (mBubbleState != BUBBLE_STATE_DEFAUL) { mBubMoveableCenter.x = event.getX(); mBubMoveableCenter.y = event.getY(); mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y); if (mBubbleState == BUBBLE_STATE_CONNECT) { // 减去MOVE_OFFSET是为了让不动气泡半径到一个较小值时就直接消失 // 或者说是进入分离状态 if (mDist < mMaxDist - MOVE_OFFSET) { mBubStillRadius = mBubbleRadius - mDist / 8; } else { mBubbleState = BUBBLE_STATE_APART; } } invalidate(); } }

处理ACTION_UP事件,

case MotionEvent.ACTION_UP: { if (mBubbleState == BUBBLE_STATE_CONNECT) { startBubbleRestAnim(); } else if (mBubbleState == BUBBLE_STATE_APART) { if (mDist < 2 * mBubbleRadius) { startBubbleRestAnim(); } else { startBubbleBurstAnim(); } } }

 处理ACTION_UP事件: 当两个气泡还是连接状态,放手以后会有一个还原动画。当两个气泡断开连接以后,会有一个爆炸动画

case MotionEvent.ACTION_UP: { //连接状态,放手,会有一个还原动画 if (mBubbleState == BUBBLE_STATE_CONNECT) { startBubbleRestAnim(); } else if (mBubbleState == BUBBLE_STATE_APART) { if (mDist < 2 * mBubbleRadius) { startBubbleRestAnim(); } else { //气泡分离,爆炸动画 startBubbleBurstAnim(); } } }

还原动画

在拖拽范围内归位的时候我们设置动画让终点圆坐标从当前位置逐渐变化到起点位置,设置OvershootInterpolator让动画出现跳动效果

private void startBubbleRestAnim() { ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(), new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y), new PointF(mBubStillCenter.x, mBubStillCenter.y)); anim.setDuration(200); anim.setInterpolator(new OvershootInterpolator(5f)); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mBubMoveableCenter = (PointF) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mBubbleState = BUBBLE_STATE_DEFAUL; } }); anim.start(); }

 爆炸动画

这里使用了4张爆炸图片作为爆炸动画

private void startBubbleBurstAnim() { //气泡改为消失状态 mBubbleState = BUBBLE_STATE_DISMISS; mIsBurstAnimStart = true; //做一个int型属性动画,从0~mBurstDrawablesArray.length结束 ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length); anim.setInterpolator(new LinearInterpolator()); anim.setDuration(500); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //设置当前绘制的爆炸图片index mCurDrawableIndex = (int) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //修改动画执行标志 mIsBurstAnimStart = false; } }); anim.start(); }

 完整代码

/** * QQ气泡效果 */ public class DragBubbleView extends View { /** * 气泡默认状态--静止 */ private final int BUBBLE_STATE_DEFAUL = 0; /** * 气泡相连 */ private final int BUBBLE_STATE_CONNECT = 1; /** * 气泡分离 */ private final int BUBBLE_STATE_APART = 2; /** * 气泡消失 */ private final int BUBBLE_STATE_DISMISS = 3; /** * 气泡半径 */ private float mBubbleRadius; /** * 气泡颜色 */ private int mBubbleColor; /** * 气泡消息文字 */ private String mTextStr; /** * 气泡消息文字颜色 */ private int mTextColor; /** * 气泡消息文字大小 */ private float mTextSize; /** * 不动气泡的半径 */ private float mBubStillRadius; /** * 可动气泡的半径 */ private float mBubMoveableRadius; /** * 不动气泡的圆心 */ private PointF mBubStillCenter; /** * 可动气泡的圆心 */ private PointF mBubMoveableCenter; /** * 气泡的画笔 */ private Paint mBubblePaint; /** * 贝塞尔曲线path */ private Path mBezierPath; private Paint mTextPaint; //文本绘制区域 private Rect mTextRect; private Paint mBurstPaint; //爆炸绘制区域 private Rect mBurstRect; /** * 气泡状态标志 */ private int mBubbleState = BUBBLE_STATE_DEFAUL; /** * 两气泡圆心距离 */ private float mDist; /** * 气泡相连状态最大圆心距离 */ private float mMaxDist; /** * 手指触摸偏移量 */ private final float MOVE_OFFSET; /** * 气泡爆炸的bitmap数组 */ private Bitmap[] mBurstBitmapsArray; /** * 是否在执行气泡爆炸动画 */ private boolean mIsBurstAnimStart = false; /** * 当前气泡爆炸图片index */ private int mCurDrawableIndex; /** * 气泡爆炸的图片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) { this(context, attrs, defStyleAttr, 0); } public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); //获取 XML layout中的属性值 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0); mBubbleRadius = array.getDimension(R.styleable.DragBubbleView_bubble_radius, mBubbleRadius); mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubble_color, Color.RED); mTextStr = array.getString(R.styleable.DragBubbleView_bubble_text); mTextSize = array.getDimension(R.styleable.DragBubbleView_bubble_textSize, mTextSize); mTextColor = array.getColor(R.styleable.DragBubbleView_bubble_textColor, Color.WHITE); //回收TypedArray array.recycle(); mBubStillRadius = mBubbleRadius; mBubMoveableRadius = mBubStillRadius; mMaxDist = 8 * mBubbleRadius; MOVE_OFFSET = mMaxDist / 4; //抗锯齿 mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBubblePaint.setColor(mBubbleColor); mBubblePaint.setStyle(Paint.Style.FILL); mBezierPath = new Path(); //文本画笔 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); mBurstRect = new Rect(); mBurstBitmapsArray = new Bitmap[mBurstDrawablesArray.length]; for (int i = 0; i < mBurstDrawablesArray.length; i++) { //将气泡爆炸的drawable转为bitmap Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]); mBurstBitmapsArray[i] = bitmap; } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); initView(w, h); } /** * 初始化气泡位置 * * @param w * @param h */ private void initView(int w, int h) { //设置两气泡圆心初始坐标 if (mBubStillCenter == null) { mBubStillCenter = new PointF(w / 2, h / 2); } else { mBubStillCenter.set(w / 2, h / 2); } if (mBubMoveableCenter == null) { mBubMoveableCenter = new PointF(w / 2, h / 2); } else { mBubMoveableCenter.set(w / 2, h / 2); } mBubbleState = BUBBLE_STATE_DEFAUL; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { //不是消失状态 if (mBubbleState != BUBBLE_STATE_DISMISS) { //两个圆心之间的距离 mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y); if (mDist < mBubbleRadius + MOVE_OFFSET) { // 加上MOVE_OFFSET是为了方便拖拽 mBubbleState = BUBBLE_STATE_CONNECT; } else { mBubbleState = BUBBLE_STATE_DEFAUL; } } } break; case MotionEvent.ACTION_MOVE: { if (mBubbleState != BUBBLE_STATE_DEFAUL) { mBubMoveableCenter.x = event.getX(); mBubMoveableCenter.y = event.getY(); mDist = (float) Math.hypot(event.getX() - mBubStillCenter.x, event.getY() - mBubStillCenter.y); if (mBubbleState == BUBBLE_STATE_CONNECT) { // 减去MOVE_OFFSET是为了让不动气泡半径到一个较小值时就直接消失 // 或者说是进入分离状态 if (mDist < mMaxDist - MOVE_OFFSET) { mBubStillRadius = mBubbleRadius - mDist / 8; } else { mBubbleState = BUBBLE_STATE_APART; } } invalidate(); } } break; case MotionEvent.ACTION_UP: { //连接状态,放手,会有一个还原动画 if (mBubbleState == BUBBLE_STATE_CONNECT) { startBubbleRestAnim(); } else if (mBubbleState == BUBBLE_STATE_APART) { if (mDist < 2 * mBubbleRadius) { startBubbleRestAnim(); } else { //气泡分离,爆炸动画 startBubbleBurstAnim(); } } } break; } return true; } /** * 爆炸动画 */ private void startBubbleBurstAnim() { //气泡改为消失状态 mBubbleState = BUBBLE_STATE_DISMISS; mIsBurstAnimStart = true; //做一个int型属性动画,从0~mBurstDrawablesArray.length结束 ValueAnimator anim = ValueAnimator.ofInt(0, mBurstDrawablesArray.length); anim.setInterpolator(new LinearInterpolator()); anim.setDuration(500); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { //设置当前绘制的爆炸图片index mCurDrawableIndex = (int) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { //修改动画执行标志 mIsBurstAnimStart = false; } }); anim.start(); } /** * 还原动画 */ private void startBubbleRestAnim() { ValueAnimator anim = ValueAnimator.ofObject(new PointFEvaluator(), new PointF(mBubMoveableCenter.x, mBubMoveableCenter.y), new PointF(mBubStillCenter.x, mBubStillCenter.y)); anim.setDuration(200); anim.setInterpolator(new OvershootInterpolator(5f)); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mBubMoveableCenter = (PointF) animation.getAnimatedValue(); invalidate(); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mBubbleState = BUBBLE_STATE_DEFAUL; } }); anim.start(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 1、画静止状态 // 2、画相连状态 // 3、画分离状态 // 4、画消失状态---爆炸动画 // 1、画拖拽的气泡 和 文字 if (mBubbleState != BUBBLE_STATE_DISMISS) { canvas.drawCircle(mBubMoveableCenter.x, mBubMoveableCenter.y, mBubMoveableRadius, mBubblePaint); mTextPaint.getTextBounds(mTextStr, 0, mTextStr.length(), mTextRect); canvas.drawText(mTextStr, mBubMoveableCenter.x - mTextRect.width() / 2, mBubMoveableCenter.y + mTextRect.height() / 2, mTextPaint); } // 2、画相连的气泡状态 if (mBubbleState == BUBBLE_STATE_CONNECT) { // 1、画静止气泡 canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubStillRadius, mBubblePaint); // 2、画相连曲线 // 计算控制点坐标,两个圆心的中点 int iAnchorX = (int) ((mBubStillCenter.x + mBubMoveableCenter.x) / 2); int iAnchorY = (int) ((mBubStillCenter.y + mBubMoveableCenter.y) / 2); //∠θ float cosTheta = (mBubMoveableCenter.x-mBubStillCenter.x) / mDist; float sinTheta = (mBubStillCenter.y-mBubMoveableCenter.y) / mDist; //A float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta; float iBubStillStartY = mBubStillCenter.y - mBubStillRadius * cosTheta; //D float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta; float iBubStillEndY = mBubStillCenter.y + mBubStillRadius * cosTheta; //C float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta; float iBubMoveableStartY = mBubMoveableCenter.y + mBubMoveableRadius * cosTheta; //B float iBubMoveableEndX = mBubMoveableCenter.x - mBubMoveableRadius * sinTheta; float iBubMoveableEndY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta; mBezierPath.reset();//清除Path中的内容, reset不保留内部数据结构(重置路径) // 画上半弧 mBezierPath.moveTo(iBubStillStartX, iBubStillStartY);//将路径的绘制位置定在(x,y)的位置 mBezierPath.quadTo(iAnchorX, iAnchorY, iBubMoveableEndX, iBubMoveableEndY);//二阶贝塞尔曲线 // 画下半弧 mBezierPath.lineTo(iBubMoveableStartX, iBubMoveableStartY);//结束点或者下一次绘制直线路径的开始点 mBezierPath.quadTo(iAnchorX, iAnchorY, iBubStillEndX, iBubStillEndY);//二阶贝塞尔曲线 //连接第一个点连接到最后一个点,形成一个闭合区域 mBezierPath.close(); canvas.drawPath(mBezierPath, mBubblePaint); } // 3、画消失状态---爆炸动画 if (mIsBurstAnimStart) { mBurstRect.set((int) (mBubMoveableCenter.x - mBubMoveableRadius), (int) (mBubMoveableCenter.y - mBubMoveableRadius), (int) (mBubMoveableCenter.x + mBubMoveableRadius), (int) (mBubMoveableCenter.y + mBubMoveableRadius)); canvas.drawBitmap(mBurstBitmapsArray[mCurDrawableIndex], null, mBurstRect, mBubblePaint); } } public void reset() { initView(getWidth(), getHeight()); invalidate(); } }

 

最新回复(0)