自定义控件实例(一) 仿QQ小红点

最近在复习Android,重新捡起自定义控件。
总结一句话,不要用小聪明规避问题,不然你下次遇到了,你还是什么都不懂。
先看效果:
在这里插入图片描述
工程以及说明: QQ小红点

首先,看到这个,想到的就是使用贝塞尔曲线了。如果你对怎么绘制不了解,可以先通过启舰大佬,了解QQ小红点怎么实现的。
自定义控件三部曲之绘图篇(十五)——QQ红点拖动删除效果实现(基本原理篇)

不过上面的文章,只能在view级别上使用,就算使用了 clipchild = false,也无法夸层级绘制的,所以这篇文章是在此基础上,让它可以在所有view上都能使用。
思路如下:

  • 用一个BezierPointView继承TextView,拿到自定义配置参数,比如大小,颜色
  • 在 BezierPointView 的Down事件时,把QQ小红点 (BezierPointWindow) 通过 Windowmanager 添加进来,再把 BezierPointView 的visiable 设置 gone
  • 当 BezierPointWindow 的Up 事件时,把坐标传递回来,然后移除BezierPointWindow,添加一个ImageView,再在此做自定义的属性动画,或者默认的图片爆炸效果
  • 移除windowmanager,让 BezierPointView 显示出来即可。

这里我没有跟启舰大佬那样,用viewgroup来实现,其实用自定义 View 即可实现。实现的原理,启舰大佬已经讲解得很清楚了
在这里插入图片描述
我们只要找准一个方向,计算切线即可。如果两个 a 相等呢?相信你的判断两条线是否平行的原理还记得。
既然知道了两个小球的坐标和半径,那么切点的坐标也很好理解了,然后再计算不断变化的半径和最大断开的长度即可。

    /**
     * 计算贝塞尔曲线
     * 由于Math 的扫脚函数,都自带正负的,所以,只需要计算一种方向即可
     * 这里的计算方向为右下
     */
    private void calculateBeizer() {
        float x0 = mStartPoint.x;
        float y0 = mStartPoint.y;
        float x = mMovePoint.x;
        float y = mMovePoint.y;

        //算出夹角
        float dx = x - x0;
        float dy = y - y0;
        double a = Math.atan(dy / dx);
        //拿到圆切点的长度偏移量
        float offsetx0 = (float) (mDrawRadius * Math.sin(a));
        float offsety0 = (float) (mDrawRadius * Math.cos(a));
        
        //拿到第二个小球的偏移量
        float offsetx = (float) (mDefaultRadius * Math.sin(a));
        float offsety = (float) (mDefaultRadius * Math.cos(a));

        //算出第一个圆的切点坐标
        float p0x = x0 + offsetx0;
        float p0y = y0 - offsety0;

        float p1x = x0 - offsetx0;
        float p1y = y0 + offsety0;
        //算出第二个圆的切点坐标
        float p2x = x + offsetx;
        float p2y = y - offsety;

        float p3x = x - offsetx;
        float p3y = y + offsety;
        //计算贝塞尔辅助点
        float anchorx = (x0 + x) / 2;
        float anchory = (y0 + y) / 2;
        //清掉上次,避免残留
        mPath.reset();

        //形成贝塞尔曲线
        mPath.moveTo(p0x, p0y);
        mPath.quadTo(anchorx, anchory, p2x, p2y);
        mPath.lineTo(p3x, p3y);
        mPath.quadTo(anchorx, anchory, p1x, p1y);
        mPath.close();

        //超过一定距离时,且圆形的半径也要跟着变小
        double distance = getDistance(mMovePoint, mStartPoint);
        mDrawRadius = (int) (mDefaultRadius - distance / 12);
        if (mDrawRadius <= 7) {
            mDrawRadius = 7;
        }
        if (distance >= mMaxMoveLength) {
            // 超过一定距离 贝塞尔和固定圆都不要画了
            mIsBreakUp = true;
            mDrawRadius = 0;
            mPath.reset();
            return;
        }
        
    }

在所有 View 上使用

我们可以通过一个 BezierPointView 拿到自定义属性之后,在 onSizeChange 的时候,配置windowmanger:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPointHelper = new PointHelper(this);
        setOnTouchListener(mPointHelper);
    }

而PointHelper 其实是一个 windowmanager 的工具类,继承View.OnTouchListener,这样就可以在 onTouch 方法中,添加windowmanager :

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ViewParent parent = v.getParent();
        if (parent == null) {
            return false;
        }
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {

            if (!mWindowView.isCanMove()) {
                return false;
            }
            //接管父控件的touch事件
            parent.requestDisallowInterceptTouchEvent(true);

            /**
             *  当按下时,再把View添加到windowmanager,此时的windowmanager是全屏的;
             *  所以,pointview的操作不会因为控件大小的限制,导致绘制不全的问题
             */

            addPointToWindow();
        }
        return mWindowView.onTouchEvent(event);

可以看到 touch之后是交给 BezierPointWindow 的touch 事件,而addPointToWindow 为具体的添加效果。


    /**
     * 把TextView 的缓存bitmap给BezierPointWindow绘制
     */
    private void addPointToWindow() {

        if (mContainer == null) {
            mContainer = new FrameLayout(mContext);
            mContainer.setClipChildren(false);
            mContainer.setClipToPadding(false);
            mWindowView.setLayoutParams(mLayoutParams);
        }
        mContainer.removeAllViews();
        mContainer.addView(mWindowView, mParams);
        mWindowManager.addView(mContainer, mParams);
        //初始化坐标
        mPointView.getLocationInWindow(mPos);
        int width = mPointView.getWidth();
        int height = mPointView.getHeight();

        //设置大小和起始位置
        mWindowView.initPoint(mPos[0] + width / 2, mPos[1] + height / 2);
        mWindowView.setPointListener(this);
        mWindowView.setVisibility(View.VISIBLE);
        //拿到bitmap
        mPointView.setDrawingCacheEnabled(true);
        //拿到TextView的缓存bitmap,给BezierPointWindow 绘制,也为后面up的 imageview 当做背景图
        Bitmap bitmap = Bitmap.createBitmap(mPointView.getDrawingCache());
        mPointView.setDrawingCacheEnabled(false);
        mWindowView.setBitmap(bitmap);
    }

为了避免闪烁问题,我们需要当 BezierPointWindow 绘制好 bitmap 之后,再让BezierPointView 消失:

    @Override
    public void onDrawReady() {
        //这个时候,可以消失了,避免闪烁
        mPointView.setVisibility(View.GONE);
    }

而在 up 之后,之后,只需要添加一个 ImageView,实现要消失的动画即可:

    @Override
    public void destroy(PointF pointF) {
        //移除所有
        mContainer.removeAllViews();
        //添加imageview,用于动画
        mContainer.addView(mImageView, mLayoutParams);
        //指定初始位置
        mImageView.setX(pointF.x - mPointView.getWidth() * 1.0f / 2);
        mImageView.setY(pointF.y - mPointView.getHeight() * 1.0f / 2);

        if (mPointView.isUseSelfAnim()){
            //使用自定义动画
            mImageView.setImageBitmap(mWindowView.getBitmap());
            if (mPointView.getListener() != null) {
                mPointView.getListener().destory(mImageView);
            }
        }else{
            //使用布局标签的属性动画
           if (mPointView.getAnimatorRes() != -1){
                mImageView.setImageBitmap(mWindowView.getBitmap());
                mAnimator = AnimatorInflater.loadAnimator(mContext, mPointView.getAnimatorRes());
                mAnimator.setTarget(mImageView);
                mAnimator.start();
                mAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        removeView();
                    }
                });
            }else{
                //使用默认动画,即图片爆炸
                mImageView.setBackgroundResource(R.drawable.anim_blow);
                mAnimationDrawable = (AnimationDrawable) mImageView.getBackground();
                mAnimationDrawable.start();
                mImageView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        removeView();
                    }
                },getAnimationDrawableTime(mAnimationDrawable));
            }
        }

    }

需要注意的是,最后需要使用 removeView 方法,不然这层 windowmanager 的view 不会消失:

    /**
     * 移除View,这个必须要实现,否则就有一层view在顶层,啥也操作不了
     */
    public void removeView() {
        if (mWindowManager != null) {
            if (mContainer != null && mContainer.isAttachedToWindow()) {
                mWindowManager.removeView(mContainer);
            }
        }
        //还原set属性,防止view被设置动画,初始位置错乱
        mWindowView.clearAnimation();
        mWindowView.setAlpha(1);
        mWindowView.setX(0);
        mWindowView.setY(0);
        mWindowView.setScaleX(1);
        mWindowView.setScaleY(1);
        mWindowView.setVisibility(View.GONE);
        if (mContainer != null && mContainer.getChildCount() > 0) {
            mContainer.removeView(mWindowView);
        }
    }
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页