雙向滑動(dòng)懸停 無需嵌套吧恃,完勝 ScrollView&ViewPager虾啦。

程序員的豐碑

介紹

PageScrollView是一個(gè)繼承于ViewGroup的自定義容器類,如其名它支持ScrollViewViewPager兩種滑動(dòng)效果。無需嵌套LinearLayout傲醉,可支持不定寬高的子View視圖蝇闭。支持水平和垂直方向的布局和手勢,支持任意子View滑動(dòng)吸頂或是吸底懸停的交互硬毕。支持ViewPager 固有的PageTransform動(dòng)畫和PageChangeListener ,ScrollChangeListener等還有 View滑動(dòng)時(shí)可見索引變化VisibleRangeChangeListener接口呻引。

以下給出兩張 gif 示例,演示其最基本的功能:

無需嵌套LinearLayout > scrollview.gif

ViewPager 模式 > viewpager.gif

產(chǎn)生的背景

項(xiàng)目一中需要使用ViewPager 翻頁吐咳,但交互上每頁有間距且要露出相鄰頁的部分逻悠,滑動(dòng)時(shí)還要有透明度和縮放動(dòng)畫。首先想到用ViewPagersetPageMarginPagerAdaptergetPageWidth 返回小于 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)了ViewPagerScrollView相應(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 寫重寫其onMeasureonLayout 分別實(shí)現(xiàn)控件本身大小的測量和子View的布局定位。若需手勢交互還得處理onInterceptTouchEventonTouchEvent事件蠢琳。下面僅以垂直方向布局來說明 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 本身有 scrollToscrollBy 來滑動(dòng)自身內(nèi)容种远。只需計(jì)算滑動(dòng)偏移量,規(guī)整到[0,scrollRange] 間仲锄。然后直接應(yīng)用 scrollTo or scrollByinvalidate戒努。
若要平滑動(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)勉抓。

onInterceptTouchEventonTouchEvent兩個(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é)可查看源碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末靴迫,一起剝皮案震驚了整個(gè)濱河市惕味,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌玉锌,老刑警劉巖名挥,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異主守,居然都是意外死亡禀倔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門参淫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來救湖,“玉大人,你說我怎么就攤上這事黄刚∩咏鳎” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵憔维,是天一觀的道長。 經(jīng)常有香客問我畏邢,道長业扒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任舒萎,我火速辦了婚禮程储,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己章鲤,他們只是感情好摊灭,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著败徊,像睡著了一般帚呼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上皱蹦,一...
    開封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天煤杀,我揣著相機(jī)與錄音,去河邊找鬼沪哺。 笑死沈自,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的辜妓。 我是一名探鬼主播枯途,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼籍滴!你這毒婦竟也來了酪夷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤异逐,失蹤者是張志新(化名)和其女友劉穎捶索,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灰瞻,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡腥例,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酝润。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片燎竖。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖要销,靈堂內(nèi)的尸體忽然破棺而出构回,到底是詐尸還是另有隱情,我是刑警寧澤疏咐,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布纤掸,位于F島的核電站,受9級(jí)特大地震影響浑塞,放射性物質(zhì)發(fā)生泄漏借跪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一酌壕、第九天 我趴在偏房一處隱蔽的房頂上張望掏愁。 院中可真熱鬧歇由,春花似錦、人聲如沸果港。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辛掠。三九已至谢谦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間公浪,已是汗流浹背他宛。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欠气,地道東北人厅各。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像预柒,于是被迫代替她去往敵國和親队塘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容