I am trying to replicate a view from iOS so that user have same look and feel throughout the android application as well.
I am having a RecyclerView with LinearLayoutManager and horizontal orientation. So far so good.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
tools:listitem="#layout/item_recycler_view" />
With the output design:
However, in case of iOS design we have items starting from center however the horizontal view is completely scrollable (meaning the scrolling can be done to full width even if the item loading from center).
I know there is no use of adding padding/margin or using a different view like HorizontalScrollView. How can we obtain such behaviour so that i give nearly same experience to users.
Let me know if there is anything that i can provide to clarify the problem statement.
Quick solution
Add an empty item on the beginning and one on the end of your list, and make your index access account for those two extra items. That should help you get the desired effect.
Not so quick solution
Android allows us to write our own custom Layout Managers for RecyclerView. It comes with three types that will cover most of the user cases:
LinearLayoutManger (For lists in general);
GridLayoutManager (For grids);
StaggeredGridLayoutManager (For grids with items with custom sizes).
I believe you could write one to always start placing the first item on the center of the screen. That will require more work, but it won't mess with your data indexes.
Read this, and this, on how to create custom Layout Managers. Also, take a look at the docs. That should be a good place to start.
There are two ways you could do this. The simplest by far would be to add horizontal padding to your RecyclerView and set the view to not clip based on padding. Something like this:
android:paddingLeft="100dp"
android:paddingRight="100dp"
android:clipToPadding="false"
The other way would be to create an ItemDecoration and add it to your RecyclerView. You could then override the getItemOffsets() method to add a left-hand offset to your first item and a right-hand offset to your last item.
This second approach is better because it won't affect the RecyclerView's scrollbars, but it is a little more complex. Here's an example to get you started:
private static class MyItemDecoration extends RecyclerView.ItemDecoration {
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int parentWidth = parent.getWidth();
int childWidth = view.getWidth();
int margin = (parentWidth - childWidth) / 2;
int position = parent.getChildAdapterPosition(view);
outRect.left = position == 0 ? margin : 0;
outRect.right = position == (parent.getAdapter().getItemCount() - 1) ? margin : 0;
}
}
I think the only way will be adding different layout for first and last position in adapter of recyclerview.
It can be done using viewType parameter in createViewHolder(ViewGroup parent, int viewType)
Related
So in my have I have recyclerviews and I want spacing between my recyclerview items, so here I have two options
I can subclass RecyclerView.ItemDecoration
I can add margin to the item itself
Cleary the second option is the easy one and using that option I am getting the desired result but over the web I see people using ItemDecoration for that and also recommends it.
I want to know is there some advantage of using ItemDecoration even if my use case is very simple like adding spacing between items?
will ItemDecoration provide performance benefits or other benefits?
What you've read across various resources is indeed true. Using ItemDecoration is much more clean and the code is reusable in some sense.
This eliminates our need to manually adjust the margins in the UI for the item_layout, even then also the topmost and the bottom most item contain margin issues.
Using ItemDecoration we can create a separate class (see below example) for adding offsets (aka. margins) which will automatically add the required spacing in between the RecyclerView items:
class MarginItemDecoration(private val spaceSize: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect, view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
with(outRect) {
if (parent.getChildAdapterPosition(view) == 0) {
top = spaceSize
}
left = spaceSize
right = spaceSize
bottom = spaceSize
}
}
}
Thanks to #cmorigaki for this above example.
Using ItemDecoration:
Easy to implement, just recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
It's natural, it only adds the gap between items, which means you won't see it on the top of the first item, or the bottom of last item.
You can customize the divider, by following:
val dividerDrawable = getDrawable(context, R.drawable.divider)
itemDecoration.setDrawable(dividerDrawable)
Using Margin on Item View Layout:
Also easy to achieve, but it has some obvious drawback: it will appear on the top of first item and the bottom of last item. Unless you write more code (logic) to deal with it.
I want to draw a default ItemDecoration only for Header Type into RecycleView. But divider is shown for each ViewType.
Custom decoration:
class DividerDecoration(context: Context, orientation: Int)
: DividerItemDecoration(context, orientation){
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
val viewType = parent.adapter!!.getItemViewType(position)
if (viewType == ITEM_VIEW_TYPE_HEADER){
super.getItemOffsets(outRect, view, parent, state)
} else {
outRect.setEmpty()
}
}
}
Set:
val itemDecoration = DividerDecoration(binding.recyclerView.context,
DividerItemDecoration.VERTICAL)
binding.recyclerView.addItemDecoration(itemDecoration)
Any suggestions to why does this happen?
UPDATE
The code above is work. But with a bug. After starting the application, the divider appears in all elements, and then only the right ones. Why is this happening?
Here's how you can show or hide divider based on view type or position. Include it in your activity/fragment.
recyclerView.addItemDecoration(new DividerItemDecoration(this, linearLayoutManager.getOrientation()) {
#Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
Drawable d = getDrawable();
for (int i = 0; i < parent.getChildCount(); i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
int viewType = parent.getAdapter().getItemViewType(position);
// Draw divider only for view type 2 (can also put position here to remove for certain positions)
if(viewType == 2) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
int top = view.getBottom() + params.bottomMargin;
int bottom = top + d.getIntrinsicHeight();
d.setBounds(0, top, parent.getRight(), bottom);
d.draw(c);
}
}
}
});
In my opinion, trying to do this from within the Decoration implementation is messy. Now the decoration has to know about the data set, so you've got a view directly accessing the adapter to access the data. And you're determining which positions are headers in multiple different classes. Spaghetti and repetition.
Ideally, the adapter class would be handling the application of Decorations so you could do it selectively, but since it doesn't, I think the best way to do it for the time being is to put the divider in your header layout, and in onBindView you can turn off its visibility if the position is 0 (the topmost header).
Problem
My mistake consists of the conviction to use ItemDecoration in order to set custom dividers for different ViewType. I have developed a clear belief that such a custom dividers should do only ItemDecoration.
According image gif from my question, it's gives a bug. I couldn't find a solution. The other posts on StackOverflow is offering to override the methods: getItemOffsets() and onDraw(). I tried to implement at least four case. And they all ended up with a drawing bug (similar to gif from my question).
Search
I don't know what causes these drawing errors. Thanks to Tenfour04's answer and comments. I changed my keywords for searching and found simple solution.
By the way, the design of my application adopts some features of the design of applications from Google. I decompiled the apk of one of those apps. And found the resources that make the separator exactly as described in the solution. In this regard, I can consider this decision the best practices from Google.
Solution
I have two ViewType. Each with its own layout. The solution is to add a separator to the header layout.
Because the ViewTypeHeader only appears if there are nested elements, in my case I don't need to add conditions with Visible for the last or first element.
layout\ViewTypeHeader.xml
<LinearLayout ...>
<TextView ... />
<!-- This -->
<View style="#style/DividerStyle" />
</LinearLayout>
values/styles.xml
<style name="DividerStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">#dimen/dividerHeight</item>
<item name="android:background">#android:color/black</item>
</style>
Conclusion
If there are several ViewType - forget about ItemDecoration. Save a lot of time.
Override your item decoration's onDraw and only perform the draw when the item
being decorated is in a position you consider ok.
Instead of creating another example, I'll just point you to a working sample that doesn't do exactly what you want, but should certainly point you in the right direction: how to selectively draw dividers.
https://stackoverflow.com/a/46216274/2684
Is there a possibility using the RecyclerView in Android Studio to not show one whole item if there are variables empty that should be shown in that item?
For example if I got 3 variables to be shown in the item, but only 2 of them have a value - don't show the item at all.
Thanks in advance :)
There's no supported API for this. The best idea I have is to set the view's height to 0 when you don't want to show it. Here's some code that hides the view at position 3 and shows the others:
#Override
public void onBindViewHolder(#NonNull MyViewHolder holder, int position) {
ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();
params.height = (position == 3) ? 0 : ViewGroup.LayoutParams.WRAP_CONTENT;
holder.itemView.setLayoutParams(params);
// other binding code here
}
If your item views aren't using wrap_content for their height, then you'd have to set the height to that fixed value. You could use this code to fetch that fixed size:
holder.itemView.getResources().getDimensionPixelSize(R.dimen.your_size)
If you use your own implementation of BaseAdapter or ArrayAdapter, you could filter ListView or RecyclerView with holding two lists in your adapter class: one for all values, and one (shown) for filtered values. Also there's no need in hacks like setting some items height to 0 (which may produce problems).
What I intend to achieve
The item view should occupy the entire height of the item
It could be that the item height is lesser than the height of the tallest item in the recyclerview, in which case it should just stick to the top like in the screenshot above.
The bug I'm running into
As in the screenshot above, views are getting truncated.
What I've tried so far
Initially I went with wrap_content on the recyclerview, now that it is supported. It didn't work when none of the views visible on the screen at the time were the tallest. This makes sense in how the view hierarchy is laid out. How can the height of something which hasn't even been bound to any data yet be calculated if the height is dependent on that data?
Workaround time :S
Instead of trying a custom layoutmanager, I first went with what I felt needed to be done - laying out all item views at the beginning to figure out their height.
There's a progressbar and an animation playing in the upper part of the screen to catch the user's attention while all this happens with recyclerview visibility set to invisible. I use two things, one didn't suffice - I've attached an observer in the adapter's onViewAttached() call and I've used a scroll change listener as well. There's a LinearSnapHelper attached to the recycler view to snap to adjacent (next or previous, depending on the scroll direction) position on scroll.
In this setup,
I'm going to each position in the recyclerview using layoutManager.smoothScrollToPosition()
Getting the child view height using
View currentChildView = binding.nextRv.getChildAt(layoutManager.findFirstCompletelyVisibleItemPosition());
if (currentChildView != null) {
currentChildHeight = currentChildView.getHeight();
}
in scroll change listener on RecyclerView.SCROLL_STATE_IDLE or by passing the height to the view attached observer mentioned above in the adapter's onViewAttachedToWindow()
#Override
public void onViewAttachedToWindow(BindingViewHolder holder) {
if (mObserver != null) {
mObserver.onViewAttached(holder.binding.getRoot().getHeight());
}
}
Storing a maxHeight that changes to the max of maxHeight and new child's height.
As is evident, this is ugly. Plus it doesn't give me the current view's height - onAttached means it's only just attached, not measured and laid out. It is the recycled view, not the view bound to current data item. Which presents problems like the truncation of view illustrated above.
I've also tried wrap_content height on the recycler view and invalidating from recycler's parent till the recycler and the child on scroll coming to SCROLL_STATE_IDLE. Doesn't work.
I'm not sure how a custom layoutmanager can help here.
Can someone guide me in the right direction?
I could not accept #Pradeep Kumar Kushwaha's answer because against one solution, I do not want different font sizes in the list. Consistency is a key element in design. Second alternative he gave couldn't work because with ellipsize I would need to give a "more" button of some sort for user to read the entire content and my text view is already taking a click action. Putting more some place else would again not be good design.
Changing the design with the simple compromise of resizing the recyclerview when the tallest, truncated item comes into focus, it turns into the simple use case of notifyItemChanged(). Even for the attempt I made using the view attached observer and scroll state listener, notifyItemChanged could be used but that approach is just too hacky. This I can live with in both code and design. Here goes the code required.
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int position = ((LinearLayoutManager) binding.nextRv.getLayoutManager())
.findFirstVisibleItemPosition();
if (position != nextSnippetAdapter.getItemCount() - 1) {
binding.nextRv.getAdapter().notifyItemRangeChanged(position, 2);
} else {
binding.nextRv.getAdapter().notifyItemChanged(position);
}
}
}
For my particular setup, calling for just these two elements works. It can further be optimized so as to call for single element at position + 1 in most cases, and checking and calling for the appropriate one in corner (literal) cases.
Inside your adapter where I can find two cards one on top and another on bottom
How I would have defined my layout is like this:
Cardview1
LinearLayout1 --> orientation vertical
cardview2 (Top card where text is written)
Linearlayout2 (where I can see icons such as like etc)-->orientation horizontal
Now fix the height of Linearlayout2 by setting it to wrap content.
And the height of cardview2 should be 0dp and add weight = 1
Now inside cardview2 add a TextView1 to matchparent in height and width.
Better inside textview1 add ellipsize to end and add max lines
If you want to show all lines try to find autoresizetextview library it can be founded here --> AutoResizeTextView
Hope it helps.
I think the recyclerview can be set to height wrap_content. And the items can be make like height to match_parent.
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layput_height="wrap_content"/>
Item as:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
// your coode
</androidx.constraintlayout.widget.ConstraintLayout>
I had little more requirement than the question. Even my problem solved in the way.
Remember I am using:
androidx.recyclerview:recyclerview:1.0.0-beta01
dependency for the project
i have a RecycleView with an adapter that show a list of servers
and the user must select one server.
when i call notifyItemChanged(previousPosition) inside the onClick() method
to make the old server unselected and the new server selected,
that's make the RecycleView list jump to up exactly in the middle of list.
and this problem happen just when i click on one of the last 2 or 3 servers inside the RecycleView list
here is the code of my RecyclerView.Adapter :
public class ServerAdapter extends RecyclerView.Adapter<ServerAdapter.ServerViewHolder> {
private List<Server> listServers = new ArrayList<>();
private int[] icons = new int[]{R.drawable.server1,R.drawable.server2,R.drawable.server3,R.drawable.server4,R.drawable.server5,R.drawable.server6,R.drawable.offline};
private int selected = 0;
private int previousSelected = 0;
public ServerAdapter(List<Server> listServers){
this.listServers = listServers;
}
#Override
public ServerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.server_relative_layout,parent,false);
return new ServerViewHolder(view);
}
#Override
public void onBindViewHolder(final ServerViewHolder holder, final int position) {
if(position == selected){
holder.getBackground().setSelected(true);
}else{
holder.getBackground().setSelected(false);
}
holder.getBackground().setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(position != selected){
previousSelected = selected;
selected = position;
holder.getBackground().setSelected(true);
notifyItemChanged(previousSelected);
}
}
});
holder.getImageServer().setImageResource(icons[position%6]);
holder.getTextNameServer().setText(listServers.get(position).getName());
holder.getTextConnected().setText(listServers.get(position).getUrl());
}
#Override
public int getItemCount() {
return listServers.size();
}
public class ServerViewHolder extends RecyclerView.ViewHolder{
private ImageView imageServer;
private TextView textNameServer;
private TextView textConnected;
private View background;
public ServerViewHolder(View itemView) {
super(itemView);
imageServer = (ImageView)itemView.findViewById(R.id.imageServer);
textNameServer = (TextView)itemView.findViewById(R.id.textNameServer);
textConnected = (TextView)itemView.findViewById(R.id.textConnected);
background = itemView;
}
public ImageView getImageServer() {
return imageServer;
}
public TextView getTextConnected() {
return textConnected;
}
public TextView getTextNameServer() {
return textNameServer;
}
public View getBackground() {
return background;
}
}
}
any solutions to solve this problem ? thanks.
The problem happened exactly when i specify the layout height and do not let it to wrap_content
<android.support.v7.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="400dp"
android:id="#+id/serverRecyclerView"
android:layout_margin="10dp"
/>
or when i put it below something for expample like that :
<android.support.v7.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/serverRecyclerView"
android:layout_margin="10dp"
android:layout_below="#+id/image"/>
my code exactly is :
<android.support.v7.widget.RecyclerView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/serverRecyclerView"
android:layout_margin="10dp"
android:layout_alignTop="#+id/imageBall"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_toRightOf="#+id/camera"
android:layout_toEndOf="#+id/camera"/>
Looks like this is a bug: https://code.google.com/p/android/issues/detail?id=203574
The best workaround seems to be Bart's answer to set the RecyclerView's LinearLayoutManager's AutoMeasure property to false.
LinearLayoutManager llm = new LinearLayoutManager(context);
llm.setAutoMeasureEnabled(false);
recyclerView.setLayoutManager(llm);
The set FixedSize to true solution had way too many side-effects...
RecyclerView.setHasFixedSize(true)
I don't know why, but I used:
RecyclerView.setHasFixedSize(true)
This worked for me. I hope it can help.
android:descendantFocusability="blocksDescendants"
android:descendantFocusability="blocksDescendants"
this attr solve my bug
RecyclerView.ItemAnimator animator = myRecyclerListView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator)animator).setSupportsChangeAnimations(false);
}
My RecyclerView was inside ConstraintLayout, and I also had such problem and calling setAutoMeasureEnabled(false) of RecyclerView's LayoutManager did not fix the issue for me, furthermore this method is deprecated in 28.0.0 version. What I did is that, I wrapped my RecyclerView with RelativeLayout and now it works like a charm. As mentioned in bugtracker, this "issue" is intented behaviour in LinearLayout and is not going to be fixed. So if it is possible, just wrap your RecyclerView something like this:
<RelativeLayout
android:id="#+id/container_messages_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#drawable/chat_back_pattern"
app:layout_constraintBottom_toTopOf="#+id/bottom_view"
app:layout_constraintTop_toBottomOf="#+id/toolbar">
<android.support.v7.widget.RecyclerView
android:id="#+id/messages_list"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</RelativeLayout>
for anyone who stumbles upon this issue, try using
yourRecyclerView.notifyItemChanged(int position, Object payload);
This one did the trick for me.
Using
setAutoMeasureEnabled(false);
also worked but in some edge cases recycler view was acting weird. Good luck!
RecyclerView can perform several optimizations if it can know in advance that RecyclerView's size is not affected by the adapter contents. RecyclerView can still change its size based on other factors (e.g. its parent's size) but this size calculation cannot depend on the size of its children or contents of its adapter (except the number of items in the adapter).
If your use of RecyclerView falls into this category, set this to true. It will allow RecyclerView to avoid invalidating the whole layout when its adapter contents change.
If we have a RecyclerView with match_parent as height/width, we should add setHasFixedSize(true) since the size of the RecyclerView itself does not change inserting or deleting items into it.
setHasFixedSize should be false if we have a RecyclerView with wrap_content as height/width because each element inserted by the adapter could change the size of the RecyclerView depending on the items inserted/deleted, so, the size of the RecyclerView will be different each time we add/delete items.
recyclerView.setHasFixedSize(true);
true if adapter changes cannot affect the size of the RecyclerView.
References
Android Developers Reference - RecyclerView
Understanding RecyclerView setHasFixedSize - Gastón Saillén
I came across the similar problem, just take care of the xml layout file.
Do not use the layout_below , layout_above or others similar properties in RecyclerView or RecyclerView's parent view.
You can use LinearLayout weight , layout_marginBottom or sth to achieve
layout_below or other.
The late answer better than nothing, if you're using NestedScrollView as the parent view of RecyclerView you should delete it.
I had a similar problem and I tryed all solutions listed above, but noone worked.
I was already padding the "Payloads" to "notifyItemChanged(position, payloads)" because I just needed to "upload" a checkbox value so I was passing the value inside "Payloads" without recalling the update of the entire viewholder.
This solution worked for all view holders in my recycler view except for the last one (and probably for all "recycled" ones, I mean those who recall the "onBindViewHolder" by "recycling" an existing view).
I think using "notifyItemChanged" will works if you have only the recyclerview and I also think that this problem of "auto-scrolling" is raised by nested scroll views & recycler views.
I was in the case exposed by "raed", so "ScroolView -> RecyclerView -> "n" x RecyclerView". I have a scroolview wich contains a recyclerview whose viewholders can contains a recycler views.
Delete the parent ScrollView is a really weird solution and I couldn't use it, so I setted the "onStopNestedScroll" inside the "ScrollView" and not inside the RecyclerView.
Personally I used it programmatically before the code part which calls the "notifyItemChanged" method by doing:
msvContainer.onStopNestedScroll(mRecyclerView);
Where "msvContainer" is my ScrollView which contains the RecyclerView, and "mRecyclerView" is my RecyclerView contained by the ScrollView.
This way worked 99% because the first time I call "notifyItemChanged" the view scroll up only for the ScrollView, so it hides a button inside my ScrollView which is below my RecyclerView but it doesn't scroll the RecyclerView items. After the first call "notifyItemChanged" works properly.
I found that calling:
msvContainer.stopNestedScroll();
works too. But i suggest to use the first method with the target view if you have multiple nested scroll views.
Anyway you should call "startNestedScroll" after you ran out of the critical part of re-updating your view holder because the targeted view, so in my case the RecyclerView, won't scroll until you call this method so it won't recycler his view holders too.
(In my case that I have multiple Recycler View inside a parent Recycler View inside a parent Scroll View if I was in need to call "notifyItemChanged" inside the most inner Recycler View i would use the "stopNestedScroll" method for every parent view and then re-activated the scroll after the scroll-critical part)
Hope this is helpful, have a nice coding!
Bye
In my case, all I did was to set the height of the recyclerview to "match_parent". Then in your MainActivity, do;
recyclerView.smoothScrollToPosition(yourAdapter.getItemCount()-1);
Thats all...