Android拖拽、回彈佈局

這一次拆解的是今日頭條的關注頁面:點擊關注的頭像會彈出一個文章列表。在邊界拖拽會出現關閉提示。此次同時實現了Android端和IOS端的效果。git

先講解Android端的實現吧,畢竟我是個Android開發仔呀

效果以下圖: github

Android端

Android端

彈出來的頁面能夠左右切換,每一個頁面是單獨的列表,能上下滑動,因此這裏直接用viewPager+recycelrView實現。 當viewPager不能左右滑動的時候,移動整個viewPager,出現文字提示,當滑動距離超過閾值時,文字改變。 當手指鬆開時,若滑動距離未到達閾值,回彈;不然結束頁面。 一樣,當recyclerView在頂部不能滑動時,移動recyclerView,出現提示,後續跟viewPager一致故再也不贅述。canvas

ReBoundLayout

這裏的回彈我自定義了一個回彈佈局,下面介紹一下回彈佈局的幾個重要方法: onInterceptTouchEvent()bash

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                //記錄座標
                break;
            case MotionEvent.ACTION_MOVE:
                int difX = (int) (ev.getX() - mDownX);
                int difY = (int) (ev.getY() - mDownY);
                if (orientation == LinearLayout.HORIZONTAL) {
                        .....
                    if (水平滑動) {
                        if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                            //右拉到邊界
                            return true;
                        }
                        if (!innerView.canScrollHorizontally(1) && difX < 0) {
                            //左拉到邊界
                            return true;
                        }
                    }
                } else {
                     ......
                    if (豎直滑動) {
                        if (!innerView.canScrollVertically(-1) && difY > 0) {
                            //下拉到邊界
                            return true;
                        }
                        if (!innerView.canScrollVertically(1) && difY < 0) {
                            //上拉到邊界
                            return true;
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
               ......重置變量
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
複製代碼

當控件方向爲橫向且滑動爲水平滑動時,檢測innerView可否在該方向上滑動;若不能,則攔截事件,交給自身處理(縱向同理)。 攔截事件後,在**onTouchEvent()**進行處理,實現移動和回彈。ide

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                if (orientation == LinearLayout.HORIZONTAL) {
                    int difX = (int) ((event.getX() - mDownX) / resistance);
                    boolean isRebound = false;
                    if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                        //右拉到邊界
                        isRebound = true;
                    } else if (!innerView.canScrollHorizontally(1) && difX < 0) {
                        //左拉到邊界
                        isRebound = true;
                    }
                    if (isRebound) {
                        //移動和回調
                        return true;
                    }
                } else {
                    int difY = (int) ((event.getY() - mDownY) / resistance);
                    boolean isRebound = false;
                    if (!innerView.canScrollVertically(-1) && difY > 0) {
                        //下拉到邊界
                        isRebound = true;
                    } else if (!innerView.canScrollVertically(1) && difY < 0) {
                        //上拉到邊界
                        isRebound = true;
                    }
                    if (isRebound) {
                        //移動和回調
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (orientation == LinearLayout.HORIZONTAL) {
                    int difX = (int) innerView.getTranslationX();
                    if (difX != 0) {
                        if (Math.abs(difX) <= resetDistance || isNeedReset) {
                            innerView.animate().translationX(0).setDuration(mDuration).setInterpolator(mInterpolator);
                        }
                        //回調
                    }
                } else {
                    int difY = (int) innerView.getTranslationY();
                    if (difY != 0) {
                        if (Math.abs(difY) <= resetDistance || isNeedReset) {
                            innerView.animate().translationY(0).setDuration(mDuration).setInterpolator(mInterpolator);
                        }
                        //回調
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
複製代碼

MOVE事件 利用**setTranslationX()setTranslationY()**改變innerView的位置,同時將滑動距離和方向經過接口回調到外面。佈局

UP事件 判斷滑動距離是否小於閾值,小於則執行回彈動畫;同時回調到外面。 以上就是回彈佈局的簡單實現,主要是對滑動事件進行攔截處理,若是不清楚事件傳遞機制能夠到這裏查看。 佈局有3個自定義屬性動畫

<declare-styleable name="ReBoundLayout">
        <attr name="reBoundOrientation" format="enum">
            <enum name="horizontal" value="0" />
            <enum name="vertical" value="1" />
        </attr>
        <attr name="resistance" format="float" />
        <attr name="reBoundDuration" format="integer" />
</declare-styleable>
複製代碼

分別是:回彈方向、阻力系數、回彈時間,剩餘屬性能夠調用**set()**方法修改。ui


好了,如今回彈實現了,接下來就是將文字提示加上,結束動畫加上。這裏有一點須要注意的是:demo中使用的是reBoundLayout+viewPager+fragment(reBoundLayout+recyclerView)的結構實現的。而文字是跟viewPager同一層級的,因此須要把fragment的回調回調到activity裏(也能夠getActivity()獲取對應的文字控件),詳見代碼。 如下是回調的僞代碼:spa

@Override
    public void onDistanceChange(int distance, int direction) {
        switch (direction) {
            case DIRECTION_LEFT:
                if (distance > showTipDistance) {
                    //文字改變,移動
                } else {
                    rightTip.setVisibility(View.GONE);
                }
                break;
            case DIRECTION_RIGHT:
                if (distance > showTipDistance) {
                   //文字改變,移動
                } else {
                    leftTip.setVisibility(View.GONE);
                }
                break;
            case DIRECTION_UP:
                break;
            case DIRECTION_DOWN:
                //fragment的回調會走到這裏
                if (distance > showTipDistance) {
                    //文字改變,移動
                } else {
                    topTip.setVisibility(View.GONE);
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void onFingerUp(int distance, int direction) {
        switch (direction) {
            case DIRECTION_LEFT:
                if (distance > mResetDistance) {
                    //結束頁面
                } else {
                    //文字重置
                }
                break;
            case DIRECTION_RIGHT:
                if (distance > mResetDistance) {
                   //結束頁面
                } else {
                    //文字重置
                }
                break;
            case DIRECTION_DOWN:
                if (distance > mResetDistance) {
                  //結束頁面
                } else {
                    //文字重置
                }
                break;
            default:
                break;
        }
    }
複製代碼

大功告成,Android端的效果比較簡單,實現起來也比較容易。

IOS端效果複雜一丟丟,你們留心看。

效果以下: .net

IOS端
當頁面不能拖動時(右滑、左滑、下滑),view的位置開始改變,而且整個頁面會縮小成一個圓;當鬆手時距離大於閾值,view縮小爲一個圓並平移到進入的那個圓位置,結束當前頁面;不然回彈(demo中只給出一個圓,若需實現頭條的效果,只需更改對應Point點得座標便可)。 一樣,自定義一個佈局進行滑動事件的處理,至於整個頁面的縮小變圓,這裏經過裁剪畫布的方式去實現(圓心固定在屏幕中央),也能夠經過別的方法(Xfermode)去實現一樣的效果,有興趣的朋友自行探索。

PS:若是想圓心跟隨手指移動,須要增長如下計算:圓最大半徑、圓可移動距離與半徑變化關係

DragZoomLayout

關鍵變量:

  • mMinRadius 圓最小半徑
  • mMaxRadius 圓最大半徑
  • mRadius 當前半徑
  • mTranslationX 當前X移動距離
  • mTranslationY 當前Y移動距離

事件攔截跟ReBoundLayout一致,因此不贅述,主要看看滑動事件的處理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                int difX = (int) ((event.getX() - mDownX) / resistance);
                int difY = (int) ((event.getY() - mDownY) / resistance);
                if (orientation == LinearLayout.HORIZONTAL) {
                    boolean needDrag = false;
                    if (!innerView.canScrollHorizontally(-1) && difX > 0) {
                        //右啦到邊界
                        needDrag = true;
                    } else if (!innerView.canScrollHorizontally(1) && difX < 0) {
                        //左拉到邊界
                        needDrag = true;
                    }
                    if (needDrag) {
                        //半徑計算
                        mTranslationX = difX;
                        mTranslationY = difY;
                        invalidate();
                        //回調
                        return true;
                    }
                } else {
                    if (!innerView.canScrollVertically(-1) && difY > 0) {
                        //下拉到邊界
                        //回調
                        return true;
                    } else if (!innerView.canScrollVertically(1) && difY < 0) {
                        //上啦到邊界
                        innerView.setTranslationY(difY);
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (orientation == LinearLayout.HORIZONTAL) {
                    //水平
                    if (Math.abs(mTranslationX) >= resetDistance) {
                        //回調
                    } else {
                        //重置狀態
                    }
                } else {
                    //豎直
                    if (innerView.getTranslationY() < 0) {
                        innerView.animate().setDuration(mDuration).translationY(0).setInterpolator(mInterpolator);
                    } else {
                        //回調
                    }
                }
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
複製代碼

這裏跟ReBoundLayout有如下幾點區別:

  • 經過裁剪畫布的方式達到view縮小成圓的效果
  • 經過移動畫布達到移動view的效果(setTranslation會觸發view的重繪,同時改變x跟y,會調用2次,而修改畫布大小又須要重繪,調用次數太多,所以不使用該方式)
  • 下滑跟左右滑動同樣,縮小、移動的都是最外層的DragZoomLayout(這樣視覺效果最好,並且能統一處理);上滑只作移動和回彈。

PS:DragZoomLayout必定要設置背景,否則調用invalidate()會無效;上下滑動的mTranslationX、mTranslationY一直都是0(由於下滑咱們已經回調給最層的DragZoomLayout),因此在ACTION_UP、ACTION_CANCEL事件,豎直方向回調時是使用當前事件的x、y跟點擊的x、y相減的值去回調。

佈局繪製
@Override
    protected void onDraw(Canvas canvas) {
        if (Math.abs(mTranslationX) > mLargeX) {
            mTranslationX = mTranslationX > 0 ? mLargeX : -mLargeX;
        }
        if (Math.abs(mTranslationY) > mLargeY) {
            mTranslationY = mTranslationY > 0 ? mLargeY : -mLargeY;
        }
        canvas.translate(mTranslationX, mTranslationY);
        mPath.reset();
        mPath.addCircle(mPoint.x, mPoint.y, mRadius, Path.Direction.CCW);
        canvas.clipPath(mPath);
        super.onDraw(canvas);
    }
複製代碼

進行了一些位置和半徑的限制。 佈局完成,接下來處理頁面間的接口回調及結束動畫


動畫的計算有一點點麻煩,數學很差的同窗請多看幾遍,仍是不懂的趁着過年回高中找數學老師要回學費吧。

先來看沒有移動畫布的狀況:
啓動頁面時,經過**getLocationOnScreen()**獲取進入時的座標,退出時的座標經過最外層的dragLayout的座標加上寬高的一半,再減去圓的最小半徑獲得,最後經過這2個差值進行平移。 那麼有平移而且半徑未到最小的狀況也能夠經過這種方式計算:
咱們已經有一個translationX了,那能夠計算出目標的translationX,而後使用ValueAnimator不斷去改變它進行重繪,獲得一個平移效果(translationY同理)。那這個值要怎麼獲得呢?上面已經說了怎麼計算了,沒看懂的再看一遍。看幾遍仍是不懂的,回去找老師要學費吧。
至於進入動畫原理相同,只是反過來執行罷了,這裏再也不贅述,詳見代碼。 有更好實現方式的歡迎下方留言討論,有bug或者疑問的也能夠留言,有空會回覆的。 因爲篇幅關係,一些細小的地方沒有說起,有興趣的朋友能夠自行查看。 最後奉上 源碼;


這是年前最後一篇博客了,今年立的flag好像都沒有實現,跟大佬的差距仍是那麼大,Bug仔仍需努力呀。

9102衝鴨
相關文章
相關標籤/搜索