Android自定义View - QQ小红点
2019-04-15 19:05
225 查看
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Viiou/article/details/89318516
本文参考:https://www.geek-share.com/detail/2664716185.html
1.原理
这是我们要实现的最基本的效果,我们先分析一下这个是由什么组成的。
很简单,两个圆加两条曲线,圆大家都会画,那这两条曲线怎么绘制?
这两条曲线是两条二阶贝塞尔曲线,只需要绘制贝塞尔曲线就行了。但在绘制一条二阶贝塞尔曲线时需要三个点,这三个点是什么点?
从图片中可以看到的是,一条贝塞尔曲线的三个点是同一边的两个外切点加中间的控制点构成,中间控制点的坐标容易得出,两个圆心的坐标相加除以2就可以了,那A,B,C,D这四个外切点的坐标怎么得到?
可以先求出a的角度,然后再求出b的角度,根据三角函数公式便可以求出A,B,C,D四个点的坐标值。
2.基础实现
public class StickyBaseView extends View { private Context mContext; private Paint mPaint; private PointF mCenter; private PointF mFixCircle; private PointF mFlexibleCircle; private PointF mControl; private PointF[] mFixTangent; private PointF[] mFlexibleTangent; private int mRadius; private int mSize; private boolean mIsDraw; private Path mPath; public StickyBaseView(Context context) { super(context); init(context); } public StickyBaseView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public StickyBaseView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { mContext = context; mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mRadius = 20; mSize = 100; mIsDraw = false; mControl = new PointF(); mPath = new Path(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mCenter = new PointF(w / 2, h / 2); mFixCircle = new PointF(w / 2, h / 2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(mFixCircle.x, mFixCircle.y, mRadius, mPaint); if (mIsDraw) { canvas.drawCircle(mFlexibleCircle.x, mFlexibleCircle.y, mRadius, mPaint); mControl.set((mFixCircle.x + mFlexibleCircle.x) / 2, (mFixCircle.y + mFlexibleCircle.y) / 2); float dy = mFlexibleCircle.y - mFixCircle.y; float dx = mFlexibleCircle.x - mFixCircle.x; if (dx != 0) { float k1 = dy / dx; float k2 = -1 / k1; mFlexibleTangent = getTangentPoints(mFlexibleCircle, mRadius, (double) k2); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) k2); } else { mFlexibleTangent = getTangentPoints(mFlexibleCircle, mRadius, (double) 0); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) 0); } mPath.reset(); mPath.moveTo(mFixTangent[0].x, mFixTangent[0].y); mPath.quadTo(mControl.x, mControl.y, mFlexibleTangent[0].x, mFlexibleTangent[0].y); mPath.lineTo(mFlexibleTangent[1].x, mFlexibleTangent[1].y); mPath.quadTo(mControl.x, mControl.y, mFixTangent[1].x, mFixTangent[1].y); mPath.close(); canvas.drawPath(mPath, mPaint); } } private PointF[] getTangentPoints(PointF pointF, float radius, Double lineK) { PointF[] pointFS = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if (lineK != null) { radian = (float) Math.atan(lineK); xOffset = (float) (Math.cos(radian) * radius); yOffset = (float) (Math.sin(radian) * radius); } else { xOffset = radius; yOffset = 0; } pointFS[0] = new PointF(pointF.x + xOffset, pointF.y + yOffset); pointFS[1] = new PointF(pointF.x - xOffset, pointF.y - yOffset); return pointFS; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { mIsDraw = true; mFlexibleCircle = new PointF(event.getX(), event.getY()); invalidate(); break; } case MotionEvent.ACTION_MOVE: { mFlexibleCircle.set(event.getX(), event.getY()); invalidate(); break; } case MotionEvent.ACTION_UP: { mIsDraw = false; break; } } return true; } }
3.进阶实现
在基础实现的基础上添加了随着拖动距离的增加,原来的圆会逐渐缩小。
public class StickyAdvancedView extends View { private Context mContext; private Paint mPaint; private PointF mCenter; private PointF mFixCircle; private PointF mFlexibleCircle; private PointF mControl; private PointF[] mFixTangent; private PointF[] mFlexibleTangent; private float mRadius; private float mFlexibleRadius; private float mSize; private boolean mIsDraw; private boolean mIsIn; private Path mPath; public StickyAdvancedView(Context context) { super(context); init(context); } public StickyAdvancedView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public StickyAdvancedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { mContext = context; mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mRadius = 30; mFlexibleRadius = 30; mSize = 250; mIsIn = true; mIsDraw = false; mControl = new PointF(); mPath = new Path(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mCenter = new PointF(w / 2, h / 2); mFixCircle = new PointF(w / 2, h / 2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mIsIn) { canvas.drawCircle(mFixCircle.x, mFixCircle.y, mRadius, mPaint); } if (mIsDraw && mIsIn) { canvas.drawCircle(mFlexibleCircle.x, mFlexibleCircle.y, mFlexibleRadius, mPaint); mControl.set((mFixCircle.x + mFlexibleCircle.x) / 2, (mFixCircle.y + mFlexibleCircle.y) / 2); float dy = mFlexibleCircle.y - mFixCircle.y; float dx = mFlexibleCircle.x - mFixCircle.x; if (dx != 0) { float k1 = dy / dx; float k2 = -1 / k1; mFlexibleTangent = getTangentPoints(mFlexibleCircle, mFlexibleRadius, (double) k2); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) k2); } else { mFlexibleTangent = getTangentPoints(mFlexibleCircle, mFlexibleRadius, (double) 0); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) 0); } mPath.reset(); mPath.moveTo(mFixTangent[0].x, mFixTangent[0].y); mPath.quadTo(mControl.x, mControl.y, mFlexibleTangent[0].x, mFlexibleTangent[0].y); mPath.lineTo(mFlexibleTangent[1].x, mFlexibleTangent[1].y); mPath.quadTo(mControl.x, mControl.y, mFixTangent[1].x, mFixTangent[1].y); mPath.close(); canvas.drawPath(mPath, mPaint); } } private PointF[] getTangentPoints(PointF pointF, float radius, Double lineK) { PointF[] pointFS = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if (lineK != null) { radian = (float) Math.atan(lineK); xOffset = (float) (Math.cos(radian) * radius); yOffset = (float) (Math.sin(radian) * radius); } else { xOffset = radius; yOffset = 0; } pointFS[0] = new PointF(pointF.x + xOffset, pointF.y + yOffset); pointFS[1] = new PointF(pointF.x - xOffset, pointF.y - yOffset); return pointFS; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { mIsDraw = true; mFlexibleCircle = new PointF(event.getX(), event.getY()); changeRadius(); invalidate(); break; } case MotionEvent.ACTION_MOVE: { mFlexibleCircle.set(event.getX(), event.getY()); changeRadius(); invalidate(); break; } default: { mIsDraw = false; mIsIn = true; mRadius = mFlexibleRadius; invalidate(); break; } } return true; } private void changeRadius() { float distance = (float) Math.sqrt(Math.pow(mFixCircle.x - mFlexibleCircle.x, 2) + Math.pow(mFixCircle.y - mFlexibleCircle.y, 2)); if (distance < mSize) { mRadius = (mFlexibleRadius * (1 - distance / mSize)); if (mRadius < 0.2 * mFlexibleRadius) { mRadius = (float) (0.2 * mFlexibleRadius); } } else { mIsIn = false; } } }
4.最终实现
添加最后抬起手指的一刻,小红点与原始小红点的距离判断,选择是生成新的小红点还是原来的小红点,并留下了接口让使用者进行处理。
public class StickyFinallyView extends View { private static final String TAG = "StickyFinallyView"; private RedSpotInOut mRedSpotInOut; private Context mContext; private Paint mPaint; private PointF mCenter; private PointF mFixCircle; private PointF mFlexibleCircle; private PointF mControl; private PointF[] mFixTangent; private PointF[] mFlexibleTangent; private float mRadius; private float mFlexibleRadius; private float mSize; private boolean mIsDraw; private boolean mIsIn; private Path mPath; public StickyFinallyView(Context context) { super(context); init(context); } public StickyFinallyView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context); } public StickyFinallyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { mContext = context; mPaint = new Paint(); mPaint.setColor(Color.RED); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL); mRadius = 30; mFlexibleRadius = 30; mSize = 250; mIsIn = true; mIsDraw = false; mControl = new PointF(); mPath = new Path(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mCenter = new PointF(w / 2, h / 2); mFixCircle = new PointF(w / 2, h / 2); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mIsIn) { canvas.drawCircle(mFixCircle.x, mFixCircle.y, mRadius, mPaint); } if (mIsDraw && mIsIn) { canvas.drawCircle(mFlexibleCircle.x, mFlexibleCircle.y, mFlexibleRadius, mPaint); mControl.set((mFixCircle.x + mFlexibleCircle.x) / 2, (mFixCircle.y + mFlexibleCircle.y) / 2); float dy = mFlexibleCircle.y - mFixCircle.y; float dx = mFlexibleCircle.x - mFixCircle.x; if (dx != 0) { float k1 = dy / dx; float k2 = -1 / k1; mFlexibleTangent = getTangentPoints(mFlexibleCircle, mFlexibleRadius, (double) k2); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) k2); } else { mFlexibleTangent = getTangentPoints(mFlexibleCircle, mFlexibleRadius, (double) 0); mFixTangent = getTangentPoints(mFixCircle, mRadius, (double) 0); } mPath.reset(); mPath.moveTo(mFixTangent[0].x, mFixTangent[0].y); mPath.quadTo(mControl.x, mControl.y, mFlexibleTangent[0].x, mFlexibleTangent[0].y); mPath.lineTo(mFlexibleTangent[1].x, mFlexibleTangent[1].y); mPath.quadTo(mControl.x, mControl.y, mFixTangent[1].x, mFixTangent[1].y); mPath.close(); canvas.drawPath(mPath, mPaint); } if (!mIsIn) { canvas.drawCircle(mFlexibleCircle.x, mFlexibleCircle.y, mFlexibleRadius, mPaint); } } private PointF[] getTangentPoints(PointF pointF, float radius, Double lineK) { PointF[] pointFS = new PointF[2]; float radian, xOffset = 0, yOffset = 0; if (lineK != null) { radian = (float) Math.atan(lineK); xOffset = (float) (Math.cos(radian) * radius); yOffset = (float) (Math.sin(radian) * radius); } else { xOffset = radius; yOffset = 0; } pointFS[0] = new PointF(pointF.x + xOffset, pointF.y + yOffset); pointFS[1] = new PointF(pointF.x - xOffset, pointF.y - yOffset); return pointFS; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { mIsDraw = true; mFlexibleCircle = new PointF(event.getX(), event.getY()); changeRadius(); invalidate(); break; } case MotionEvent.ACTION_MOVE: { mFlexibleCircle.set(event.getX(), event.getY()); changeRadius(); invalidate(); break; } case MotionEvent.ACTION_UP: { float endX = event.getX(); float endY = event.getY(); float distance = (float) Math.sqrt(Math.pow(endX - mFixCircle.x, 2) + Math.pow(endY - mFixCircle.y, 2)); if (distance < mSize) { inSize(); } else { 8000 outSize(); } mIsDraw = false; mIsIn = true; mRadius = mFlexibleRadius; invalidate(); break; } default: { mIsDraw = false; mIsIn = true; mRadius = mFlexibleRadius; invalidate(); break; } } return true; } public void setRedSpotInOut(RedSpotInOut redSpotInOut) { mRedSpotInOut = redSpotInOut; } private void inSize() { Log.i(TAG, "inSize: 没有超出范围还是原来的气泡"); if (mRedSpotInOut != null) { mRedSpotInOut.inSize(); } } private void outSize() { Log.i(TAG, "outSize: 超出了范围,气泡碎了"); if (mRedSpotInOut != null) { mRedSpotInOut.outSize(); } } private void changeRadius() { float distance = (float) Math.sqrt(Math.pow(mFixCircle.x - mFlexibleCircle.x, 2) + Math.pow(mFixCircle.y - mFlexibleCircle.y, 2)); if (distance < mSize) { mRadius = (mFlexibleRadius * (1 - distance / mSize)); if (mRadius < 0.2 * mFlexibleRadius) { mRadius = (float) (0.2 * mFlexibleRadius); } } else { mIsIn = false; } } interface RedSpotInOut { void inSize(); void outSize(); } }
相关文章推荐
- Android 自定义View实现QQ运动积分抽奖转盘
- android自定义View 之仿QQ消息头像
- android自定义View实现图片上传进度显示(仿手机QQ上传效果)
- android:自定义HorizontalScrollView实现qq侧滑菜单 标签: HorizontalScrollView自定义viewqq侧滑菜单 2016
- android:自定义HorizontalScrollView实现qq侧滑菜单
- android自定义View实现图片上传进度显示(仿手机QQ上传效果)
- Android 自定义View修炼-仿QQ5.0 的侧滑菜单效果的实现
- Android 仿QQ侧边栏,自定义view的学习 <Garry进阶(三)>
- Android 自定义View系列之贝塞尔曲线+QQ未读消息拖拽效果实现+水波浪充电效果
- Android自定义View 仿QQ侧滑菜单的实现代码
- Android自定义View之ListView条目滑动删除(仿QQ5.1)
- 恭喜发财! -- 手把手教你仿造一个qq下拉抢红包 Android自定义view
- android自定义View实现图片上传进度显示(仿手机QQ上传效果)
- Android自定义View之高仿QQ健康
- 恭喜发财! -- 手把手教你仿造一个qq下拉抢红包 Android自定义view
- Android自定义View仿QQ等级天数进度
- Android自定义View实现仿QQ实现运动步数效果
- Android 酷炫自定义 View 高仿 QQ 窗帘菜单
- Android自定义View模仿QQ主页的开关
- Android 自定义 View 之高仿 QQ 健康