I have a RecyclerView
inside a NestedScrollView
that show some data downloaded asynchronously. The problem is that there is a significant lag when the items are initilized. After some tests I found out that the problem is that onCreateViewHolder is called for every item and it took some time to inflate the layout. This is my adapter:
public class EpisodeAdapter extends RecyclerView.Adapter<EpisodeAdapter.ViewHolder> {
private static final String TAG = "EpisodeAdapter";
private static final int NO_POSITION = -1;
private static final int EXPAND = 1;
private static final int COLLAPSE = 2;
private SparseArray<Episode> episodes;
private OnItemClickListener<Episode> downloadClickListener;
private OnItemClickListener<Episode> playClickListener;
private RecyclerView recyclerView;
private final EpisodeAnimator episodeAnimator;
private final Transition expandCollapse;
private int expandedPosition = NO_POSITION;
public EpisodeAdapter() {
episodes = new SparseArray<>();
episodeAnimator = new EpisodeAnimator();
expandCollapse = new AutoTransition();
}
//Called when first loading items
public void swapEpisodes(SparseArray<Episode> newEpisodes){
final int previousSize = episodes.size();
episodes = newEpisodes;
expandedPosition = NO_POSITION;
Log.e(TAG, "Swap called");
if(previousSize == 0) {
notifyItemRangeInserted(0, episodes.size());
}
else {
notifyItemRangeChanged(0, Math.max(previousSize, episodes.size()));
}
}
//Called when downloading other information, this seems to work fine without delay
public void setEpisodesDetails(final List<TmdbEpisode> episodeList){
for (TmdbEpisode episode : episodeList){
final int position = episodes.indexOfKey(episode.getNumber());
notifyItemChanged(position, episode);
}
}
@Override
public EpisodeAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.e(TAG, "Start createViewHolder");
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_episode, parent, false);
ViewHolder viewHolder = new ViewHolder(view);
viewHolder.downloadButton.setOnClickListener(v -> {
if(downloadClickListener != null)
downloadClickListener.onItemClick(v, episodes.valueAt(viewHolder.getAdapterPosition()));
});
viewHolder.playButton.setOnClickListener(v -> {
if(playClickListener != null)
playClickListener.onItemClick(v, episodes.valueAt(viewHolder.getAdapterPosition()));
});
viewHolder.itemView.setOnClickListener(v -> {
final int position = viewHolder.getAdapterPosition();
if(position == NO_POSITION) return;
TransitionManager.beginDelayedTransition(recyclerView, expandCollapse);
episodeAnimator.setAnimateMoves(false);
//Collapse any currently expanded items
if(expandedPosition != NO_POSITION){
notifyItemChanged(expandedPosition, COLLAPSE);
}
//Expand clicked item
if(expandedPosition != position){
expandedPosition = position;
notifyItemChanged(position, EXPAND);
}
else {
expandedPosition = NO_POSITION;
}
});
Log.e(TAG, "Finish createViewHolder");
return viewHolder;
}
@Override
public void onBindViewHolder(EpisodeAdapter.ViewHolder holder, int itemPosition) {
Log.e(TAG, "Start");
holder.number.setText(String.valueOf(episodes.keyAt(itemPosition)));
holder.details.setVisibility(View.GONE);
holder.itemView.setActivated(false);
Log.e(TAG, "Finish");
}
@Override
public void onBindViewHolder(ViewHolder holder, int position, List<Object> payloads) {
Log.e(TAG, "Start payloads");
if(payloads.contains(EXPAND) || payloads.contains(COLLAPSE)){
setExpanded(holder, position == expandedPosition);
}
else if(!payloads.isEmpty() && payloads.get(0) instanceof TmdbEpisode){
TmdbEpisode episode = (TmdbEpisode) payloads.get(0);
holder.title.setText(episode.getName());
holder.details.setText(episode.getOverview());
}
else {
onBindViewHolder(holder, position);
}
Log.e(TAG, "Finish payloads");
}
private void setExpanded(ViewHolder holder, boolean isExpanded) {
holder.itemView.setActivated(isExpanded);
holder.details.setVisibility((isExpanded) ? View.VISIBLE : View.GONE);
}
public void setPlayClickListener(OnItemClickListener<Episode> onItemClickListener){
playClickListener = onItemClickListener;
}
public void setDownloadClickListener(OnItemClickListener<Episode> onItemClickListener){
downloadClickListener = onItemClickListener;
}
@Override
public int getItemCount() {
return episodes.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
View itemView;
TextView number;
FadeTextSwitcher title;
ImageButton downloadButton;
FloatingActionButton playButton;
TextView details;
ViewHolder(View itemView) {
super(itemView);
Log.e(TAG, "Start constructor");
this.itemView = itemView;
number = itemView.findViewById(R.id.number);
title = itemView.findViewById(R.id.title);
downloadButton = itemView.findViewById(R.id.download_button);
playButton = itemView.findViewById(R.id.play_button);
details = itemView.findViewById(R.id.details);
Log.e(TAG, "Finish constructor");
}
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
this.recyclerView = recyclerView;
this.recyclerView.setItemAnimator(episodeAnimator);
expandCollapse.setDuration(recyclerView.getContext().getResources().getInteger(R.integer.episode_expand_collapse_duration));
expandCollapse.setInterpolator(AnimationUtils.loadInterpolator(this.recyclerView.getContext(), android.R.interpolator.fast_out_slow_in));
expandCollapse.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(android.transition.Transition transition) {
EpisodeAdapter.this.recyclerView.setOnTouchListener((v, event) -> true);
}
@Override
public void onTransitionEnd(android.transition.Transition transition) {
episodeAnimator.setAnimateMoves(true);
EpisodeAdapter.this.recyclerView.setOnTouchListener(null);
}
@Override
public void onTransitionCancel(android.transition.Transition transition) {}
@Override
public void onTransitionPause(android.transition.Transition transition) {}
@Override
public void onTransitionResume(android.transition.Transition transition) {}
});
}
static class EpisodeAnimator extends SlideInItemAnimator {
private boolean animateMoves = false;
EpisodeAnimator() {
super();
}
void setAnimateMoves(boolean animateMoves) {
this.animateMoves = animateMoves;
}
@Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
if (!animateMoves) {
dispatchMoveFinished(holder);
return false;
}
return super.animateMove(holder, fromX, fromY, toX, toY);
}
}
}
Is there a way to force reusing the same ViewHolder
for every items? So onCreateViewHolder
will be called once.
I've also set nestedScrollingEnabled="false"
in the recyclerview.