自定義 ViewGroup 支持無限循環翻頁之三(響應回調事件)

大家如果喜歡我的博客,請關注一下我的微博,請點擊這裏(http://weibo.com/kifile),謝謝

轉載請標明出處,再次感謝

#######################################################################

自定義 ViewGroup 支持無限循環翻頁系列

自定義 ViewGroup 支持無限循環翻頁之一(重寫 onLayout以及 dispatchDraw)

自定義 ViewGroup 支持無限循環翻頁之二(處理觸摸事件)

自定義 ViewGroup 支持無限循環翻頁之三(響應回調事件)

#######################################################################


在上一篇博文中,我曾提到過,我是用了一個 TouchHandler 來處理 view 的 touch 事件,再在 TouchHandler 內通過回調接口通知 view 處理對應的回調事件.

現在我們就來詳細分析一下,如何通過處理回調事件,實現界面的滑動與切換

首先,我們先了解一下在 Android 的View 類中是如何進行界面位置的

在 View 類中,有兩個方法 scrollBy,scrollTo, 來控制當前屏幕的顯示區域,同時內部有兩個變量 mScrollX,mScrollY 來記錄當前的位移量

對於 scrollBy 方法,他其實是通過之前記錄的 mScrollX,mScrollY 來計算出應該位移到的座標點,然後調用 scrollTo 方法進行移動,這個方法的作用也正如其名,就是用來將整個視圖進行偏移的.

此外 Android 還爲我們提供了一個 Scroller 類幫助我們進行平滑的移動, Scroller 類主要通過一個插值器計算經過某段時間後,當前界面的座標位置,然後 Android 系統通過在調用 invalidate方法時所調用到得 computeScroll 方法中,對界面位置進行重新獲取,從而讓移動看起來連續.

Ok,現在我們正式開始響應回調事件,我們先查看一下起始界面,


如圖所示,在默認情況下,我們一共有三組完全一致的圖片,可能會被顯示到屏幕上,以實現我們連續滑動的效果.

當我們手指按下,開始移動的時候,我們也希望界面能夠跟隨我們的手指進行移動,例如當我們手指向左滑動的時候,我們希望看到的界面是這樣子的


因此,我們希望屏幕的顯示區域進行一個位移,來達到這種效果.

我們在 TouchHandler 中的 handleTouchEvent 方法中,有過對手指移動的事件進行處理,如果手指移動區域超過某個限值後,我們會獲取每次 touch 事件的差值,調用 onScrollBy 的回調方法,所以我們只需要在 SerialScreenLayout 中繼承這個接口,並實現方法即可.

實現方案如下:

@Override
    public void onScrollBy(int dx, int dy) {
        scrollBy(dx, 0);
    }

根據接口方法的傳入值進行界面偏移即可,注意由於我們只需要在 x 軸上做變換,因此 y 軸的偏移我們可以令他恆爲0.

這樣一來,當我們的手指在屏幕上進行滑動的時候,界面就會跟隨我們手指的移動而移動了.

然後當我們的手指釋放的時候,我們其實並不希望界面停留在之前的位置,而是希望他根據之前我們手指的位移偏量或者滑動速度,切換到一個獨立的 View 界面去,如下:


所以我們在 TouchHandler 類中,當手指停止觸摸,觸發 ACTION_UP 的時候,我們向 View, 進行一次 onRelease 回調,然後在方法中對當前位置和速度進行判別,以確定應該跳轉的屏幕位置,然後再進行一次屏幕平滑移動,詳細代碼如下:

@Override
    public void onRelease(int velocityX, int velocityY, boolean cancel) {
        cleanPosition();
        int targetPosition = mCurrPosition;
        if (mDirty) {
            mDirty = false;
            if (Math.abs(mLastPosition - getScrollX()) > mGutterSize && Math.abs(velocityX) > mMinimumVelocity) {
                if (mTmpDirectionLeft) {
                    if (velocityX > 0) {
                        targetPosition -= 1;
                    } else {
                        if (getScrollX() > mCurrPosition * mWidth) {
                            targetPosition += 1;
                        }
                    }
                } else {
                    if (velocityX > 0) {
                        if (getScrollX() > mCurrPosition * mWidth) {
                            targetPosition -= 1;
                        }
                    } else {
                        targetPosition += 1;
                    }
                }
            }
            scrollToPosition(targetPosition, 0);
        } else if (!cancel) {
            int startX = mWidth * mCurrPosition;
            int deltaX = getScrollX() - startX;
            if (deltaX > 0) {
                if (velocityX <= 0) {
                    if (Math.abs(deltaX) > mGutterSize) {
                        targetPosition = mCurrPosition + 1;
                    } else if (Math.abs(velocityX) > mMinimumVelocity) {
                        targetPosition = mCurrPosition + 1;
                    }
                }
            } else {
                if (velocityX >= 0) {
                    if (Math.abs(deltaX) > mGutterSize) {
                        targetPosition = mCurrPosition - 1;
                    } else if (Math.abs(velocityX) > mMinimumVelocity) {
                        targetPosition = mCurrPosition - 1;
                    }
                }
            }
            scrollToPosition(targetPosition, velocityX);
        }else{
            scrollToPosition(targetPosition, velocityX);
        }
    }


上面代碼中的 mDirty 將會在之後的部分被討論,我們先來看看其他的部分, cancel 僅當 touch 事件爲 ACTION_CANCEL 時爲 true,說明整個滑動過程被取消,所以移動到之前的位置,否則就根據當前的速度以及移動偏移進行判斷,然後選擇下一個位置進行平滑位移.

平滑位移的代碼如下:

private void scrollToPosition(int targetPosition, int velocityX) {
        cleanPosition();
        targetPosition = formatPosition(targetPosition);
        if (mCurrPosition == 0 && targetPosition == getChildCount() - 1) {
            targetPosition = -1;
        } else if (mCurrPosition == getChildCount() - 1 && targetPosition == 0) {
            targetPosition = getChildCount();
        }
        int startX = getScrollX();
        int finalX = targetPosition * mWidth;
        final int delta = Math.abs(finalX - startX);
        velocityX = Math.abs(velocityX);

        int duration = (int) (1.0f * DEFAULT_DURATION * delta / mWidth);
        if (velocityX > mMinimumVelocity) {
            final int width = mWidth;
            final int halfWidth = width / 2;
            final float distanceRatio = Math.min(1f, 1.0f * delta / width);
            final float distance = halfWidth + halfWidth *
                    distanceInfluenceForSnapDuration(distanceRatio);
            int velocityDuration = 4 * Math.round(1000 * Math.abs(distance / velocityX));
            duration = Math.min(duration, velocityDuration);
        }
        mScroller.startScroll(startX, 0, finalX - startX, 0, duration);
        invalidate();
        mCurrPosition = targetPosition;
    }

在這裏,我們根據滑動速度計算出一個移動耗時,然後啓動 mScroller, 進行滑動位置計算,而後調用 invalidate 進行位移

這樣一來,當我們鬆開手指,界面就會平滑的移動到對應的 View 去.

當時這樣一來,一個新的問題就冒出來了,假如現在我們的位置是這樣的:

那麼我們移動的時候我們手指向右滑動,左側仍然會出現空白的 View 而非最後一張,這樣的話,其實我們就只是解決了中間部分的連續,但是邊緣的連續還是會出現問題,爲了解決這個問題,我們建了一個 cleanPosition()方法,用來計算當前的位置,代碼如下:

  private void cleanPosition() {
        final int position = mCurrPosition;
        final int sx = position * mWidth;
        final int cx = getScrollX();
        int delta = cx - sx;
        if (Math.abs(delta) >= mWidth) {
            //scrolled to a new page
            if (delta > 0) {
                mCurrPosition = cx / mWidth;
            } else {
                mCurrPosition = cx / mWidth - 1;
            }
        }
        final int tmp = formatPosition(mCurrPosition);
        if (tmp != mCurrPosition) {
            scrollBy((tmp - mCurrPosition) * mWidth, 0);
            mCurrPosition = tmp;
        }
    }

    private int formatPosition(int position) {
        if (position < 0) {
            do {
                position += getChildCount();
            } while (position < 0);
        } else if (position >= getChildCount()) {
            do {
                position -= getChildCount();
            } while (position >= getChildCount());
        }
        return position;
    }

當我們的移動範圍超過 View 的真實顯示範圍之後,我們會通過計算,將顯示範圍平移回正常的界面,這樣,我們最終的顯示範圍始終都在基本的三個 View 內部,這樣就解決了可能出現的邊緣不連續問題.

當時,在實際滑動過程中,我們滑動的時候,如果雙指交替滑動,那麼我們就可以強行將 view 滑動至邊緣而出現不連續問題,所以我們針對多點觸控的回調方法中,再調用了 cleanPosition, 來保持 view 始終處於正確位置.

實際滑動過程中,還可能出現某次滑動未結束,我們手指就已經開始了下一次的滑動,那麼,之前的 onRelease 判斷就會出現邏輯問題,因爲之前我們是根據滑動速度和偏移量進行判斷的下一個位置,但是現在滑動還未結束,起始的偏移量和位置都與之前不同,因此我們在 onTouchEvent 中,做了如下實現:

 @Override
    public void onTouch(MotionEvent event) {
        if (!mScroller.isFinished()) {
            if (Math.abs(mScroller.getCurrX() - mScroller.getFinalX()) > mGutterSize) {
                mDirty = true;
                cleanPosition();
                mLastPosition = getScrollX();
                if (mScroller.getFinalX() > mScroller.getCurrX()) {
                    mTmpDirectionLeft = true;
                } else {
                    mTmpDirectionLeft = false;
                }
                mScroller.abortAnimation();
            }
        } else {
            mDirty = false;
        }
    }

在這裏,我們記錄下當前滑動的一些相關屬性,並令 mDirty 變量爲 true,使得自身能夠判斷是否是在滑動過程中突然中斷,然後出發事件,然後再在 onRelease 中根據當前的相關屬性進行判斷,選擇下一位置.

寫到這裏,整個 SerialScreenLayout 的實現邏輯已經介紹完了,其主要原理就是:

在 onLayout 中根據每個 view 的順序讓他們單獨享用一個屏幕的固定空間,再在 dispatchDraw 的時候,在左右兩側分別多繪製一組圖像,使得顯示連續,最後處理 touch 事件,使得整個自定義 ViewGroup 能夠移動,並在停止觸摸後能夠移動到某段屏幕位置

發佈了28 篇原創文章 · 獲贊 7 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章