Android自定义View - QQ小红点

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Viiou/article/details/89318516

本文参考:https://blog.csdn.net/mabeijianxi/article/details/50560361

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 {
                    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();
    }
}

猜你喜欢

转载自blog.csdn.net/Viiou/article/details/89318516
今日推荐