介紹
PageScrollView是一個(gè)繼承于ViewGroup
的自定義容器類,如其名它支持ScrollView
和ViewPager
兩種滑動(dòng)效果。無需嵌套LinearLayout
傲醉,可支持不定寬高的子View
視圖蝇闭。支持水平和垂直方向的布局和手勢,支持任意子View
滑動(dòng)吸頂或是吸底懸停的交互硬毕。支持ViewPager
固有的PageTransform
動(dòng)畫和PageChangeListener
,ScrollChangeListener
等還有 View滑動(dòng)時(shí)可見索引變化VisibleRangeChangeListener
接口呻引。
以下給出兩張 gif 示例,演示其最基本的功能:
產(chǎn)生的背景
項(xiàng)目一中需要使用ViewPager
翻頁吐咳,但交互上每頁有間距且要露出相鄰頁的部分逻悠,滑動(dòng)時(shí)還要有透明度和縮放動(dòng)畫。首先想到用ViewPager
的setPageMargin
和PagerAdapter
的getPageWidth
返回小于 1韭脊,雖能達(dá)到效果童谒,然無法使選中的 Item 居中在屏幕上,就果斷放棄沪羔。簡閱 ViewPager 源碼后結(jié)合需求寫了一個(gè)自定義的PageLayout
滿足了需求饥伊,它就是PageScrollView
的前身。
項(xiàng)目二有個(gè)視圖切換的 tab
要求隨視圖滑動(dòng)到 TitleBar
下方吸頂蔫饰,當(dāng)時(shí)是用FrameLayout
嵌套一個(gè)ScrollView
和一個(gè)假的副本TabLayout
同步數(shù)據(jù)和交互琅豆,并處理滑動(dòng)事件來完成的。需求完成后篓吁,就捉摸著重寫一個(gè) 比ScrollView
功能更強(qiáng)大的滑動(dòng)控件茫因,要支持任意子視圖滑動(dòng)懸停。
想到剛寫了個(gè)PageLayout
可改進(jìn)下就能兼容ScrollView
的滑動(dòng)效果越除,讀了ScrollView
源碼后就開始動(dòng)手寫了节腐。從此更名為PageScrollView
,真正實(shí)現(xiàn)了ViewPager
和ScrollView
相應(yīng)一樣的交互和已知接口摘盆。改善后支持水平和垂直方向的布局和手勢滑動(dòng),以及設(shè)定的任意View
懸停在邊緣等 饱苟。
使用場景:
- 完全可替代
ScrollView
&HorizontalScrollView
的使用場景 且少了一層LinearLayout
嵌套孩擂。 - 方便監(jiān)聽滑動(dòng)時(shí)子 View 可見性的變化,隨時(shí)知道可見 View 的索引范圍箱熬。
- 當(dāng)滑動(dòng)視圖內(nèi)有某子
View
需要隨滑動(dòng)吸頂或是吸底時(shí)类垦。 - 當(dāng)滑動(dòng)視圖內(nèi)部所有子
View
都要隋滑動(dòng)做Transform
動(dòng)畫時(shí)。 - 當(dāng)需要使用像
ViewPager
或是Gallery
交互城须,特別適合內(nèi)部子視圖寬高不同或不足一屏?xí)r同時(shí)滑動(dòng)選中要求居中蚤认。 - 支持布局方向和滑動(dòng)方向 動(dòng)態(tài)隨時(shí)切立即生效,且能恢復(fù)選中狀態(tài)糕伐。
- 可設(shè)置滑動(dòng)容器的最大寬或高時(shí)砰琢,視圖內(nèi)容不足父容器大小時(shí),可強(qiáng)制填充到父窗口大小。
如何使用
1. 在 xml 布局中使用陪汽,添加PageScrollView
標(biāo)簽設(shè)置可選的屬性训唱,像LinearLayout
去添加子視圖的標(biāo)簽
<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 中使用,可以通過設(shè)置覆蓋以上 xml 中的屬性挚冤。
PageScrollView scrollView = (PageScrollView)findViewById(R.id.pageScrollView);
//設(shè)置布局方向况增,僅支持 水平HORIZONTAL 和 垂直VERTICAL.
scrollView.setOrientation(PageScrollView.VERTICAL);
//僅當(dāng)ViewPager 模式時(shí)才能有像其一樣滑動(dòng)效果,OnPageChangeListener才能生效训挡。
scrollView.setViewPagerStyle(false);
//每一個(gè)子視圖按(HORIZONTAL 時(shí))寬或(VERTICAL 時(shí))高的百分比固定測繪澳骤。
scrollView.setSizeFixedPercent(0);
// 設(shè)置第幾個(gè)視圖可吸頂或吸底 取值在 [0,scrollView.getItemCount()-1]間,-1 將被忽略。
scrollView.setFloatViewStartIndex(0);
scrollView.setFloatViewEndIndex(pageScrollView.getItemCount()-1);
//強(qiáng)制所有子視圖的 layout_gravity 屬性按Gravity.CENTER 定位澜薄。
scrollView.setChildCenter(true);
//if content size less than parent size , setChildFillParent as true to match parent size.
scrollView.setChildFillParent(true);
//設(shè)置滾動(dòng)方向的子視圖間距宴凉。
scrollView.setMiddleMargin(30);
//設(shè)置容器本身在測繪時(shí)的最大寬和高。
scrollview.setMaxWidth(400);
scrollview.setMaxHeigh(800);
3. (可選)綁定事件表悬,實(shí)現(xiàn)接口弥锄。
//接著上面
scrollView.setPageHeadView(headerView); //設(shè)置頭部 View
scrollView.setPageFooterView(footerView); 設(shè)置尾部 View
//設(shè)置 PageTransformer 動(dòng)畫,實(shí)現(xiàn)滑動(dòng)視圖的變換蟆沫。
scrollView.setPageTransformer(new PageScrollView.PageTransformer() {
@Override
public void transformPage(View view, float position, boolean horizontal) {
//在這里根據(jù)滑動(dòng)相對(duì)偏移量 position,實(shí)現(xiàn)該視圖的動(dòng)畫效果籽暇。
}
@Override
public void recoverTransformPage(View view, boolean horizontal) {
//清除視圖的動(dòng)畫效果,在setPageTransformer(null)時(shí)會(huì)調(diào)用饭庞。
}
});
PageScrollView.OnPageChangeListener pagerScrollListener = new PageScrollView.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
// ViewPager 滑動(dòng)視圖時(shí)戒悠,相對(duì)偏移適時(shí)回調(diào)。
}
@Override
public void onPageSelected(int position, int oldPosition) {
// ViewPager 模式時(shí) 選中回調(diào)舟山。
}
@Override
public void onScrollChanged(int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
//視圖滑動(dòng)回調(diào) View.onScrollChanged
}
@Override
public void onScrollStateChanged(int state, int oldState) {
//state 的取值如下绸狐,標(biāo)明著容器的滑動(dòng)狀態(tài)。
// SCROLL_STATE_IDLE = 0; // 滑動(dòng)停止?fàn)顟B(tài)累盗。
// SCROLL_STATE_DRAGGING = 1;//用戶正開始拖拽滑動(dòng) 寒矿。
// SCROLL_STATE_SETTLING = 2;//開始松開手指快速滑動(dòng)。
}
};
scrollView.setOnPageChangeListener(pagerScrollListener);
// 設(shè)置視圖滾動(dòng)的監(jiān)聽若债。
scrollView.setOnScrollChangeListener(pagerScrollListener);
//設(shè)置可見子 View 發(fā)生變化時(shí) 可見索引區(qū)間的監(jiān)聽符相。
scrollView.setOnVisibleRangeChangeListener(new OnVisibleRangeChangeListener(){
public void onVisibleRangeChanged(int firstVisible, int lastVisible, int oldFirstVisible, int oldLastVisible){
}
});
實(shí)現(xiàn)簡介
實(shí)現(xiàn)一個(gè)基本的容器控件需繼承于 ViewGroup
寫重寫其onMeasure
和 onLayout
分別實(shí)現(xiàn)控件本身大小的測量和子View
的布局定位。若需手勢交互還得處理onInterceptTouchEvent
和onTouchEvent
事件蠢琳。下面僅以垂直方向布局來說明 PageScrollView
的實(shí)現(xiàn)步驟(水平方向同理)以此講解如何自定義一個(gè)最簡單的 ViewGroup
1.onMeasure測量內(nèi)容和自身大小啊终,終需調(diào)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(); //處理滑動(dòng)傲须,這個(gè) contentHeight 是要存起來的.
done
measureWidth=resolveSize
(contentWidth,widthMeasureSpec
);//暫忽padding ,minimumWidth 下同
measureHeight=resolveSize
(contentHeight,heightMeasureSpec
);
setMeasuredDimension(measureWidth,measureHeight);//此方法調(diào)用后自身大小就定了蓝牲。
end
2. onLayout定位所有子View 在自身窗口上的位置,調(diào)用 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. 計(jì)算滑動(dòng)區(qū)間&編寫滑動(dòng)方法
重寫computeVerticalScrollRange
同方向相關(guān)有三個(gè)方法(Offset/Extra)例衍。
據(jù)第一步就得到了 contentHeight,再根據(jù)自身高度 getHeight()就可等到滑動(dòng)區(qū)間了艇劫。
scrollRange=contentHeight-getHeight();暫時(shí)忽略 容器的padding.
View 本身有 scrollTo
和 scrollBy
來滑動(dòng)自身內(nèi)容种远。只需計(jì)算滑動(dòng)偏移量,規(guī)整到[0,scrollRange] 間仲锄。然后直接應(yīng)用 scrollTo
or scrollBy
并invalidate
戒努。
若要平滑動(dòng)畫就需要 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. 處理觸屏事件 關(guān)注垂直方向 Touch 事件達(dá)到交互上的滑動(dòng)勉抓。
在onInterceptTouchEvent
和onTouchEvent
兩個(gè)方法中都要處理ACTION_MOVE
時(shí)判斷垂直方向的絕對(duì)滑動(dòng)值是否大于臨界值 且大于水平滑動(dòng)絕對(duì)值 來標(biāo)志當(dāng)前正準(zhǔn)備滑動(dòng) isBingDragged=true
;讓 onInterceptTouchEvent 返回值為isBeingDragged
當(dāng)然ACTION_DOWN
時(shí)需要返回false .
當(dāng)onInterceptTouchEvent
返回true 表示攔截了事件,將會(huì)走自身的onTouchEvent
讓它返回true惑申,所以后面所有要處理滑動(dòng)的邏輯只需要在onTouchEvent
里處理即可具伍。
根據(jù)每次ACTION_MOVE
滑動(dòng)的 dy 來計(jì)算內(nèi)容視圖需要滑動(dòng)到的newScrollY. scrollTo(0,newScrollY) .
當(dāng) ACTION_CANCEL&ACTION_UP
時(shí),計(jì)算滑動(dòng)速度(VelocityTracker
)并根據(jù)滑動(dòng)方向來 處理自動(dòng)滑動(dòng)交互 mScroller.startScroll 圈驼。此時(shí)的滑動(dòng)目標(biāo)距離和動(dòng)畫時(shí)間可借鑒ScrollView和ViewPager
的源碼邏輯人芽。
總結(jié)
以上實(shí)現(xiàn)部分講的是最基本的原理,功能和支持的屬性越多實(shí)際考慮的細(xì)節(jié)和實(shí)現(xiàn)就會(huì)越復(fù)雜绩脆。
示例工程在 github
上持續(xù)更新,可直接搜索** PageScrollView
**萤厅,有興趣同學(xué)可查看源碼 。