從原理上說說ScrollView巢狀ListView的問題

公眾號_Android開發者家園發表於2019-02-24

文章最早釋出於我的微信公眾號 Android_De_Home 中,歡迎大家掃描下面二維碼關注微信公眾獲取更多知識內容。
本文為sydMobile原創文章,可以隨意轉載,但請務必註明出處!

ScrollView巢狀ListView會出現的問題,相信大家已經見到的非常多了,對於解決方法也是瞭如指掌了。但是原理你清楚了嗎?這裡主要講為什麼會出現這種問題,已經解決這個問題的原理。

ScrollView巢狀ListView出現的問題

ScrollView巢狀ListView會出現的問題,相信大家都已經見的非常多了,對於怎麼解決也不陌生了。

這裡再來說一下:
出現的問題:
ListView會顯示不全

解決方法:
最常見的一種方法:

自己繼承ListView,重寫onMeasure()方法

	@Override
	public void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
		super.onMeasure(widthMeasureSpec,MeasureSpec.makeMeasureSpec
		(Integer.MAX_VALUe)>>2,MeasureSpec.AT_MOST));

	}
複製程式碼

一般的我們僅僅需要這樣重寫這個方法就可以很順利的解決ScrollView巢狀ListView出現的ListView顯示不全的問題了。那麼是因為什麼呢?下面我們就從原理上說說!

解決原理

說起原理就可MeasureSpec類分不開了,先來介紹一下這個類。

MeasureSpec類

MeasureSpec類是View的一個內部靜態類,MeasureSpec類封裝了從父佈局到子佈局傳遞的佈局需求。每個MeasureSpec物件代表了寬度和高度的要求。一個MeasureSpec類的表示由控制元件大小和模式兩組成。有三種模式:

  • UNSPECIFIED
    父佈局沒有對子佈局施加任何的限制,子佈局可以是任何他想要的大小
  • EXACTLY
    父佈局已經確定了子佈局的大小,子佈局會在父佈局給出的界限內。子佈局的大小是精確的。父佈局給多大就是多大。
  • AT_MOST
    父佈局會給定一個最大的值,子佈局的大小是不能超過這個值的。但是可以比這個值小。

MeasureSpec類為了減少物件的分配用了一個整數來實現這個功能(父佈局傳遞到子佈局的的佈局需求),這個整數是用模式和大小來表示。

那麼這個整數是怎麼來實現這個功能的呢?

int表示形式

我們都知道int型別的是32位,那麼表示形式就是,向上面圖中的那樣,前兩位代表了模式(就是前面提到的那三種),後30位代表了元件的大小。這樣就用整數形式來表示模式和大小了。

具體的看一下三種模式

UNSPECIFIED 模式
UNSPECIFIED == 0 << MODE_SHIFT 也就是 0 向左位移30位,結果就是int型別的最高位是 00

EXACTLY 模式
EXACTLY == 1 << MODE_SHIFT ;也就是 01向左位移30位,結果就是int型別的最高兩位是 01

AT_MOST 模式 AT_MOST = 2 << MODE_SHIFT ;也就是 10 向左位移30位,結果就是int型別的最高兩位是 10

makeMeasureSpec()方法

在這個MeasureSpec類中最重要的一個方法恐怕就是makeMeasureSpec這個方法了。

makeMeasureSpec

這個方法就是用給定的大小和模式建立一個int型別的數來滿足父佈局到子佈局傳遞的佈局需求。第一個引數 size就是父佈局給子佈局傳遞的大小,第二個引數是模式(就是在上面的三個模式中選擇一個)。好了,到這裡makeMeasureSpec()這個方法也講了。

ScrollView巢狀ListView解決方法

方法一:

上面已經講了,重寫ListView的onMeasure()方法

	
	@Override
	protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
			super.onMeasure(widthMeasureSpec,MeasureSpec.makeMeasureSpec(Integer

			.MAX_VALUE >> 2,MeasureSpec.AT_MOST));
	}
	
複製程式碼

我們在修改的時候並沒有改變widthMeasureSpec,僅僅是修改了heightMeasureSpec,因為ScrollView設定成了上下滑動,橫向並沒有滑動,所有在橫向上並沒有和ListView產生衝突。所以傳入的widthMeasureSpec是正確的,而heightMeasureSpec是不正確的,因為ListView巢狀在ScrollView中,也就是說ScrollView是ListView的子佈局,這個時候他們的滑動事件發生了衝突,這個值也就不正確了,不是LIstView的實際高度。所以我們要重寫傳入height,第一個引數為什麼是Integer.MAX_VALUE >>2 呢 ?我們說了MeasureSpec用 int型別表示前兩位代表模式,後30位代表大小,我們就需要讓後面30位是int型別中最大的值就可以了。為什麼選擇AT_MOST模式呢?這個模式是父佈局給定一個值,不能超過這個值,我們很顯然已經給了最大值了。

方法二:

既然測不出高度,那麼我就手中在程式碼中設定ListView的高度。

	public static void setListViewHeightBasedOnChildren(ListView listView) {
	    if(listView == null) return;

	    ListAdapter listAdapter = listView.getAdapter();
	    if (listAdapter == null) {
	        // pre-condition
	        return;
	    }

	    int totalHeight = 0;
	    for (int i = 0; i < listAdapter.getCount(); i++) {
	        View listItem = listAdapter.getView(i, null, listView);
	        listItem.measure(0, 0);
	        totalHeight += listItem.getMeasuredHeight();
	    }

	    ViewGroup.LayoutParams params = listView.getLayoutParams();
	    params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
	    listView.setLayoutParams(params);
}
複製程式碼

這種方法有前提條件限制:
Adapter中getView方法返回的View的必須由LinearLayout組成,因為只有LinearLayout才有measure()方法,如果使用其他的佈局如RelativeLayout,在呼叫listItem.measure(0, 0);時就會拋異常,因為除LinearLayout外的其他佈局的這個方法就是直接拋異常!

總結 自定義ListView比較好用,還有一個問題就是無論使用上面哪一個方法,當你的ListView在載入資料的時候,如果當前頁面沒展示完全,那麼scrollView會自動往下滑動一點,也就是造成了你進入這個頁面的時候,預設頁面是往下滑動了一下,而不是在最頂端。 造成這個問題主要的原因還是焦點問題,ListView預設情況下,isFocusableInTouchMode和isFocusable都是false的,但是當在載入資料後這兩個值就會變為true了。如果在佈局中沒有其他view獲取焦點,這個時候ListView就爭奪到了焦點,也就造成了滑動。

這個問題的解決方法:
1.在載入完資料後設定ScrollView滑動到頂部scrollView.smoothScrollTo(0,0)
這種做法是有缺點的,你會看到螢幕滑動一下。

2.使用descendantFousability屬性

descendantFocusability有三種屬性
beforeDescendant:viewgroup會優先其子類控制元件而獲取到焦點。
afterDescendant:viewgroup只有當其子類控制元件不需要獲取焦點的時候才獲取焦點。
blocksDescendants: viewgroup會覆蓋子類控制元件而直接獲得焦點

在ScrollView的LinearLayout中新增android:denscendantFocusability = "blocksDescendants"就可以了。

歡迎大家關注我的微信公眾號,和我交流分享

相關文章