介紹
PageScrollView是一個繼承于ViewGroup
的自定義容器類,如其名它支持ScrollView
和ViewPager
兩種滑動效果。無需嵌套LinearLayout
,可支持不定寬高的子View
視圖。支持水平和垂直方向的布局和手勢,支持任意子View
滑動吸頂或是吸底懸停的交互。支持ViewPager
固有的PageTransform
動畫和PageChangeListener
,ScrollChangeListener
等還有 View滑動時可見索引變化VisibleRangeChangeListener
接口。
以下給出兩張 gif 示例,演示其最基本的功能:
產生的背景
項目一中需要使用ViewPager
翻頁,但交互上每頁有間距且要露出相鄰頁的部分,滑動時還要有透明度和縮放動畫。首先想到用ViewPager
的setPageMargin
和PagerAdapter
的getPageWidth
返回小于 1,雖能達到效果,然無法使選中的 Item 居中在屏幕上,就果斷放棄。簡閱 ViewPager 源碼后結合需求寫了一個自定義的PageLayout
滿足了需求,它就是PageScrollView
的前身。
項目二有個視圖切換的 tab
要求隨視圖滑動到 TitleBar
下方吸頂,當時是用FrameLayout
嵌套一個ScrollView
和一個假的副本TabLayout
同步數據和交互,并處理滑動事件來完成的。需求完成后,就捉摸著重寫一個 比ScrollView
功能更強大的滑動控件,要支持任意子視圖滑動懸停。
想到剛寫了個PageLayout
可改進下就能兼容ScrollView
的滑動效果,讀了ScrollView
源碼后就開始動手寫了。從此更名為PageScrollView
,真正實現了ViewPager
和ScrollView
相應一樣的交互和已知接口。改善后支持水平和垂直方向的布局和手勢滑動,以及設定的任意View
懸停在邊緣等 。
使用場景:
- 完全可替代
ScrollView
&HorizontalScrollView
的使用場景 且少了一層LinearLayout
嵌套。 - 方便監聽滑動時子 View 可見性的變化,隨時知道可見 View 的索引范圍。
- 當滑動視圖內有某子
View
需要隨滑動吸頂或是吸底時。 - 當滑動視圖內部所有子
View
都要隋滑動做Transform
動畫時。 - 當需要使用像
ViewPager
或是Gallery
交互,特別適合內部子視圖寬高不同或不足一屏時同時滑動選中要求居中。 - 支持布局方向和滑動方向 動態隨時切立即生效,且能恢復選中狀態。
- 可設置滑動容器的最大寬或高時,視圖內容不足父容器大小時,可強制填充到父窗口大小。
如何使用
1. 在 xml 布局中使用,添加PageScrollView
標簽設置可選的屬性,像LinearLayout
去添加子視圖的標簽
<com.rexy.widget.PageScrollView
android:id="@+id/pageScrollView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:maxWidth="400dp"
android:minHeight="100dp"
android:maxHeight="900dp"
android:orientation="horizontal"
android:gravity="center"
rexy:childCenter="true"
rexy:floatViewEndIndex="-1"
rexy:floatViewStartIndex="-1"
rexy:middleMargin="10dp"
rexy:overFlingDistance="0dp"
rexy:viewPagerStyle="true"
rexy:sizeFixedPercent="0">
<include layout="@layout/merge_childs_layout" />
</com.rexy.widget.PageScrollView>
2. (可選)在 Java 中使用,可以通過設置覆蓋以上 xml 中的屬性。
PageScrollView scrollView = (PageScrollView)findViewById(R.id.pageScrollView);
//設置布局方向,僅支持 水平HORIZONTAL 和 垂直VERTICAL.
scrollView.setOrientation(PageScrollView.VERTICAL);
//僅當ViewPager 模式時才能有像其一樣滑動效果,OnPageChangeListener才能生效。
scrollView.setViewPagerStyle(false);
//每一個子視圖按(HORIZONTAL 時)寬或(VERTICAL 時)高的百分比固定測繪。
scrollView.setSizeFixedPercent(0);
// 設置第幾個視圖可吸頂或吸底 取值在 [0,scrollView.getItemCount()-1]間,-1 將被忽略。
scrollView.setFloatViewStartIndex(0);
scrollView.setFloatViewEndIndex(pageScrollView.getItemCount()-1);
//強制所有子視圖的 layout_gravity 屬性按Gravity.CENTER 定位。
scrollView.setChildCenter(true);
//if content size less than parent size , setChildFillParent as true to match parent size.
scrollView.setChildFillParent(true);
//設置滾動方向的子視圖間距。
scrollView.setMiddleMargin(30);
//設置容器本身在測繪時的最大寬和高。
scrollview.setMaxWidth(400);
scrollview.setMaxHeigh(800);
3. (可選)綁定事件,實現接口。
//接著上面
scrollView.setPageHeadView(headerView); //設置頭部 View
scrollView.setPageFooterView(footerView); 設置尾部 View
//設置 PageTransformer 動畫,實現滑動視圖的變換。
scrollView.setPageTransformer(new PageScrollView.PageTransformer() {
@Override
public void transformPage(View view, float position, boolean horizontal) {
//在這里根據滑動相對偏移量 position,實現該視圖的動畫效果。
}
@Override
public void recoverTransformPage(View view, boolean horizontal) {
//清除視圖的動畫效果,在setPageTransformer(null)時會調用。
}
});
PageScrollView.OnPageChangeListener pagerScrollListener = new PageScrollView.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// ViewPager 滑動視圖時,相對偏移適時回調。
}
@Override
public void onPageSelected(int position, int oldPosition) {
// ViewPager 模式時 選中回調。
}
@Override
public void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
//視圖滑動回調 View.onScrollChanged
}
@Override
public void onScrollStateChanged(int state, int oldState) {
//state 的取值如下,標明著容器的滑動狀態。
// SCROLL_STATE_IDLE = 0; // 滑動停止狀態。
// SCROLL_STATE_DRAGGING = 1;//用戶正開始拖拽滑動 。
// SCROLL_STATE_SETTLING = 2;//開始松開手指快速滑動。
}
};
scrollView.setOnPageChangeListener(pagerScrollListener);
// 設置視圖滾動的監聽。
scrollView.setOnScrollChangeListener(pagerScrollListener);
//設置可見子 View 發生變化時 可見索引區間的監聽。
scrollView.setOnVisibleRangeChangeListener(new OnVisibleRangeChangeListener(){
public void onVisibleRangeChanged(int firstVisible, int lastVisible, int oldFirstVisible, int oldLastVisible){
}
});
實現簡介
實現一個基本的容器控件需繼承于 ViewGroup
寫重寫其onMeasure
和 onLayout
分別實現控件本身大小的測量和子View
的布局定位。若需手勢交互還得處理onInterceptTouchEvent
和onTouchEvent
事件。下面僅以垂直方向布局來說明 PageScrollView
的實現步驟(水平方向同理)以此講解如何自定義一個最簡單的 ViewGroup
1.onMeasure測量內容和自身大小,終需調setMeasuredDimension
。
begin: contentWidth=0,contentHeight=0;
for child(only not GONE) in all views do
child.measure(childMeasureSpecWidth
,childMeasureHeight
);
contentWidth=Math.max(contentWidth,child.getMeasureWidth());//(暫忽略 layoutMargin ,下同)
contentHeight+=child.getMeasureHeight(); //處理滑動,這個 contentHeight 是要存起來的.
done
measureWidth=resolveSize
(contentWidth,widthMeasureSpec
);//暫忽padding ,minimumWidth 下同
measureHeight=resolveSize
(contentHeight,heightMeasureSpec
);
setMeasuredDimension(measureWidth,measureHeight);//此方法調用后自身大小就定了。
end
2. onLayout定位所有子View 在自身窗口上的位置,調用 child.layout
begin: childTop=getPaddingTop(),baseLeft=getPaddingLeft();
for child(only not GONE) in all views do //忽LayoutParams
childLeft=baseLeft,childRight=childLeft+child.getMeasureWidth();
childBottom=childTop+child.getMeasureHeight();
child.layout(childLeft,childTop,childRight,childBottom);
childTop=childBottom;
done
end
至此所有的子 View 就能在垂直方向排列顯示出來了。
3. 計算滑動區間&編寫滑動方法
重寫computeVerticalScrollRange
同方向相關有三個方法(Offset/Extra)。
據第一步就得到了 contentHeight,再根據自身高度 getHeight()就可等到滑動區間了。
scrollRange=contentHeight-getHeight();暫時忽略 容器的padding.
View 本身有 scrollTo
和 scrollBy
來滑動自身內容。只需計算滑動偏移量,規整到[0,scrollRange] 間。然后直接應用 scrollTo
or scrollBy
并invalidate
。
若要平滑動畫就需要 Scroller 類 startScroll,并處理computeScroll() 如下:
@Override public void computeScroll() {
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
if (oldX != x || oldY != y) {
scrollTo(x, y);
}
ViewCompat.postInvalidateOnAnimation(this);
} else {
if (mScrollState == ViewPager.SCROLL_STATE_SETTLING) {
post(mIdleExecute);
}
}
}
4. 處理觸屏事件 關注垂直方向 Touch 事件達到交互上的滑動。
在onInterceptTouchEvent
和onTouchEvent
兩個方法中都要處理ACTION_MOVE
時判斷垂直方向的絕對滑動值是否大于臨界值 且大于水平滑動絕對值 來標志當前正準備滑動 isBingDragged=true
;讓 onInterceptTouchEvent 返回值為isBeingDragged
當然ACTION_DOWN
時需要返回false .
當onInterceptTouchEvent
返回true 表示攔截了事件,將會走自身的onTouchEvent
讓它返回true,所以后面所有要處理滑動的邏輯只需要在onTouchEvent
里處理即可。
根據每次ACTION_MOVE
滑動的 dy 來計算內容視圖需要滑動到的newScrollY. scrollTo(0,newScrollY) .
當 ACTION_CANCEL&ACTION_UP
時,計算滑動速度(VelocityTracker
)并根據滑動方向來 處理自動滑動交互 mScroller.startScroll 。此時的滑動目標距離和動畫時間可借鑒ScrollView和ViewPager
的源碼邏輯。
總結
以上實現部分講的是最基本的原理,功能和支持的屬性越多實際考慮的細節和實現就會越復雜。
示例工程在 github
上持續更新,可直接搜索** PageScrollView
**,有興趣同學可查看源碼 。