Reputation: 23313
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
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:
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
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
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
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.
(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:
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