It’s 3 AM right now. But still, I need to put this stuff into ink (figuratively speaking) when it is still fresh in my mind.
For a long time, Android developers have had a tough time putting a ListView or a RecyclerView inside a ScrollView, cause it simply didn’t work.
Developers had to manage to enable that UI feature by using workarounds, like dividing the RecyclerView itself into many parts(usually a header, list of items, and a footer). Although this workaround does help, it makes it really complicated to expose the header item view’s listeners to the parent activity.
Without Nested Scrolling
Notice that the RecyclerView elements are visible only in the area under the header view elements, even if scrolled.With Nested Scrolling
Here the entire screen seems to scroll, along with the header view elements and the RecyclerView list items.
This is when the official Android team came up with a new view called the NestedScrollView. The NestedScrollView, in essence, was to allow a scrolling view such as ListView or RecyclerView to be put alongside non-scrolling views such as Button(s) and TextView(s).
For comparison, let me show the code that I used to implement that same nested scrolling behavior without NestedScrollView in the GirdThySword project that I have been working on:
public class OverviewRecycleListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
ArrayList<Chunk> chunks;
private int itemResource;
private int headerResource;
Activity activity;
ArrayList<String> months;
public OverviewRecycleListAdapter(Activity activity, ArrayList<Chunk> chunks) {
this.activity = activity;
this.chunks = chunks;
this.itemResource = R.layout.custom_chunk_overview_list;
this.headerResource = R.layout.custom_calendar_item;
months = new ArrayList<>(Arrays.asList(activity.getString(R.string.january),
activity.getString(R.string.february),
activity.getString(R.string.march),
activity.getString(R.string.april),
activity.getString(R.string.may),
activity.getString(R.string.june),
activity.getString(R.string.july),
activity.getString(R.string.august),
activity.getString(R.string.september),
activity.getString(R.string.october),
activity.getString(R.string.november),
activity.getString(R.string.december)));
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if(viewType == TYPE_ITEM) {
View rootView = LayoutInflater.from(parent.getContext()).inflate(itemResource,parent,false);
return new VHItem(rootView);
} else if(viewType == TYPE_HEADER) {
View rootView = LayoutInflater.from(parent.getContext()).inflate(headerResource,parent,false);
return new VHHeader(rootView);
}
throw new RuntimeException("There is no type that matches the type" + viewType + " make sure you're using types correctly");
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
int updatedPosition = position - 1;
if(holder instanceof VHItem) {
((VHItem) holder).chunkTitle.setText(chunks.get(updatedPosition).toString());
((VHItem) holder).sectionTitle.setText(chunks.get(updatedPosition).toString());
((VHItem) holder).nextDateOfReview.setText(chunks.get(updatedPosition).getNextDateOfReview());
} else if (holder instanceof VHHeader) {
Date currDate = new Date();
((VHHeader) holder).compactCalendarView.setCurrentDate(currDate);
((VHHeader) holder).date.setText(String.format(activity.getString(R.string.date),
currDate.getDate(),
months.get(currDate.getMonth()),
currDate.getYear() + 1900));
((VHHeader) holder).compactCalendarView.setListener(new CompactCalendarView.CompactCalendarViewListener() {
@Override
public void onDayClick(Date dateClicked) {
((VHHeader) holder).date.setText(String.format(activity.getString(R.string.date),
dateClicked.getDate(),
months.get(dateClicked.getMonth()),
dateClicked.getYear() + 1900));
}
@Override
public void onMonthScroll(Date firstDayOfNewMonth) {
((VHHeader) holder).date.setText(String.format(activity.getString(R.string.date),
firstDayOfNewMonth.getDate(),
months.get(firstDayOfNewMonth.getMonth()),
firstDayOfNewMonth.getYear() + 1900));
}
});
}
}
@Override
public int getItemViewType(int position) {
if(isPositionHeader(position)) {
return TYPE_HEADER;
}
return TYPE_ITEM;
}
private boolean isPositionHeader(int position) {
return position == 0;
}
@Override
public int getItemCount() {
return chunks.size() + 1;
}
public class VHItem extends RecyclerView.ViewHolder {
TextView chunkTitle;
TextView sectionTitle;
TextView nextDateOfReview;
public VHItem(View itemView) {
super(itemView);
chunkTitle = itemView.findViewById(R.id.chunkTitle);
sectionTitle = itemView.findViewById(R.id.sectionTitle);
nextDateOfReview = itemView.findViewById(R.id.nextDateOfReview);
}
}
public class VHHeader extends RecyclerView.ViewHolder {
CompactCalendarView compactCalendarView;
TextView date;
RadioGroup radioGroup;
public VHHeader(View itemView) {
super(itemView);
compactCalendarView = itemView.findViewById(R.id.calendar);
date = itemView.findViewById(R.id.date);
radioGroup = itemView.findViewById(R.id.radioGroup);
}
}
}
Now let’s see how this same Adapter class changes when NestedScrollView is used:
public class OverviewRecycleListAdapter extends RecyclerView.Adapter<OverviewRecycleListAdapter.ViewHolder>{
ArrayList<Chunk> chunks;
private int itemResource;
Activity activity;
ArrayList<String> months;
public OverviewRecycleListAdapter(Activity activity, ArrayList<Chunk> chunks) {
this.activity = activity;
this.chunks = chunks;
this.itemResource = R.layout.custom_chunk_overview_list;
months = new ArrayList<>(Arrays.asList(activity.getString(R.string.january),
activity.getString(R.string.february),
activity.getString(R.string.march),
activity.getString(R.string.april),
activity.getString(R.string.may),
activity.getString(R.string.june),
activity.getString(R.string.july),
activity.getString(R.string.august),
activity.getString(R.string.september),
activity.getString(R.string.october),
activity.getString(R.string.november),
activity.getString(R.string.december)));
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View rootView = LayoutInflater.from(parent.getContext()).inflate(itemResource,parent,false);
return new ViewHolder(rootView);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.chunkTitle.setText(chunks.get(updatedPosition).toString());
holder.sectionTitle.setText(chunks.get(updatedPosition).toString());
holder.nextDateOfReview.setText(chunks.get(position).getNextDateOfReview());
}
@Override
public int getItemCount() {
return chunks.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView chunkTitle;
TextView sectionTitle;
TextView nextDateOfReview;
public ViewHolder(View itemView) {
super(itemView);
chunkTitle = itemView.findViewById(R.id.chunkTitle);
sectionTitle = itemView.findViewById(R.id.sectionTitle);
nextDateOfReview = itemView.findViewById(R.id.nextDateOfReview);
}
}
}
The former was 127 lines, while the latter was just 59. And I’m not just pointing out the massive decrease in the number of lines, but more importantly the complexity. The latter version is much cleaner and intuitive whereas the former was a mess that you probably didn’t care reading through.
Okay man, so how do I implement it?
Thought you would never ask. You just put the RecyclerView inside the NestedScrollView along with the other non-scrollable views.
Here is how mine looked:
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="25sp"
android:text="@string/default_date_in_words"/>
<android.support.v7.widget.CardView
android:id="@+id/cardView"
app:cardBackgroundColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="210dp"
android:layout_centerHorizontal="true"
android:layout_below="@+id/date"
android:layout_margin="5dp">
<com.github.sundeepk.compactcalendarview.CompactCalendarView
android:id="@+id/calendar"
android:layout_width="240dp"
android:layout_height="200dp"
android:layout_gravity="center"
app:compactCalendarTextSize="20sp"/>
</android.support.v7.widget.CardView>
<RadioGroup
android:id="@+id/radioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="horizontal"
android:layout_below="@+id/cardView">
<RadioButton
android:id="@+id/showOfDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_of_selected_date"/>
<RadioButton
android:id="@+id/showAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/show_all" />
</RadioGroup>
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@+id/radioGroup"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
After doing this though, you have to do a small tweak. You can do that by referring to the link below.
https://android.jlelse.eu/recyclerview-within-nestedscrollview-scrolling-issue-3180b5ad2542
Just wanted to make a quick blog post covering this topic. Adios!!