自定義控件實例(一) 仿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);
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章