I want to add empty spaces between items of a RecyclerView with GridLayoutManager but I can't find anything which solves my problem.
I am creating a custom calendar and I have to make a custom grid from scratch. The first day of the month should be placed farther from the start when it is not the first day of the week.
I have achieved a similar result by passing a firstItemLocation to my RecyclerView adapter and overriding the onBindViewHolder method like this:
#Override
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
if (position < firstItemLocation) {
holder.itemView.setVisibility(View.GONE);
} else {
holder.tv.setText(String.format(Locale.US, "%d", list.get(position - firstItemLocation)));
}
}
It works fine but in a hypothetical situation when the user has to scroll it adds random empty spaces and glitches like this.
Any seggestions to make it better without that glitch?
read about "recycling pattern", how it works and why - RecyclerView got its name because of this, so its very important to understand how this works
but in short:
inside onBindViewHolder you have to call desired methods ALWAYS. you are calling setVisibility only inside if and not in else. try with bellow snippet
if (position < firstItemLocation) {
holder.itemView.setVisibility(View.GONE);
holder.tv.setText("")
} else {
holder.itemView.setVisibility(View.VISIBLE);
holder.tv.setText(String.format(Locale.US, "%d", list.get(position - firstItemLocation)));
}
thats because holder may be a "recycled" instance, used previously to draw another list item, and its set up is still applied to views
Related
I am implementing a recycler view with two ViewHolder types. After creating the first item as first-type view holder, I have a list of items of the same, second type. Now, I would like to juxtapose the second type items in order to define a two-column recycler view list considering the second type. Is it possible to do that? I have no idea of what should be implemented in the adapter and honestly I have found no good suggestions here. I guess I might not post my adapter code, since I don't know whether it is possible to do what I aim, I hope a conceptual answer may be sufficient too. I have an image, made with a not good image editor of my smartphone, I hope it is clear:
I am late but I found out a solution.
I associate to the recyclerview a GridLayoutManager with span count = 2.
Since I have, at the top of the recyclerview, an EditText (I use it as a search bar), as you can see in the image, I have to set the lookup of the grid. This is done by setSpanSizeLookup(). So, in the onViewCreated() method of my fragment class that initializes the recyclerview (I have a Fragment hosting the recyclerview), I insert the following code:
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(), 2);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
int type = adapter.getItemViewType(position);
if (type == RecyclerAdapter.SEARCH)
return 2;
else
return 1;
}
});
recyclerView.setLayoutManager(gridLayoutManager);
where the part "if (type == RecyclerAdapter.SEARCH) return 2;" means that the view holder for the search bar is going to occupy one whole row, 2 grid cells (since the span = 2).
In my recyclerview adapter, the method getItemViewType is essential:
#Override
public int getItemViewType(int position) {
if (position == 0)
return SEARCH;
else
return DESCRIPTION;
}
where SEARCH and DESCRIPTION are two static final variables.
Description: I am working on one demo with the functionality that user can select an option from the item of horizontal Recyclerview. In this, I got stuck in below queries.
Queries:
1: How to make Recyclerview cyclic?
For example, I have 10 items named 1,2,3,4,5,6,7,8,9,10. Now if user scrolls the recyclerview then he/she will be able to scroll it endlessly. i.e 1,2,3,4,5...9,10,1,2...9,10,1,2,3..9,10 like this.
To achieve this I have used this answer. Somehow it worked but only in forward direction.
2: How to give Snap center to the particular item which is selected by the user?
For example: As shown in the image, if the user clicks on '15' then that item will come to the center. Same as if the user clicks on '07' then it should come to the center position.
I have implemented this demo. But it doesn't work on click on the item.
The code which I have done so far is as below:
Layout Manager:
final CenterZoomLayoutManager mLayoutManager = new CenterZoomLayoutManager(mContext,
CenterZoomLayoutManager.HORIZONTAL, false);// To get centered item zoom
recyclerview.setLayoutManager(mLayoutManager);
Adapter object:
mAdapter = new CustomAdapter(mContext, arrayList, new RecyclerViewClickListener() {
#Override
public void recyclerViewListClicked(View v, int position) {
}
});
recyclerview.setAdapter(mAdapter);
Any help is appreciated!
Thanks.
CarouselView may fulfill your requirements. Please check following GitHub links for the same.
https://android-arsenal.com/details/1/1481
https://github.com/nicklockwood/iCarousel
https://www.pocketmagic.net/a-3d-carousel-view-for-android/
https://github.com/clkasd/CarouselViewProject
How do I create a circular (endless) RecyclerView?
There is no way of making it infinite, but there is a way to make it
look like infinite.
in your adapter override getCount() to return something big like
Integer.MAX_VALUE:
#Override
public int getCount() {
return Integer.MAX_VALUE;
}
in getItem() and getView() modulo divide (%) position by real item
number:
#Override
public Fragment getItem(int position) {
int positionInList = position % fragmentList.size();
return fragmentList.get(positionInList);
}
at the end, set current item to something in the middle (or else, it
would be endless only in downward direction).
// scroll to middle item
recyclerView.getLayoutManager().scrollToPosition(Integer.MAX_VALUE / 2);
For snapping
use SnapHelper or scrollToPositionWithOffset, where offset is (width/2 - itemWidth/2)
I can use this library
You can solve your problem by personalizing this library. By selecting each item, you can use the ScrollToX function to navigate to that item.
https://github.com/mahdimoaz/RecyclerViewHorizontalCenterSelected
I am trying to get a number of lines, in Espresso, of the TextView from one of the elements from recycler view. This is my macher for it:
public static TypeSafeMatcher<RecyclerView.ViewHolder> isTextInLines(final int lines) {
return new TypeSafeMatcher<RecyclerView.ViewHolder>() {
#Override
protected boolean matchesSafely(RecyclerView.ViewHolder item) {
Log.e("Lines", ((TextView) item.itemView.findViewById(R.id.textTextView)).getLineCount() + "");
return ((TextView) item.itemView.findViewById(R.id.textTextView)).getLineCount() == lines;
}
#Override
public void describeTo(Description description) {
description.appendText("isTextInLines");
}
};
}
The problem is that it prints that all of them has 0 lines. As I understand the problem might be that it is still in drawing phase according to this:
https://stackoverflow.com/a/24035591/6470918
Any idea how to solve it?
PS. This is the code which uses this matcher:
onView(withId(R.id.recyclerView))
.perform(scrollToHolder(isTextInLines(12)));
Any RecyclerViewAction::scrollTo... binds all adapter items to the in memory ViewHolders and loops over those to find the adapter position of the item that you are interested in and then scrolls to that position. And because of this, these ViewHolders are never laid out.
Unfortunately, to get the number of lines of the TextView it has to be laid out first.
You can work your problem around with normal ViewAssertion like so onView(matcher).check(matches(isTextInLines(12)) or even onView(isTextInLines(12)).check(matches(isDisplayed())
You might need to scroll to the ViewHolder using other matchers that don't require layout pass first, in case the ViewHolder is off the screen.
I have a list with 13 items (although items may be added or removed), positions 0-12. When the fragment containing the RecyclerView is first shown, only positions 0 through 7 are visible to the user (position 7 being only half visible). In my adapter I Log every time a view holder is binded/bound (idk if grammar applies here) and record its position.
Adapter
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
From my Log I see that positions 0-7 are bound:
I have a selectAll() method that gets each ViewHolder by adapter position. If the returned holder is NOT null I use the returned holder to update the view to show it's selected. If the returned holder IS null I call selectOnBind() a method that flags the view at that position update to show it's selected when it's binded rather than in real time since it's not currently shown:
public void selectAll() {
for (int i = 0; i < numberOfItemsInList; i++) {
MyAdapter.ViewHolder holder = (MyAdapter.ViewHolder)
mRecyclerView.findViewHolderForAdapterPosition(i);
Log.d(TAG, "holder at position " + i + " is " + holder);
if (holder != null) {
select(holder);
} else {
selectOnBind(i);
}
}
}
In this method I Log the holder along with its position:
So up to this point everything seems normal. We have positions 0-7 showing, and according to the Log these are the positions bound. When I hit selectAll() without changing the visible views (scrolling) I see that positions 0-7 are defined and 8-12 are null. So far so good.
Here's where it gets interesting. If after calling selectAll() I scroll further down the list positions 8 and 9 do not show they are selected.
When checking the Log I see that it's because they are never bound even though they were reported to be null:
Even more confusing is that this does not happen every time. If I first launch the app and test this it may work. But it seems to happen without fail afterwards. I'm guessing it has something to do with the views being recycled, but even so wouldn't they have to be bound?
EDIT (6-29-16)
After an AndroidStudio update I cannot seem to reproduce the bug. It works as I expected it to, binding the null views. If this problem should resurface, I will return to this post.
This is happening because:
The views are not added to the recyclerview (getChildAt will not work and will return null for that position)
They are cached also (onBind will not be called)
Calling recyclerView.setItemViewCacheSize(0) will fix this "problem".
Because the default value is 2 (private static final int DEFAULT_CACHE_SIZE = 2; in RecyclerView.Recycler), you'll always get 2 views that will not call onBind but that aren't added to the recycler
In your case views for positions 8 and 9 are not being recycled, they are being detached from the window and will be attached again. And for these detached view onBindViewHolder is not called, only onViewAttachedToWindow is called. If you override these function in your adapter, you can see what I am talking.
#Override
public void onViewRecycled(ViewHolder vh){
Log.wtf(TAG,"onViewRecycled "+vh);
}
#Override
public void onViewDetachedFromWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewDetachedFromWindow "+viewHolder);
}
Now in order to solve your problem you need to keep track of the views which were supposed to recycled but get detached and then do your section process on
#Override
public void onViewAttachedToWindow(ViewHolder viewHolder){
Log.wtf(TAG,"onViewAttachedToWindow "+viewHolder);
}
The answers by Pedro Oliveira and Zartha are great for understanding the problem, but I don't see any solutions I'm happy with.
I believe you have 2 good options depending on what you're doing:
Option 1
If you want onBindViewHolder() to get called for an off-screen view regardless if it's cached/detached or not, then you can do:
RecyclerView.ViewHolder view_holder = recycler_view.findViewHolderForAdapterPosition( some_position );
if ( view_holder != null )
{
//manipulate the attached view
}
else //view is either non-existant or detached waiting to be reattached
notifyItemChanged( some_position );
The idea is that if the view is cached/detached, then notifyItemChanged() will tell the adapter that view is invalid, which will result in onBindViewHolder() getting called.
Option 2
If you only want to execute a partial change (and not everything inside onBindViewHolder()), then inside of onBindViewHolder( ViewHolder view_holder, int position ), you need to store the position in the view_holder, and execute the change you want in onViewAttachedToWindow( ViewHolder view_holder ).
I recommend option 1 for simplicity unless your onBindViewHolder() is doing something intensive like messing with Bitmaps.
When you have large number of items in the list you have passed to recyclerview adapter you will not encounter the issue of onBindViewHolder() not executing while scrolling.
But if the list has less items(I have checked on list size 5) you may encounter this issue.
Better solution is to check list size.
Please find sample code below.
private void setupAdapter(){
if (list.size() <= 10){
recycler.setItemViewCacheSize(0);
}
recycler.setAdapter(adapter);
recycler.setLayoutManager(linearLayoutManager);
}
I think playing with view is not a good idea in recyclerview. The approach I always use to follow to just introduce a flag to the model using for RecyclerView. Let assume your model is like -
class MyModel{
String name;
int age;
}
If you are tracking the view is selected or not then introduce one boolean to the model. Now it will look like -
class MyModel{
String name;
int age;
boolean isSelected;
}
Now your check box will be selected/un-selected on the basis of the new flag isSelected (in onBindViewHolder() ). On every selection on view will change the value of corresponding model selected value to true, and on unselected change it to false. In your case just run a loop to change all model's isSelected value to true and then call notifyDataSetChanged().
For Example, let assume your list is
ArrayList<MyModel> recyclerList;
private void selectAll(){
for(MyModel myModel:recyclerList)
myModel.isSelected = true;
notifyDataSetChanged();
}
My suggestion, while using recyclerView or ListView to less try to play with views.
So in your case -
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.clickableView.setTag(position);
holder.selectableView.setTag(position);
holder.checkedView.setChecked(recyclerList.get(position).isSelected);
Log.d(TAG, "onBindViewHolder() position: " + position);
...
}
#Override
public void onClick(View view){
int position = (int)view.getTag();
recyclerList.get(position).isSelected = !recyclerList.get(position).isSelected;
}
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
int position = (int)buttonView.getTag();
recyclerList.get(position).isSelected = isChecked;
}
Hope it will help you, Please let me know if you need any further explanation :)
So I think you question is answered below by #Pedro Oliveira. The main sense of RecycleView, that he using special algorithms for caching ViewHolder in any time. So next onBindViewHolder(...) may not work, for ex. if view is static, or something else.
And about your question you think to use RecycleView for dynamic changed Views. DON'T DO IT! Because RecycleView invalidates views and has caching system, so you will have a lot of problems.
Use LinkedListView for this task!
I'm working on a note taking app. I add a note, and it get's added to the bottom of the list. As the last assertion in the espresso test, I want to make sure that the ListView displays a listItem that has just been added. This would mean grabbing the last item in the listView. I guess you might be able to do it in other ways? (e.g. get the size of adapted data, and go to THAT position? maybe?), but the last position of the list seems easy, but I haven't been able to do it. Any ideas?
I've tried this solution, but Espresso seems to hang. http://www.gilvegliach.it/?id=1
1. Find the number of elements in listView's adapter and save it in some variable. We assume the adapter has been fully loaded till now.:
final int[] numberOfAdapterItems = new int[1];
onView(withId(R.id.some_list_view)).check(matches(new TypeSafeMatcher<View>() {
#Override
public boolean matchesSafely(View view) {
ListView listView = (ListView) view;
//here we assume the adapter has been fully loaded already
numberOfAdapterItems[0] = listView.getAdapter().getCount();
return true;
}
#Override
public void describeTo(Description description) {
}
}));
2. Then, knowing the total number of elements in listView's adapter you can scroll to the last element:
onData(anything()).inAdapterView(withId(R.id.some_list_view)).getPosition(numberOfAdapterItems[0] - 1).perform(scrollTo())