SudoPlz
SudoPlz

Reputation: 23313

How to prevent a child of a RecyclerView cell from moving while the user scrolls

I have very long width cells on my horizontal RecyclerView, and I want them to have a header that remains still as the user scrolls horizontally.

- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Header (C) <-- We want that to be still
-   -   -   Content (D)

Here's what it looks like visually:

Thus, I'm looking for a way to either:

1) Stop the header (C) from changing positions while the user is dragging their finger on the RecyclerView (A)

or

2) Scroll the cell (B) like normal, but change the position of it's child (C) to the opposite direction, in order to make the header appear still even though it is moving (in the opposite direction of the parent (B).

Here's what I'm trying to build:

Any ideas?

p.s 1: I noticed many SO answers, suggest the use of ItemDecoration, but all of the possible answers have code for VERTICAL implementations, which are very different from the HORIZONTAL implementations.

p.s 2 I'm creating all my view content programmatically so I won't be using layout files. (That's because the content is going to be react-native views, and I can't create those with layout files).

p.s 3: I also noticed that ItemDecoration is old tactic, and more recent 3rd party libraries extend the LayoutManager.

Please shed some light, thank you.

Upvotes: 8

Views: 1807

Answers (4)

SudoPlz
SudoPlz

Reputation: 23313

What I ended up doing (thanks to the inspiration Cheticamp gave me) is the following:

- Helper Header (C) <-- We now have an extra title view
- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Header (C) <-- Plus the typical titles within our cells
-   -   -   Content

As you can see:

  • We now have a helper Header view that lives outside our RecyclerView
  • The Header views that live within our RecyclerView keep on moving, but right above them we place the helper view

Here's some actual code to see what happens:

public class CalendarView extends LinearLayout { protected LinearLayoutManager mLayoutManager; protected HeaderView helperHeaderView; protected RecyclerView recyclerView;

public CalendarView(final ReactContext context) {
    super(context);


    setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    setOrientation(LinearLayout.VERTICAL);

    helperHeaderView = new HeaderView(context);
    addView(helperHeaderView);




    final DailyViewAdapter adapter = new DailyViewAdapter(context) {
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            super.onBindViewHolder(holder, position);

            // if our header is not assinged any position yet (we haven't given it any data yet)
            if (helperHeaderView.getLastPosition() == null) {
                updateHeaderData(helperHeaderView, globals.getInitialPosition()); // hydrate it
            }
        }
    };

    recyclerView = new SPRecyclerView(context) {
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);

            if (mLayoutManager == null) {
                mLayoutManager = (LinearLayoutManager) getLayoutManager();
            }

            // the width of any header
            int headerWidth = helperHeaderView.getWidth();

            // get the position of the first visible header in the recyclerview
            int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();

            // get a ref of the Cell that contains that header
            DayView firstView = (DayView) mLayoutManager.findViewByPosition(firstVisiblePos);

            // get the X coordinate of the first visible header
            float firstViewX = firstView.getX();



            // get the position of the last visible header in the recyclerview
            int lastVisiblePos = mLayoutManager.findLastVisibleItemPosition();

            // get a ref of the Cell that contains that header
            DayView lastView = (DayView) mLayoutManager.findViewByPosition(lastVisiblePos);

            // get the X coordinate of the last visible header
            float lastViewX = lastView.getX();


            // if the first visible position is not the one our header is set to
            if (helperHeaderView.getLastPosition() != firstVisiblePos) {
                // update the header data
                adapter.updateHeaderData(helperHeaderView, firstVisiblePos);
            }

            // if the first visible is not also the last visible (happens when there's only one Cell on screen)
            if (firstVisiblePos == lastVisiblePos) {
                // reset the X coordinates
                helperHeaderView.setX(0);
            } else { // else if there are more than one cells on screen
                // set the X of the helper header, to whatever the last visible header X was, minus the width of the header
                helperHeaderView.setX(lastViewX - headerWidth);
            }

        }
    };


    // ...
  • All that is left to do now, turn the parent layout to a RelativeLayout in order to make the actual views overlap (the helper header view goes right above the recycler view).

  • Also you might want to experiment with setting the helper view alpha to zero whenever needed, to make sure it looks good

I hope that helps someone in the future.

Upvotes: 0

Rajesh
Rajesh

Reputation: 2618

Hope this library help : TableView

<com.evrencoskun.tableview.TableView
    android:id="@+id/content_container"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"

    app:column_header_height="@dimen/column_header_height"
    app:row_header_width="@dimen/row_header_width"
    app:selected_color="@color/selected_background_color"
    app:shadow_color="@color/shadow_background_color"
    app:unselected_color="@color/unselected_background_color" />

Upvotes: 1

Tuby
Tuby

Reputation: 3253

I'm using this answer as solution stackoverflow.com/a/44327350/4643073 Works great!

If you want horizontal sticky header, just change eveything related to "verticalness", change getY() to getX(), getTop() to getRight(), getHeight() to getWidth().

Why do you think ItemDecoration is old tactic? It is not deprecated, it doesn't mess your adapter to extend some specific class, it's working well.

Upvotes: 0

Cheticamp
Cheticamp

Reputation: 62841

Although it may be possible to leave the title view within the RecyclerView and make it static, I suggest an alternate approach.

The title can continue to be represented internally within the RecyclerView, but the display will be taken outside to the top of the RecyclerView as follows:

- Title (C) <-- We want that to be still
- Recycler View (A)
-   -   Cell (parent) (B)
-   -   -   Content

A RecyclerView.OnScrollListener will listen for the appearance of new items and change the title accordingly. In this way, as new items appear, the title which is a TextView will display the new title. The following demonstrates this.

enter image description here

(This is a bare-bones implementation for demonstration purposes. A full app would display the dog breed images and some sort of meaningful description.)

Here is the code that accomplishes this effect:

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private LinearLayoutManager mLayoutManager;
    private RecyclerViewAdapter mAdapter;
    private TextView mBreedNameTitle;
    private int mLastBreedTitlePosition = RecyclerView.NO_POSITION;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<String> breedList = createBreedList();

        // This is where the breed title is displayed.
        mBreedNameTitle = findViewById(R.id.breedNameTitle);

        // Set up the RecyclerView.
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        mAdapter = new RecyclerViewAdapter(breedList);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setAdapter(mAdapter);

        // Add the OnScrollListener so we know when to change the breed title.
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                int lastVisible = mLayoutManager.findLastVisibleItemPosition();
                if (lastVisible == RecyclerView.NO_POSITION) {
                    return;
                }
                if (lastVisible != mLastBreedTitlePosition) {
                    mBreedNameTitle.setText(mAdapter.getItems().get(lastVisible));
                    mLastBreedTitlePosition = lastVisible;
                }
            }
        });
    }

    private List<String> createBreedList() {
        List<String> breedList = new ArrayList<>();
        breedList.add("Affenpinscher");
        breedList.add("Afghan Hound");
        breedList.add("Airedale Terrier");
        breedList.add("Akita");
        breedList.add("Alaskan Malamute");
        breedList.add("American Cocker Spaniel");
        breedList.add("American Eskimo Dog (Miniature)");
        breedList.add("American Eskimo Dog (Standard)");
        breedList.add("American Eskimo Dog (Toy)");
        breedList.add("American Foxhound");
        breedList.add("American Staffordshire Terrier");
        breedList.add("American Eskimo Dog (Standard)");
        return breedList;
    }

    @SuppressWarnings("unused")
    private final static String TAG = "MainActivity";
}

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<String> mItems;

    RecyclerViewAdapter(List<String> items) {
        mItems = items;
    }

    @Override
    @NonNull
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;

        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new RecyclerViewAdapter.ItemViewHolder(view);
    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        RecyclerViewAdapter.ItemViewHolder vh = (RecyclerViewAdapter.ItemViewHolder) holder;

        vh.mBreedImage.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.no_image));
        vh.mBreedName = mItems.get(position);
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public List<String> getItems() {
        return mItems;
    }

    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private ImageView mBreedImage;
        private String mBreedName;

        ItemViewHolder(View itemView) {
            super(itemView);
            mBreedImage = itemView.findViewById(R.id.breedImage);
        }
    }
}

activity_main.xml

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/activity_horizontal_margin"
    android:orientation="vertical">

    <TextView
        android:id="@+id/breedNameTitle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:fontFamily="sans-serif"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        tools:text="Breed name" />

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />
</LinearLayout>

item_layout.xml

<android.support.constraint.ConstraintLayout 
    android:id="@+id/cont_item_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <ImageView
        android:id="@+id/breedImage"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:contentDescription="Dog breed image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="1.0"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:text="@string/large_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/breedImage"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Update: Here is another approach that sets the left padding of a TextView to make the header sticky. The negative x-offset of the TextView is taken as padding for the header to make it slide to the right within the TextView and stick to the left side of the screen.

Here is the result:

enter image description here

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private LinearLayoutManager mLayoutManager;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        List<String> breedList = createBreedList();

        // Set up the RecyclerView.
        mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
        RecyclerView recyclerView = findViewById(R.id.recyclerView);
        RecyclerViewAdapter adapter = new RecyclerViewAdapter(breedList);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setAdapter(adapter);

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                // Pad the left of the breed name so it stays aligned with the left side of the display.
                int firstVisible = mLayoutManager.findFirstVisibleItemPosition();
                View firstView = mLayoutManager.findViewByPosition(firstVisible);
                firstView.findViewById(R.id.itemBreedName).setPadding((int) -firstView.getX(), 0, 0, 0);

                // Make sure the other breed name has zero padding because we may have changed it.
                int lastVisible = mLayoutManager.findLastVisibleItemPosition();
                View lastView = mLayoutManager.findViewByPosition(lastVisible);
                lastView.findViewById(R.id.itemBreedName).setPadding(0, 0, 0, 0);
            }
        });
    }

    private List<String> createBreedList() {
        List<String> breedList = new ArrayList<>();
        breedList.add("Affenpinscher");
        breedList.add("Afghan Hound");
        breedList.add("Airedale Terrier");
        breedList.add("Akita");
        breedList.add("Alaskan Malamute");
        breedList.add("American Cocker Spaniel");
        breedList.add("American Eskimo Dog (Miniature)");
        breedList.add("American Eskimo Dog (Standard)");
        breedList.add("American Eskimo Dog (Toy)");
        breedList.add("American Foxhound");
        breedList.add("American Staffordshire Terrier");
        breedList.add("American Eskimo Dog (Standard)");
        return breedList;
    }

    @SuppressWarnings("unused")
    private final static String TAG = "MainActivity";

}

class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private final List<String> mItems;

    RecyclerViewAdapter(List<String> items) {
        mItems = items;
    }

    @Override
    @NonNull
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;

        view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new RecyclerViewAdapter.ItemViewHolder(view);
    }


    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        RecyclerViewAdapter.ItemViewHolder vh = (RecyclerViewAdapter.ItemViewHolder) holder;

        vh.mBreedImage.setImageDrawable(holder.itemView.getResources().getDrawable(R.drawable.no_image));
        vh.mBreedName.setPadding(0, 0, 0, 0);
        vh.mBreedName.setText(mItems.get(position));
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    static class ItemViewHolder extends RecyclerView.ViewHolder {
        private ImageView mBreedImage;
        private TextView mBreedName;

        ItemViewHolder(View itemView) {
            super(itemView);
            mBreedImage = itemView.findViewById(R.id.breedImage);
            mBreedName = itemView.findViewById(R.id.itemBreedName);
        }
    }
}

activity_main.xml

<LinearLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/activity_horizontal_margin"
    android:orientation="vertical">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal" />
</LinearLayout>

item_layout.xml

<android.support.constraint.ConstraintLayout 
    android:id="@+id/cont_item_root"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <TextView
        android:id="@+id/itemBreedName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:ellipsize="none"
        android:fontFamily="sans-serif"
        android:singleLine="true"
        android:textColor="@android:color/black"
        android:textSize="16sp"
        tools:text="Breed name" />

    <ImageView
        android:id="@+id/breedImage"
        android:layout_width="64dp"
        android:layout_height="64dp"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:contentDescription="Dog breed image"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/itemBreedName"
        app:layout_constraintVertical_bias="1.0"
        tools:ignore="HardcodedText" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:text="@string/large_text"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/breedImage"
        app:layout_constraintTop_toBottomOf="@+id/itemBreedName" />

</android.support.constraint.ConstraintLayout>

Upvotes: 4

Related Questions