RecyclerView adapter taking wrong values during scrolling - android

I have read several questions on stackoverflow addressing this problem but I could not solve my problem. The recyclerview shows webviews. My issue is that the adapter, initially, load the right values (below image):
but when I scroll down, and scroll up to first position again, wrong values are displayed in webview (below image):
I think this problem may originate from previously started loads into the webview, but I don't have any idea about handling this behavior. Here is related parts of my code:
/* in my notation, pg is equivalent to page.*/
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_ebook_list, container, false);
mEbookRecyclerView = (RecyclerView) view.findViewById(R.id.ebook_recycler_view);
mLayoutManager = new LinearLayoutManager(getActivity());
mEbookRecyclerView.setLayoutManager(mLayoutManager);
updateUI();
return view;
}
#Override
public void onResume() {
super.onResume();
updateUI();
}
private void updateUI() {
if (mAdapter == null) {
EbookLab ebookLab = EbookLab.get(getActivity());
List<String> pgs = ebookLab.getPgs();
mAdapter = new EbookAdapter(pgs);
mEbookRecyclerView.setAdapter(mAdapter);
}
}
/* ******codes for preparation of menus which I have ignored *******/
private class EbookHolder extends RecyclerView.ViewHolder {
private String mPg;
public EbookHolder(LayoutInflater inflater, ViewGroup container) {
super(inflater.inflate(R.layout.list_item_ebook, container, false));
mListWebView = (WebView) itemView.findViewById(R.id.list_web_View);
mListWebView.getSettings().setLoadWithOverviewMode(true);
}
public void bindEbook(String pg) {
mPg = pg;
mListWebView.loadDataWithBaseURL("file:///android_asset/", "Stack Over Flow " + String.valueOf(glbPos), "text/html", "UTF-8", null);
}
}
private class EbookAdapter extends RecyclerView.Adapter<EbookHolder> {
private List<String> mPgs;
public EbookAdapter(List<String> pgs) {
mPgs = pgs;
}
#Override
public EbookHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
return new EbookHolder(layoutInflater, parent);
}
#Override
public void onBindViewHolder(EbookHolder holder, int position) {
String pg = mPgs.get(position);
glbPos = position;//A global variable for position
holder.bindEbook(pg);
}
#Override
public int getItemCount() {
return mPgs.size();
}
public void setPgs(List<String> pgs) {
mPgs.clear();
mPgs.addAll(pgs);
notifyDataSetChanged();
}
}
Any helps is appreciated.
Update:In my code , I defined mListWebView as a global variable. But it should be defined inside private class EbookHolder extends RecyclerView.ViewHolder.
Then I removed bindEbook(String pg) method and directly updated mListWevView
inside public void onBindViewHolder(EbookHolder holder, int position):
holder.mListWebView.loadDataWithBaseURL("file:///android_asset/", "Stack Over Flow " + String.valueOf(glbPos), "text/html", "UTF-8", null);
My Question >> Does calling bindEbook(String pg) takes longer time compared to scrolling recyclerview or it is related to Android Architecture??

Please try few things:
1) put log after String pg = mPgs.get(position); and see, which pg value is after that line;
In bindEbook():
2) put log to see which pg value is incoming;
3) put mListWebView.clearHistory() or mListWebView.loadUrl("about:blank") before mListWebView.loadDataWithBaseURL() just to see is your webview really loading new page
Also - try to replace webview with simple textView - you will see, is problem in webview or in adapter.

I had a very similar problem which I solved after extensive debugging. I hope this can help you:
In my project, I am showing a horizontal RecyclerView within a vertical RecyclerView. The code can be seen here and here and I explained what I do on my website here.
I noticed that when scrolling down the outer (vertical) RecyclerView, the values of the inner (horizontal) RecyclerView where messed up. That is similar to your problem. Set some console log commands (Log.v) here and there in my inner RecyclerView adapter (called MeasurementsAdapter), one specifically in the inner view holder class, where the data is bound:
public class MeasurementsViewHolder extends RecyclerView.ViewHolder {
[code omitted here]
public MeasurementsViewHolder (View itemView) {
super(itemView);
[code omitted here]
}
public void bindMeasurement (String name, double value, String unit, int color) {
mNameView.setText(name);
mValueView.setText(String.valueOf(Math.round(value)));
mUnitsView.setText(unit);
mBoxLayout.setBackgroundColor(color);
// Log.v(TAG, name + " " + value); <-- here I was logging
}
I was observing that when I was scrolling at some point this "bindMeasurement" method was no longer executed.
My guess is that RecyclerView was "recycling" some random data. This is not what I wanted and my result looked like yours' very much.
So, after I read the notifyDataSetChanged-method in the RecyclerView documentation. In the adapter of my outer RecyclerView (called Stationsadapter) I modified the binding method in the inner ViewHolder class and execute the adapter.notifyDataHasChanged method there (see line 84 in 1)
public class StationsViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {public TextView mStationInfoView;
private MeasurementsAdapter mMeasurementsAdapter;
/**
* Constructor of inner class StationsViewHolder
* #param itemView
*/
public StationsViewHolder(View itemView) {
super(itemView);
[code omitted here]
}
/**
* Bind weather station to view
* #param station is a class containing data of one weather station
*/
public void bindStation (Station station) {
[code omitted here]
mMeasurementsAdapter.notifyDataSetChanged();
}
Now my RecyclerViews show the right data. I very much hope this helps you and others.
Cheers
Ben

Related

How to build an Arraylist of objects from toggle OnClickListeners inside RecyclerView Adapter's items

I'm building an Android app of media, and trying to add a Playlist feature to it, the user will be able to create a playlist of his own and modify it.
I'm using a RecyclerView to show the user list of songs which he can choose from.
The problem is I don't understand how to pass the Arraylist of chosen songs from the adapter to the fragment.
I've tried to use the Observer pattern but the don't know how to use that information.
This is my Fragment for creating the playlist:
public class CreatePlaylistFragment extends Fragment implements PlaylistAdapterInterface {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_create_playlist, container, false);
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ArrayList<ProgramsData> dataArrayList = ProgramsReceiver.getPrograms();
ArrayList<ProgramsData> sortedList = new ArrayList<>(dataArrayList);
adapter = new CreatePlaylistAdapter(dataArrayList, view.getContext(), this);
adapter.adapterInterface = this;
ivCreatePlaylist.setOnClickListener(v -> {
Toast.makeText(v.getContext(), "Creating Playlist!", Toast.LENGTH_SHORT).show();
new PlaylistsJsonWriter(playlistArrayList,getContext()).execute();
});
}
#Override
public void OnItemClicked(ArrayList<ProgramsData> programs) {
programsToCreate = programs;
String s = etListName.getText().toString();
playlistArrayList.add(new Playlist(s, programsToCreate));
}
}
This is the Recycler Adapter with ViewHolder as inner class:
public class CreatePlaylistAdapter extends RecyclerView.Adapter<CreatePlaylistViewHolder> {
List<ProgramsData> programsDataList;
Context context;
public PlaylistAdapterInterface adapterInterface = null;
public CreatePlaylistAdapter(List<ProgramsData> programsDataList, Context context , PlaylistAdapterInterface adapterInterface) {
this.programsDataList = programsDataList;
this.context = context;
this.adapterInterface = adapterInterface;
}
#NonNull
#Override
public CreatePlaylistViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.chose_program_to_playlist_item, viewGroup, false);
return new CreatePlaylistViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CreatePlaylistViewHolder holder, int i) {
ProgramsData programsData = programsDataList.get(i);
holder.tvProgramName.setText(programsData.getProgramName());
if (programsData.getStudentName() != null)
holder.tvStudentName.setText(programsData.getStudentName());
else holder.tvLine.setText(""); //if there is no student the line won't be printed
holder.ivProfilePic.setImageResource(programsData.getProfilePic());
holder.programsData = programsData;
// holder.mAdapterInterface = adapterInterface;
adapterInterface.OnItemClicked(holder.programs);
}
#Override
public int getItemCount() {
return programsDataList.size();
}
}
class CreatePlaylistViewHolder extends RecyclerView.ViewHolder {
TextView tvProgramName;
TextView tvStudentName;
TextView tvLine;
CircleImageView ivProfilePic;
ToggleButton tbCheck;
ProgramsData programsData;
ArrayList<ProgramsData> programs;
PlaylistAdapterInterface mAdapterInterface;
public CreatePlaylistViewHolder(#NonNull View itemView) {
super(itemView);
tvProgramName = itemView.findViewById(R.id.tvProgramName);
tvStudentName = itemView.findViewById(R.id.tvStudentName);
ivProfilePic = itemView.findViewById(R.id.ivProfilePic);
tvLine = itemView.findViewById(R.id.tvLine);
tbCheck = itemView.findViewById(R.id.tbCheck);
programs= new ArrayList<>();
tbCheck.setOnClickListener(v -> {
if (tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_radio_button_checked);
programs.add(programsData);
} else if (!tbCheck.isChecked()) {
tbCheck.setBackgroundResource(R.drawable.ic_check);
programs.remove(programsData);
}
});
}
}
And this is the interface for the Observer Pattern:
public interface PlaylistAdapterInterface {
void OnItemClicked(ArrayList<ProgramsData> programs);
}
I know it's a lot of code, but I just don't understand how to pass the data from the adapter back to the fragment...
I don't understand exactly what are you trying to do.
The code contains several errors that I'll try to explain.
A clear error that you have made stays in onBindViewholder where you call the listener at the creation of every item instead than after clicking on it.
You have simply add an onClickListener in the viewHolder.getItemView() or in a specific view of the viewholder and then perform the operation you need to do once an item is clicked.
If you set a listener inside onBindViewHolder, you also have a method called
holder.getAdapterPosition() that you can use to understand which item are you clicking on.
The viewholder should be used only to setup the views accordingly to the data you are binding and nothing else. For this reason, you should not pass any object or listener to it and instead use the approach above.
If you have just to retrieve the selected songs after an user confirms it's playlist you can just add a public method on your adapter
public List<ProgramsData> getSelectedSongs()
that you can call from your fragment when an user click a confirm button.
In order to have a list of all selected song, you can have another list
ArrayList<ProgramsData> selectedPrograms;
that you are going to fill after the click.
The content of the listener inside the onBindViewHolder could be
ProgramsData currentProgram = programs.get(holder.getAdapterPosition());
if(selectedPrograms.contains(currentProgram){
selectedPrograms.remove(currentProgram);
}else{
selectedPrograms.add(currentProgram);
}
notifyItemChanged(holder.getAdapterPosition); //You can use this to update the view of the selected item
Then inside the onBindViewHolderMethod you can check whether the items you are binding are part of the selectedList and update the views accordingly.
You can use callback method. Maintain list of selected items in array list and send back to fragment when done button is clicked or any other button you have placed for complete action.
Follow these steps
-Create an Interface with list parameter.
-Fragment should implement this interface.
-Then when you initialize Recyclerview adapter pass this interface object.
-When done is clicked call overridden method of this interface and send selected songs list as argument.

Saving SwitchCompat State in the recycler view while scrolling

I have a recyclerview which its items contain textView and switchCompat. And in the same activity I have also a textView that have a numerical value in it. The task is when the switchCompat turned on the text view above the recyclerview which contain the numerical value should increase by the value in the recyclerview item textview. I already did that but when scrolling in the recyclerview the switchCompat back to the default state and the value of the numerical textview backs to its old value,
Any help with that?
I Apology for not being able to post a part of the code now and I'll do this as soon as i can, I just posted it now in case anyone pass through something like this before
Thank you
The key to a recycler view or any adapter view in Android is to have the adapter adapt your models to the view. In your case your view is a TextView plus a Switch, so your adapter must adapt some model to this view. In this case I'd choose a simple model like this:
class ItemModel {
String text;
boolean on;
}
I've omitted getters and setters for simplicity
This model contains an string text which reflects the text in your text view and a boolean on that reflects the state of the switch. When true the switch is checked and when false it's unchecked.
There's tons of ways to represent this model. I've chosen this one, you may choose a different one. The point is, you need to save the state somewhere and this is what I mean by model - the view model.
Now let's build an adapter that can do 2 things - Update the models when the switch is clicked and tell the activity that the switch changed state. Here's one way to do this:
public class ItemsAdapter extends
RecyclerView.Adapter<ItemsAdapter.ViewHolder> {
#NonNull
private final List<ItemModel> itemModels;
#Nullable
private OnItemCheckedChangeListener onItemCheckedChangeListener;
ItemsAdapter(#NonNull List<ItemModel> itemModels) {
this.itemModels = itemModels;
}
#NonNull
#Override
public ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
return new ViewHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item, parent, false));
}
#Override
public void onBindViewHolder(#NonNull final ViewHolder holder, int position) {
ItemModel item = itemModels.get(position);
holder.text.setText(item.text);
holder.switchCompat.setChecked(item.on);
// Make sure we update the model if the user taps the switch
holder.switchCompat.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int adapterPosition = holder.getAdapterPosition();
ItemModel tapped = itemModels.get(adapterPosition);
itemModels.set(adapterPosition, new ItemModel(tapped.text, isChecked));
if (onItemCheckedChangeListener != null) {
onItemCheckedChangeListener.onItemCheckedChanged(adapterPosition, isChecked);
}
}
});
}
#Override
public void onViewRecycled(#NonNull ViewHolder holder) {
super.onViewRecycled(holder);
holder.switchCompat.setOnCheckedChangeListener(null);
}
#Override
public int getItemCount() {
return itemModels.size();
}
public void setOnItemCheckedChangeListener(#Nullable OnItemCheckedChangeListener onItemCheckedChangeListener) {
this.onItemCheckedChangeListener = onItemCheckedChangeListener;
}
interface OnItemCheckedChangeListener {
/**
* Fired when the item check state is changed
*/
void onItemCheckedChanged(int position, boolean isChecked);
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView text;
SwitchCompat switchCompat;
ViewHolder(View itemView) {
super(itemView);
text = itemView.findViewById(R.id.item_text);
switchCompat = itemView.findViewById(R.id.item_switch);
}
}
}
There's a lot to digest, but let's focus on the important bits - the method onBindViewHolder. The first 3 lines are the classic recycling of the view. We grab the model at the correct position and set the elements in the view that correspond to model's attributes.
Then it gets more interesting. We set a OnCheckedChangeListener to update the model and the activity every time the switch changes state. The first 3 lines change the model in the adapter and the rest uses the custom interface OnItemCheckedChangeListener to notify the listener about the switch change. It's important to notice that inside the method OnCheckedChangeListener you should no longer use position, but rather use holder.getAdapterPosition. This will give you the correct position in the adapter's data list.
Since now the adapter has always the correct models inside the data list, every time the method onBindViewHolder is called the adapter knows exactly how to setup the view. This means that while scrolling and recycling the views, it will preserve the state of each item within the models inside the data list.
It's important to remove the OnCheckedChangeListener when the view gets recycled - onViewRecycled. This avoids messing the count when the adapter is setting the value of switchCompat in the onBindViewHolder.
Here's an example of how the activity could look like:
public class MainActivity extends AppCompatActivity {
private int count = 0;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
List<ItemModel> data = new ArrayList<>();
for (int i = 1; i <= 100; i++)
data.add(new ItemModel("Item " + i, false));
ItemsAdapter adapter = new ItemsAdapter(data);
((RecyclerView) findViewById(R.id.recyclerview)).setAdapter(adapter);
final TextView countTextView = findViewById(R.id.count);
drawCount(countTextView);
adapter.setOnItemCheckedChangeListener(new ItemsAdapter.OnItemCheckedChangeListener() {
#Override
public void onItemCheckedChanged(int position, boolean isChecked) {
if (isChecked)
count++;
else
count--;
drawCount(countTextView);
}
});
}
private void drawCount(TextView countTextView) {
countTextView.setText(String.valueOf(count));
}
}
This code is meant to demonstrate the idea, not to follow :) In any case, we setup all the initial state and then set up the custom listener OnItemCheckedChangeListener to update the text view in the activity.
The layout files shouldn't be relevant here, but as you can imagine the activity has a text view with id count and there's a recycler view with the id recyclerview.
Hope this helps
It solved for me after adding the below method to the adapter:
#Override
public int getItemViewType(int position) {
return position;
}

Recyclerview not reloading data when notifyDataSetChanged() is called

I am using recycler view in my application. I am initialising the adapter and recycler view inside onActivity Created as given below,
adapter = new CardsRecyclerAdapterInternal(WalletStoreFragment.this);
cardSummaryList = new ArrayList<LoyaltyCardSummary>();
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setHasFixedSize(true);
Now I am calling an API to load data for the recycler view. Here is my hierarchy
Fragment --> Fragment presenter --> API Layer
So my fragment invokes a method in presenter which in turns calls the API. The API returns data back to the presenter. I am using EventBus for that.
The fragment implements an interface given below,
public interface StoreView
{
void reloadCardList (List<CardSummary> cards);
void dismissCardLoad();
}
Presenter calls the interface method to reload the recycler view data, using the code given below.
#Override
public void reloadCardList(final List<CardSummary> cards)
{
cardSummaryList.clear();
cardSummaryList.addAll(cards);
adapter.notifyDataSetChanged();
recyclerView.invalidate();
}
Here is the code for adapter
public class CardsRecyclerAdapterInternal extends
RecyclerView.Adapter<CardsRecyclerAdapterInternal.ViewHolder>
{
private Fragment frag;
public CardsRecyclerAdapterInternal()
{
//this.frag = _frag;
}
public void setCardSummaryList(List<CardSummary> cards)
{
//this.cardSummaryList = cards;
}
#Override
public CardsRecyclerAdapterInternal.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
// Inflate the custom layout
View view = inflater.inflate(R.layout.fragment_wallet_store_item, parent, false);
// Return a new holder instance
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
}
#Override
public void onBindViewHolder(CardsRecyclerAdapterInternal.ViewHolder holder, int position) {
CardSummary card = cardSummaryList.get(position);
holder.txt_CardName.setText(card.getCardName());
holder.txt_CardCategory.setText(card.getCardCategory());
String cardURL = Global.CARD_IMAGE + Uri.encode(card.getCardImage());
Glide
.with(WalletStoreFragment.this)
.load(cardURL)
.into(holder.cardImage);
}
#Override
public int getItemCount() {
return cardSummaryList.size();
}
public class ViewHolder extends RecyclerView.ViewHolder {
TextView txt_CardName;
TextView txt_CardCategory;
ImageView cardImage;
public ViewHolder(View convertView) {
super(convertView);
txt_CardName = (TextView)convertView.findViewById(R.id.txt_CardName);
txt_CardCategory = (TextView)convertView.findViewById(R.id.txt_CardCategory);
cardImage = (ImageView)convertView.findViewById(R.id.img_CardImage);
}
}
}
But this is not reloading the data in recycler view. Not even calling the getItemCount() inside the recycler view. Whats going wrong here?
****************************Update************************
I have more inputs to my issue. I am having a tabbed view pager aplication, and the recycler view is loaded from one of the tabs when a button is clicked. If I directly load the recycler view fragment in main TabPage everything works fine, but when its inside another view the recycler is not reloading. Here is how I load the new fragment,
FragmentManager manager = getFragmentManager();
Fragment fragment = new StoreFragment();
manager.beginTransaction()
.add(R.id.container, fragment)
.addToBackStack("STORE_FRAGMENT")
.commit();
and here is my container within the XML file,
<FrameLayout android:id="#+id/container" android:layout_width="match_parent"
android:layout_height="match_parent"/>
Thanks
The variable (List) cardSummaryListis not declared anywhere in the Adapter class. So the cardSummaryList.size(); won't return anything and if at all it should raise NullPointerException.
You need change your Adapter code by: including a local variable List<CardSummary> cardSummaryList; and then create a constructor that takes a List as one of its argument -
private List<CardSummary> cardSummaryList;
....
public CardsRecyclerAdapterInternal(List<CardSummary> cardsList)
{
cardSummaryList = cardsList;
}
you should also add a method that updates your list (call it something like reloadCards(List<CardSummary> cards) and then the body of this method is something like:
public void reloadCards(List<CardSummary> cards){
cardSummaryList.clear();
cardSummaryList.addAll(cards);
}
Finally, you then change your reloadCardList(final List<CardSummary> cards) code into:
public void reloadCardList(final List<CardSummary> cards)
{
adapter.reloadCards(cards);
adapter.notifyDataSetChanged();
recyclerView.invalidate();
}
Please give it a try and let me know if this helps you resolve your problem.
By the way, please look at an example of how to use RecyclerView - you can also look at the tutorial here on Creating Lists and Cards

How to change an image in a ListView, when the image is clicked?

EDIT: I've solved this issue, if interested, please take a look at my answer to see how I did it!
I am currently working in Android Studio. I have a ListView that I populate with several items. Within each of these items is an ImageButton that has a "+" as the image. What I want to do is, whenever that image is clicked (not the entire ListView item, just the image), I want that image of "+" to become another image. Any help would be appreciated, as this has been an ongoing issue for a while!
Here is the current code that I attempt to use to achieve this:
final ImageButton movieSeen = (ImageButton convertView.findViewById(R.id.movieWatched);
movieSeen.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
movieSeen.setImageResource(R.drawable.ic_check_circle_black_24dp);
}
});
Currently this does update the image that I click correctly, BUT it also updates images that are not yet rendered on the screen, so when I scroll the list view down, other objects are also changed to ic_check_circle_black_24dp.
What I want is pretty straightforward, I just don't know how to achieve it. I just want to click an ImageButton, that's inside an item on a ListView, and have that ImageButton change its image resource.
Here is my custom array adapter as requested!
private class MovieScrollAdapter extends ArrayAdapter<Movie> {//custom array adapter
private Context context;
private List<Movie> movies;
public MovieScrollAdapter(Context context, List<Movie> movies){
super(context, -1, movies);
this.context = context;
this.movies = movies;
if(this.movies.isEmpty()){//if no results were returned after all processing, display a toast letting the user know
Toast.makeText(getApplicationContext(), R.string.no_matches, Toast.LENGTH_SHORT).show();
}
}
#Override
public View getView(final int position, View convertView, ViewGroup parent){
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = inflater.inflate(R.layout.movie_layout, parent, false);
}
TextView title = (TextView) convertView.findViewById(R.id.title);
title.setText(movies.get(position).getTitle());
TextView plot = (TextView) convertView.findViewById(R.id.plot);
plot.setText(movies.get(position).getPlot());
TextView genre = (TextView) convertView.findViewById(R.id.genre);
genre.setText(movies.get(position).getGenre());
TextView metaScore = (TextView) convertView.findViewById(R.id.metascore);
if(movies.get(position).getMetaScore() == -1){//if the metaScore is set to -1, that means movie has not been rated, which by inference means it is not yet released
metaScore.setText(R.string.movie_not_released);
metaScore.setTextSize(TypedValue.COMPLEX_UNIT_SP, 9.5f);//smaller text so it fits without breaking anything
metaScore.setTextColor(getColor(R.color.colorAccent));
} else {
metaScore.setText(" " + Integer.valueOf(movies.get(position).getMetaScore()).toString() + " ");//using white space for minor formatting, instead of altering margins each time this is rendered
metaScore.setTextSize(TypedValue.COMPLEX_UNIT_SP, 25);
//setting up a "highlighted" background to achieve metacritic square effect
Spannable spanText = Spannable.Factory.getInstance().newSpannable(metaScore.getText());
spanText.setSpan(new BackgroundColorSpan(getColor(R.color.metaScore)), 3, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
metaScore.setText(spanText);
metaScore.setTextColor(getColor(android.R.color.primary_text_dark));
}
ImageView image = (ImageView) convertView.findViewById(R.id.imageView);
new ImageDownloadTask((ImageView)image).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, movies.get(position).getPosterURL());//because there are several images to load here, we let these threads run parallel
title.setOnClickListener(new View.OnClickListener() {//setting up a simple onClickListener that will open a link leading to more info about the movie in question!
#Override
public void onClick(View v) {
Uri uri = Uri.parse(movies.get(position).getMovieURL());
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
}
});
final ImageButton movieSeen = (ImageButton) convertView.findViewById(R.id.movieWatched);
movieSeen.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
movieSeen.setImageResource(R.drawable.ic_check_circle_black_24dp);
}
});
return convertView;
}
}
The problem is on a ListView, the views are being reused to save memory and avoid creating a lot of views, so when you change a view it keeps the state while it's being reused to show another item.
For example, you have 100 elements, you touch the first element ImageButton and that button is changed. Maybe on the screen there are 5 elements of the list showing, and you changed the first one. But if you scroll to the element number 15 the system is not creating 15 views, is taking the first one you clicked before and is changing the content.
So, you are expecting to have a view with a "+" ImageButton icon but you see another icon, that's because you must keep the view state inside a model object and set the state every time 'getView' is called.
Post your list adapter to see how is implemented.
UPDATE:
Now I see your adapter implementation I suggest you to add an int field inside Movie class to save the resource id you want to show on the ImageButton. Then inside the onClickListener you must set to this field the resource you want to show on the view when its clicked, and call notifyDataSetChanged(). After that you must do inside getView():
movieSeen.setImageResource(movies.get(position).getButtonImageResource());
Use RecyclerView and set the OnItemClickListener on your ImageButton within your view holder.
This already answered question should help.
The adapted code below is coming from this nice tutorial. Using ReciclerView with an adapter like this will solve your concern.
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ArrayList<String> mDataset;
public class ViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public TextView txtHeader;
public ViewHolder(View v) {
super(v);
txtHeader = (TextView) v.findViewById(R.id.xxx);
imageView = (ImageView) v.findViewById(R.id.yyy);
}
}
public MyAdapter(ArrayList<String> myDataset) {
mDataset = myDataset;
}
#Override
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.rowlayout, parent, false);
ViewHolder vh = new ViewHolder(v);
return vh;
}
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
final String name = mDataset.get(position);
holder.txtHeader.setText(mDataset.get(position));
holder.imageView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// Do here what you need to change the image content
}
});
holder.itemView.setBackground(....); // Initialize your image content here...
}
//...
}
Here is my suggestion to achieve what you want :
Create An Interface in your adapter :
public interface YourInterface{
void selectedImage(int position,ImageView iamgeView);
}
Create variable interface in your adapter that you just created :
private YourInterface yourInterface;
and make your adapter constructor like this :
public YourAdapterConstructor(YourInterface yourInterface){
this.yourInterface = yourInterface;
}
in your ImageView onClickListener :
final ImageButton movieSeen = (ImageButton) convertView.findViewById(R.id.movieWatched);
movieSeen.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
yourInterface.selectedImage(position, imageView);
}
});
and then finally in your class activity, Implements YourInterface and change you ImageView there :
#Override
public void selectedImage(final int position,final ImageView imageView) {
//change your image view here
}
I'd like to thank everyone for their support. Unfortunately, with the way my code is written (rather messily and without much regard for what my professors taught me), I was unable to get most of these solutions to work. I did however, find a solution that falls in line with my own framework that I've had going into this. Unfortunately I could not redo my entire adapter method, or implement various interfaces that would cause me to have to rewrite a huge chunk of code for something seemingly trivial.
So, if anyone finds themselves in this situation in the future, here is my solution:
In the Movie class, I add a boolean value that represents my values, along with some getters and setters:
private boolean watchedStatus;
public boolean hasSeen() {return watchedStatus;}
public void toggleWatchedStatus(){
watchedStatus = !watchedStatus;
}
In the getView method, I simply get a reference to the ImageButton, and then based on the boolean value returned by "hasSeen," I set the ImageResource to one of two states:
#Override
public View getView(final int position, View convertView, ViewGroup parent){
ImageButton movieSeen = (ImageButton) convertView.findViewById(R.id.movieSeen);
if(movies.get(position).hasSeen()){
movieSeen.setImageResource(R.drawable.ic_check_circle_black_24dp);
} else {
movieSeen.setImageResource(R.drawable.ic_add_circle_black_24dp);
}
}
Next, I override the OnClickListener, and make it so that whenever the button is clicked, the boolean value in the Movie.java class is toggled. The key here was using the ArrayAdapter's method "notifyDataSetChanged()" This completes the process, and lets the ListView know that it should update itself:
final ImageButton movieSeenForClick = (ImageButton) convertView.findViewById(R.id.movieSeen);
movieSeen.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//movieSeenForClick.setImageResource(R.drawable.ic_check_circle_black_24dp);
movies.get(position).toggleWatchedStatus();
System.out.println(movies.get(position).hasSeen() + " ------- position: " + position);
notifyDataSetChanged();
}
});
Thanks again for the time taken to provide information, a lot of it really did steer me int he right direction, I just had to use the information correctly with the way my code was structured.

How to handle card's button OnClick event in GridView? Best practice

I'm trying to find the best solution to handle OnClick event, which generates by my card's button (see the picture bellow) within GridView.
So as you can see, I have just a normal GridView with cells made of my custom Card.
I just initialize GridView and it's adapter:
mGrid = (GridView) findViewById(R.id.grid);
mAdapter = new ImageTopicsAdapter(..blah blah blah..);
mGrid.setAdapter(mAdapter);
As you probably know I can easily handle OnClick events generated by GridView. But it will work only if I click on the card itself:
mGrid.setOnItemClickListener(..blah blah blah..);
I want to build something similar to this (see code bellow), so I can easily "implement" my Activity to handle my card's button OnClick event:
mGrid.setOnItemButtonClickListener(..blah blah blah..);
What is the best (clean\easy\elegant) way to do this?
Any help is truly appreciated. Alex. P.S. Sorry for my English:)
Since you want to dispatch to your activity, I would recommend exposing a method in the activity and call it directly from your click listener. The shortest (and cleanest from my perspective):
in your Adapter, say ArrayAdapter
define to listen for clicks (to avoid multitude of anonymous listener instances)
dispatch a call directly to your activity (since every view context is an activity)
context above can be treated as your ApplicationActivity only if you didn't manually provide some other context, say application context
private final MyAdapter extends ArrayAdapter implements View.OnClickListener {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// inflate your card then get a reference to your button
View card = ....;
card.findViewById(R.id.YOUR_BUTTON_ID).setOnClickListener(this);
return card;
}
#Override
public void onClick(View view) {
ApplicationActivity activity = (ApplicationActivity) view.getContext();
if (activity != null && !activity.isFinishing()) {
applicationActivity.onCardButtonClick();
}
}
}
// in your ApplicationActivity
public final class ApplicationActivity extends Activity {
...
public void onCardButtonClick() {
// deal with your click
}
}
There are other, textbook options (setting a listener, or activity in your view creation and so forth) but I avoid them since they don't solve absolutely anything.
They just add more dust in your code.
Any View context defined properly points to the activity (since it is a context too) which holds all view structure. This way you can access your activity quick and relatively easy.
BTW Event bus is not a good option since event buses are great for one-to-many relations (one dispatcher, many listeners) but add more complexity when used intensively for one-to-one calls (dispatcher-listener)
Addition for the comment
You can tweak a little the code and rather using the adapter, you can dispatch directly from your cell. In other words rather using the adapter as a delegate, create an anonymous listener and then reach and call the activity directly from your card button click:
public final MyAdapter extends ArrayAdapter {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
// inflate your card then get a reference to your button
View card = ....;
card.findViewById(R.id.YOUR_BUTTON_ID).setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
ApplicationActivity activity = (ApplicationActivity) view.getContext();
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
applicationActivity.onCardButtonClick();
}
}
});
return card;
}
}
Addition for the comment - Compound View
To encapsulate all cell logic, you can create a custom view from scratch or use a compound view. The example below is using a compound view:
public class ApplicationActivity extends Activity {
....
public void onCardButtonClick(Cell cell) {
// do whatever you want with the model/view
}
}
// ViewModel instances are used in your adapter
public final class ViewModel {
public final String description;
public final String title;
public ViewModel(String title, String description) {
this.title = title != null ? title.trim() : "";
this.description = description != null ? description.trim() : "";
}
}
public final class Cell extends LinearLayout {
private View button;
private ViewModel model;
// ViewModel is data model and is the list of items in your adapter
public void update(ViewModel model) {
this.model = model;
// update your card with your model
}
public ViewModel getModel() {
return model;
}
#Override
protected void onAttachedToWindow() {
button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener {
#Override
public void onClick(View view) {
ApplicationActivity activity = (ApplicationActivity) view.getContext();
if (model != null && activity != null && !activity.isFinishing() && !activity.isDestroyed() {
activity.onCardButtonClick(Cell.this);
}
}
});
}
}
// then your adapter `getView()` needs to inflate/create your compound view and return it
public final MyAdapter extends ArrayAdapter {
private final List<ViewModel> items;
public MyAdapter() {
// update your models from outside or create on the fly, etc.
this.items = ...;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// inflate - say it is a layout file 'cell.xml'
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.cell);
}
((Cell) convertView).update(items.get(position));
return convertView;
}
}
Adapter should handle this. Generally your Adapter should have method like setOnOptionsClickListener(OnOptionsClickListener listener) assuming that we are talking about ellipsis button.
So in your Activity/Fragment you use following code
public interface OnOptionsClickListener {
void onOptionsClicked(View view, PictureItem item);
}
mAdapter= new MyGridAdapter();
mAdapter.setOnOptionsClickListener(new OnOptionsClickListener() {
public void onClick(View view, PictureItem item) {
//process click
}
});
And following inside Adapter
public void setOnOptionsClickListener(OnOptionsClickListener l) {
mOnOptionsClickListener = l;
}
findViewById(R.id.btn_options).setOnClickListener(new OnClickListener(){
public void OnClick(View view) {
mOnOptionsClickListener.onOptionsClicked(view, currentPictureItem);
}
});
Please notice. You need to declare interface only if you need to have extra parameters in OnClick() method (for example currentPictureItem to get image url or item id). Otherwise, you can use just OnClickListener.
Edit
So here is explanation. Adapter serves like a View-provider for your GridView. It creates views and it configure it basic state. That's why all click listeners should be set in Adapter during views initializing. Moreover, we don't want to have a messy Activity with nested Adapter, but we want to have Adapter as a separate class. This is the reason you will usually need to create additional interface in order to have an access to currentItem object to extract data from.
Looks like nobody knows how to do this. So I found solution myself with help of #Dimitar G. and #Konstantin Kiriushyn. Thank you, guys.
1) I will create my own custom CardView using Compound View system, which will be pretty simple: LinearLayout + ImageView + TextView + Button.
public class TopicCardView extends LinearLayout {
private ImageView mImage;
private Button mButtonMenu;
private TextView mTitle;
public TopicCardView (Context context) {
initializeViews(context);
}
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.topic_card_view, this);
}
private void setTitle(...) {
...
}
private void setImage(...) {
...
}
private void setMenuClickListener(...) {
...
}
// and so on...
}
2) Then I will create method called createListOfGridCardsFromDB(...) in Activity\Fragment. It will generate list (LinkedList) of my custom CardViews (and it will also set titles\images and listeners to CardViews).
3) And then I will pass this generated LinkedList of my CardViews to GridViewAdapter.
This system makes able to use only one Adapter for all my card-grids in app. It also makes able to do nothing with clicks, interfaces, listeners and stuff in Adapter.

Categories

Resources