I am trying to get a ViewPager and a RecyclerView to work in sync in displaying an image. The ViewPager contains an ImageView which shows a full sized image and the recycler view shows thumbnails of previous and upcoming images.
If the user swipes on the ViewPager then the RecyclerView moves left/right in sync. If the user scrolls and then clicks a photo, the ViewPager sets that position.
This all works, just giving background to my app.
The part I am stuck on is showing a border around the thumbnail that is currently selected in the RecyclerView from onPageSelected. The element which is considered selected is the one that is currently show on the ViewPager
A lot of the other questions deal with this problem using the onClickListener. My onClickListener just calls
viewPager.setCurrentItem(index)
So any time an item is selected, onPageSelected is called, either naturally from the ViewPager or from the click event. So this is where I feel I need to set the border.
So far I'm using a bit of a hack since the number of images is only 20.
for (int i = 0, ii = recyclerViewAdapter.getItemCount(); i < ii; i++) {
//RecyclerView.ViewHolder holder = recyclerView.findViewHolderForItemId(recyclerViewAdapter.getItemId(i));
RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(position);
if (holder instanceof RecyclerViewAdapter.RecyclerViewHolder) {
if (i == position) {
...
((RecyclerViewAdapter.RecyclerViewHolder) holder).getViewHolderContainer().setBackground(border);
} else {
((RecyclerViewAdapter.RecyclerViewHolder) holder).getViewHolderContainer().setBackground(null);
}
}
}
This doesn't work. If get's the right position since I'm provided that by the ViewPager but neither method for retrieving the ViewHolder at 'position' seems to work.
So, after all that, my question is if anyone can recommend me a clean way (or really even a hack at this point) that will let me update the current position of an element based on the position from onPageSelected of the ViewPager.
Thank you kindly.
So I ended up getting this working using two interfaces. No ugly looping hacks required.
ViewPager.OnPageChangeListener and OnChildAttachStateListener.
Basically my issue was that if I scrolled away from the currently selected item in the RecyclerView then I couldn't unselect that item later because the ViewHolder was recycled or null. Basically I couldn't access the previous view to unselect it so multiple items would end up selected when you scroll through it.
So with the ViewPager it was easy.
#Override
public void onPageSelected(int position)
int previousItemPos = currentItemPos; //currentItemPos is a class attr
currentItemPos = position;
int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation()
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_270) { //portrait
portraitScroll(position) //scrolls the recyclerView to currently selected item based on viewWidth
} else if .. landscape do the same
changeItem(position, previousItemPos);
}
#Override
public void onChildViewAttachedToWindow(View view) {
int childPosition = recyclerView.getChildAdapterPosition(view);
if (childPosition == currentItemPos) {
highlightItem(view); //applies an effect
}
}
#Override
public void onChildViewDetachedToWindow(View view) {
unHighlightItem(view);
}
private void changeItem(int newItem, int oldItem) {
ViewHolder holder = recyclerView.findViewHolderForItemId(recyclerView.getItemId(newItem));
if (holder instance of RecyclerViewHolder) {
highlightItem((RecyclerViewHolder) holder).getViewHolderContainer());
}
ViewHolder oldHolder = recyclerView.findViewHolderForItemId(recyclerView.getItemId(oldItem));
if (oldHolder instance of RecyclerViewHolder) {
unHighlightItem((RecyclerViewHolder) holder).getViewHolderContainer());
}
So basically each time we scroll so that the currently selected item in the ViewPager goes out of sight, we unselect it when it is detached. If we scroll back to it and the position hasn't changed, we reselect it.
If the ViewPager changes item, first we scroll to that item based on the width of the each RecyclerViews view, then we select it in onPageSelected.
Related
I have strange behaviour in my recyclerview which i can not figure out. I am using following code in my OnBindViewHolder()
#Override
public void onBindViewHolder(#NonNull ProductAdapter.OurViewHolder viewHolder, int i) {
Product currentItem = adapterData.get(i);
viewHolder.productNamesFragmentItemsBinding.setProductName(currentItem);
if (currentItem.getPhotoUris() != null) {
Picasso.get().load(currentItem.getPhotoUris().get(0))
.into(viewHolder.productNamesFragmentItemsBinding.ivProductImage);
}
if (mViewModel.isCartContainsThisProduct(currentItem.getIdProducts())) {
System.out.println("calling is cart contains");
viewHolder.productNamesFragmentItemsBinding.btnAddToCart.setVisibility(View.GONE);
viewHolder.productNamesFragmentItemsBinding.btnAdded.setVisibility(View.VISIBLE);
viewHolder.productNamesFragmentItemsBinding.btnCancel.setVisibility(View.VISIBLE);
} else {
System.out.println("calling cart not contains");
viewHolder.productNamesFragmentItemsBinding.btnAddToCart.setVisibility(View.VISIBLE);
viewHolder.productNamesFragmentItemsBinding.btnAdded.setVisibility(View.GONE);
viewHolder.productNamesFragmentItemsBinding.btnCancel.setVisibility(View.GONE);
}
}
I have five items in my adapter , at position 2 there is no url, still it loads the image sometimes but not always.
One more thing i noticed that there is a button "Add to cart", which refresh item at this position when clicked. So if i click this button at any position say at position 5, then again if i click at position 2 (where i have no url), image of position 5 loads at 2.
Its not strange behavior at all . This is how RecyclerView works its basically reuse the same ViewHolder for next items.
The problem with your code you have covers if (currentItem.getPhotoUris() != null) case you have missed the else part i.e clearing the image or setting a placeholder.
if (!TextUtils.isEmpty(currentItem.getPhotoUris())) {
Picasso.get().load(currentItem.getPhotoUris().get(0))
.into(viewHolder.productNamesFragmentItemsBinding.ivProductImage);
}else{
// you can set a placeholder image here
viewHolder.productNamesFragmentItemsBinding.ivProductImage.setImageResource(0)
}
I have made horizontal RecyclerView to show images where you select different actions by touching image. I have also highlighted current active selection by using background drawable and set that on onBindViewHolder.
public void onBindViewHolder(final MyViewHolder holder, final int position) {
holder.itemView.setSelected(selectedPos == position);
holder.imageView.setImageResource(horizontalList.get(position).imageId);
then i have button which is not part RecyclerView so you can also roll selection in recyclerview to next one by pressing it.
It's working fine except highlighting doesn't change when pressing button.
I can test that when selecting currently selected item, i get my instructions activity opened otherwise different activity will be opened.
So how i can get that current selected item highlight when pressing a button??
update 1.
I have in my activity under myButtonOnClick
int mItemCount = horizontalAdapter.getItemCount() - 1;
horizontalAdapter.notifyItemChanged(selectedItem);
if (selectedItem == mItemCount) {
selectedItem = 0;
} else {
selectedItem += 1;
}
horizontalAdapter.notifyItemChanged(selectedItem);
which doesn't do much.
update 2.
Found the problem which was at my onBindViewHolder as i declared there different variable which messed things up.
so horizontalAdapter.notifyItemChanged(selectedItem); worked just fine and my
onBindViewHolder now i have holder.itemView.setSelected(selectedItem == position);
so manipulate same variable from both button and recyclerview to get things right.
You can add an OnScrollListener to your RecyclerView to observe any change in scroll state and position.
ref to this
If it doesn't help, you may paste your code inside the button.OnClickListener for further discussion.
I have a RecyclerView with a Horizontal LinerLayout. It displays numbers from 10 to 1, that is used to rate something.
When I select 10 and scroll back to 1 and select 1. I have to update the UI to remove selection on 10 and update selection on 1. But, when I use findViewHolderForAdapterPosition() to remove the selection on 10 it gives me a NullPointerException
I am getting the position in the ViewHolder with getAdapterPosition().
Then, I use that position to get the ViewHolder by calling findViewHolderForAdapterPosition() on my recycler view object and update the UI to remove the selection from 10.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
vh.textRating.setBackgroundResource(R.drawable.rating_background_selected_orange);;
With some tests, I found out when I try to do the same thing without scrolling it works fine. However, only when I am scrolling it gives me a NullPointerException
How do I fix this?
As requested here is some important code from Adapter class.
#Override
public void onBindViewHolder(RatingRecyclerAdapter.ViewHolder holder, int position) {
String itemText = itemList.get(position);
holder.textRating.setText(itemText);
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView textRating;
public ViewHolder(View itemView) {
super(itemView);
textRating = (TextView) itemView.findViewById(R.id.text_rating);
textRating.setOnClickListener(ratingClickListener);
}
private final View.OnClickListener ratingClickListener = new View.OnClickListener() {
#Override
public void onClick(View v) {
int position = getAdapterPosition();
if (callback != null) {
callback.onClickRating(v, position);
}
}
};
}
Activity Class
#Override
public void onClickRating(View view, int position) {
RatingRecyclerAdapter.ViewHolder vh;
int color;
int previousPosition = mAdapter.getSelectedPosition(); //Get previously clicked postion if any.
if (previousPosition == Constants.NO_ITEM_SELECTED) {
// An item was selected first time
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color);
return;
}
if (position == previousPosition) // Same item was selected
return;
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(previousPosition);
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setUnselectedRatingResource(vh, color); // Remove the previous selected item drawables.
vh = (RatingRecyclerAdapter.ViewHolder)
mRecycler.findViewHolderForAdapterPosition(position);
mAdapter.setSelectedPosition(position); // Save new item selected position.
color = Utility.getItemColor(mAdapter.getSelectedRating());
mAdapter.setSelectedRatingResource(vh, color); // Set the new selected item drawables. Setting some background to indicate selection.
}
As Sevastyan has written in the comment, the RecyclerView immediately recycles the view as soon as the item is out of the screen. So if we call findViewHolderForAdapterPosition() for a view which is outside the screen we get a null value. (I am not confirming this is the actual case. But, this is what it seems to me.)
So I created a class that stores all the data about an item in the RecyclerView and stored all the colours and value of that item in the class. And when we are populating the view, set the all the colours based on data stored in that class.
PS: I THANK Sevastyan for not giving me the answer directly. But, only giving me the reason for getting that Exception.
If your view is out of the screen, it can be recycled OR cached.
In case it's recycled, you can handle in onViewRecycled() method or setup the view again inside onBind() when the view becomes visible (you can save the state on the object of your list if needed).
In case it's not recycled (onViewRecycled method not called for that position), it's probably cached. You can set the cache size to zero to prevent this state from happening.
recycler.setItemViewCacheSize(0)
I'm using recyclerView to implement ExpandableList and I want when user clicks on parent view, other expanded parents collapse. It works nice with code below:
private void collapseOthers(int position) {
for (int i = 0; i < ol.size(); i++) {
OrderForShow o = ol.get(i);
if (i != position) {
if (!o.isChild()) {
if (o.getChilds() == null) {
RecyclerView.ViewHolder vh = rView.findViewHolderForAdapterPosition(i);
if (vh != null)
vh.itemView.findViewById(R.id.root_layer).performClick();
}
}
}
}
}
but when user scrolls down, upper items will be null and vh goes null and I can't collapse invisible item, Is there any way to handle this issue?
Try assigning ids to the layout (or item within layout) inside onBindViewHolder and make changes on basis of id only.
Another alternative is to maintain a HashMap<Integer,String> where Integer will be the position of the item and String can be used to store it's status like "visible" or "invisible".
This is because while scrolling, very often, internally RecyclerView does make some mess in maintaining the dirty views and maintain their ids. Better, we maintain it and take control of them.
This link explains how RecyclerView works.
There is a way to horizontally center the last item in a recycler view.
So...if there is only one item this item will be centered and will not scroll, if there is two or more that can fill the width it will be possible to scroll, but the last item will appears first on center
For example. In the following images, every time a new search category (iPhone, Celulares e Smartphones) are added, the last item is centralized.
You will have to do it in your adapter. I have never did something like this, but my try would be something like this:
On the method onBindViewHolder:
#Override
public void onBindViewHolder(MyHolder holder, int position) {
OrderItem item = items.get(position);
//last item
if(position == item.size()-1){
holder.textview.setLayoutParams(Gravity.CENTER_VERTICAL);
}
}
Try adapt this idea in your project!