title: Very slow performance on RecyclerView with ConstraintLayout and inside ViewPager on Android
body: <p>I have a <strong>RecyclerView</strong> with about 20 items. It takes long time to render and blocks de UI. </p>
  
  <p>I am using <strong>ConstraintLayout</strong> for positioning items in each row. They are not complicated as you can see in the screenshots - <a href="https://i.stack.imgur.com/QFaWo.gif" rel="noreferrer">Link to animated gif</a>.</p>
  
  <p>I also have a <strong>ViewPager</strong> for main navigation (See bottom bar). On each page Fragment there is also a ViewPager. In this inner ViewPager is where you find the RecyclerView (inside a NestedScrollView).</p>
  
  <p>Additionally I have a scroll listener on the NestedScrollView to do a parallax effect on the header.</p>
  
  <p><strong>I notice that if I remove some of the items and have the RecyclerView with only 3 items, it becomes a lot more faster. So it really seems to be some problem with the rows of the RecyclerView.</strong></p>
  
  <p><strong>I also made a test and replace the ConstraintLayout of each row with a FrameLayout with fixed height - The performance was the same</strong></p>
  
  <p>I am also using <strong>Iconics</strong> (for font icon) and <strong>Calligraphy</strong> (for custom typography) - Can this be affecting the performance ?</p>
  
  <hr>
  
  <h2><strong>Preview:</strong></h2>
  
  <p>Notice the circle when i'm trying to scroll immediately after changing to the first page and the UI is blocked. Also notice that the ViewPager slides when going to the second page but when coming from the second to the first it just blocks the animation and the list appears immediately.</p>
  
  <p><a href="https://i.stack.imgur.com/QFaWo.gif" rel="noreferrer">Link to animated gif</a></p>
  
  <p><strong>Row Item</strong> (history_item.xml):</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
  
      &lt;android.support.constraint.ConstraintLayout
          android:id="@+id/container"
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          android:layout_width="match_parent"
          android:layout_height="76dp"
          android:clickable="true"&gt;
  
          &lt;TextView
              android:id="@+id/date"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintHorizontal_chainStyle="spread"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintLeft_toLeftOf="parent"
              android:textSize="11sp"
              android:textAllCaps="true"
              android:textColor="@color/black"
              android:text="12 JUN"
              android:layout_marginTop="@dimen/margin_xs"
              android:layout_marginLeft="@dimen/margin_small"/&gt;
  
          &lt;com.mikepenz.iconics.view.IconicsTextView
              android:id="@+id/icon"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toBottomOf="@id/date"
              app:layout_constraintLeft_toLeftOf="@id/date"
              app:layout_constraintRight_toLeftOf="@+id/kms"
              android:layout_marginRight="5dp"
              android:layout_marginTop="5dp"
              android:text="{evz_workout_running}"
              android:textColor="@color/colorPrimary"
              android:textSize="22sp"
              android:gravity="left" /&gt;
  
  
  
  
          &lt;TextView
              android:id="@+id/kms"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintLeft_toRightOf="@id/icon"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintRight_toLeftOf="@+id/icon_time"
              app:layout_constraintHorizontal_chainStyle="spread"
              android:layout_marginTop="@dimen/margin_xs"
              android:textSize="34sp"
              android:textColor="@color/black"
              android:textAppearance="@style/textStyleBlack"
              android:text="325,4"
              android:translationY="-12dp" /&gt;
  
          &lt;TextView
              android:id="@+id/kms_label"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toTopOf="@id/kms"
              app:layout_constraintRight_toRightOf="@id/kms"
              android:layout_marginTop="32dp"
              android:textSize="9sp"
              android:textColor="@color/black"
              android:text="KMS"
              android:textAppearance="@style/textStyleBold"/&gt;
  
  
  
  
          &lt;com.mikepenz.iconics.view.IconicsTextView
              android:id="@+id/icon_time"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintLeft_toRightOf="@id/kms"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintRight_toLeftOf="@+id/icon_speed"
              android:layout_marginTop="@dimen/margin_xs"
              android:layout_marginLeft="@dimen/margin_medium"
              android:layout_marginRight="@dimen/margin_medium"
              app:layout_constraintHorizontal_chainStyle="spread"
              android:text="{evz_config_duration}"
              android:textColor="@color/gray"
              android:textSize="26sp" /&gt;
  
          &lt;TextView
              android:id="@+id/time"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toBottomOf="@id/icon_time"
              app:layout_constraintLeft_toLeftOf="@id/icon_time"
              app:layout_constraintRight_toRightOf="@id/icon_time"
              android:layout_marginTop="5dp"
              android:textSize="10sp"
              android:textColor="@color/black"
              android:textAppearance="@style/textStyleBold"
              android:text="00:00:07"/&gt;
  
  
  
          &lt;com.mikepenz.iconics.view.IconicsTextView
              android:id="@+id/icon_speed"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toTopOf="@id/icon_time"
              app:layout_constraintLeft_toRightOf="@id/icon_time"
              app:layout_constraintRight_toLeftOf="@+id/icon_points"
              app:layout_constraintHorizontal_chainStyle="spread"
              android:paddingTop="2sp"
              android:paddingBottom="2sp"
              android:text="{evz_rate}"
              android:textColor="@color/gray"
              android:textSize="22sp" /&gt;
  
          &lt;TextView
              android:id="@+id/speed"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toBottomOf="@id/icon_speed"
              app:layout_constraintLeft_toLeftOf="@id/icon_speed"
              app:layout_constraintRight_toRightOf="@id/icon_speed"
              android:layout_marginTop="5dp"
              android:textSize="10sp"
              android:textColor="@color/black"
              android:textAppearance="@style/textStyleBold"
              android:text="00'00''"/&gt;
  
  
          &lt;com.mikepenz.iconics.view.IconicsTextView
              android:id="@+id/icon_points"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toTopOf="@id/icon_time"
              app:layout_constraintLeft_toRightOf="@id/icon_speed"
              app:layout_constraintRight_toRightOf="parent"
              android:layout_marginRight="@dimen/margin_small"
              android:paddingTop="2dp"
              android:paddingBottom="2dp"
              android:text="{evz_star}"
              android:textColor="@color/gray"
              android:textSize="22sp"/&gt;
  
          &lt;TextView
              android:id="@+id/points"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              app:layout_constraintTop_toBottomOf="@id/icon_points"
              app:layout_constraintLeft_toLeftOf="@id/icon_points"
              app:layout_constraintRight_toRightOf="@id/icon_points"
              android:layout_marginTop="5dp"
              android:textSize="10sp"
              android:textColor="@color/black"
              android:textAppearance="@style/textStyleBold"
              android:text="3500"/&gt;
  
          &lt;Button
              android:id="@+id/undo_button"
              android:layout_width="0dp"
              android:layout_height="0dp"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintLeft_toLeftOf="parent"
              app:layout_constraintRight_toRightOf="parent"
              app:layout_constraintBottom_toBottomOf="parent"
              android:layout_margin="0dp"
              android:layout_gravity="end|center_vertical"
              android:background="@color/red"
              android:textColor="@color/white"
              style="@style/Base.Widget.AppCompat.Button.Borderless"
              android:textAppearance="@style/textStyleBold"
              android:textAllCaps="true"
              android:text="Anular"
              android:textSize="18sp"
              android:visibility="gone"
              /&gt;
  
  &lt;/android.support.constraint.ConstraintLayout&gt;
  </code></pre>
  
  <p>Fragment that has the RecyclerView (pager_fragment.xml):</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
      &lt;merge
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"&gt;
  
          &lt;android.support.v4.widget.NestedScrollView
              android:id="@+id/sub_scroll"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:fillViewport="true"&gt;
  
              &lt;LinearLayout
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:orientation="vertical"
                  android:paddingTop="210dp"&gt;
  
                  &lt;pt.sportzone.everyzone.ui.DynamicViewPager
                      android:id="@+id/sub_pager"
                      android:layout_width="match_parent"
                      android:layout_height="match_parent"/&gt;
  
              &lt;/LinearLayout&gt;
  
  
          &lt;/android.support.v4.widget.NestedScrollView&gt;
  
          &lt;android.support.constraint.ConstraintLayout
              android:id="@+id/sub_header"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"&gt;
  
              ...
  
  
          &lt;/android.support.constraint.ConstraintLayout&gt;
  
  &lt;/merge&gt;
  </code></pre>
  
  <p>And the Fragment class (PagerFragment.java):</p>
  
  <pre><code>public class PagerFragment extends Fragment implements IPagerFragment, ViewPager.OnPageChangeListener, View.OnClickListener {
  
      ...
  
      @Override
      public void onCreate(@Nullable Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setHasOptionsMenu(true);
      }
  
      @Override
      public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
          super.onViewCreated(view, savedInstanceState);
  
          // Get elements from Layout
          mPager = (DynamicViewPager) view.findViewById(R.id.sub_pager);
  
          if (mButton1 != null &amp;&amp; mButton2 != null &amp;&amp; mButton3 != null) {
  </code></pre>
  
  <p>...</p>
  
  <pre><code>            if (mHeader != null) {
                  // Listen to when tree is ready so we can setup parallax
                  mHeader.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                      @Override
                      public void onGlobalLayout() {
                          mHeader.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                          setupParallax();
                      }
                  });
              }
          }
  
  
          if (mPager != null) mPager.setCurrentItem(0);
          activate();
      }
  
      @Override
      public void setUserVisibleHint(boolean isVisibleToUser) {
          super.setUserVisibleHint(isVisibleToUser);
          if (!isVisibleToUser) activated = false;
          active = isVisibleToUser;
      }
  
      protected void activate() {
          if (active &amp;&amp; getView() != null &amp;&amp; getActivity() != null &amp;&amp; !activated) {
              activated = true;
          }
      }
  
  
      @Override
      public void onPageSelected(int position) {
          if (mPager == null) return;
  
          //Log.d(EZ.TAG, "Page Selected: " + position);
          FragmentPagerAdapter adapter = (FragmentPagerAdapter) mPager.getAdapter();
          targetPosition = position;
          IPagerFragment fragmentAppear = (IPagerFragment) adapter.getItem(targetPosition);
          IPagerFragment fragmentDisappear = (IPagerFragment) adapter.getItem(currentPosition);
          fragmentDisappear.willDisappear(targetPosition);
          fragmentAppear.willAppear(currentPosition);
  
          mButton1.setSelected(false);
          mButton2.setSelected(false);
          mButton3.setSelected(false);
  
          switch (position) {
              case 0:
                  mButton1.setSelected(true);
                  break;
              case 1:
                  mButton2.setSelected(true);
                  break;
              case 2:
                  mButton3.setSelected(true);
                  break;
          }
      }
  
      @Override
      public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
          if (mPager == null) return;
  
          //Log.d(EZ.TAG, position + " - " + positionOffset + " - " + positionOffsetPixels);
          FragmentPagerAdapter adapter = (FragmentPagerAdapter) mPager.getAdapter();
          //currentPosition = position;
          if (position == targetPosition &amp;&amp; positionOffsetPixels == 0) {
              IPagerFragment fragmentAppear = (IPagerFragment) adapter.getItem(targetPosition);
              IPagerFragment fragmentDisappear = (IPagerFragment) adapter.getItem(currentPosition);
              currentPosition = targetPosition;
              fragmentDisappear.didDisappear();
              fragmentAppear.didAppear();
          }
      }
  
  
      @Override
      public void onPageScrollStateChanged(int state) {
      }
  
      public void setupParallax() {
  
          // Listen for scroll events
          if (scrollChangedListener == null) {
  
              // Get initial data
              initialWidth = mBackground.getWidth();
              initialHeight = mBackground.getHeight();
              initialRatio = initialHeight / initialWidth;
              curveInitialHeight = mCurve.getHeight();
  
              if (mScroll != null &amp;&amp; collapseHeader) {
                  mScroll.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
                      @Override
                      public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                          doParallax(v, scrollX, scrollY, oldScrollX, oldScrollY);
                      }
                  });
              }
  
              UI.flattenConstraintRatio(mBackground);
              UI.flattenConstraintRatio(mBackgroundSpacer);
              UI.flattenConstraintRatio(mCurve);
  
              mCurve.setScaleType(ImageView.ScaleType.FIT_XY);
  
              // Run first parallax iteration
              doParallax(mScroll, 0, 0, 0, 0);
  
          }
      }
  
      public void doParallax(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
  
          // No height, something's wrong, just go away
          if (initialHeight == 0) return;
  
          // Get the percentage of scroll till hitting the max scroll target (initial Height)
          float percent = (float)scrollY / curveInitialHeight;
  
          float percentLeft = 1 - percent;
  
          if (percent &lt; 0) percent = 0;
          if (percent &gt; 1) percent = 1;
  
          float percentFast = Math.min(percent * 1.4f, 1);
          float percentLeftFast = 1 - percentFast;
  
          // Adjust the profile background image
          int headerHeight = Math.max((int)initialHeight - (int)(percent * curveInitialHeight), (int)initialHeight - (int)curveInitialHeight); // 24 -&gt; status bar, 53 -&gt; action bar
          UI.setConstraintHeight(mBackground, headerHeight);
          int curveHeight = Math.max((int)(percentLeft * curveInitialHeight), 0);
          UI.setConstraintHeight(mCurve, curveHeight);
  
  
          if (mButton1 != null) {
              mButton1.getButtonLabel().setAlpha(percentLeft);
              mButton2.getButtonLabel().setAlpha(percentLeft);
              mButton3.getButtonLabel().setAlpha(percentLeft);
          }
  
          // Adjust Elements alpha
          //mBackground.getDrawable().setAlpha((int)(percentLeftFast * 255));
          //mGradient.setAlpha(percentLeft);
  
      }
  </code></pre>
  
  <p>And the Adapter (HistoryAdapter.java):</p>
  
  <pre><code>public class HistoryAdapter extends RecyclerView.Adapter&lt;HistoryAdapter.ViewHolder&gt; {
  
  private static final int PENDING_REMOVAL_TIMEOUT = 3000; // 3sec
  
  private Fragment fragment;
  
  List&lt;Music&gt; items;
  List&lt;Music&gt; itemsPendingRemoval;
  boolean undoOn;
  
  private Handler handler = new Handler(); // hanlder for running delayed runnables
  HashMap&lt;Music, Runnable&gt; pendingRunnables = new HashMap&lt;&gt;(); // map of items to pending runnables, so we can cancel a removal if need be
  
  public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
  
      Music track;
  
      public TextView mDate;
      public IconicsTextView mIcon;
      public TextView mKms;
      public IconicsTextView mIconTime;
      public TextView mTime;
      public IconicsTextView mIconSpeed;
      public TextView mSpeed;
      public IconicsTextView mIconPoints;
      public TextView mPoints;
      public ConstraintLayout mContainer;
      public Button mUndo;
  
      OnHistoryItemClickedListener mListener;
  
      public ViewHolder(Fragment fragment, View holder) {
          super(holder);
  
          // Force interface implementation
          if (fragment instanceof OnHistoryItemClickedListener) {
              mListener = (OnHistoryItemClickedListener) fragment;
          } else {
              throw new RuntimeException(fragment.toString()
                      + " must implement OnHistoryItemClickedListener");
          }
          mContainer = (ConstraintLayout) holder.findViewById(R.id.container);
          mDate = (TextView) holder.findViewById(R.id.date);
          mIcon = (IconicsTextView) holder.findViewById(R.id.icon);
          mKms = (TextView) holder.findViewById(R.id.kms);
          mIconTime = (IconicsTextView) holder.findViewById(R.id.icon_time);
          mTime = (TextView) holder.findViewById(R.id.time);
          mIconSpeed = (IconicsTextView) holder.findViewById(R.id.icon_speed);
          mSpeed = (TextView) holder.findViewById(R.id.speed);
          mIconPoints = (IconicsTextView) holder.findViewById(R.id.icon_points);
          mPoints = (TextView) holder.findViewById(R.id.points);
  
          mUndo = (Button) holder.findViewById(R.id.undo_button);
  
          mContainer.setOnClickListener(this);
      }
  
      @Override
      public void onClick(View v) {
          if (v == mContainer) {
              mListener.onHistoryItemClicked(this.getLayoutPosition(), track);
          }
      }
  
  }
  
  public interface OnHistoryItemClickedListener {
      void onHistoryItemClicked(int position, Music music);
      void onHistoryItemRemoved(int position, Music music);
  }
  
  public HistoryAdapter(Fragment fragment, List&lt;Music&gt; items) {
      this.fragment = fragment;
      this.items = items;
      this.itemsPendingRemoval = new ArrayList&lt;&gt;();
  }
  
  @Override
  public HistoryAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      LayoutInflater inflater = (LayoutInflater) fragment.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
      View track = inflater.inflate(R.layout.history_item, parent, false);
      return new HistoryAdapter.ViewHolder(fragment, track);
  }
  
  @Override
  public void onBindViewHolder(HistoryAdapter.ViewHolder holder, int position) {
      final Music item = items.get(position);
  
      if (itemsPendingRemoval.contains(item)) {
          // we need to show the "undo" state of the row
          holder.itemView.setBackgroundColor(Color.RED);
          //holder.titleTextView.setVisibility(View.GONE);
          holder.mUndo.setVisibility(View.VISIBLE);
          holder.mUndo.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(View v) {
                  // user wants to undo the removal, let's cancel the pending task
                  Runnable pendingRemovalRunnable = pendingRunnables.get(item);
                  pendingRunnables.remove(item);
                  if (pendingRemovalRunnable != null) handler.removeCallbacks(pendingRemovalRunnable);
                  itemsPendingRemoval.remove(item);
                  // this will rebind the row in "normal" state
                  notifyItemChanged(items.indexOf(item));
              }
          });
      } else {
          // we need to show the "normal" state
          holder.itemView.setBackgroundColor(Color.WHITE);
          //holder.titleTextView.setVisibility(View.VISIBLE);
          //holder.titleTextView.setText(item);
          holder.mUndo.setVisibility(View.GONE);
          holder.mUndo.setOnClickListener(null);
      }
  
      holder.track = item;
  
      if (position == getItemCount() - 1) {
          holder.mContainer.setBackground(ContextCompat.getDrawable(fragment.getContext(), R.drawable.row_with_arrow_no_border));
      } else {
          holder.mContainer.setBackground(ContextCompat.getDrawable(fragment.getContext(), R.drawable.row_with_arrow));
      }
  }
  
  ...
  </code></pre>
  
  <p>Where I add items to the adapter (as for testing I use a postDelayed runnable to simulate loading time)</p>
  
  <pre><code>private void dataLoaded() {
          Handler handler = new Handler();
          handler.postDelayed(new Runnable() {
              @Override
              public void run() {
                  // specify an adapter (see also next example)
                  if (getView() != null) {
                      mAdapter = new HistoryAdapter(MainActivityHistoryFragment.this, data.getTracks());
                      mAdapter.setUndoOn(true);
                      mHistory.setAdapter(mAdapter);
  
                  }
              }
          }, 500);
      }
  </code></pre>