仿QQ未读消息的动画效果
仿QQ未读消息的动画效果
虽然目前使用QQ的人不是很多了,尤其是工作以后,大家基本上都是在使用微信,但是QQ未读消息的动画效果还是很好的,之前一直想自己实现下这个效果,终于抽时间动手实现了。这篇用来记录自己是如何一步一步实现的。
1.绘制红色圆点和未读消息数字
先抛开拖拽的动态效果,最直观的展示就是一个小红点里面有未读数字,如图所示。
QQ未读消息效果.PNG实现这个红色圆圈和数字的效果,效果图如图所示:
静态红色圆点效果.png实现的代码如下UnReadRedPoint.java
:
package com.tom.unreadmessageview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.blankj.utilcode.util.SizeUtils;
/**
* <p>Title: UnReadRedPoint</p>
* <p>Description: 未读信息小红点,可以设置未读的数字</p>
*
* @author tom
* @date 2018/12/17 11:18
**/
public class UnReadRedPoint extends View {
private Context mContext;
//屏幕中心点的XY坐标
private float mCenterX, mCenterY;
//未读消息的圆点的圆心坐标
private PointF mUnReadCenter;
//未读消息的圆点的半径
private float mUnReadRadius;
//未读消息数目
private int mUnReadCount;
//画笔
private Paint mPaint;
public UnReadRedPoint(Context context) {
this(context, null);
}
public UnReadRedPoint(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public UnReadRedPoint(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initResource();
}
/**
* <p>初始化相关的资源参数</p>
*/
private void initResource() {
mUnReadRadius = SizeUtils.dp2px(20);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
mCenterX = width / 2;
mCenterY = height / 2;
setUnReadCenter(new PointF(mCenterX, mCenterY));
}
public int getUnReadCount() {
return mUnReadCount;
}
public void setUnReadCount(int unReadCount) {
mUnReadCount = unReadCount;
}
public PointF getUnReadCenter() {
return mUnReadCenter;
}
public void setUnReadCenter(PointF unReadCenter) {
mUnReadCenter = unReadCenter;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设定画笔抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔的填充方式
mPaint.setStyle(Paint.Style.FILL);
//设置画笔颜色
mPaint.setColor(Color.RED);
//画圆
canvas.drawCircle(mUnReadCenter.x, mUnReadCenter.y, mUnReadRadius, mPaint);
//画笔设置为黑色
mPaint.setColor(Color.BLACK);
//辅助线
canvas.drawLine(0, mCenterY, mCenterX * 2, mCenterY, mPaint);
canvas.drawLine(mCenterX, 0, mCenterX, mCenterY * 2, mPaint);
//设置文字大小
mPaint.setTextSize(SizeUtils.sp2px(18));
//设置文字居中
String s = String.valueOf(mUnReadCount);
mPaint.setTextAlign(Paint.Align.CENTER);
Rect bounds = new Rect();
mPaint.getTextBounds(s, 0, s.length(), bounds);
//绘制文字
canvas.drawText(s, mUnReadCenter.x - bounds.left / 2f, mUnReadCenter.y - bounds.centerY(), mPaint);
}
}
布局文件比较简单,但是还是贴出来吧,文件名为activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<com.tom.unreadmessageview.view.UnReadRedPoint
android:id="@+id/unread_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
/>
</android.support.constraint.ConstraintLayout>
代码中的使用如下MainActivity.java
。
package com.tom.unreadmessageview;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.tom.unreadmessageview.view.UnReadRedPoint;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MainActivity extends AppCompatActivity {
Unbinder mBinder;
@BindView(R.id.unread_view)
UnReadRedPoint mUnreadView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBinder = ButterKnife.bind(this);
}
@Override
protected void onResume() {
super.onResume();
//设置未读数目
mUnreadView.setUnReadCount(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
mBinder.unbind();
}
}
这里使用了ButterKnife这个插件,这个插件的配置和使用方法都比较简单,把gradle
文件的dependencies
块里面添加如下代码就可以了
implementation 'com.jakewharton:butterknife:9.0.0-rc2'
annotationProcessor 'com.jakewharton:butterknife-compiler:9.0.0-rc2'
这样,第一步就算完成了。
2.实现移动圆跟随手指移动
跟随手指拖动效果.gif跟随手指移动的效果这里需要有一点要注意的地方,只有手指按下的位置在移动圆内,才执行效果,否则不执行。这里用到一个工具了,用来计算各种和数学相关的内容,直接拿来主义,工具类代码如下GeometryUtil
。
package com.tom.unreadmessageview.utils;
import android.graphics.PointF;
/**
* <p>Title: GeometryUtil</p>
* <p>Description: 坐标计算的相关方法</p>
*
* @author tom
* @date 2018/12/12 16:28
**/
public class GeometryUtil {
/**
* As meaning of method name.
* 获得两点之间的距离
*
* @param p0
* @param p1
* @return
*/
public static float getDistanceBetween2Points(PointF p0, PointF p1) {
float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
return distance;
}
/**
* Get middle point between p1 and p2.
* 获得两点连线的中点
*
* @param p1
* @param p2
* @return
*/
public static PointF getMiddlePoint(PointF p1, PointF p2) {
return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
}
/**
* Get point between p1 and p2 by percent.
* 根据百分比获取两点之间的某个点坐标
*
* @param p1
* @param p2
* @param percent
* @return
*/
public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
return new PointF(evaluateValue(percent, p1.x, p2.x), evaluateValue(percent, p1.y, p2.y));
}
/**
* 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
*
* @param fraction
* @param start
* @param end
* @return
*/
public static float evaluateValue(float fraction, Number start, Number end) {
return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
}
/**
* Get the point of intersection between circle and line.
* 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
*
* @param pMiddle The circle center point.
* @param radius The circle radius.
* @param lineK The slope of line which cross the pMiddle.
* @return
*/
public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
PointF[] points = new PointF[2];
float radian, xOffset = 0, yOffset = 0;
if (lineK != null) {
radian = (float) Math.atan(lineK);
xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);
} else {
xOffset = radius;
yOffset = 0;
}
points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);
return points;
}
}
在自定义View的类UnReadRedPoint
新增两个变量,用来记录按下的时候的坐标
//按下的点的坐标
private float mDownX, mDownY;
重写onTouchEvent
方法,采用的是将移动圆的圆心坐标修改为手指的坐标,然后重绘,进而实现跟随手指移动的效果。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
PointF downPoint = new PointF(mDownX, mDownY);
//只有在按下的位置是移动圆的位置的时候,才继续执行
if (GeometryUtil.getDistanceBetween2Points(downPoint, mUnReadCenter) < mUnReadRadius + 2f) {
setUnReadCenter(downPoint);
invalidate();
}
else {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
mDownX = event.getX();
mDownY = event.getY();
setUnReadCenter(new PointF(mDownX, mDownY));
invalidate();
break;
}
return true;
}
3.实现固定圆跟随移动圆的距离而变小的效果
实现随着移动圆远离最初位置(这里为屏幕中心点),固定圆的半径逐渐变小直到变为最小值,当离开最大范围时,固定圆消失。实现效果如图。
添加固定圆.gif需要新定义一些变量,包括固定圆的圆心,最大的范围,固定圆的半径等。
//固定圆的圆心
private PointF mFixedCenter;
//固定圆的半径
private float mFixedRadius;
//移动点距离固定点(屏幕中心)的距离
private float mDistanceBetweenMoveAndFixed;
//有拖拽效果的最大范围
private float mMaxRadius;
在onDraw
,onTouchEvent
方法中,需要添加绘制固定圆的方法,同时要计算移动圆与固定圆的距离,当距离大于最大范围时,则固定圆消失,否则固定圆半径按照一定比例减小。此时的onDraw
方法如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设定画笔抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔的填充方式
mPaint.setStyle(Paint.Style.FILL);
//设置画笔颜色
mPaint.setColor(Color.RED);
//画固定圆
mPaint.setColor(Color.BLUE);
//固定圆的半径
//如果手指和固定圆圆心的距离大于最大半径(范围外),则不需要画出固定圆
if (mDistanceBetweenMoveAndFixed - mUnReadRadius> mMaxRadius) {
mFixedRadius = 0;
}
else {
mFixedRadius = SizeUtils.dp2px(15) - SizeUtils.dp2px(8) * mDistanceBetweenMoveAndFixed / mMaxRadius;
}
canvas.drawCircle(mFixedCenter.x, mFixedCenter.y, mFixedRadius, mPaint);
//画移动圆
mPaint.setColor(Color.RED);
canvas.drawCircle(mUnReadCenter.x, mUnReadCenter.y, mUnReadRadius, mPaint);
//画笔设置为黑色
mPaint.setColor(Color.BLACK);
//设置画笔的填充方式
mPaint.setStyle(Paint.Style.STROKE);
//辅助线
canvas.drawLine(0, mCenterY, mCenterX * 2, mCenterY, mPaint);
canvas.drawLine(mCenterX, 0, mCenterX, mCenterY * 2, mPaint);
canvas.drawCircle(mCenterX, mCenterY, mMaxRadius, mPaint);
//设置文字大小
mPaint.setTextSize(SizeUtils.sp2px(18));
//设置文字居中
String s = String.valueOf(mUnReadCount);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setStyle(Paint.Style.FILL);
Rect bounds = new Rect();
mPaint.getTextBounds(s, 0, s.length(), bounds);
//绘制文字
canvas.drawText(s, mUnReadCenter.x - bounds.left / 2f, mUnReadCenter.y - bounds.centerY(), mPaint);
}
onTouchEvent
方法如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
PointF downPoint = new PointF(mDownX, mDownY);
//只有在按下的位置是移动圆的位置的时候,才继续执行
if (GeometryUtil.getDistanceBetween2Points(downPoint, mUnReadCenter) < mUnReadRadius + 2f) {
setUnReadCenter(downPoint);
invalidate();
}
else {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
mDownX = event.getX();
mDownY = event.getY();
PointF movePointF = new PointF(mDownX, mDownY);
setUnReadCenter(movePointF);
mDistanceBetweenMoveAndFixed = GeometryUtil.getDistanceBetween2Points(mFixedCenter, movePointF);
invalidate();
break;
}
return true;
}
4.绘制贝塞尔曲线
两圆之间的效果是通过绘制贝塞尔曲线实现的,关于绘制贝塞尔曲线的相关内容,我也是参考网上的内容,这一篇文章贝塞尔曲线能提供很详细的内容。先奉上实现的效果图。
添加贝塞尔曲线效果.gif这里面会涉及到贝塞尔曲线的相关知识,不想了解的直接复制下面的代码就可以了。具体的如何绘制,可以一步一步的调试来理解这个效果的实现。
/**
* <p>绘制贝塞尔曲线</p>
*
* @param canvas
*/
private void drawBezier(Canvas canvas) {
//设置画笔属性
mPaint.setColor(Color.RED);
//获取两点连线的中点
PointF middlePoint = GeometryUtil.getMiddlePoint(mUnReadCenter, mFixedCenter);
//计算两圆圆心连线的斜率
float offSetY = mUnReadCenter.y - mFixedCenter.y;
float offSetX = mUnReadCenter.x - mFixedCenter.x;
Double lineK = null;
if (offSetX != 0) {
lineK = Double.valueOf(offSetY / offSetX);
}
else {
LogUtils.d("没有斜率,为直线");
}
//得到固定圆的附着点
PointF[] fixedPoints = GeometryUtil.getIntersectionPoints(mFixedCenter, mFixedRadius, lineK);
PointF[] unReaderPoints = GeometryUtil.getIntersectionPoints(mUnReadCenter, mUnReadRadius, lineK);
//画出辅助点
canvas.drawPoint(fixedPoints[0].x, fixedPoints[0].y, mPaint);
//绘制路径
//两圆连线的中点为曲线的控制点,同侧的附着点为固定点
Path path = new Path();
path.moveTo(fixedPoints[0].x, fixedPoints[0].y);
//画出一半贝塞尔曲线
path.quadTo(middlePoint.x, middlePoint.y, unReaderPoints[0].x, unReaderPoints[0].y);
//连接两点
path.lineTo(unReaderPoints[1].x, unReaderPoints[1].y);
//画出另一半贝塞尔曲线
path.quadTo(middlePoint.x, middlePoint.y, fixedPoints[1].x, fixedPoints[1].y);
//闭合路径
path.close();
//根据设置的路径作画
canvas.drawPath(path, mPaint);
}
5.实现回弹效果
回弹需要是手指抬起时进行处理,也就需要在onTouchEvent
中,添加MotionEvent.ACTION_UP
的处理。需要判断抬起时,抬起位置是否在最大范围内,如果在最大范围内,则需要实现回弹效果,否则不需要。这里新定义了一些变量,会在最后贴出完整代码。
此效果的关键代码为动画。
/**
* <p>回弹动画</p>
*/
private void startBackAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
//获取手指离开的距离
PointF pointByPercent = GeometryUtil.getPointByPercent(mUpPointF, mFixedCenter, animatedFraction);
//动态设定距离
setUnReadCenter(pointByPercent);
invalidate();
}
});
valueAnimator.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
float f = 0.571429f;
return (float) (Math.pow(2, -4 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1);
}
});
valueAnimator.setDuration(300);
valueAnimator.start();
}
这里用到了值动画还有插值器,相关的内容就自己学习,资料很多。
6.超出范围爆炸效果
手指抬起的位置如果是在范围之外,是有一个爆炸消失的效果,接下来添加这个效果,需要用到4个资源图片,文末会附上,也可以使用其他的图片资源。
消失爆炸效果.gif这里调整了一下代码的格式,至此已经完成了需要实现的动画效果,附上动画的代码。
/**
* <p>消失动画效果</p>
*/
private void startDismissAnim() {
ValueAnimator animatorDismiss = ValueAnimator.ofInt(0, mExplodes.length);
animatorDismiss.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mExplodeIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
animatorDismiss.setDuration(300);
animatorDismiss.start();
}
7.其他细节部分
貌似一看,效果已经实现了,但是还有一些细节需要处理。比如当移动圆离开最大范围再回到范围内时,是没有贝塞尔曲线和回弹效果的,这里可以自行对比QQ,还可以加上回调,用来处理不同状态的情况。最终效果:
完整效果.gif
最后附上完整的代码:
布局文件activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
>
<com.tom.unreadmessageview.view.UnReadRedPoint
android:id="@+id/unread_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
/>
</android.support.constraint.ConstraintLayout>
主Activity文件MainActivity.java
package com.tom.unreadmessageview;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import com.tom.unreadmessageview.utils.ToastUtils;
import com.tom.unreadmessageview.view.UnReadRedPoint;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MainActivity extends AppCompatActivity {
Unbinder mBinder;
@BindView(R.id.unread_view)
UnReadRedPoint mUnreadView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBinder = ButterKnife.bind(this);
}
@Override
protected void onResume() {
super.onResume();
//设置未读数目
mUnreadView.setUnReadCount(0);
mUnreadView.setOnUnReadRedPointListener(new UnReadRedPoint.OnUnReadRedPointListener() {
@Override
public void onDismiss() {
ToastUtils.setToast(MainActivity.this,"消失了");
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
mBinder.unbind();
}
}
自定义View UnReadRedPoint.java
package com.tom.unreadmessageview.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.OvershootInterpolator;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.tom.unreadmessageview.R;
import com.tom.unreadmessageview.utils.GeometryUtil;
/**
* <p>Title: UnReadRedPoint</p>
* <p>Description: 未读信息小红点,可以设置未读的数字</p>
*
* @author tom
* @date 2018/12/17 11:18
**/
public class UnReadRedPoint extends View {
private static final int RADIUS_UNREAD = 15;
private static final int RADIUS_FIXED = 12;
private static final int RADIUS_MIN = 6;
private static final int RADIUS_MAX = 120;
private static final int SIZE_TEXT = 16;
private Context mContext;
//屏幕中心点的XY坐标
private float mCenterX, mCenterY;
//未读消息的圆点的圆心坐标
private PointF mUnReadCenter;
//未读消息的圆点的半径
private float mUnReadRadius;
//未读消息数目
private int mUnReadCount;
//画笔
private Paint mPaint;
//按下的点的坐标
private float mDownX, mDownY;
//固定圆的圆心
private PointF mFixedCenter;
//固定圆的半径
private float mFixedRadius;
//移动点距离固定点(屏幕中心)的距离
private float mDistanceBetweenMoveAndFixed;
//有拖拽效果的最大范围
private float mMaxRadius;
//抬起点的坐标
private PointF mUpPointF;
//抬起点和固定圆的距离
private float mDistanceBetweenUPAndFixed;
//爆炸动画相关
private int mExplodeIndex;
private int[] mExplodes = {R.mipmap.explode1, R.mipmap.explode2, R.mipmap.explode3, R.mipmap.explode4, R.mipmap
.explode5};
private Bitmap[] mExplodeBitmaps;
//是否超出过范围
private boolean isBeyond = false;
//回调监听接口
private OnUnReadRedPointListener mOnUnReadRedPointListener;
public UnReadRedPoint(Context context) {
this(context, null);
}
public UnReadRedPoint(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public UnReadRedPoint(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
initResource();
}
/**
* <p>初始化相关的资源参数</p>
*/
private void initResource() {
mUnReadRadius = SizeUtils.dp2px(RADIUS_UNREAD);
mFixedRadius = SizeUtils.dp2px(RADIUS_FIXED);
mMaxRadius = SizeUtils.dp2px(RADIUS_MAX);
mExplodeBitmaps = new Bitmap[mExplodes.length];
for (int i = 0; i < mExplodes.length; i++) {
mExplodeBitmaps[i] = BitmapFactory.decodeResource(getResources(), mExplodes[i]);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
mCenterX = width / 2;
mCenterY = height / 2;
PointF centerPointF = new PointF(mCenterX, mCenterY);
setUnReadCenter(centerPointF);
setFixedCenter(centerPointF);
}
public int getUnReadCount() {
return mUnReadCount;
}
public void setUnReadCount(int unReadCount) {
mUnReadCount = unReadCount;
}
public PointF getUnReadCenter() {
return mUnReadCenter;
}
public void setUnReadCenter(PointF unReadCenter) {
mUnReadCenter = unReadCenter;
}
public PointF getFixedCenter() {
return mFixedCenter;
}
public void setFixedCenter(PointF fixedCenter) {
mFixedCenter = fixedCenter;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设定画笔抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//画笔设置为黑色
mPaint.setColor(Color.BLACK);
//设置画笔的填充方式
mPaint.setStyle(Paint.Style.STROKE);
//辅助线
canvas.drawLine(0, mCenterY, mCenterX * 2, mCenterY, mPaint);
canvas.drawLine(mCenterX, 0, mCenterX, mCenterY * 2, mPaint);
canvas.drawCircle(mCenterX, mCenterY, mMaxRadius, mPaint);
//设置画笔的填充方式
mPaint.setStyle(Paint.Style.FILL);
//设置画笔颜色
mPaint.setColor(Color.RED);
//画固定圆
//固定圆的半径
//如果手指和固定圆圆心的距离大于最大半径(范围外),则不需要画出固定圆
if (mDistanceBetweenMoveAndFixed - mUnReadRadius > mMaxRadius) {
mFixedRadius = 0;
}
else {
mFixedRadius = SizeUtils.dp2px(RADIUS_FIXED) - SizeUtils.dp2px(RADIUS_MIN) * mDistanceBetweenMoveAndFixed
/ mMaxRadius;
//绘制贝塞尔曲线
if (!isBeyond) {
drawBezier(canvas);
}
//画固定圆
if (!isBeyond) {
canvas.drawCircle(mFixedCenter.x, mFixedCenter.y, mFixedRadius, mPaint);
}
}
if (mDistanceBetweenUPAndFixed - mUnReadRadius > mMaxRadius) {
if (mExplodeIndex < mExplodes.length) {
canvas.drawBitmap(mExplodeBitmaps[mExplodeIndex], mUnReadCenter.x, mUnReadCenter.y, mPaint);
}
}
else {
//画移动圆
canvas.drawCircle(mUnReadCenter.x, mUnReadCenter.y, mUnReadRadius, mPaint);
//设置文字大小
mPaint.setTextSize(SizeUtils.sp2px(SIZE_TEXT));
//设置画笔颜色
mPaint.setColor(Color.BLACK);
//设置文字居中
String s = String.valueOf(mUnReadCount);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setStyle(Paint.Style.FILL);
Rect bounds = new Rect();
mPaint.getTextBounds(s, 0, s.length(), bounds);
//绘制文字
canvas.drawText(s, mUnReadCenter.x - bounds.left / 2f, mUnReadCenter.y - bounds.centerY(), mPaint);
}
}
/**
* <p>绘制贝塞尔曲线</p>
*
* @param canvas
*/
private void drawBezier(Canvas canvas) {
//设置画笔属性
mPaint.setColor(Color.RED);
//获取两点连线的中点
PointF middlePoint = GeometryUtil.getMiddlePoint(mUnReadCenter, mFixedCenter);
//计算两圆圆心连线的斜率
float offSetY = mUnReadCenter.y - mFixedCenter.y;
float offSetX = mUnReadCenter.x - mFixedCenter.x;
Double lineK = null;
if (offSetX != 0) {
lineK = Double.valueOf(offSetY / offSetX);
}
else {
LogUtils.d("没有斜率,为直线");
}
//得到固定圆的附着点
PointF[] fixedPoints = GeometryUtil.getIntersectionPoints(mFixedCenter, mFixedRadius, lineK);
PointF[] unReaderPoints = GeometryUtil.getIntersectionPoints(mUnReadCenter, mUnReadRadius, lineK);
//绘制路径
//两圆连线的中点为曲线的控制点,同侧的附着点为固定点
Path path = new Path();
path.moveTo(fixedPoints[0].x, fixedPoints[0].y);
//画出一半贝塞尔曲线
path.quadTo(middlePoint.x, middlePoint.y, unReaderPoints[0].x, unReaderPoints[0].y);
//连接两点
path.lineTo(unReaderPoints[1].x, unReaderPoints[1].y);
//画出另一半贝塞尔曲线
path.quadTo(middlePoint.x, middlePoint.y, fixedPoints[1].x, fixedPoints[1].y);
//闭合路径
path.close();
//根据设置的路径作画
canvas.drawPath(path, mPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
PointF downPoint = new PointF(mDownX, mDownY);
//只有在按下的位置是移动圆的位置的时候,才继续执行
if (GeometryUtil.getDistanceBetween2Points(downPoint, mUnReadCenter) < mUnReadRadius + 2f) {
setUnReadCenter(downPoint);
invalidate();
}
else {
return false;
}
break;
case MotionEvent.ACTION_MOVE:
mDownX = event.getX();
mDownY = event.getY();
PointF movePointF = new PointF(mDownX, mDownY);
setUnReadCenter(movePointF);
mDistanceBetweenMoveAndFixed = GeometryUtil.getDistanceBetween2Points(mFixedCenter, movePointF);
if (mDistanceBetweenMoveAndFixed > mMaxRadius) {
isBeyond = true;
}
invalidate();
break;
case MotionEvent.ACTION_UP:
float upX = event.getX();
float upY = event.getY();
mUpPointF = new PointF(upX, upY);
mDistanceBetweenUPAndFixed = GeometryUtil.getDistanceBetween2Points(mFixedCenter, mUpPointF);
//抬起点在最大范围之内
if (mDistanceBetweenUPAndFixed - mUnReadRadius <= mMaxRadius) {
//开始动画
startBackAnim();
invalidate();
}
else {
// 抬起点在最大范围之外
// 开始爆炸效果动画
startDismissAnim();
invalidate();
isBeyond = false;
setDismiss();
}
break;
}
return true;
}
/**
* <p>消失动画效果</p>
*/
private void startDismissAnim() {
ValueAnimator animatorDismiss = ValueAnimator.ofInt(0, mExplodes.length);
animatorDismiss.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mExplodeIndex = (int) animation.getAnimatedValue();
invalidate();
}
});
animatorDismiss.setDuration(300);
animatorDismiss.start();
}
/**
* <p>回弹动画</p>
*/
private void startBackAnim() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
//获取手指离开的距离
PointF pointByPercent = GeometryUtil.getPointByPercent(mUpPointF, mFixedCenter, animatedFraction);
//动态设定距离
setUnReadCenter(pointByPercent);
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isBeyond = false;
}
});
if (!isBeyond) {
valueAnimator.setInterpolator(new TimeInterpolator() {
@Override
public float getInterpolation(float input) {
float f = 0.571429f;
return (float) (Math.pow(2, -4 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1);
}
});
valueAnimator.setDuration(300);
}
else {
valueAnimator.setInterpolator(new OvershootInterpolator(1));
valueAnimator.setDuration(50);
}
valueAnimator.start();
}
/**
* <p>定义接口</p>
*/
public interface OnUnReadRedPointListener {
void onDismiss();
}
public void setOnUnReadRedPointListener(OnUnReadRedPointListener onUnReadRedPointListener) {
mOnUnReadRedPointListener = onUnReadRedPointListener;
}
public void setDismiss() {
if (mOnUnReadRedPointListener != null) {
mOnUnReadRedPointListener.onDismiss();
}
}
}
工具类 GeometryUtil.java
package com.tom.unreadmessageview.utils;
import android.graphics.PointF;
/**
* <p>Title: GeometryUtil</p>
* <p>Description: 坐标计算的相关方法</p>
*
* @author tom
* @date 2018/12/12 16:28
**/
public class GeometryUtil {
/**
* As meaning of method name.
* 获得两点之间的距离
*
* @param p0
* @param p1
* @return
*/
public static float getDistanceBetween2Points(PointF p0, PointF p1) {
float distance = (float) Math.sqrt(Math.pow(p0.y - p1.y, 2) + Math.pow(p0.x - p1.x, 2));
return distance;
}
/**
* Get middle point between p1 and p2.
* 获得两点连线的中点
*
* @param p1
* @param p2
* @return
*/
public static PointF getMiddlePoint(PointF p1, PointF p2) {
return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);
}
/**
* Get point between p1 and p2 by percent.
* 根据百分比获取两点之间的某个点坐标
*
* @param p1
* @param p2
* @param percent
* @return
*/
public static PointF getPointByPercent(PointF p1, PointF p2, float percent) {
return new PointF(evaluateValue(percent, p1.x, p2.x), evaluateValue(percent, p1.y, p2.y));
}
/**
* 根据分度值,计算从start到end中,fraction位置的值。fraction范围为0 -> 1
*
* @param fraction
* @param start
* @param end
* @return
*/
public static float evaluateValue(float fraction, Number start, Number end) {
return start.floatValue() + (end.floatValue() - start.floatValue()) * fraction;
}
/**
* Get the point of intersection between circle and line.
* 获取 通过指定圆心,斜率为lineK的直线与圆的交点。
*
* @param pMiddle The circle center point.
* @param radius The circle radius.
* @param lineK The slope of line which cross the pMiddle.
* @return
*/
public static PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {
PointF[] points = new PointF[2];
float radian, xOffset = 0, yOffset = 0;
if (lineK != null) {
radian = (float) Math.atan(lineK);
xOffset = (float) (Math.sin(radian) * radius);
yOffset = (float) (Math.cos(radian) * radius);
} else {
xOffset = radius;
yOffset = 0;
}
points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);
points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);
return points;
}
}
ToastUtils.java
package com.tom.unreadmessageview.utils;
import android.content.Context;
import android.widget.Toast;
/**
* <p>Title: ToastUtils</p>
* <p>Description: </p>
*
* @author tom
* @date 2018/12/13 17:13
**/
public class ToastUtils {
private static Toast toast;
public static void setToast(Context context, String msg) {
if (toast == null) {
toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
}
toast.setText(msg);
toast.show();
}
}
还有爆炸效果的图片文件
有疑问的欢迎留言共同交流学习,共同进步。