How to prevent a child of a RecyclerView cell from moving while the user scrolls
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>