title: selectable RecyclerView performance issues
body: <p>I am currently trying to achieve an app to edit weekly timeframes.
  Like mechanical timers for sockets, but in my case for every weekday.</p>
  
  <p><a href="https://i.stack.imgur.com/S8kfh.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/S8kfh.png" alt="enter image description here"></a></p>
  
  <p>The granularity is in the first place secondary (i guess it will be 15 or 30min).</p>
  
  <p>My approach was a <code>RecyclerView</code> with <code>GridLayoutManager</code> and an <code>ArrayAdapter</code> with Items for every Cell.</p>
  
  <p>To select more cells you can longpress a cell and drag over others. To achieve this I used the Listeners of the following Library <a href="https://github.com/MFlisar/DragSelectRecyclerView" rel="nofollow noreferrer">DragSelectRecyclerView</a>.</p>
  
  <p>It works pretty well and you can select the items quite good but especially on the emulator or older phones its very slow and laggy. In the debug logcat you can see also that the Choreographer has to skip many frames at rendering the View and on selection of multiple cells.</p>
  
  <p><a href="https://i.stack.imgur.com/ULvu2.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/ULvu2.png" alt="enter image description here"></a></p>
  
  <p>Is there an better approach to achieve such an behavior. Or is there any big mistake in the code which is very slow and crappy?</p>
  
  <p><strong>EDIT:</strong></p>
  
  <p>after changing  <code>notifyItemChanged(pos);</code> to <code>notifyItemRangeChanged(pos, pos);</code> its way less laggy but still not performing as well as it should.</p>
  
  <p>I also removed everything that was responsible for autoscrolling (which was a feature of the library i mentioned above) to make the code simpler.</p>
  
  <p>Here the source of my <code>Fragment</code></p>
  
  <pre><code>    public class TestFragment extends Fragment
      {
          @BindView(R.id.gridView_hours) GridView gridView_hours;
          @BindView(R.id.weekdays_container) LinearLayout weekdays_container;
  
          private String[] hours = new String[]{"00:00", "01:00", "02:00", "03:00", "04:00", "05:00", "06:00", "07:00", "08:00", "09:00", "10:00", "11:00", "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00", "21:00", "22:00", "23:00"};
          private String[] hoursShort = new String[]{"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"};
  
          @Override
          public void onCreate(@Nullable Bundle savedInstanceState)
          {
              super.onCreate(savedInstanceState);
          }
  
          @Nullable
          @Override
          public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)
          {
              View view = inflater.inflate(R.layout.fragment_test, container, false);
              ButterKnife.bind(this, view);
  
              for (int i = 1; i &lt;= 7; i++)
              {
                  RecyclerView recyclerView = new RecyclerView(getActivity());
                  initAdapter(recyclerView);
  
                  GridLayoutManager glm = new GridLayoutManager(getActivity(), 48, GridLayoutManager.VERTICAL, false);
                  recyclerView.setLayoutManager(glm);
  
                  weekdays_container.addView(recyclerView);
  
              }
              gridView_hours.setAdapter(new ArrayAdapter&lt;String&gt;(getActivity(), R.layout.hour_view, hoursShort));
  
      //        GridLayoutManager glm = new GridLayoutManager(getActivity(), 48, GridLayoutManager.VERTICAL, false);
      //        recyclerView.setLayoutManager(glm);
  
              return view;
          }
  
          private void initAdapter(RecyclerView recyclerView)
          {
              TestAutoDataAdapter adapter = new TestAutoDataAdapter(getActivity(), 48);
              recyclerView.setAdapter(adapter);
  
              DragSelectionProcessor dragSelectionProcessor = new DragSelectionProcessor(new DragSelectionProcessor.ISelectionHandler() {
                  @Override
                  public HashSet&lt;Integer&gt; getSelection() {
                      return adapter.getSelection();
                  }
  
                  @Override
                  public boolean isSelected(int index) {
                      return adapter.getSelection().contains(index);
                  }
  
                  @Override
                  public void updateSelection(int start, int end, boolean isSelected, boolean calledFromOnStart) {
                      adapter.selectRange(start, end, isSelected);
                  }
              });
  
              DragSelectTouchListener dragSelectTouchListener = new DragSelectTouchListener()
                      .withSelectListener(dragSelectionProcessor);
              recyclerView.addOnItemTouchListener(dragSelectTouchListener);
  
              adapter.setClickListener(new TestAutoDataAdapter.ItemClickListener()
              {
                  @Override
                  public void onItemClick(View view, int position)
                  {
                      adapter.toggleSelection(position);
                  }
  
                  @Override
                  public boolean onItemLongClick(View view, int position)
                  {
                      dragSelectTouchListener.startDragSelection(position);
                      return true;
                  }
              });
          }
      }
  </code></pre>
  
  <p>and the <code>RecyclerView.Adapter</code></p>
  
  <pre><code>    public class TestAutoDataAdapter extends RecyclerView.Adapter&lt;TestAutoDataAdapter.ViewHolder&gt;
      {
  
          private int dataSize;
          private Context context;
          private ItemClickListener clickListener;
  
          private HashSet&lt;Integer&gt; selected;
  
          public TestAutoDataAdapter(Context context, int size)
          {
              this.context = context;
              dataSize = size;
              selected = new HashSet&lt;&gt;();
          }
  
          @Override
          public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
          {
              View view = LayoutInflater.from(context).inflate(R.layout.test_cell, parent, false);
              ViewHolder viewHolder = new ViewHolder(view);
              return viewHolder;
          }
  
          @Override
          public void onBindViewHolder(ViewHolder holder, int position)
          {
              holder.tvText.setText("");
              if (selected.contains(position))
                  holder.tvText.setBackgroundColor(Color.RED);
              else
                  holder.tvText.setBackgroundColor(Color.WHITE);
          }
  
          @Override
          public int getItemCount()
          {
              return dataSize;
          }
  
          // ----------------------
          // Selection
          // ----------------------
  
          public void toggleSelection(int pos)
          {
              if (selected.contains(pos))
                  selected.remove(pos);
              else
                  selected.add(pos);
              notifyItemChanged(pos);
          }
  
          public void select(int pos, boolean selected)
          {
              if (selected)
                  this.selected.add(pos);
              else
                  this.selected.remove(pos);
              notifyItemRangeChanged(pos, pos);
          }
  
          public void selectRange(int start, int end, boolean selected)
          {
              for (int i = start; i &lt;= end; i++)
              {
                  if (selected)
                      this.selected.add(i);
                  else
                      this.selected.remove(i);
              }
              notifyItemRangeChanged(start, end - start + 1);
          }
  
          public void deselectAll()
          {
              // this is not beautiful...
              selected.clear();
              notifyDataSetChanged();
          }
  
          public void selectAll()
          {
              for (int i = 0; i &lt; dataSize; i++)
                  selected.add(i);
              notifyDataSetChanged();
          }
  
          public int getCountSelected()
          {
              return selected.size();
          }
  
          public HashSet&lt;Integer&gt; getSelection()
          {
              return selected;
          }
  
          // ----------------------
          // Click Listener
          // ----------------------
  
          public void setClickListener(ItemClickListener itemClickListener)
          {
              clickListener = itemClickListener;
          }
  
          public interface ItemClickListener
          {
              void onItemClick(View view, int position);
              boolean onItemLongClick(View view, int position);
          }
  
          // ----------------------
          // ViewHolder
          // ----------------------
  
          public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener
          {
              public TextView tvText;
  
              public ViewHolder(View itemView)
              {
                  super(itemView);
                  tvText = itemView.findViewById(R.id.tvText);
                  itemView.setOnClickListener(this);
                  itemView.setOnLongClickListener(this);
              }
  
              @Override
              public void onClick(View view)
              {
                  if (clickListener != null)
                      clickListener.onItemClick(view, getAdapterPosition());
              }
  
              @Override
              public boolean onLongClick(View view)
              {
                  if (clickListener != null)
                      return clickListener.onItemLongClick(view, getAdapterPosition());
                  return false;
              }
          }
      }
  </code></pre>
  
  <p>the SelectTouchListener</p>
  
  <pre><code>    public class DragSelectTouchListener implements RecyclerView.OnItemTouchListener
      {
          private static final String TAG = "DSTL";
  
          private boolean mIsActive;
          private int mStart, mEnd;
          private int mLastStart, mLastEnd;
  
          private OnDragSelectListener mSelectListener;
  
          public DragSelectTouchListener()
          {
              reset();
          }
  
          /**
           * sets the listener
           * &lt;p&gt;
           *
           * @param selectListener the listener that will be notified when items are (un)selected
           */
          public DragSelectTouchListener withSelectListener(OnDragSelectListener selectListener)
          {
              this.mSelectListener = selectListener;
              return this;
          }
  
          // -----------------------
          // Main functions
          // -----------------------
  
          /**
           * start the drag selection
           * &lt;p&gt;
           *
           * @param position the index of the first selected item
           */
          public void startDragSelection(int position)
          {
              setIsActive(true);
              mStart = position;
              mEnd = position;
              mLastStart = position;
              mLastEnd = position;
              if (mSelectListener != null &amp;&amp; mSelectListener instanceof OnAdvancedDragSelectListener)
                  ((OnAdvancedDragSelectListener)mSelectListener).onSelectionStarted(position);
          }
  
          // -----------------------
          // Functions
          // -----------------------
  
          @Override
          public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e)
          {
              if (!mIsActive || rv.getAdapter().getItemCount() == 0)
                  return false;
  
              int action = e.getAction();
              switch (action)
              {
                  case MotionEvent.ACTION_POINTER_DOWN:
                  case MotionEvent.ACTION_DOWN:
                      reset();
                      break;
              }
  
              return true;
          }
  
          @Override
          public void onTouchEvent(RecyclerView rv, MotionEvent e)
          {
              if (!mIsActive)
                  return;
  
              int action = e.getAction();
              switch (action)
              {
                  case MotionEvent.ACTION_MOVE:
                      updateSelectedRange(rv, e);
                      break;
                  case MotionEvent.ACTION_CANCEL:
                  case MotionEvent.ACTION_UP:
                  case MotionEvent.ACTION_POINTER_UP:
                      reset();
                      break;
              }
          }
  
          private void updateSelectedRange(RecyclerView rv, MotionEvent e)
          {
              updateSelectedRange(rv, e.getX(), e.getY());
          }
  
          private void updateSelectedRange(RecyclerView rv, float x, float y)
          {
              View child = rv.findChildViewUnder(x, y);
              if (child != null)
              {
                  int position = rv.getChildAdapterPosition(child);
                  if (position != RecyclerView.NO_POSITION &amp;&amp; mEnd != position)
                  {
                      mEnd = position;
                      notifySelectRangeChange();
                  }
              }
          }
  
          private void notifySelectRangeChange()
          {
              if (mSelectListener == null)
                  return;
              if (mStart == RecyclerView.NO_POSITION || mEnd == RecyclerView.NO_POSITION)
                  return;
  
              int newStart, newEnd;
              newStart = Math.min(mStart, mEnd);
              newEnd = Math.max(mStart, mEnd);
              if (mLastStart == RecyclerView.NO_POSITION || mLastEnd == RecyclerView.NO_POSITION)
              {
                  if (newEnd - newStart == 1)
                      mSelectListener.onSelectChange(newStart, newStart, true);
                  else
                      mSelectListener.onSelectChange(newStart, newEnd, true);
              }
              else
              {
                  if (newStart &gt; mLastStart)
                      mSelectListener.onSelectChange(mLastStart, newStart - 1, false);
                  else if (newStart &lt; mLastStart)
                      mSelectListener.onSelectChange(newStart, mLastStart - 1, true);
  
                  if (newEnd &gt; mLastEnd)
                      mSelectListener.onSelectChange(mLastEnd + 1, newEnd, true);
                  else if (newEnd &lt; mLastEnd)
                      mSelectListener.onSelectChange(newEnd + 1, mLastEnd, false);
              }
  
              mLastStart = newStart;
              mLastEnd = newEnd;
          }
  
          private void reset()
          {
              setIsActive(false);
              if (mSelectListener != null &amp;&amp; mSelectListener instanceof OnAdvancedDragSelectListener)
                  ((OnAdvancedDragSelectListener)mSelectListener).onSelectionFinished(mEnd);
              mStart = RecyclerView.NO_POSITION;
              mEnd = RecyclerView.NO_POSITION;
              mLastStart = RecyclerView.NO_POSITION;
              mLastEnd = RecyclerView.NO_POSITION;
          }
  
          @Override
          public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept)
          {
              // ignore
          }
  
          public void setIsActive(boolean isActive)
          {
              this.mIsActive = isActive;
          }
  
          // -----------------------
          // Interfaces and simple default implementations
          // -----------------------
  
          public interface OnAdvancedDragSelectListener extends OnDragSelectListener
          {
              /**
               * @param start      the item on which the drag selection was started at
               */
              void onSelectionStarted(int start);
  
              /**
               * @param end      the item on which the drag selection was finished at
               */
              void onSelectionFinished(int end);
          }
  
          public interface OnDragSelectListener
          {
              /**
               * @param start      the newly (un)selected range start
               * @param end        the newly (un)selected range end
               * @param isSelected true, it range got selected, false if not
               */
              void onSelectChange(int start, int end, boolean isSelected);
          }
      }
  </code></pre>
  
  <p>and the Implementation of the needed Interface</p>
  
  <pre><code>    public class DragSelectionProcessor implements DragSelectTouchListener.OnAdvancedDragSelectListener {
  
          private ISelectionHandler mSelectionHandler;
          private HashSet&lt;Integer&gt; mOriginalSelection;
          private boolean mFirstWasSelected;
          private boolean mCheckSelectionState = false;
  
          public DragSelectionProcessor(ISelectionHandler selectionHandler)
          {
              mSelectionHandler = selectionHandler;
          }
  
          @Override
          public void onSelectionStarted(int start)
          {
              mOriginalSelection = new HashSet&lt;&gt;();
              Set&lt;Integer&gt; selected = mSelectionHandler.getSelection();
              if (selected != null)
                  mOriginalSelection.addAll(selected);
              mFirstWasSelected = mOriginalSelection.contains(start);
  
              mSelectionHandler.updateSelection(start, start, !mFirstWasSelected, true);
  
          }
  
          @Override
          public void onSelectionFinished(int end)
          {
              mOriginalSelection = null;
          }
  
          @Override
          public void onSelectChange(int start, int end, boolean isSelected)
          {
              for (int i = start; i &lt;= end; i++)
                  checkedUpdateSelection(i, i, isSelected ? !mFirstWasSelected :  mOriginalSelection.contains(i));
          }
  
          private void checkedUpdateSelection(int start, int end, boolean newSelectionState)
          {
              if (mCheckSelectionState)
              {
                  for (int i = start; i &lt;= end; i++)
                  {
                      if (mSelectionHandler.isSelected(i) != newSelectionState)
                          mSelectionHandler.updateSelection(i, i, newSelectionState, false);
                  }
              }
              else
                  mSelectionHandler.updateSelection(start, end, newSelectionState, false);
          }
  
          public interface ISelectionHandler
          {
              Set&lt;Integer&gt; getSelection();
  
              boolean isSelected(int index);
  
              void updateSelection(int start, int end, boolean isSelected, boolean calledFromOnStart);
          }
      }
  </code></pre>