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

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

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

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

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

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

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

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

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

之前由於公司產品需求,需要一個可以無限循環滑動的佈局,來展示圖片,從網上找了很多樣例都不能很好的滿足我們親愛的產品汪的要求,在首頁以及尾頁的切換時會出現不太連續的問題,於是我就自己手動寫了一份自定義的 ViewGroup 來支持,我對這個佈局的命名爲 SerialScreenLayout(連續的屏幕布局)

這個佈局的代碼,我放到了 Github 上,項目地址:https://github.com/KiFile/Sample,這裏是我平時的 Demo, SerialScreenLayout 的具體位置是在https://github.com/KiFile/Sample/tree/master/Widget/src/main/java/com/kifile/widget

首先我們來看看產品需求,我們所需要的是一個可以無限滑動的 ViewGroup, 並且我們要求對於放置在 ViewGroup 中的每一個 View, 我們都將它視爲單獨的一頁,它能夠根據一些佈局屬性,設置 gravity 位置.對於這種情況,我想到了 FrameLayout,FrameLayout 中每個佈局的位置都是隻與父佈局的位置有關,與同一級的兄弟佈局基本沒有關係,那麼我們直接讓SerialScreenLayout 繼承自 FrameLayout, 這樣我們就不必自己寫 onMeasure 方法來衡量每個子佈局的大小了

雖然我們繼承自 FrameLayout, 但是由於我們希望每一個子 View 作爲單獨的一屏作爲展示,那麼我們就需要重寫佈局的 onLayout 函數,對每一個子 View 的位置進行一個偏移量設定,保證每個 View 都處於一個單獨的界面中.

具體代碼如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        int left = l + getPaddingLeft();
        int right = r - l - getPaddingRight();
        int top = t + getPaddingTop();
        int bottom = b - t - getPaddingTop();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();
                int childLeft;
                int childTop;
                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = Gravity.TOP | Gravity.START;
                }

                switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = left + (right - left - width) / 2 +
                                lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        childLeft = right - width - lp.rightMargin;
                        break;
                    case Gravity.LEFT:
                    default:
                        childLeft = left + lp.leftMargin;
                }

                switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
                    case Gravity.TOP:
                        childTop = top + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = top + (bottom - top - height) / 2 +
                                lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = bottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = top + lp.topMargin;
                }
                child.layout(childLeft, childTop, childLeft + width, childTop + height);
                left += mWidth;
                right += mWidth;
            }
        }
    }

這裏的主要代碼,也是參考的 FrameLayout 的佈局代碼,唯一做出的改動就是在每次佈局結束的時候,對於 left 以及 right 變量做一次屏幕寬度的變化,使得每個子 View 都佔據一個屏幕的範圍的大小,而屏幕的寬度,則是在 onMeasure 中,在執行完 super.onMeasure 方法之後,進行獲取的.

這樣一來,我們其實在整個佈局控件的畫布上就按照了每個子 View 的屬性定義,根據他們的順序,逐個擺放到了了屬於他們的位置去.

現在的界面佈局如下:


但是如果僅僅是這樣的話,可能會出現一種情況,當滑動尾頁到了某個位置,突然尾頁消失,首頁突兀的出現.這是由於你目前佈局時是通過按順序排列子 View 的位置,那麼系統在繪製整個 ViewGroup 的時候,就會按照你所設定的順序來進行繪製,那麼首頁和尾頁本身就不連續,必然會出現兩者切換閃屏的情況.

爲了保證圖像的連續性,我們就考慮在繪製圖像的時候,在當前 ViewGroup 的尾部以及首部多繪製一份圖片,這樣當界面滑動到超過首尾的時候,你可以自然而然的看到下一張圖片,而不必擔心界面突然閃屏.

那麼我們所希望看到繪製的界面是這樣的:

實現的代碼如下:

 @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        canvas.save();
        canvas.translate(mWidth * getChildCount(), 0);
        super.dispatchDraw(canvas);
        canvas.restore();
        canvas.save();
        canvas.translate(-mWidth * getChildCount(), 0);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

在這裏,我們通過重寫 ViewGroup 的 dispatchDraw 方法,在裏面進行 canvas 的座標平移變換,然後調用三次 dispatchDraw 來實現在屏幕上繪製三段長度相同,位置不同的ViewGroup


由於 Android 本身的繪製機制,他不會繪製不能顯示在界面上的 view,所以雖然我們調用了三次 super.dispatchDraw, 但是卻並不代表他本身的繪製效率會降低.

在設計這裏的時候,我也曾經擔心過調用三次 dispatchDraw 可能帶來的效率問題,因此,有一個測試版的 dispatchDraw,如下:

@Override
    protected void dispatchDraw(Canvas canvas) {
        int position = getScrollX() / mWidth;
        drawChildAtPosition(canvas, position);
        drawChildAtPosition(canvas, position + 1);
        drawChildAtPosition(canvas, position - 1);
    }

    private void drawChildAtPosition(Canvas canvas, int position) {
        int offset = position / getChildCount();
        if (position < 0) {
            offset -= 1;
        }
        position = formatPosition(position);
        canvas.save();
        canvas.translate(getChildCount() * mWidth * offset, 0);
        drawChild(canvas, getChildAt(position), getDrawingTime());
        canvas.restore();
    }

測試發現,通過上面的代碼只對當前位置的左中右三個位置的子 View 進行繪製,所耗費的時間與之前的時間其實是一致的,既然兩者之間的效率一致,就使用了上面的代碼,以確保底部邏輯同 android 本身的 dispatch 相符合.如果各位有什麼好的建議, 請告訴我,看看能夠如何進行優化.

到了這裏,我們對整個自定義佈局的位置擺放和繪製已經做了處理, 我們已經成功生成了一個首尾頁連續的 ViewGroup, 接下來我們就要開始針對 ViewGoup 的點擊事件進行處理,這將被我放在下一篇博客中,歡迎持續關注.




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