第一,贝塞尔曲线
第二,Path的使用,附录一篇文章
path可以理解为路径,它的主要作用是绘制直线,曲线,或者其它的一些几何图形。也可以用于绘制文字。联想canvas.draw()方法,path也是类似的功能。只是canvas比较简单,path绘制出来的图形会更加的复杂。比如我们要绘制五角星,用canvas就很困难了,但是path实现起来却可以很简单。
看下代码,绘制两个简单的圆
package com.example.beisaier;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* @author writing
* @time 2019/12/18 12:34
* @note
*/
public class PathView extends View {
private Path path1;
private Path path2;
private Paint paint;
public PathView(Context context) {
super(context);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
intit();
}
private void intit() {
path1 = new Path();
path2 = new Path();
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* Path.Direction
* Path.Direction.CCW 逆时针
* Path.Direction.CW 顺时针
*/
path1.addCircle(200,200,150, Path.Direction.CCW);
path1.addCircle(300,300,150, Path.Direction.CW);
canvas.drawPath(path1,paint);
canvas.drawPath(path2,paint);
}
}
效果图
绘制0.x圆
package com.example.beisaier;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* @author writing
* @time 2019/12/18 12:34
* @note
*/
public class PathView extends View {
private Path path1;
private Path path2;
private Path path3;
private Paint paint;
public PathView(Context context) {
super(context);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
intit();
}
private void intit() {
path1 = new Path();
path2 = new Path();
path3 = new Path();
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* Path.Direction
* Path.Direction.CCW 逆时针
* Path.Direction.CW 顺时针
*/
//半圆
path1.addArc(200,200,500,500,0,90);
path2.addArc(200,600,500,900,0,-90);
canvas.drawPath(path1,paint);
canvas.drawPath(path2,paint);
paint.setStyle(Paint.Style.FILL);
path3.addArc(200,900,500,1200,0,-90);
canvas.drawPath(path3,paint);
}
}
package com.example.beisaier;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* @author writing
* @time 2019/12/18 12:34
* @note
*/
public class PathView extends View {
private Path path1;
private Path path2;
private Path path3;
private Paint paint;
public PathView(Context context) {
super(context);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
intit();
}
private void intit() {
path1 = new Path();
path2 = new Path();
path3 = new Path();
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* Path.Direction
* Path.Direction.CCW 逆时针
* Path.Direction.CW 顺时针
*/
//0.x椭圆,椭圆的一部分
path1.arcTo(200, 200, 300, 300, 0, 90, true);
//椭圆
path1.addOval(300, 300, 400, 450, Path.Direction.CW);
//矩形
path1.addRect(100, 400, 300, 500, Path.Direction.CW);
//圆角矩形
path1.addRoundRect(100, 600, 300, 700, 20, 40, Path.Direction.CW);
canvas.drawPath(path1, paint);
}
}
绘制更加复杂的图案
package com.example.beisaier;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
/**
* @author writing
* @time 2019/12/18 12:34
* @note
*/
public class PathView extends View {
private Path path1;
private Path path2;
private Paint paint;
public PathView(Context context) {
super(context);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
intit();
}
public PathView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
intit();
}
private void intit() {
path1 = new Path();
path2 = new Path();
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(4);
paint.setStyle(Paint.Style.STROKE);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* Path.Direction绘制方向
* Path.Direction.CCW 逆时针
* Path.Direction.CW 顺时针
*/
path1.addCircle(200, 200, 100, Path.Direction.CW); //绘制圆
path2.addCircle(300, 300, 100, Path.Direction.CW);
/**
* Path.op对两个Path进行布尔运算(即取交集、并集等操作)
* Path.Op.DIFFERENCE 减去path1中path1与path2都存在的部分;
* path1 = (path1 - path1 ∩ path2)
* Path.Op.INTERSECT 保留path1与path2共同的部分;
* path1 = path1 ∩ path2
* Path.Op.UNION 取path1与path2的并集;
* path1 = path1 ∪ path2
* Path.Op.REVERSE_DIFFERENCE 与DIFFERENCE刚好相反;
* path1 = path2 - (path1 ∩ path2)
* Path.Op.XOR 与INTERSECT刚好相反;
* path1 = (path1 ∪ path2) - (path1 ∩ path2)
*/
//五种情况
// path1.op(path2,Path.Op.DIFFERENCE);
// path1.op(path2,Path.Op.INTERSECT);
// path1.op(path2,Path.Op.UNION);
// path1.op(path2,Path.Op.XOR);
// path1.op(path2,Path.Op.REVERSE_DIFFERENCE);
canvas.drawPath(path1, paint);
canvas.drawPath(path2, paint);
}
}
path的其它一些API
mPath1.moveTo(100, 100);//将路径的绘制位置定在(x,y)的位置
/**
* 在前一个点的基础上开始绘制,如果前面一个点是(x,y),
* rMoveTo(dx,dy)相当于moveTo(x+dx,y+dy),如果前面没有调用moveTo,
* 相当于从(dx,dy)开始绘制
*/
mPath1.rMoveTo(100, 100);
不带r的方法是基于原点的坐标系(偏移量), rXxx方法是基于当前点坐标系(偏移量)
关于带r的方法与不带r方法的区别,详细请看这篇文章
第三使用Path绘制贝塞尔曲线
path只可以绘制2、3阶贝塞尔曲线。
绘制2阶贝塞尔曲线
path1.moveTo(100,100);
path1.quadTo(400,200,10,500);
canvas.drawPath(path1,paint);
绘制3阶贝塞尔曲线
mPath1.moveTo(100, 100);
mPath1.cubicTo(400, 200,10, 500,300, 700);
canvas.drawPath(mPath1, mPaint);
第四绘制n阶贝塞尔曲线
package com.example.beisaier;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* @author writing
* @time 2019/12/20 14:44
* @note
*/
public class BezierView extends View {
private Paint mPaint;
private Paint mLinePointPaint;
private Path mPath;
private List<PointF> mControlPoints;
public BezierView(Context context) {
super(context);
init();
}
public BezierView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public BezierView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
//绘制贝塞尔曲线的画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(10);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.BLACK);
//绘制点的画笔
mLinePointPaint = new Paint();
mLinePointPaint.setAntiAlias(true);
mLinePointPaint.setStrokeWidth(10);
mLinePointPaint.setStyle(Paint.Style.STROKE);
mLinePointPaint.setColor(Color.RED);
//贝塞尔曲线路径
mPath = new Path();
//控制点集合(包含数据点)
mControlPoints = new ArrayList<>();
//随机生成控制点,我们这里绘制的是四阶贝塞尔曲线,
// 如果大家想绘制更高阶的贝塞尔曲线,修改循环的次数就可以
Random random = new Random();
for (int i = 0; i < 5; i++) {
//产生200-1000的随机数
int x = random.nextInt(800) + 200;
int y = random.nextInt(800) + 200;
Log.i("zhang_xin", "随机数x:" + x + ",随机数y:" + y);
PointF pointF = new PointF(x, y);
mControlPoints.add(pointF);
}
//如果这里大家不想用随机数生成点,可以自己创建点
// PointF pointF0 = new PointF(200, 200);
// PointF pointF1 = new PointF(400, 500);
// PointF pointF2 = new PointF(600, 650);
// PointF pointF3 = new PointF(800, 500);
// PointF pointF4 = new PointF(1000, 150);
// mControlPoints.add(pointF0);
// mControlPoints.add(pointF1);
// mControlPoints.add(pointF2);
// mControlPoints.add(pointF3);
// mControlPoints.add(pointF4);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制起始点、终止点、控制点以及各点的连线
int size = mControlPoints.size();
PointF pointF;
for (int i = 0; i < size; i++) {
pointF = mControlPoints.get(i);
if (i > 0) {
mLinePointPaint.setColor(Color.GREEN);
canvas.drawLine(mControlPoints.get(i - 1).x, mControlPoints.get(i - 1).y, pointF.x, pointF.y, mLinePointPaint);
}
//起点终点换颜色
if (i == 0) {
mLinePointPaint.setColor(Color.BLUE);
} else if (i == size - 1) {
mLinePointPaint.setColor(Color.BLUE);
}
canvas.drawCircle(pointF.x, pointF.y, 20, mLinePointPaint);
}
//曲线连接
buildBezierPoints();
canvas.drawPath(mPath,mPaint);
}
private void buildBezierPoints() {
mPath.reset();
ArrayList<PointF> pointFS = new ArrayList<>();
int order = mControlPoints.size()-1;//贝塞尔的阶数
//份数,表示一条曲线绘制多少个点,我们这里设置为1000
float delta = 1.0f / 1000;
//for循环,求出每一个点
for (float t = 0; t<=1;t=t+delta) {
//bezier点集
PointF pointF = new PointF(deCastelJau(order, 0, t, true), deCastelJau(order, 0, t, false));//计算在曲线上点位置
pointFS.add(pointF);
if (pointFS.size() == 1) {
//如果是起点就移动到起点
mPath.moveTo(pointFS.get(0).x, pointFS.get(0).y);
} else {
//不是起点就连接起来
mPath.lineTo(pointF.x, pointF.y);
}
}
}
/**
* p(i,j) = (1-t) * p(i-1,j) + t * p(i-1,j+1);
*
* @param i 阶数
* @param j 控制点
* @param t 时间,比值
* @param calculateX 计算哪个坐标值 true=x
* @return
*/
private float deCastelJau(int i, int j, float t, boolean calculateX) {
if (i == 1) {
//一阶曲线
return calculateX ? (1 - t) * mControlPoints.get(j).x + t * mControlPoints.get(j + 1).x :
(1 - t) * mControlPoints.get(j).y + t * mControlPoints.get(j + 1).y;
} else {
//递归降阶
return (1 - t) * deCastelJau(i - 1, j, t, calculateX) + t * deCastelJau(i - 1, j + 1, t, calculateX);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
init();
invalidate();
}
return super.onTouchEvent(event);
}
}
绘制仿QQ气泡
package com.dn_alan.myapplication;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PointFEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
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.os.Build;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.view.animation.OvershootInterpolator;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* 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};
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DragBubbleView(Context context) {
this(context,null);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DragBubbleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public DragBubbleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr,0
);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
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;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@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();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
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);
// O1E/O1O2
float cosTheta = (mBubMoveableCenter.x - mBubStillCenter.x) / mDist;
// O2E/O1O2
float sinTheta = (mBubMoveableCenter.y - mBubStillCenter.y) / mDist;
//求出ABCD四个点的坐标
float iBubStillStartX = mBubStillCenter.x - mBubStillRadius * sinTheta;
float iBubStillStartY = mBubStillCenter.y + mBubStillRadius * cosTheta;
float iBubStillEndX = mBubStillCenter.x + mBubStillRadius * sinTheta;
float iBubStillEndY = mBubStillCenter.y - mBubStillRadius * cosTheta;
float iBubMoveableStartX = mBubMoveableCenter.x + mBubMoveableRadius * sinTheta;
float iBubMoveableStartY = mBubMoveableCenter.y - mBubMoveableRadius * cosTheta;
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();
}
}
package com.dn_alan.myapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class MainActivity extends AppCompatActivity {
private DragBubbleView dragBubbleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// //qq气泡
setContentView(R.layout.activity_main);
dragBubbleView = findViewById(R.id.drag_buddle_view);
}
public void reset(View view) {
dragBubbleView.reset();
}
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#666666">
<!--android:clipChildren="false"-->
<com.dn_alan.myapplication.DragBubbleView
android:id="@+id/drag_buddle_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
app:bubble_text="30"
app:bubble_textColor="#ffffff"
app:bubble_textSize="12dp"
app:bubble_radius="12dp"
app:bubble_color="#ff0000"
/>
<Button
android:id="@+id/reset_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="reset"
android:layout_alignParentBottom="true"
android:layout_margin="20dp"
android:textColor="#666666"
android:text="还原" />
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DragBubbleView">
<attr name="bubble_radius" format="dimension"/>
<attr name="bubble_color" format="color"/>
<attr name="bubble_text" format="string"/>
<attr name="bubble_textSize" format="dimension"/>
<attr name="bubble_textColor" format="color"/>
</declare-styleable>
</resources>
看下效果