11

I want to use a RecyclerView to emulate the behavior of a MultiViewPager, in particular I'd like to have the selected item at the center of the screen, including the first and the last element.

As you can see in this image, the first item is centered and this would be my expected result. Expected result

What I did was to setup a RecyclerView with an horizontal LinearLayoutManager and a LinearSnapHelper. The problem with this solution is that the first and the last item will never be horizontally centered as selection. Should I switch my code so that it uses a MultiViewPager or is it possible to achieve a similar result taking advantage of a RecyclerView?

Vektor88
  • 4,429
  • 9
  • 52
  • 102

4 Answers4

10

You can implement this with an RecyclerView.ItemDecoration in getItemOffsets(), to offset the first and last item appropriately.

Retrieve any offsets for the given item. Each field of outRect specifies the number of pixels that the item view should be inset by, similar to padding or margin. The default implementation sets the bounds of outRect to 0 and returns.

If you need to access Adapter for additional data, you can call getChildAdapterPosition(View) to get the adapter position of the View.

You might need to use the messured size of the item and the RecyclerView as well. But these information is available to be used anyhow.

Community
  • 1
  • 1
tynn
  • 33,607
  • 8
  • 80
  • 121
  • 1
    This seems to me the proper way to go, the suggestion in the other answer by @CzarMatt might work but it seems a bit hacky to me. – Vektor88 Jun 21 '17 at 09:11
  • 5
    @Vektor88, usage of `decoration` is not that good as it seems to be. I did it that way, but it break a lot of `RecyclerView` rules, because `decoration`s are intended to be an extension of the item. It means left/right offsets for centering will be handled as it was real view `padding/margin`, so it's `LayoutManager.getDecoratedMeasuredWidth()` will be really big. It will break, e.g., `LinearSnapHelper`. First/Last element will be snaped to next/prev on even slight touches, because they think your view is hude. – Nexen Jun 28 '17 at 13:42
  • 3
    @Vektor88, this is much more preferable way to go - https://stackoverflow.com/q/29117916/2653714 – Nexen Jun 28 '17 at 13:51
  • @Nexen the approach you suggested is much better than this, and also solves my other question https://stackoverflow.com/questions/44766186/linearsnaphelper-doesnt-snap-on-edge-items-of-recyclerview – Vektor88 Jun 28 '17 at 19:36
  • @Vektor88, glad to hear! Well then I will post this as an answer there – Nexen Jun 28 '17 at 20:01
3

The problem with this solution is that the first and the last item will never be horizontally centered as selection.

This is probably because your RecycleView is responsible for showing, within its layout bounds, exactly the number of items that are inside of your data set.

In the example image you provided, you can achieve that effect by adding a "placeholder" item in the first and last position of your dataset. This way, you can have an invisible item taking up the first slot, thus offsetting the item you want to be centered.

This placeholder item should not respond to touch events and should not interfere with handling of click events on other items (specifically, the position handling).

You will have to modify your adapters getItemCount and perhaps getItemType.

CzarMatt
  • 1,575
  • 17
  • 20
2

Just add padding on RecyclerView and add clipToPadding=false and it'll only affect the items on the ends.

I.S
  • 1,414
  • 16
  • 40
0

An improvement on @I.S answer which works 100% of the time and is very easy to implement without any glitchy animation. First we have to use PagerSnapHelper() to have view pager like scroll. To center the items you need to add a large padding on the recyclerView and then clip to the padding. Then use a customized LinearSmoothScroller to smoothly center your item. To center the item on load, just use smooth scroll to position 0. Below is the code

<android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_selection"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginLeft="30dp"
        android:layout_marginRight="30dp"
        android:paddingTop="5dp"
        android:paddingBottom="5dp"
        android:paddingLeft="150dp"
        android:paddingRight="150dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/text_selection_alert"
        app:layout_constraintBottom_toBottomOf="@+id/guideline_1"
        android:clipToPadding="false"
        android:background="@drawable/bg_stat"/>

And in Code (in C#)

RecyclerView.LayoutManager lm = new LinearLayoutManager(Context, LinearLayoutManager.Horizontal, false);

recycler_selection = view.FindViewById<RecyclerView>(Resource.Id.recycler_selection);
recycler_selection.SetLayoutManager(new LinearLayoutManager(Context, LinearLayoutManager.Horizontal, false));
// <Set Adapter to the Recycler>

RecyclerView.SmoothScroller smoothScroller = new CenterScroller(recycler_selection.Context);

SnapHelper helper = new PagerSnapHelper();
helper.AttachToRecyclerView(recycler_selection);

smoothScroller.TargetPosition = 0;
lm.StartSmoothScroll(smoothScroller);

public class CenterScroller : LinearSmoothScroller
    {
        float MILLISECONDS_PER_INCH = 350f;

        public CenterScroller(Context context) : base(context)
        {
        }

        public override int CalculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference)
        {
            return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
        }

        protected override float CalculateSpeedPerPixel(DisplayMetrics displayMetrics)
        {
            return MILLISECONDS_PER_INCH / displayMetrics.Xdpi;
        }
    }
Bilal Kazi
  • 119
  • 1
  • 10