I have a recycler view that uses LinearSnapHelper to snap the items as the user scrolls it. Now, I'd like to listen to the snaps, preferably getting the index of the item that was snapped. However, I can't really figure out if there's a way to do that.
Initially I thought that the LinearSnapHelper's findTargetSnapPosition() would return the index to snap (as the documentation says), however this isn't true. It randomly returns -1 or 0 for the first item, and when the list is scrolled, it gets randomly invoked. Sometimes, the method's not called at all; sometimes, the index is incorrect, and sometimes it's correct. It seems that it's no use trying to find the index using this.
So: How would I find out which item the recycler view snaps to?
I managed to get this working. I'm not sure if it's the best solution, but here's what I did:
private int selectedPosition = -1;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
LinearSnapHelper snapHelper = new LinearSnapHelper() {
#Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
View view = super.findSnapView(layoutManager);
if (view != null) {
final int newPosition = layoutManager.getPosition(view);
if (newPosition != selectedPosition && recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_IDLE) {
onViewSnapped(newPosition);
selectedPosition = newPosition;
}
}
return view;
}
};
...
}
private void onViewSnapped(int index) {
// YOUR CODE HERE
}
Related
Edit #1: Through debugging I've discovered that the bug 'disappears'. Basically I set a breakpoint and slowly go through steps of checking each multiChoiceItem and the heights of the other RecyclerView child items do not change. Does this mean it is a drawing/timing related issue?
Edit #2: Also, a new find, if I change the height of Child: 6 it changes for Child: 3 and Child: 0
I apologize for the long question. I've checked other answers regarding the same problem and none of them apply. I've tried solving this myself and just couldn't so I would love some help. If there is anything I can do to make this easier to read, please let me know and I'll get right on it!
With the way my code is written, this technically should be impossible to happen but yet here it is.
The Problem: I have an onClickListener() for a TextView within a RecyclerView item. The onClickListener() calls a multiChoiceItem AlertDialog in the container class of the RecyclerAdapter which then calls notifyDataSet(), after completed, with an addOnLayoutChangeListener() at the end which measures the height after the new RecyclerView is drawn.
Notifying that the data set ended then causes the TextView within the RecyclerView item to change to show the text of each Checked item. Then this height is measured in the addOnLayoutChangeListener() and sent to a ViewModel which measures the height of the same position item of three fragments and sets the items height to the max height so they all look the same height.
The Confusing Part: This problem only occurs for one of the three fragments AND the other effected item heights do not match the other two fragments. Which tells me that this is localized to one fragment (which has its own class)
The Code:
The code is long so I reduced it to what I think was important
The ViewHolder
class TextViewViewHolder extends RecyclerView.ViewHolder {
TextView vhTVTextView;
TextView vhTVMainTextView;
CardView vhTVCardView;
TextViewClickedListener vhTextViewClickedListener;
// Gets current position from 'onBindViewHolder'
int vhPosition = 0;
public TextViewViewHolder(View itemView, TextViewClickedListener textViewClickedListener) {
super(itemView);
this.vhTextViewClickedListener = textViewClickedListener;
this.vhTVCardView = itemView.findViewById(R.id.thoughtCard);
this.vhTVTextView = itemView.findViewById(R.id.thoughtNumber);
this.vhTVMainTextView = itemView.findViewById(R.id.textEntry);
/*
When the main TextView is clicked, it calls a function in the container
'FragTextView' which pops up an AlertDialog. It was chosen to do it in the
container instead of here because the Adapter is so adapt the lists data to the view
and the container is what dictates what the lists data actually is.
*/
vhTVMainTextView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(vhTextViewClickedListener != null) {
vhTextViewClickedListener.onTextViewClicked(vhPosition);
}
}
});
}
}
onBindViewHolder
#Override
public int getItemViewType(int position) {
/*
If mThoughtEntries is not null, then that means we can find the ViewType we are working
with inside of it. Otherwise, we are mDistortions and we must be working on TYPE_TEXTVIEW
*/
if(mThoughtEntries != null) return mThoughtEntries.get(position).getViewType();
else return Constants.TYPE_TEXTVIEW;
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
int adapterPosition = holder.getAdapterPosition();
switch (holder.getItemViewType()) {
case Constants.TYPE_EDITTEXT:
EditTextViewHolder editTextViewHolder = (EditTextViewHolder)holder;
// update MyCustomEditTextListener every time we bind a new item
// so that it knows what item in mDataset to update
editTextViewHolder.mMyCustomEditTextListener.setTWPosition(holder.getAdapterPosition());
//Displaying list item to its correct position
editTextViewHolder.vhETTextView.setText(String.valueOf(adapterPosition + 1));
editTextViewHolder.vhETEditText.setText(mThoughtEntries.get(adapterPosition).getThought());
break;
case Constants.TYPE_TEXTVIEW:
TextViewViewHolder textViewViewHolder = (TextViewViewHolder)holder;
// Send current position to viewHolder so when the text listener is called, it knows
// exactly which position of the Distortions list to change
textViewViewHolder.vhPosition = adapterPosition;
//Displaying list item to its correct position
textViewViewHolder.vhTVTextView.setText(String.valueOf(adapterPosition + 1));
textViewViewHolder.vhTVMainTextView.setText(distortionsToString(mDistortions.get(adapterPosition)));
break;
}
}
AlertDialog in Parent
#Override
public void onTextViewClicked(int position) {
//pass the 'context' here
AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext());
final int recyclerPosition = position;
/*
Turning the distortions into a list of strings and an array of what should, or should
not, be checked.
*/
final String[] distortionStrings = distortionNameToStringArray(mDistortions.get(position));
final boolean[] checkedDistortions = distortionCheckToBooleanArray(mDistortions.get(position));
alertDialog.setMultiChoiceItems(distortionStrings, checkedDistortions,
new DialogInterface.OnMultiChoiceClickListener() {
#Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if (isChecked) {
// If the user checked the item, add it to the selected items
mDistortions.get(recyclerPosition).get(which).setChecked(true);
} else {
// Else, if the item is already in the array, remove it
mDistortions.get(recyclerPosition).get(which).setChecked(false);
}
/*
Because the RecyclerView takes a while to draw, if we call the below function
as we normally we would, it would appear to have no effect because it would
be automatically overwritten when the RecyclerView is drawn. So we call this
onLayout change listener to wait til the view is drawn and then we call
the function
*/
mRecyclerView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
mRecyclerView.removeOnLayoutChangeListener(this);
// Send new height to the ViewModel
if(mLayoutManager.findViewByPosition(recyclerPosition) != null) {
// Get view of item measuring
View recyclerChild = mLayoutManager.findViewByPosition(recyclerPosition);
// Get LinearLayout from view
LinearLayout linearLayout = recyclerChild.findViewById(R.id.horizontalLayout);
// This is called to find out how big a view should be. The constraints are to check
// measurement when it is set to 'wrap_content'.
linearLayout.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Get height of the specified view
int height = linearLayout.getMeasuredHeight();
// Send to child abstracted class which then calls function from 'SharedEntryFragments'
setViewModelHeight(height, recyclerPosition);
}
}
});
mAdapter.notifyDataSetChanged();
}
});
alertDialog.setPositiveButton("Okay", new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
// DO SOMETHING HERE
dialog.cancel();
}
});
AlertDialog dialog = alertDialog.create();
dialog.show();
}
The function that makes all the fragment item heights equal
I know this part of the code doesn't affect it because where the views that heights are changed are skipped by if(positionalHeight.get(i) != 0) {} So technically...they should never change!
/*
This is the listener that will set all the RecyclerViews childrens heights. It
listens to getTallestLiveHeight() inside of 'SharedEntryFragments.java' and when
a change occurs, this is called
*/
if(getActivity() != null) {
// The container holds the ViewModel so this must make sure getActivity() is not null
mViewModel = ViewModelProviders.of(getActivity()).get(SharedEntryFragments.class);
/*
Creates the observer which updates the UI. The observer takes the
PositionalHeight class as an input. This class keeps track of which index
of the RecyclerView to change and what height it will be changed to.
*/
final Observer<List<Integer>> maxHeight = new Observer<List<Integer>>() {
#Override
public void onChanged(#Nullable final List<Integer> positionalHeight) {
if (positionalHeight != null) {
// Get the index that we are going to change and its height
//int position = positionalHeight.getPosition();
//int height = positionalHeight.getHeight();
/*
We're going to run through each child of mRecyclerView and change
its height accordingly
*/
int listSize = positionalHeight.size();
for(int i = 0; i < listSize; i++) {
// If height reads zero then skip because it will make our view disappear
if(positionalHeight.get(i) != 0) {
// This is the child item that we will be changing
View recyclerChild = mLayoutManager.findViewByPosition(i);
// Ensure that the child exists before continuing
if (recyclerChild != null) {
// We will be changing the CardView's height
// TODO might have to add a check to detect which viewholder
CardView cardView = recyclerChild.findViewById(R.id.thoughtCard);
// Get the LayoutParams first to ensure everything stays the same
ViewGroup.LayoutParams lparams = cardView.getLayoutParams();
// Get and set height
lparams.height = positionalHeight.get(i);
cardView.setLayoutParams(lparams);
}
}
}
}
}
};
mViewModel.getTallestLiveHeight().observe(this, maxHeight);
}
}
I wish I could provide a better answer for other people but this is what I discovered:
For some reason when I call mAdapter.notifyDataSetChanged(); in the AlertDialog function, every third item in the RecyclerView changed to the equaled height. I decided to change it to mAdapter.notifyItemChanged(recyclerPosition); to save on memory and, coincidentally, the bug has disappeared.
If someone could explain why, I will set that as the accepted answer but as of now, this satisfies the question so I will keep it as an answer.
I am working on a project where I have a let's say 5x5 grid of TextViews and I want to check if an entire row or column has equal elements. I am using an Adapter class to inflate my gridview with simply one textview element. Here is the code that I have tried but I cannot seem to make it work:
final int size = gridView.getCount(); //25
int temp = 0;
for (int i = 0; i < size; i++) {
ViewGroup gridChild = (ViewGroup) gridView.getChildAt(i);
childSize = gridChild.getChildCount();
for (int j = 0; j < childSize; j++) {
if (gridChild.getChildAt(j) instanceof TextView &&
((TextView) gridChild.getChildAt(j)).getText().toString().equals("x")) {
temp++;
}
The thing is when i tried to debug, debugger showed null values for childSize variable and could not properly get the value from getChildAt. Basically, what I am trying to do is get inside the if statement. Also this is the first time I am working with ViewGroup calss, and the methods that I call. Any help would be appreciated.
Edit:I am looking for a way to do this outside the getView method in the adapter class and not in a onClick method as well. (Code sample answers would be highly appreciated). Also, the getChildAt method call returns null so the code I have shown would not work because I am assigning a null value to the gridChild.
This is the onClick that I use for the TextViews:
`
public void numberFill(View view) {
if (((TextView) view).getText().toString().isEmpty()) {
((TextView) view).setText(String.valueOf(numbCounter + 1));
numbCounter++;
}
else if (!((TextView) view).getText().toString().isEmpty() && numbCounter >= 16) {
((TextView) view).setText("x");
}
}
This is my adapter class:
public class GridAdapter extends BaseAdapter {
private final Context mContext;
private String[] numbers;
public GridAdapter(Context context, String[] numbers) {
this.mContext = context;
this.numbers = numbers;
}
#Override
public int getCount() {
return numbers.length;
}
#Override
public long getItemId(int position) {
return 0;
}
#Override
public Object getItem(int position) {
return numbers[position];
//return null;
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
LayoutInflater inflater = (LayoutInflater)
mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View gridView;
if (convertView == null) {
gridView = new View(mContext);
gridView = inflater.inflate(R.layout.textview_layout, null);
TextView textView = (TextView) gridView.findViewById(R.id.cell);
textView.setText(numbers[position]);
} else {
gridView = (View) convertView;
}
return gridView;
}
}
numberFill reworked:
public void numberFill(View view) {
int index = (Integer) view.getTag();
if (numbers[index].toString().isEmpty()) {
numbers[index] = String.valueOf(numbCounter + 1);
numbCounter++;
}
else if (!numbers[index].toString().isEmpty() && numbCounter >= 25) {
numbers[index] = "x";
}
gridAdapter.notifyDataSetChanged();
}
`
When using an AdapterView – such as your GridView – you generally don't want to directly access and manipulate its child Views outside of its Adapter. Instead, the dataset backing the Adapter should be updated, and the GridView then refreshed.
In your case, you presumably have a setup similar to this in your Activity:
private GridAdapter gridAdapter;
private String[] numbers;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
numbers = new String[25];
gridAdapter = new GridAdapter(this, numbers);
}
Here, the numbers array is what you want to directly modify, rather than the text on the GridView's child TextViews. That array is then easily iterated over to do your row and column value checks.
Since the array will be modified in the Activity, we need a way to pass the clicked TextView's position in the Adapter to the Activity's click method, as we'll need it to access the correct array element. For this, we can utilize the tag property available on all View's, via the setTag() and getTag() methods. For example, in GridAdapter's getView() method:
...
TextView textView = (TextView) gridView.findViewById(R.id.cell);
textView.setText(numbers[position]);
textView.setTag(position);
...
In the click method, the position can be easily retrieved with getTag(), and used as the index to get the clicked TextView's text from the numbers array. You can then do the necessary processing or calculation with that text, set the modified value back to the array element, and trigger a refresh on the Adapter.
public void numberFill(View view) {
int index = (Integer) view.getTag();
// Do your processing with numbers[index]
numbers[index] = "new value";
gridAdapter.notifyDataSetChanged();
}
The notifyDataSetChanged() call will cause the GridView to update its children, and your new value will be set in the appropriate TextView. The numbers array now also has the current values, and is readily available in the Activity to perform the necessary checks there.
I have a list where textView #+id/job_status is present number of times in the page. I need to find the count of it and assert it.
Scenario:
Filter(5)
<Item1>
<Item2>
<Item3>
I need to check whether the count displayed Filter("5") is same as the number of items displayed in the list.
I have tried the below method:
public static class RecyclerViewAssertion implements ViewAssertion {
private final int expectedCount;
public RecyclerViewAssertion(int expectedCount) {
this.expectedCount = expectedCount;
}
#Override
public void check(View view, NoMatchingViewException
noViewFoundException) {
if (noViewFoundException != null) {
throw noViewFoundException;
}
RecyclerView recyclerView = (RecyclerView) view;
RecyclerView.Adapter adapter = recyclerView.getAdapter();
assertThat(adapter.getItemCount(), is(expectedCount));
}
}
onView (withId(android.R.id.list)).check(new
JobsPage.RecyclerViewAssertion(5));
It throws me error saying : android.R.id.list matches multiple views in the hierrarchy. This id has been used in multiple places of the app. Is there any other way for this problem ?
How to get the scrollposition in Recyclerview or the Layoutmanager?
I can measure the scrollposition by adding an OnScrollListener, but when the Orientation changes scrollDy is 0.
mRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
int scrollDy = 0;
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
scrollDy += dy;
}
});
For future use, If you are switching between Fragments within the same activity and all you want to do is save scroll-position for recyclerview and then restore recyclerview to the same scroll-position, you can do as follows:
In your onStop()/onDestroyView()/onPause() whatever callback is more appropriate, do this:
Parcelable recylerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
And In your onStart()/onCreateView()/onResume() whatever callback is more appropriate, do this:
recyclerView.getLayoutManager().onRestoreInstanceState(recylerViewState);
This way you can successfully keep your recyclerView's state in a parcelable and restore it whenever you want.
You cannot get it because it does not really exist. LayoutManager only knows about the views on screen, it does not know the views before, what their size is etc.
The number you can count using the scroll listener is not reliable because if data changes, RecyclerView will do a fresh layout calculation and will not try to re-calculate a real offset (you'll receive an onScroll(0, 0) if views moved).
RecyclerView estimates this value for scrollbars, you can use the same methods from View class.
computeHorizontalScrollExtent
computeHorizontalScrollRange
computeHorizontalScrollOffset
These methods have their vertical counterparts as well.
I came to the question just wanting to get the item position index that is currently scrolled to. For others who want to do the same, you can use the following:
LinearLayoutManager myLayoutManager = myRecyclerView.getLayoutManager();
int scrollPosition = myLayoutManager.findFirstVisibleItemPosition();
You can also get these other positions:
findLastVisibleItemPosition()
findFirstCompletelyVisibleItemPosition()
findLastCompletelyVisibleItemPosition()
Thanks to this answer for help with this. It also shows how to save and restore the scroll position.
recyclerView.computeVerticalScrollOffset() does the trick.
Found a work around to getting last scrolled position within the RecyclerView with credits to Suragch solution...
Declare a globally accessible LinearLayoutManager so a to be able to access it within inner methods...
When ever the populateChatAdapter method is called, if its the first call the scroll to position will be the last sent message and if the user had scrolled to view previous messages and method is called again to update new messages then they will retain their last scrolled to position...
LinearLayoutManager linearLayoutManager;
int lastScrollPosition = 0;
RecyclerView messages_recycler;
Initiate them...
messages_recycler = findViewById(R.id.messages_recycler);
linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setStackFromEnd(true);
messages_recycler.setLayoutManager(linearLayoutManager);
On your populate adapter method
private void populateChatAdapter(ObservableList<ChatMessages> msgs) {
if (lastScrollPosition==0) lastScrollPosition = msgs.size()-1;
ChatMessagesRecycler adapter = new ChatMessagesRecycler(msgs);
messages_recycler.setAdapter(adapter);
messages_recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
lastScrollPosition = linearLayoutManager.findLastCompletelyVisibleItemPosition();
}
});
messages_recycler.scrollToPosition(lastScrollPosition);
}
You have to read scrollDy in
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
then you can get recyclerView's scroll position
To get ScroolPosition try this:
You will need this:
mLayoutManager = new LinearLayoutManager(getApplicationContext(), LinearLayoutManager.HORIZONTAL, false);
And then get the position of the mLayoutManager (like this):
recyclerView
.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(#NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
int pos = mLayoutManager.getPosition(v);
RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(pos);
}
});
With this "pos" you get the position, startin with the 0.
int pos = mLayoutManager.getPosition(v);
I have got a list view with 9 rows in it. Every row has two TextViews and a ImageButton which plays a song specific for that row. If it is playing one of the two TextViews should change color and change the text every second to get a result like '1:12 - 7:35'. And that's where my problem lies.
The first time the list view loads all elements that are on screen work fine but whenever I scroll down, tap on the playButton it highlights the wrong cell. Probably because list view's position returns the position on the screen and not the position in the list.
private MediaPlayer mp;
private Handler handler;
private int playingCellPosition = -999;
public View getView(final int position, View convertView, final ViewGroup parent) {
// Find the oefening to work with
final Oefening currentExercise = myExercises.get(position);
LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
final View itemView = vi.inflate(R.layout.cell, null);
// Get textviews
final TextView durationTextView = (TextView) itemView.findViewById(R.id.cell_duur);
final Runnable updateDurationTextView = new Runnable() {
#Override
public void run() {
runOnUiThread(new Runnable(){
public void run(){
Oefening playingExercise = currentExercise.get(playingCellPosition);
TextView durationTextViewToUpdate = (TextView) parent.getChildAt(playingCellPosition).findViewById(R.id.cell_duur);
durationTextViewToUpdate.setText(getFormattedCurrent(mp.getCurrentPosition()) + " - " + playingExercise.getDuration());
durationTextViewToUpdate.setTextColor(Color.parseColor("#b71393"));
}
});
handler.postDelayed(this, 1000);
}
};
ImageButton playButton = (ImageButton) itemView.findViewById(R.id.cell_playButton);
playButton.setOnClickListener(new OnClickListener() {
public void onClick(final View v) {
if (mp.isPlaying()) {
// Pause it here. Not very important to this problem since it occurs when it starts playing and not when it stops
mp.pause();
v.setBackgroundResource(R.drawable.play_icon);
durationTextView.setText(currentExercise.getDuration());
durationTextView.setTextColor(Color.BLACK);
handler.removeCallbacks(updateDurationTextView);
} else {
mp = MediaPlayer.create(MainActivity.this, currentExercise.getAudioFile());
mp.start();
v.setBackgroundResource(R.drawable.pause_icon);
handler.post(updateDurationTextView);
}
}
});
}
The Oefening playingExercise = currentExercise.get(playingCellPosition); works fine though, since it shows the information of the cell whose play button I tapped on. It just shows the information on the wrong cell.
As Áron Nemmondommegavezetéknevem pointed out, the problem is in parent.getChildAt(...);
Note 1: I don't reuse views with convertView since that messed up positions a lot. This is the closest I have come to what I have to achieve.
Note 2: I left out a lot of the code for the MediaPlayer. It is constructed well, so don't worry about that.
Note 3: If someone has a better suggestion for a title, please edit this one. Couldn't come up with a better one.
The problem is obviously with
parent.getChildAt(...);
In the case of a ListView it doesn't return the view of the specific position. Actually it's quite unpredictable what it returns with.
To illustrate why it doesn't return with the view of the specified position: imagine a list view with 10000 or more items. ListView has only a few views, and doesn't have views for all the 10000 items. What could it return with if you would request the 2786. view? Nothing, it doesn't have a view for that item.
Edit:
Suggestion:
Although I don't see how your code works, you should store which item view belongs to an individual item. For example, you can call setTag(position) on convertView before you return with it. Then you can write a function which finds the appropriate view for an item, if it exists. Something similar to this:
public View findViewAtPosition(int position) {
for (int i=0; i < listView.getChildCount(); i++) {
if (listView.getChildAt(i).getTag() == position) {
return(listView.getChildAt(i));
}
}
return(null);
}
I fixed it with an answer on another topic: https://stackoverflow.com/a/2679284/1352169
Áron Nemmondommegavezetéknevem's answer looks good, but this one is a little bit better since it e.g. keeps headers in mind too.