Generally
I want to control the ViewHolder inflated Views of my RecyclerView from outside of the ViewHolder and the RecyclerView classes. In other words, I want to have control of these views from other methods/classes.
My case (en example)
In my specific case, I made a photo gallery activity which allows the user to perform selection and deselection of each inflated view, notifying which items are selected by highlighting them.
For now, the user is able to do that by clicking each generated object / View; then, actions on specific child of RecyclerView / adapter are possible thanks to "setOnClickListener" and "setOnLongClickListener" methods, which perform the corresponding actions in methods inside the ViewHolder class.
But when activity is restarted (i.e. for device rotation) the selection goes lost and the user should perform the selection again (i.e. for deleting photos).
Assuming that positions of the selected photos are kept (for example via bundle, or via an array) is possible to restore selection (i.e. highlighting the corresponding item / views) on the adapter views after that the activity is re-started? If yes, how?
Some code
The code below contains the Recyclerview class and the AdapterView class, which both are child of an activity Class.
private class ImageGalleryAdapter extends RecyclerView.Adapter<ImageGalleryAdapter.MyViewHolder> {
private ArrayList<PhotoObject.PhotoElement> photoAL;
private Context mContext;
public ImageGalleryAdapter(Context context, ArrayList<PhotoObject.PhotoElement> photosToPreviewInGallery) {
mContext = context;
photoAL = photosToPreviewInGallery;
}
#Override
public ImageGalleryAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the layout
View itemView = inflater.inflate(R.layout.item_photo, parent, false);
ImageGalleryAdapter.MyViewHolder viewHolder = new ImageGalleryAdapter.MyViewHolder(itemView);
// Retrieving the itemView
return viewHolder;
}
#Override
public void onBindViewHolder(ImageGalleryAdapter.MyViewHolder holder, int position) {
PhotoObject.PhotoElement previewPhotoInGallery = photoAL.get(position);
ImageView imageView = holder.mPhotoImageView;
GlideApp.with(mContext)
.load(previewPhotoInGallery.getUrl())
.placeholder(R.drawable.ic_cloud_off_red)
.into(imageView);
}
//The method which gives back the number of items to load as photo.
#Override
public int getItemCount() {
return (photoAL.size());
}
// The class that assigns a view holder for each Image and checkbox in the RecyclerView.
public class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public ImageView mPhotoImageView;
public CheckBox mPhotoCheckBox;
public MyViewHolder(View item_view) {
super(item_view);
mPhotoImageView = (ImageView) item_view.findViewById(R.id.item_photo_iv);
mPhotoCheckBox = (CheckBox) item_view.findViewById(R.id.item_photo_checkbox);
item_view.setOnClickListener(this);
item_view.setOnLongClickListener(this);
// Retrieving the item_view
}
// The method for managing the click on an image.
#Override
public void onClick(View view) {
itemSelection(view);
}
// Manages the selection of the items.
private void itemSelection(View item) {
// Retrieving the item
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
if (!item.isSelected()) {
// Add clicked item to the selected ones
MultiPhotoShootingActivity.manageSelection(true, position);
// Visually highlighting the ImageView
item.setSelected(true);
mPhotoCheckBox.setChecked(true);
mPhotoCheckBox.setVisibility(View.VISIBLE);
} else {
// Remove clicked item from the selected ones
MultiPhotoShootingActivity.manageSelection(false, position);
// Removing the visual highlights on the ImageView
item.setSelected(false);
mPhotoCheckBox.setChecked(false);
mPhotoCheckBox.setVisibility(View.INVISIBLE);
}
}
}
// The method for managing the long click on an image.
#Override
public boolean onLongClick(View view) {
int position = getAdapterPosition();
if(position != RecyclerView.NO_POSITION) {
Intent intent = new Intent(mContext, PhotoDetail.class);
intent.putExtra("KEY4URL", activityPhotoObject.getPath(position));
startActivity(intent);
}
// return true to indicate that the click was handled (if you return false onClick will be triggered too)
return true;
}
}
}
Thank you for your time.
You shouldn't "control" views from outside the adapter. Instead, Override onSaveState and onRestoreState in your activity. Make same methods in your adapter with passing the bundle to the adapter in order to save state. save an integer array of positions that were selected into the bundle(that you passed into an adapter). In corresponding way, you can get the array of selected positions from the bundle of On restore state.
activity:
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState){
adapter.onRestoreInstanceState(savedInstanceState);
}
in your adapter:
public void onRestoreInstanceState(Bundle state){
selectedItemsArray = state.getIntArray("my_array_key")
}
#Alessandro
You can handle the Runtime changes by yourself.
In your manifest, you can define the changes that your activity will handle by itself and it will not be restarted.
android:configChanges="orientation|keyboardHidden"
After that, you'll have to handle the Configuration changes that you declared in your manifest using this method in your activity:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// Do your thing
}
}
SOLVED
Find out that for solving the problem I had to accomplish two little tasks:
saving and restoring the selected item selection state (for example via an array, as helpfully suggested by #Inkognito);
retrieving the views for applying the selection, based on the position inside the RecyclerView.
So, I had to modify some code.
Before proceeding, I would like to point out that the Activity class has a sub-class, which is the Adapter class (named ImageGalleryAdapter); the Adapter subclass, in turn, has its own subclass, which is the ViewHolder class (named MyViewHolder).
So: Activity class -> Adapter class -> ViewHolder class
Code modified in the parent class (the activity class, in which the RecyclerView is)
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
adapter.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
adapter.onRestoreInstanceState(savedInstanceState);
}
In the onSaveInstanceState and onRestoreInstanceState methods, I added the references for saving and restoring instance states of the "adapter" sub-class.
Code added in the adapter class (which is inside the RecyclerView class)
private boolean [] selectedItemsArray;
private void onSaveInstanceState(Bundle outState) {
outState.putBooleanArray("my_array_key" , selectedItemsArray = mpsaPO.getItemsSelected());
}
private void onRestoreInstanceState(Bundle state) {
if (state != null) {
selectedItemsArray = state.getBooleanArray("my_array_key");
}
}
The selectedItemsArray is a boolean array in which the information of which elements of the RecyclerView are selected (true = selected; false = not selected) is contained.
Then, adding this element in the saved instance and retrieved via the activity class, makes the app able to know which are the views selected after that the activity is re-created.
Code added inside the onBindViewHolder method, which is inside the adapter class
if (selectedItemsArray != null) {
if (selectedItemsArray[position]) {
holder.itemView.setSelected(true);
holder.mPhotoCheckBox.setChecked(true);
holder.mPhotoCheckBox.setVisibility(View.VISIBLE);
}
}
With this last part of code, we are applying the selection to the corresponding views based on which items/views were selected before that the activity was saved.
The holer object contains the itemView and mPhotoCheckBox objectsm on which we can perform the selection.
Related
I am actually making some visibility changes to items that are clicked of the recycler view. But when the user clicks on one object and then clicks on the other object then the previous object should come to its initial state.
The manager.findViewByPosition(position) is working fine if the view is in focus of the screen but I am not able to get the view if the element is not in current focus.
For example:- the user clicks on 1st(position) item then clicks on the last position then the findViewByPosition returns a null.
Please help and let me know if there is some other way of doing it.
The expected result should be the view of the last item to be refreshed but it's not happening for the views that are not in the current focus of the screen.
Below is my code snippet. Updated with what you suggested.
public class BodyPartWithMmtRecyclerView extends
RecyclerView.Adapter<BodyPartWithMmtRecyclerView.ViewHolder>
{
//variables defined.
int selectedPosition = -1;
static class ViewHolder extends RecyclerView.ViewHolder {
//All the view items declared here.
ViewHolder(View view) {
super(view);
//All the views are defined here.
}
}
public BodyPartWithMmtRecyclerView(List<BodyPartWithMmtSelectionModel> bodyPartsList, Context context){
//array list initialization and shared preference variables initialization
}
public BodyPartWithMmtRecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
//Creating a new view.
}
public void onBindViewHolder(#NonNull final BodyPartWithMmtRecyclerView.ViewHolder holder, #SuppressLint("RecyclerView") final int position) {
BodyPartWithMmtSelectionModel bodyPartWithMmtSelectionModel = bodyPartsList.get(position);
holder.iv_bodypart.setImageResource(bodyPartWithMmtSelectionModel.getIv_body_part());
holder.tv_body_part_name.setText(bodyPartWithMmtSelectionModel.getExercise_name());
if(selectedPosition!=position && selectedPosition!=-1){
//updated the elements view to default view. Like made the visibility and other changes here.
}
//some click listeners on the sub-elements of the items. Like textviews, spinner, etc
holder.iv_bodypart.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((BodyPartSelection)context).setFabVisible();
if(selectedPosition!=-1){
((BodyPartSelection)context).visibilityChanged(selectedPosition,position);
/*here what I was doing is whenever the user clicks on an item I check weather a previous item is clicked or not then if yes then I send the position to a function that makes it to default but the issue was that if the item is not in the focus of the screen the findViewByPosition returns null.*/
}
selectedPosition = position;
bodypartSelected = holder.tv_body_part_name.getText().toString();
holder.iv_bodypart.setVisibility(View.INVISIBLE);
holder.rl_left_right.setVisibility(View.VISIBLE);
}
});
//and other listeners below
}
#Override
public int getItemCount() {
return bodyPartsList==null?0:bodyPartsList.size();
}
#Override
public int getItemViewType(int position) {
return position;
}
}
VisibilityChanged function
public void visibilityChanged(int position, int clicked){
View view = manager.findViewByPosition(position);
if(view!=null) {
Log.i("inside","visibility change");
ImageView imageView = view.findViewById(R.id.bodypartImage);
//other elements and changing the visibility of elemets to default.
}
}
I have updated my code based on the snippet you updated. Please don't change the visibility condition if-else I have added with any different logic which I saw in your code snippet. As you did, it will not update both selected and default view as RecyclerView reuse the view layout. So if the condition is not proper, you may see multiple items as selected or some other types of unwated behaviour.
public void onBindViewHolder(#NonNull final BodyPartWithMmtRecyclerView.ViewHolder holder, #SuppressLint("RecyclerView") final int position) {
BodyPartWithMmtSelectionModel bodyPartWithMmtSelectionModel = bodyPartsList.get(position);
holder.iv_bodypart.setImageResource(bodyPartWithMmtSelectionModel.getIv_body_part());
holder.tv_body_part_name.setText(bodyPartWithMmtSelectionModel.getExercise_name());
if(selectedPosition == position){
//updated the elements view to SELECTED VIEW. Like made the visibility and other changes here.
} else {
//updated the elements view to default view. Like made the visibility and other changes here.
}
//some click listeners on the sub-elements of the items. Like textviews, spinner, etc
holder.iv_bodypart.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((BodyPartSelection)context).setFabVisible();
/Comment by Hari: Don't try to change the visibility of default as it will be done automatically after calling notifyDataSetChanged(). */
if(selectedPosition!=-1){
((BodyPartSelection)context).visibilityChanged(selectedPosition,position);
/*here what I was doing is whenever the user clicks on an item I check weather a previous item is clicked or not then if yes then I send the position to a function that makes it to default but the issue was that if the item is not in the focus of the screen the findViewByPosition returns null.*/
/*Comment by Hari: This snippet is valuable which is missing as you are getting null issue here.
However Don't try to change the visibility of default as it will be done automatically after calling notifyDataSetChanged(). */
}
selectedPosition = position;
bodypartSelected = holder.tv_body_part_name.getText().toString();
holder.iv_bodypart.setVisibility(View.INVISIBLE);
holder.rl_left_right.setVisibility(View.VISIBLE);
//Keep this as last statement in onClick
notifyDataSetChanged();
}
});
//and other listeners below
}
Let me know your further response.
Based on #Hari N Jha's Answer.
Call notifyDataSetChanged() when you update anything. E.g
int selectedPosition = -1;
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
//....
if(position == selectedPosition) {
//Add background color change of your layout or as you want for selected item.
} else {
//Add background color change of your layout or as you want for default item.
}
notifyDataSetChanged(); //Call notifyDataSetChanged() here after done all the stufs
//...
}
In my Fragment, I have 2 views. A Textview and a RecyclerView. The Textview essentially displays the current size of the RecyclerView. So, when a row is removed in the adapter class, I need to update the TextView's value accordingly.
The row is removed successfully when removeBtn is clicked, but I need to update the TextView in the Fragment accordingly.
FRAGMENT
titleText.text = "SIZE (" + arrayStringList().size.toString() + ")"
//...
//...
//Set RecyclerView Adapter
mRecyclerView.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
val adapter = MRecyclerView(context!!, arrayStringList)
mRecyclerView.adapter = adapter
RECYCLERVIEW
holder.removeBtn{
mData.removeAt(position)
notifyItemRemoved(position)
}
Is there some sort of listener that I can put in my fragment to detect when the data changes? Or is there a way to send data from the recyclerview back to the fragment when removeBtn is clicked?
Create a interface in recyclerView class , and implement that interface in fragment . Once you click the remove button , call this interface . In Fragment class , update the text view with adapter.getItemCount.
In Adapter
interface ItemCallback{
void updateTextView();
}
This interface will be implemented in fragment class ,where you can update your textView with itemCount .
public class Fragment implements Adapter.ItemCallback{
#Override
public void updateTextView() {
tvTextView.setText(adapter.getItemCount()); // This will return the current item count of adapter
}
}
You can do this via call back. Make an interface and implement that on Fragment. And pass the reference of the interface to the adapter. When you trigger the event to delete item from recycler view call the interface method (That you implemented on Activity). And in that call back method, you need to set the value to your text view. To set value on TextView, you can call size() method and set the value whatever is returned from it. For example the List you are passing to the adapter is mData. When you get call back the call size method on the reference and set the value as mentioned below code.
titleText.setText(String.valueOf(mData.size())).
Hope this will help you.
NOTE: You need to call String.valueOf method because size method returns integer value so it needs to be cast.
"Or is there a way to send data from the recyclerview back to the fragment when removeBtn is clicked?" the answer is yes
One approach is to pass your adapter your own click listener (an interface), this way you can control the click from the fragment something like this,
A1) create an interface
public interface ItemTouchListener {
void onCardClick(View view, int position);
boolean onCardLongClick(View view, int position);
}
A2) implement this interface in your fragment
public class YourFragment extends Fragment implements ItemTouchListener
A3) implement the methods you created in your fragment
#Override
public void onCardClick(View view, int position) {
if (view.getId() == R.id.removeBtn){
//update your text
}else{
//other buttons
}
}
#Override
public boolean onCardLongClick(View view, int position) {
if (view.getId() == R.id.removeBtn){
//update your text
} else {
//other buttons
}
return true;
}
A4) create this interface in your adapter and set it to your button
private ItemTouchListener onItemTouchListener;
public YourAdapter(List<Object> list, ItemTouchListener onItemTouchListener) {
this.onItemTouchListener = onItemTouchListener;
this.list = list;
}
removeBtn = view.findViewById(R.id.removeBtn);
removeBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onItemTouchListener.onCardClick(v, getAdapterPosition());
}
});
removeBtn.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View view) {
onItemTouchListener.onCardLongClick(view, getAdapterPosition());
return true;
}
});
A5) and now when you create your adapter it will ask for the ItemTouchListener which you can give it by just passing 'this'
YourAdapter adapter = new YourAdapter(this);
A6) you may also want to give your adapter a method something like getCount which return the list size
public int getCount(){
list.size();
}
and then you can call this like myAdapter.getCount
I have a custom object array to show in recycler view and I'm using GridLayout manager for my recylerview and showing (2x4),(3x3) etc. squares.
When user click these squares it's color changed.
But when screen orientate, view is refreshing.
My question is how to keep this data (selected square's color ) when screen orientation ?
Create an interface:
public interface ClickCallback {
void onItemClicked(String id);
}
In your activity create a callback and pass it to your adapter like this:
ClickCallback callback = new ClickCallback() {
#Override
public void onItemClicked(String id) {
//your list item object must have a method to get color
color = yourDataList.get(id).getColor();
}
};
protected void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putInt("color", color);
}
YourAdapter adapter = new YourAdapter (yourDataList, callback)
In your adapter don't forget to set OnClickListener and call appropriate method, it could be like this:
public ListItemViewHolder(View itemView) {
super(itemView);
//your code
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
callback.onItemClicked(getAdapterPosition());
}
});
}
}
In OnCreate extract your color data:
public void onCreate(Bundle bundle) {
if (bundle != null) {
value = bundle.getInt("color");
}
}
You can store statuses of each square in a proper format and save them
on onSaveInstanceState and retrieve them on onCreate method and manipulate the adapter as for your requirement.
Read more...
Hope this helps!
"But when screen orientate, view is refreshing."
on screen rotation a new instance of the activity is created
You can have a ViewModel class which stores your arraylist of data including their current state. When activity is recreated via rotation this will be persisted and you will get the correct state.
Ref : https://medium.com/androiddevelopers/viewmodels-a-simple-example-ed5ac416317e
I got a RecyclerView and want to change the appearance of any clicked row. For that I have a callbackFunction in my Activity which I pass to the Adapter, which then is called inside the Adapter, as soon as I click on any row in the RecyclerView.
The clicked row is then changed, but it happens, that not only the clicked rows are changed but also other rows, that weren't clicked and were never clicked before. I checked the ArrayList that contains the data, but everything is fine there. Only the clicked elements contain the trigger to change the appearance of the row.
What is causing the other rows to change, although they have not been clicked?
Interface inside activity for callback
public interface onHeaderClickListener{
void onHeaderClicked(int index);
}
Inside RecyclerView Adapter
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof ViewHolderHeader){
((ViewHolderHeader)holder).dateHeaderTextView.setText( Integer.toString(((objClass_offerDateHeader) arrayList.get(position)).getDate()));
if(((objClass_offerDateHeader) arrayList.get(position)).isSelected()){
((ViewHolderHeader)holder).dateHeaderTextView.setBackgroundColor(Color.parseColor("#b642f4"));
}
((ViewHolderHeader)holder).dateHeaderTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
onHeaderClickListener.onHeaderClicked(position);
}
});
}
}
Adapter initialisation inside activity
customAdapterRecyclerViewAddOffersTo = new customAdapterRecyclerViewAddOffers(offerArrayList,"dragTo", new onHeaderClickListener() {
#Override
public void onHeaderClicked(int index) {
if (offerArrayList.get(index) instanceof objClass_offerDateHeader){
if(((objClass_offerDateHeader) offerArrayList.get(index)).isSelected()){
((objClass_offerDateHeader) offerArrayList.get(index)).setSelected(false);
}
else {
((objClass_offerDateHeader) offerArrayList.get(index)).setSelected(true);
}
customAdapterRecyclerViewAddOffersTo.notifyDataSetChanged();
}
}
});
In your onBindViewHolder method you have to set the background of the unselected cell, keep in mind the the cells are reused and you only set the background of selected cells so when it is reused the background is not returned to the normal color
So in code you will have to add an else condition
if(((objClass_offerDateHeader) arrayList.get(position)).isSelected()){
((ViewHolderHeader)holder).dateHeaderTextView.setBackgroundColor(Color.parseColor("#b642f4"));
} else {
((ViewHolderHeader)holder).dateHeaderTextView.setBackgroundColor(Color.parseColor("#FFFFFF")); // I assume you need it to be white you can change it to any other color
}
You need to add an else condition here:
if(((objClass_offerDateHeader) arrayList.get(position)).isSelected()){
((ViewHolderHeader)holder).dateHeaderTextView.setBackgroundColor(Color.parseColor("#b642f4"));
}
Viewholders get recycled, so you cannot be sure of the current state when onBindViewHolder is called.
I have a ViewHolder with an OnClickListener, which sends the click over an Interface to the underlying Activity. The only information i send is the AdapterPosition. In the Activity i then get the information out of the Activitie's reference to the ArrayList (which also fills the Adapter), using the position i get passed from the Adapter. I then use this to open a new Activity where i show this data.
Now, this works, but i just came to mind that this could cause problems, if i don't properly call notifyDataSetChanged/ItemInserted/Removed etc. and the Adapter shows a different List than the actualy up-to-date List in the Activity. I am right about this? Should i get the values out of the Objects IN the Adapter and pass them to the Activity or is my approach correct?
ViewHolder:
public class ExampleViewHolder extends RecyclerView.ViewHolder {
public ImageView mImageView;
public TextView mTextViewCreator;
public TextView mTextViewLikes;
public ExampleViewHolder(View itemView) {
super(itemView);
mImageView = itemView.findViewById(R.id.image_view);
mTextViewCreator = itemView.findViewById(R.id.text_view_creator);
mTextViewLikes = itemView.findViewById(R.id.text_view_likes);
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mListener != null) {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
mListener.onItemClick(position);
}
}
}
});
}
}
MainActivity OnItemClick implementation:
#Override
public void onItemClick(int position) {
Intent detailIntent = new Intent(this, DetailActivity.class);
ExampleItem clickedItem = mExampleList.get(position);
detailIntent.putExtra("imageUrl", clickedItem.getImageUrl());
detailIntent.putExtra("creatorName", clickedItem.getCreator());
detailIntent.putExtra("likeCount", clickedItem.getLikeCount());
startActivity(detailIntent);
}
The answer is simple: Always make sure that you notify the adapter about changes.
Otherwise its behavior becomes unpredictable.
Apart from that, your code seems to be fine
My personal preference is that you send the model to the activity, that's what it's interested in, it doesn't care about your adapter position, and even if in the future your list became an expandable list or even a ViewPager, all the Callback (Activity) wants is the model to start the new screen.
You can change your listener method to
onItemClick(ExampleItem item)
and then you can set your listener when onBindViewHolder is called.