大家如果喜歡我的博客,請關注一下我的微博,請點擊這裏(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 的點擊事件進行處理,這將被我放在下一篇博客中,歡迎持續關注.