雙向滑動懸停 無需嵌套,完勝 ScrollView&ViewPager。

程序員的豐碑

介紹

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

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

無需嵌套LinearLayout > scrollview.gif

ViewPager 模式 > viewpager.gif

產生的背景

項目一中需要使用ViewPager 翻頁,但交互上每頁有間距且要露出相鄰頁的部分,滑動時還要有透明度和縮放動畫。首先想到用ViewPagersetPageMarginPagerAdaptergetPageWidth 返回小于 1,雖能達到效果,然無法使選中的 Item 居中在屏幕上,就果斷放棄。簡閱 ViewPager 源碼后結合需求寫了一個自定義的PageLayout 滿足了需求,它就是PageScrollView 的前身。

項目二有個視圖切換的 tab 要求隨視圖滑動到 TitleBar 下方吸頂,當時是用FrameLayout嵌套一個ScrollView和一個假的副本TabLayout 同步數據和交互,并處理滑動事件來完成的。需求完成后,就捉摸著重寫一個 比ScrollView 功能更強大的滑動控件,要支持任意子視圖滑動懸停。

想到剛寫了個PageLayout 可改進下就能兼容ScrollView的滑動效果,讀了ScrollView 源碼后就開始動手寫了。從此更名為PageScrollView ,真正實現了ViewPagerScrollView相應一樣的交互和已知接口。改善后支持水平和垂直方向的布局和手勢滑動,以及設定的任意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 寫重寫其onMeasureonLayout 分別實現控件本身大小的測量和子View的布局定位。若需手勢交互還得處理onInterceptTouchEventonTouchEvent事件。下面僅以垂直方向布局來說明 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 本身有 scrollToscrollBy 來滑動自身內容。只需計算滑動偏移量,規整到[0,scrollRange] 間。然后直接應用 scrollTo or scrollByinvalidate
若要平滑動畫就需要 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 事件達到交互上的滑動。

onInterceptTouchEventonTouchEvent兩個方法中都要處理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**,有興趣同學可查看源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容