Android ScrollView RecyclerView

Android ScrollVew套RecyclerView發現的問題以及解決辦法

潘逸倫 2019/08/12 09:41:29
6714

n   前言

一般我們在使用RecyclerView的時候,因為它本身就帶有可以scroll的功能,所以我們並不需要再另外用ScrollView將其置於其中,但有時候因為客戶的設計需求,就有可能會不得不需要將RecyclerView套在ScrollView之中。例如下方顯示的結構,如果我們想要手機呈現可以上下滑動的動作,我們可以直接將UI元件至於ScrollView(藍色部分)中即可,但如果所包含的其中一個元件是RecyclerView(紅色部份)就會遇到一些問題。

n   問題一: ScrollView RecyclerView scroll功能各自作動的問題

            如前言所述,如果我們照著常用的方式去進行設計,直覺地將RecyclerView直接放入ScrollView中,就有會產生ScrollViewRecyclerView兩個元件各自滑動的結果,而達不到一開始就想要的整頁可直覺上下滑動的效果。

 

無法達到效果的Layout如下: 

<ScrollView
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">

    <android.support.constraint.ConstraintLayout
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content">

        <FrameLayout
           
android:id="@+id/activity_main4_ll_image"
           
android:layout_width="match_parent"
           
android:layout_height="250dp"
           
android:layout_margin="8dp"
           
android:background="@color/colorPrimary"
            
app:layout_constraintTop_toTopOf="parent"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <TextView
           
android:id="@+id/activity_main4_tv_title"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:text="This is the Title"
           
android:layout_marginBottom="8dp"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_ll_image"
            
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <android.support.v7.widget.RecyclerView
           
android:id="@+id/activity_main4_rcv"
           
android:layout_width="0dp"
           
android:layout_height="wrap_content"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_tv_title"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

</ScrollView>

       

      為了要解決這個問題,就必須先限制RecyclerViewscroll的功能,禁止其可以滑動才能夠達到只有外層的ScrollView能夠滑動的效果。而要實作限制RecyclerView能否scroll功能,我們必須要改寫RecyclerViewLayoutManager的功能。我們建立一個class並繼承特定的LayoutManager,並且改寫canScrollVerticallya這個function   

 

public class MyLinearLayoutManager extends LinearLayoutManager {
   
private static final String TAG  = MyLinearLayoutManager.class.getSimpleName();

    private boolean
isScrollEnabled = true;

    public
MyLinearLayoutManager(Context context, boolean isScrollEnabled) {
       
super(context);
        this
.isScrollEnabled = isScrollEnabled;
   
}

   
public MyLinearLayoutManager(Context context,int orientation,boolean reverseLayout){
       
super(context, orientation, reverseLayout);
   
}

   
@Override
   
public boolean canScrollVertically() {
       
//設定是否禁止滑動
       
return isScrollEnabled && super.canScrollVertically();
   
}
}

 

接著讓RecyclerView直接使用我們自定義的LayoutManager就可以先解決兩個元件各自滑動的問題。

 

mRecyclerView = findViewById(R.id.activity_main4_rcv);
mRecyclerView.setLayoutManager(new MyLinearLayoutManager(this,false));

 

n   問題二: RecyclerView顯示不全的問題

上面我們解決了scroll的問題,但是馬上就會發現為什麼RecyclerView卻沒有如我們預期的將資料顯示完全,假設我們預期RecyclerView因該要顯示20 筆資料,但卻沒有全部都顯示出來。網路上對於這方面的問題有多種的解決辦法,在這裏推薦我個人覺得最佳的解決方案。

ScrollView直接替換成NestedScrollView

如下方Layout

<android.support.v4.widget.NestedScrollView
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">

    <android.support.constraint.ConstraintLayout
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content">

        <FrameLayout
           
android:id="@+id/activity_main4_ll_image"
           
android:layout_width="match_parent"
           
android:layout_height="250dp"
           
android:layout_margin="8dp"
           
android:background="@color/colorPrimary"
           
app:layout_constraintTop_toTopOf="parent"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <TextView
           
android:id="@+id/activity_main4_tv_title"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:text="This is the Title"
           
android:layout_marginBottom="8dp"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_ll_image"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <android.support.v7.widget.RecyclerView
           
android:id="@+id/activity_main4_rcv"
           
android:layout_width="0dp"
           
android:layout_height="wrap_content"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_tv_title"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

</android.support.v4.widget.NestedScrollView>

 

n   問題三: 畫面一開啟並非直接置頂

使用NestedScrollView則會有另外一個問題,就是畫面開啟後,畫面一開始的呈現並沒有顯示畫面的置頂,如下所示:

 

解決辦法,在NestedScrollView內的子Layout(通常只有一個)內新增下方屬性:

            android:descendantFocusability="blocksDescendants"

該屬性是在定義ViewGroup與其子控件取得焦點的關係,設定blocksDescendants則代表ViewGroup會覆蓋子控件而直接取得焦點。

 

Layout 如下:

<android.support.v4.widget.NestedScrollView
   
android:layout_width="match_parent"
   
android:layout_height="match_parent">

    <android.support.constraint.ConstraintLayout
       
android:layout_width="match_parent"
       
android:layout_height="wrap_content"
       
android:descendantFocusability="blocksDescendants">

        <FrameLayout
           
android:id="@+id/activity_main4_ll_image"
           
android:layout_width="match_parent"
           
android:layout_height="250dp"
           
android:layout_margin="8dp"
           
android:background="@color/colorPrimary"
           
app:layout_constraintTop_toTopOf="parent"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <TextView
           
android:id="@+id/activity_main4_tv_title"
           
android:layout_width="wrap_content"
           
android:layout_height="wrap_content"
           
android:text="This is the Title"
           
android:layout_marginBottom="8dp"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_ll_image"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

        <android.support.v7.widget.RecyclerView
           
android:id="@+id/activity_main4_rcv"
           
android:layout_width="0dp"
           
android:layout_height="wrap_content"
           
app:layout_constraintTop_toBottomOf="@+id/activity_main4_tv_title"
           
app:layout_constraintLeft_toLeftOf="parent"
           
app:layout_constraintRight_toRightOf="parent"/>

    </android.support.constraint.ConstraintLayout>

</android.support.v4.widget.NestedScrollView>

 

n   問題四: RecyclerView的高度設定wrap_content失效問題

以上如果RecyclerView的版本23.2.0以上,都可以正常運作,但是如果因為某些特定原因(例如專案需求等等)被迫需要使用低於23.2.0以下的版本,就會面臨高度設定wrap_content卻失效的問題,這問題是被確認為一個bug,但google23.2.0的版本已經修復,所以如果無法使用較新版本的RecyclerView,就必須改寫LayoutManageronMeasure方法,根據item的內容高度重新量測RecyclerView的高度。

 

可以根據以下個code來進行RecyclerView量測高度的動作。

@Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
   
final int widthMode = View.MeasureSpec.getMode(widthSpec);
    final int
heightMode = View.MeasureSpec.getMode(heightSpec);
    final int
widthSize = View.MeasureSpec.getSize(widthSpec);
    final int
heightSize = View.MeasureSpec.getSize(heightSpec);
    int
width = 0;
    int
height = 0;
    for
(int i = 0; i < getItemCount(); i++) {
        measureScrapChild(recycler
, i,
               
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
               
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
               
mMeasuredDimension);

        if
(getOrientation() == HORIZONTAL) {
            width = width +
mMeasuredDimension[0];
            if
(i == 0) {
                height =
mMeasuredDimension[1];
           
}
        }
else {
            height = height +
mMeasuredDimension[1];
            if
(i == 0) {
                width =
mMeasuredDimension[0];
           
}
        }
    }
   
switch (widthMode) {
       
case View.MeasureSpec.EXACTLY:
            width =
widthSize;
        case
View.MeasureSpec.AT_MOST:
       
case View.MeasureSpec.UNSPECIFIED:
    }

   
switch (heightMode) {
       
case View.MeasureSpec.EXACTLY:
            height =
heightSize;
        case
View.MeasureSpec.AT_MOST:
       
case View.MeasureSpec.UNSPECIFIED:
    }

    setMeasuredDimension(width
, height);
}

private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,int heightSpec, int[] measuredDimension) {
    View
view = recycler.getViewForPosition(position);
    if
(view != null) {
        RecyclerView.LayoutParams
p = (RecyclerView.LayoutParams)view.getLayoutParams();
        int
childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
               
getPaddingLeft() + getPaddingRight(), p.width);
        int
childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
               
getPaddingTop() + getPaddingBottom(), p.height);
       
view.measure(childWidthSpec, childHeightSpec);
       
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
       
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
       
recycler.recycleView(view);
   
}
}

 

n   結論

首先我們透過限制RecyclerViewscroll功能,來避免RecyclerViewScrollView兩個元件scroll衝突的問題。接著我們將ScrollView換成NestedScrollView來解決RecyclerView在顯示上會遇到的問題。最後,分享了如果是使用RecyclerView 23.2.0以下版的時wrap_content屬性失效的解決方案,但面對warp_content失效的問題,對好的解決方案還是升級RecyclerView版本。以上這些相關的問題,在網路上也可能會有其他方法,而在這邊提供的是本人認為最適當的解決方案供大家參考。

 

參考資料:

https://www.itread01.com/content/1548244837.html

https://www.jianshu.com/p/9d83e5c76923

https://www.jianshu.com/p/d16ec64181f2

https://www.jianshu.com/p/5dfc90656665

https://www.jianshu.com/p/035fb8baf466

https://codeday.me/bug/20170430/14143.html

 

 

 

 

 

潘逸倫