Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RecyclerView 13000

RecyclerView 13000

RecyclerView – класс, который встречается в любом приложении и не один раз. Но уверены ли вы, что знаете про все его возможности, особенно те, что скрыты? Вряд ли это возможно, с учетом того, что класс RecyclerView состоит почти из 13 тысяч строк кода, не считая другие вспомогательные классы. Мы рассмотрим нетривиальные возможности использования RecyclerView, самые сложные задачи и интересные подробности устройства этого класса. Оптимизация, сложные краши, префетч элементов, кэширование холдеров, новая paging library от Google – эти и другие вопросы будут разобраны в рамках доклада.

Artur Vasilov

November 22, 2017
Tweet

More Decks by Artur Vasilov

Other Decks in Programming

Transcript

  1. RecyclerView static final boolean FORCE_INVALIDATE_DISPLAY_LIST = Build.VERSION.SDK_INT == 18 ||

    Build.VERSION.SDK_INT == 19 || Build.VERSION.SDK_INT == 20; static final boolean ALLOW_SIZE_IN_UNSPECIFIED_SPEC = Build.VERSION.SDK_INT >= 23; static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16; private static final boolean FORCE_ABS_FOCUS_SEARCH_DIRECTION = Build.VERSION.SDK_INT <= 15; private static final boolean IGNORE_DETACHED_FOCUSED_CHILD = Build.VERSION.SDK_INT <= 15;
  2. RecyclerView 13000? › RecyclerView ins and outs https://www.youtube.com/watch?v=LqBlYJTfLP4 › Radical

    RecyclerView https://www.youtube.com/watch?v=TS_J0Qw4zl0 › … › No, not yet another 5
  3. 6

  4. 8

  5. 10

  6. FixedSnapHelper public class FixedSnapHelper extends LinearSnapHelper { @Size(2) @Override public

    int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { int[] distanceToSnap = new int[2]; //... return distanceToSnap; } @Override public View findSnapView(@NonNull RecyclerView.LayoutManager layoutManager) { // ... return super.findSnapView(layoutManager); } }
  7. Swipe-to-dismiss public abstract class SwipeDismissTouchCallback extends ItemTouchHelper.SimpleCallback { public SwipeDismissTouchCallback(int

    swipeDirs) { super(0, swipeDirs); } @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { // Remove your item } }
  8. Swipe-to-dismiss ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new SwipeDismissTouchCallback(ItemTouchHelper.RIGHT) { @Override public

    void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { DemoItem demoItem = (DemoItem) viewHolder.itemView.getTag(R.id.demo_item_key); mDemoAdapter.removeItem(demoItem); } }); itemTouchHelper.attachToRecyclerView(recyclerView);
  9. ItemTouchHelper customization @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder

    viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } @Override public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); }
  10. 17

  11. Custom swipe-to-dismiss › Custom view › Swipe out only what

    you want to swipe out › More control › More bugs  (don’t forget to reset properties of the view) 18
  12. Custom swipe-to-dismiss @Override public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { //

    ... boolean isIntercepted = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = lastX = (int) event.getX(); downY = (int) event.getY(); isIntercepted = false; break; case MotionEvent.ACTION_MOVE: int disX = (int) (event.getX() - downX); int disY = (int) (event.getY() - downY); isIntercepted = Math.abs(disX) > scaledTouchSlop && Math.abs(disX) > Math.abs(disY); break; // ... } return isIntercepted; }
  13. Custom swipe-to-dismiss @Override public boolean onInterceptTouchEvent(@NonNull MotionEvent event) { //

    ... boolean isIntercepted = false; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: downX = lastX = (int) event.getX(); downY = (int) event.getY(); isIntercepted = false; break; case MotionEvent.ACTION_MOVE: int disX = (int) (event.getX() - downX); int disY = (int) (event.getY() - downY); isIntercepted = Math.abs(disX) > scaledTouchSlop && Math.abs(disX) > Math.abs(disY); break; // ... } return isIntercepted; }
  14. Custom swipe-to-dismiss @Override public boolean onTouchEvent(MotionEvent event) { // ...

    switch (event.getAction()) { case MotionEvent.ACTION_MOVE: int disX = (int) (lastX - event.getX()); int disY = (int) (lastY - event.getY()); if (!dragging && Math.abs(disX) > scaledTouchSlop && Math.abs(disX) > Math.abs(disY)) { ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } dragging = true; } // ... break; // ... } return super.onTouchEvent(event); }
  15. Custom swipe-to-dismiss <ru.arturvasilov.recyclerview.demo.swipe.SwipeHorizontalLayout android:id="@+id/swipe_view_layout" <!-- --> > <ImageView android:id="@+id/swipe_view_menu" <!--

    --> /> <FrameLayout android:id="@+id/swipe_view_content" <!-- --> > </FrameLayout> </ru.arturvasilov.recyclerview.demo.swipe.SwipeHorizontalLayout>
  16. 27

  17. notifyItem… public void addItem(@NonNull DemoItem demoItem, int position) { if

    (position < 0 || position > items.size()) { throw new IllegalArgumentException("Position must be in list bounds"); } items.add(position, demoItem); notifyItemInserted(position); } public void removeItem(@NonNull DemoItem demoItem) { int position = items.indexOf(demoItem); items.remove(position); notifyItemRemoved(position); }
  18. DiffUtil public class DiffUtilCallback extends DiffUtil.Callback { // ... @Override

    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return oldItems.get(oldItemPosition).equals(newItems.get(newItemPosition)); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldItems.get(oldItemPosition).equals(newItems.get(newItemPosition)); } }
  19. DiffUtil backgrounds An O(ND) Difference Algorithm and Its Variations (Eugene

    W. Myers) http://www.xmailserver.org/diff2.pdf 32
  20. Custom animations public class DemoItemAnimator extends BaseItemAnimator { @Override protected

    void animateAddImpl(RecyclerView.ViewHolder holder) { ViewCompat.animate(holder.itemView) .scaleX(1) .scaleY(1) .setDuration(getAddDuration()) .setInterpolator(new AccelerateInterpolator()) .setListener(new DefaultAddVpaListener(holder)) .setStartDelay(getAddDelay(holder)) .start(); } // ... }
  21. Custom animations public class DemoItemAnimator extends BaseItemAnimator { @Override protected

    void animateAddImpl(RecyclerView.ViewHolder holder) { ViewCompat.animate(holder.itemView) .scaleX(1) .scaleY(1) .setDuration(getAddDuration()) .setInterpolator(new AccelerateInterpolator()) .setListener(new DefaultAddVpaListener(holder)) .setStartDelay(getAddDelay(holder)) .start(); } // ... }
  22. Custom animations public class DemoItemAnimator extends BaseItemAnimator { @Override protected

    void animateAddImpl(RecyclerView.ViewHolder holder) { ViewCompat.animate(holder.itemView) .scaleX(1) .scaleY(1) .setDuration(getAddDuration()) .setInterpolator(new AccelerateInterpolator()) .setListener(new DefaultAddVpaListener(holder)) .setStartDelay(getAddDelay(holder)) .start(); } // ... }
  23. Custom animations public class DemoItemAnimator extends BaseItemAnimator { @Override protected

    void preAnimateAddImpl(RecyclerView.ViewHolder holder) { holder.itemView.setScaleX(0.5f); holder.itemView.setScaleY(0.5f); } // ... }
  24. 39

  25. Animation listeners public final boolean isRunning(ItemAnimatorFinishedListener listener) { boolean running

    = isRunning(); if (listener != null) { if (!running) { listener.onAnimationsFinished(); } else { mFinishedListeners.add(listener); } } return running; }
  26. Animation listeners public final void dispatchAnimationsFinished() { final int count

    = mFinishedListeners.size(); for (int i = 0; i < count; ++i) { mFinishedListeners.get(i).onAnimationsFinished(); } mFinishedListeners.clear(); }
  27. Animating? public boolean willItemsAnimate(@NonNull RecyclerView recyclerView) { LinearLayoutManager layoutManager =

    (LinearLayoutManager) recyclerView.getLayoutManager(); boolean willAnimate = false; int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition(); int lastVisiblePosition = layoutManager.findLastVisibleItemPosition(); for (int position = firstVisiblePosition; position <= lastVisiblePosition; position++) { RecyclerView.ViewHolder holder = recyclerView.findViewHolderForLayoutPosition(position); if (/* your condition for items here*/true) { willAnimate = true; } } return willAnimate; }
  28. Animating? public boolean willItemsAnimate(@NonNull RecyclerView recyclerView) { //cast below are

    safe since this delegate will be used only on screens with such lists LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); boolean willAnimate = false; int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition(); int lastVisiblePosition = layoutManager.findLastVisibleItemPosition(); for (int position = firstVisiblePosition; position <= lastVisiblePosition; position++) { RecyclerView.ViewHolder holder = recyclerView.findViewHolderForLayoutPosition(position); if (/*your condition for items here*/true) { willAnimate = true; } } return willAnimate; }
  29. Increase duration public class LongRunningItemAnimator extends DefaultItemAnimator { private static

    final long LONG_ANIMATION_DURATION = 3000; @Override public long getAddDuration() { return LONG_ANIMATION_DURATION; } @Override public long getRemoveDuration() { return LONG_ANIMATION_DURATION; } // ... }
  30. 57

  31. Time dividers › Part of the item › Different item

    (different view type) › Part of ItemDecoration (?) 58
  32. GapWorker final int size = mRecyclerViews.size(); long latestFrameVsyncMs = 0;

    for (int i = 0; i < size; i++) { RecyclerView view = mRecyclerViews.get(i); if (view.getWindowVisibility() == View.VISIBLE) { latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs); } } long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs; prefetch(nextFrameNs);
  33. Recycler#tryGetViewHolderForPositionByDeadline if (holder == null) { long start = getNanoTime();

    if (deadlineNs != FOREVER_NS && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { // abort - we have a deadline we can't meet return null; } holder = mAdapter.createViewHolder(RecyclerView.this, type); // ... long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); }
  34. RecycledViewPool boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { long

    expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } void factorInCreateTime(int viewType, long createTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mCreateRunningAverageNs = runningAverage( scrapData.mCreateRunningAverageNs, createTimeNs); } long runningAverage(long oldAverage, long newValue) { if (oldAverage == 0) { return newValue; } return (oldAverage / 4 * 3) + (newValue / 4); }
  35. RecycledViewPool boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { long

    expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } void factorInCreateTime(int viewType, long createTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mCreateRunningAverageNs = runningAverage( scrapData.mCreateRunningAverageNs, createTimeNs); } long runningAverage(long oldAverage, long newValue) { if (oldAverage == 0) { return newValue; } return (oldAverage / 4 * 3) + (newValue / 4); }
  36. RecycledViewPool boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) { long

    expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs; return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs); } void factorInCreateTime(int viewType, long createTimeNs) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mCreateRunningAverageNs = runningAverage( scrapData.mCreateRunningAverageNs, createTimeNs); } long runningAverage(long oldAverage, long newValue) { if (oldAverage == 0) { return newValue; } return (oldAverage / 4 * 3) + (newValue / 4); }
  37. Prefetch in LayoutManager public class CustomLayoutManager extends RecyclerView.LayoutManager { @Override

    public void collectInitialPrefetchPositions(int adapterItemCount, LayoutPrefetchRegistry layoutPrefetchRegistry) { super.collectInitialPrefetchPositions(adapterItemCount, layoutPrefetchRegistry); } @Override public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, LayoutPrefetchRegistry layoutPrefetchRegistry) { layoutPrefetchRegistry.addPosition(0, 0); layoutPrefetchRegistry.addPosition(1, 500); // ... } }
  38. 69

  39. 70

  40. MessageQueue.IdleHandler public class NextIdleHandler implements MessageQueue.IdleHandler { @Override public boolean

    queueIdle() { pollAndExecuteIdleTask(); return !mTaskQueue.isEmpty(); } private void pollAndExecuteIdleTask() { Runnable task = mTaskQueue.peek(); if (task == null) { return; } try { task.run(); } finally { removeExecutedTask(task); } } }
  41. Execute idle tasks @MainThread public void register(@NonNull IdleTask task) {

    if (mTaskQueue.isEmpty()) { mMessageQueue.addIdleHandler(mHandler); } mTaskQueue.add(task); } @MainThread public void unregister(@NonNull IdleTask task) { mTaskQueue.remove(task); if (mTaskQueue.isEmpty()) { mMessageQueue.removeIdleHandler(mHandler); } }
  42. onCreateViewHolder /** * Called when RecyclerView needs a new {@link

    ViewHolder} of the given type to represent * an item. */ public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);