title: RecyclerView - Horizontal LinearLayoutManager create / bind methods called way too often
body: <p>Currently I'm at the end of my ideas on following issue with LinearLayoutManagers and RecyclerViews on Android:</p>
  
  <p><strong>What scenario I wanted to achieve</strong></p>
  
  <p>A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling. The items being fullscreen sized making them as big as the recyclerview itself. When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
  (I'm using support revision 25.1.0)</p>
  
  <p><strong>code snippets</strong></p>
  
  <p><em>The Pager-class itself</em></p>
  
  <pre><code>public class VelocityPager extends RecyclerView {
  
      private int mCurrentItem = 0;
  
      @NonNull
      private LinearLayoutManager mLayoutManager;
  
      @Nullable
      private OnPageChangeListener mOnPageChangeListener = null;
  
      @NonNull
      private Rect mViewRect = new Rect();
  
      @NonNull
      private OnScrollListener mOnScrollListener = new OnScrollListener() {
  
          private int mLastItem = 0;
  
          @Override
          public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
              if (mOnPageChangeListener == null) return;
              mCurrentItem = mLayoutManager.findFirstVisibleItemPosition();
              final View view = mLayoutManager.findViewByPosition(mCurrentItem);
              view.getLocalVisibleRect(mViewRect);
              final float offset = (float) mViewRect.left / ((View) view.getParent()).getWidth();
              mOnPageChangeListener.onPageScrolled(mCurrentItem, offset, 0);
              if (mCurrentItem != mLastItem) {
                  mOnPageChangeListener.onPageSelected(mCurrentItem);
                  mLastItem = mCurrentItem;
              }
          }
  
          @Override
          public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
              if (mOnPageChangeListener == null) return;
              mOnPageChangeListener.onPageScrollStateChanged(newState);
          }
  
      };
  
      public VelocityPager(@NonNull Context context) {
          this(context, null);
      }
  
      public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public VelocityPager(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
          mLayoutManager = createLayoutManager();
          init();
      }
  
      @NonNull
      private LinearLayoutManager createLayoutManager() {
          return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          addOnScrollListener(mOnScrollListener);
      }
  
      @Override
      protected void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          removeOnScrollListener(mOnScrollListener);
      }
  
      @Override
      public void onScrollStateChanged(int state) {
          // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
          // This code fixes this. This code is not strictly necessary but it improves the behaviour.
          if (state == SCROLL_STATE_IDLE) {
              LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
  
              int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
  
              // views on the screen
              int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
              View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
              int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
              View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
  
              // distance we need to scroll
              int leftMargin = (screenWidth - lastView.getWidth()) / 2;
              int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
              int leftEdge = lastView.getLeft();
              int rightEdge = firstView.getRight();
              int scrollDistanceLeft = leftEdge - leftMargin;
              int scrollDistanceRight = rightMargin - rightEdge;
  
              if (leftEdge &gt; screenWidth / 2) {
                  smoothScrollBy(-scrollDistanceRight, 0);
              } else if (rightEdge &lt; screenWidth / 2) {
                  smoothScrollBy(scrollDistanceLeft, 0);
              }
          }
      }
  
      private void init() {
          setLayoutManager(mLayoutManager);
          setItemAnimator(new DefaultItemAnimator());
          setHasFixedSize(true);
      }
  
      public void setCurrentItem(int index, boolean smoothScroll) {
          if (mOnPageChangeListener != null) {
              mOnPageChangeListener.onPageSelected(index);
          }
          if (smoothScroll) smoothScrollToPosition(index);
          if (!smoothScroll) scrollToPosition(index);
      }
  
      public int getCurrentItem() {
          return mCurrentItem;
      }
  
      public void setOnPageChangeListener(@Nullable OnPageChangeListener onPageChangeListener) {
          mOnPageChangeListener = onPageChangeListener;
      }
  
      public interface OnPageChangeListener {
  
          /**
           * This method will be invoked when the current page is scrolled, either as part
           * of a programmatically initiated smooth scroll or a user initiated touch scroll.
           *
           * @param position             Position index of the first page currently being displayed.
           *                             Page position+1 will be visible if positionOffset is nonzero.
           * @param positionOffset       Value from [0, 1) indicating the offset from the page at position.
           * @param positionOffsetPixels Value in pixels indicating the offset from position.
           */
          void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
  
          /**
           * This method will be invoked when a new page becomes selected. Animation is not
           * necessarily complete.
           *
           * @param position Position index of the new selected page.
           */
          void onPageSelected(int position);
  
          /**
           * Called when the scroll state changes. Useful for discovering when the user
           * begins dragging, when the pager is automatically settling to the current page,
           * or when it is fully stopped/idle.
           *
           * @param state The new scroll state.
           * @see VelocityPager#SCROLL_STATE_IDLE
           * @see VelocityPager#SCROLL_STATE_DRAGGING
           * @see VelocityPager#SCROLL_STATE_SETTLING
           */
          void onPageScrollStateChanged(int state);
  
      }
  
  }
  </code></pre>
  
  <p><em>The item's xml layout</em></p>
  
  <p>(Note: the root view has to be clickable for other purposes inside the app)</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
  &lt;RelativeLayout 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="match_parent"
      android:clickable="true"&gt;
  
      &lt;LinearLayout
          android:id="@+id/icon_container_top"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_alignParentEnd="true"
          android:layout_alignParentRight="true"
          android:layout_alignParentTop="true"
          android:layout_gravity="top|end"
          android:layout_marginEnd="16dp"
          android:layout_marginRight="16dp"
          android:layout_marginTop="16dp"
          android:alpha="0"
          android:background="@drawable/info_background"
          android:orientation="horizontal"
          android:padding="4dp"
          tools:alpha="1"&gt;
  
          &lt;ImageView
              android:id="@+id/delete"
              style="@style/SelectableItemBackground"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:clickable="true"
              android:contentDescription="@string/desc_delete"
              android:padding="12dp"
              android:src="@drawable/ic_delete_white_24dp"
              android:tint="@color/icons" /&gt;
  
      &lt;/LinearLayout&gt;
  
      &lt;LinearLayout
          android:id="@+id/icon_container_bottom"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_alignParentEnd="true"
          android:layout_alignParentRight="true"
          android:layout_centerVertical="true"
          android:layout_marginBottom="16dp"
          android:layout_marginEnd="16dp"
          android:layout_marginRight="16dp"
          android:alpha="0"
          android:background="@drawable/info_background"
          android:orientation="vertical"
          android:padding="4dp"
          tools:alpha="1"&gt;
  
          &lt;ImageView
              android:id="@+id/size"
              style="@style/SelectableItemBackground"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:clickable="true"
              android:contentDescription="@string/desc_size"
              android:padding="12dp"
              android:src="@drawable/ic_straighten_white_24dp"
              android:tint="@color/icons" /&gt;
  
          &lt;ImageView
              android:id="@+id/palette"
              style="@style/SelectableItemBackground"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:clickable="true"
              android:contentDescription="@string/desc_palette"
              android:padding="12dp"
              android:src="@drawable/ic_palette_white_24dp"
              android:tint="@color/icons" /&gt;
  
      &lt;/LinearLayout&gt;
  &lt;/RelativeLayout&gt;
  </code></pre>
  
  <p><em>The xml layout with the pager itself</em></p>
  
  <p>(Quite nested? Might be a cause of the problem? I don't know... )</p>
  
  <pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
  &lt;android.support.v4.widget.DrawerLayout 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:id="@+id/drawer"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      android:fitsSystemWindows="true"
      tools:openDrawer="end"&gt;
  
      &lt;SwipeRefreshLayout
          android:id="@+id/refresh_layout"
          android:layout_width="match_parent"
          android:layout_height="match_parent"&gt;
  
          &lt;android.support.design.widget.CoordinatorLayout
              android:id="@+id/coordinator"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:fitsSystemWindows="false"&gt;
  
              &lt;FrameLayout
                  android:id="@+id/container"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent" /&gt;
  
              &lt;com.my.example.OptionalViewPager
                  android:id="@+id/view_pager"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:scrollbars="horizontal"
                  app:layout_behavior="com.my.example.MoveUpBehavior" /&gt;
  
              &lt;android.support.v7.widget.Toolbar
                  android:id="@+id/toolbar"
                  android:layout_width="match_parent"
                  android:layout_height="?attr/actionBarSize"
                  android:background="@android:color/transparent"
                  android:clickable="false"
                  android:fitsSystemWindows="false"
                  app:contentInsetLeft="0dp"
                  app:contentInsetStart="0dp"
                  app:contentInsetStartWithNavigation="0dp"
                  app:layout_collapseMode="pin"
                  app:navigationIcon="@drawable/ic_menu_white_24dp" /&gt;
  
          &lt;/android.support.design.widget.CoordinatorLayout&gt;
  
      &lt;/SwipeRefreshLayout&gt;
  
      &lt;include layout="@layout/layout_drawer" /&gt;
  
  &lt;/android.support.v4.widget.DrawerLayout&gt;
  </code></pre>
  
  <p><em>part of my adapter that is relevant for ViewHolders</em></p>
  
  <pre><code>@Override
      public int getItemCount() {
          return dataset.size();
      }
  
      @Override
      public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          Log.v("Adapter", "CreateViewHolder");
          final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
          final View rootView = layoutInflater.inflate(R.layout.page, parent, false);
          return new MyViewHolder(rootView);
      }
  
      @Override
      public void onBindViewHolder(MyViewHolder page, int position) {
          Log.v("Adapter", String.format("BindViewHolder(%d)", position));
          final ViewData viewData = dataset.get(position);
          page.bind(viewData);
          listener.onViewAdded(position, viewData.getData());
      }
  
      @Override
      public void onViewRecycled(MyViewHolder page) {
          if (page.getData() == null) return;
          listener.onViewRemoved(page.getData().id);
      }
  
      @Override
      public int getItemViewType(int position) {
          return 0;
      }
  </code></pre>
  
  <p><strong>The ViewHolder</strong></p>
  
  <pre><code>public class MyViewHolder extends RecyclerView.ViewHolder implements MyListener {
  
      @BindView(R.id.info_container)
      ViewGroup mInfoContainer;
  
      @BindView(R.id.icon_container_top)
      ViewGroup mIconContainerTop;
  
      @BindView(R.id.icon_container_bottom)
      ViewGroup mIconContainerBottom;
  
      @BindView(R.id.info_rows)
      ViewGroup mInfoRows;
  
      @BindView(R.id.loading)
      View mIcLoading;
  
      @BindView(R.id.sync_status)
      View mIcSyncStatus;
  
      @BindView(R.id.delete)
      View mIcDelete;
  
      @BindView(R.id.ic_fav)
      View mIcFavorite;
  
      @BindView(R.id.size)
      View mIcSize;
  
      @BindView(R.id.palette)
      View mIcPalette;
  
      @BindView(R.id.name)
      TextView mName;
  
      @BindView(R.id.length)
      TextView mLength;
  
      @BindView(R.id.threads)
      TextView mThreads;
  
      @BindView(R.id.price)
      TextView mPrice;
  
      @Nullable
      private MyModel mModel = null;
  
      @Nullable
      private Activity mActivity;
  
      public MyViewHolder(View itemView) {
          super(itemView);
          ButterKnife.bind(this, itemView);
          mActivity= (Activity) itemView.getContext();
          if (mActivity!= null) mActivity.addMyListener(this);
      }
  
      @OnClick(R.id.delete)
      protected void clickDeleteBtn() {
          if (mActivity == null || mActivity.getMode() != Mode.EDIT) return;
          if (mModel == null) return;
          Animations.pop(mIcDelete);
          final int modelId = mModel.id;
          if (mModel.delete()) {
              mActivity.delete(modelId);
          }
      }
  
      @OnClick(R.id.size)
      protected void clickSizeBtn() {
          if (mActivity== null) return;
          mActivity.setUIMode(Mode.EDIT_SIZE);
          Animations.pop(mIcSize);
      }
  
      @OnClick(R.id.palette)
      protected void clickPaletteBtn() {
          if (mActivity== null) return;
          mActivity.setUIMode(Mode.EDIT_LENGTH);
          Animations.pop(mIcPalette);
      }
  
      private void initModelViews() {
          if (mData == null) return;
          final Locale locale = Locale.getDefault();
          mName.setValue(String.format(locale, "Model#%d", mModel.id));
          mLength.setValue(Html.fromHtml(String.format(locale, itemView.getContext().getString(R.string.template_length), mModel.meters)));
      }
  
      /**
       * set the icon container to be off screen at the beginning
       */
      private void prepareViews() {
          new ExpectAnim().expect(mIconContainerTop).toBe(outOfScreen(Gravity.END), visible())
                  .toAnimation()
                  .setNow();
          new ExpectAnim().expect(mIconContainerBottom).toBe(outOfScreen(Gravity.END), visible())
                  .toAnimation()
                  .setNow();
  
      }
  
      @Nullable
      public MyModel getData() {
          return mModel;
      }
  
      private void enableEdit() {
          new ExpectAnim()
                  .expect(mIconContainerBottom)
                  .toBe(atItsOriginalPosition())
                  .toAnimation()
                  .start();
      }
  
      private void disableEdit() {
          new ExpectAnim()
                  .expect(mIconContainerBottom)
                  .toBe(outOfScreen(Gravity.END))
                  .toAnimation()
                  .start();
      }
  
      private void enableInfo() {
          new ExpectAnim()
                  .expect(mInfoContainer)
                  .toBe(atItsOriginalPosition())
                  .toAnimation()
                  .start();
      }
  
      private void disableInfo() {
          new ExpectAnim()
                  .expect(mInfoContainer)
                  .toBe(outOfScreen(Gravity.BOTTOM))
                  .toAnimation()
                  .start();
      }
  
      private void enableDelete() {
          if (mIconContainerTop == null) return;
          new ExpectAnim()
                  .expect(mIconContainerTop)
                  .toBe(atItsOriginalPosition(), visible())
                  .toAnimation()
                  .start();
      }
  
      private void disableDelete() {
          if (mIconContainerTop == null) return;
          new ExpectAnim()
                  .expect(mIconContainerTop)
                  .toBe(outOfScreen(Gravity.END), invisible())
                  .toAnimation()
                  .start();
      }
  
      public void bind(@NonNull final ViewData viewData) {
          mModel = viewData.getData();
          prepareViews();
          initModelViews();
      }
  
  }
  </code></pre>
  
  <p><strong>So, here's my issue with these!</strong></p>
  
  <p>When intializing the adapter I insert about 15 to 17 items via an observable. This seems to be correct:</p>
  
  <p><a href="https://i.stack.imgur.com/CEycr.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/CEycr.png" alt="Logging for initializing is okay"></a></p>
  
  <p>but when swiping horizontally the recyclerView's callbacks seem to be totally messed up and produce weird results:</p>
  
  <p><a href="https://i.stack.imgur.com/sexfM.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/sexfM.png" alt="Messed up loggings"></a></p>
  
  <p>Do you see that the recycler does not try to recycle old viewHolders at all? The image just shows a small portion of the "spamming" that is going on. Sometimes it will create a new viewHolder even more than two times for the same position while I scroll the recycler slowly!</p>
  
  <p><a href="https://i.stack.imgur.com/LnaKl.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/LnaKl.png" alt="onBind spam"></a></p>
  
  <p>Another side problem is: The listener currently should allow me to pass the bind / recycle events to an underlying game engine which will create destroy entities on the screen. Due the excessive spamming of the events it will currently create those entities also excessively!</p>
  
  <p>I excpected the Recycler to create a new ViewHolder for the first (let's say in my example 17) times and then just reuse the items how it should.</p>
  
  <p>Please help, I'm stuck on this problem for 2 days now and I'm frustrated after  searching people with same issues but without luck.
  Thank you!</p>