offset set with getItemOffsets disappears after scroll - android-recyclerview

I have a horizontal RecyclerView which works with a FlexLayoutManager. I also have some decorations set with RecyclerView.ItemDecoration.
My getItemOffsets method looks something like this:
override fun getItemOffsets(outRect: Rect, view: View, recyclerView: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, recyclerView, state)
val position: Int = recyclerView.getChildAdapterPosition(view)
if(position meets some rules){
outRect.top = some values here
}
if(viewType == CERTAIN_VIEW_TYPE){
outRect.bottom = some value
}
}
..onDrawOver(){
//I draw the decorations here
}
This works, the views that I set as decorations are shown and they are at the right position.
The problem that I have is that AFTER I SCROLL TO RIGHT AND THEN BACK TO LEFT, the offset set by outRect.top is set to 0 and my decorations overlap my items.
The curious stuff is that offset set by outRect.bottom doesn't disappear or cause any issues.
I just specify that the offset set by outRect.top is set only for certain positions.
Also my decorations don't disappear, just the margin set initially by outRect.top is not there anymore
Can you please help me with this issue?
Thank you
EDIT:
This can be the result of view recycling I guess, because I see that after scroll other items now have top offset even though I did not intend to set it for them

I had the same problem with item offsets when scrolling.
My case was ->
I had a RecyclerView with a GridLayoutManager, orientation was horizontal and span count set to 2
I've added a custom item decoration to the RecyclerView related to the requirements that I had, by just adding some space around the items and making calculations.
Used the same overridden function as you and set the value to top, bottom, left and right.
Anyway the problem that I had, was at how I was getting the position of the view that I wanted to make my changes.
val viewHolder = recyclerView.findContainingViewHolder(view) ?: return
val adapterPosition = viewHolder.absoluteAdapterPosition
val childPosition = parent.indexOfChild(view)
val isEvenPosition = adapterPosition % 2 == 0
Previously I was using childPosition but when scrolling I was having the problem, when I've used adapterPosition the views were getting the correct amount of space.

Related

Custom CollapsingTopAppBar Jetpack Compose

The essence of the problem is that I want to write my own version of the AppBar that would include content as another Compose function. After looking at the source code of the current CollapsingTopAppBar implementation, I saw the following lines:
#Composable
private fun TwoRowsTopAppBar(
...
scrollBehavior: TopAppBarScrollBehavior?
) {
...
val pinnedHeightPx: Float = 64.dp
val maxHeightPx: Float = 152.dp
LocalDensity.current.run {
pinnedHeightPx = pinnedHeight.toPx()
maxHeightPx = maxHeight.toPx()
}
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
// visible when collapsed.
SideEffect {
if (scrollBehavior?.state?.heightOffsetLimit != pinnedHeightPx - maxHeightPx) {
scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx
}
}
...
Surface(...) {
Column {
TopAppBarLayout(
...
heightPx = pinnedHeightPx
...
)
TopAppBarLayout(
...
heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset
?: 0f),
...
)
}
}
}
As I understand it, scrollBehavior is used to handle the collapse and expansion behavior. In the current implementation, just constant values are put in heightOffsetLimit. And since I need my appbar implementation to be able to contain content of any size, I need to somehow know the size of this content in advance and put this value in heightOffsetLimit.
I have already written the code for my AppBar, so that it also contains content. But since I can't pass the height value of the content to scrollBehavior, the AppBar doesn't collapse to the end.
you need to calculate the height that the appbar will have before drawing it into the screen. I have followed this issue and solved my problem with the last solution. hope it helps:
Get height of element Jetpack Compose
use the content you can put (ex. an image or a huge text) as the MainContent
use your appbar as the DependentContent and use the size given in lambda to give the height to your appbar
finally set placeMainContent false as I believe you don't need to draw the image (or any other composable) directly in a box
and you will good to go

Why did the border radius disappear for an RecyclerView item with a custom background?

I try to figure out Android development, and sometimes I have beginner question. For now I faced with issue creating the RecyclerView items with different background color. I created a simple
RecyclerView items list First I set one background (light green) for all items.
Then I decided to set a separate background for each item. Here's how I did it in adapter file:
override fun onBindViewHolder(holder: ChapterListAdapter.ViewHolder, position: Int) {
holder.chapter_item.text = chapter_titles[position]
holder.chapter_details.text = chapter_descrs[position]
holder.chapter_image.setImageResource(chapter_images[position])
when(position){
0 -> holder.chapter_card.setBackgroundColor(Color.parseColor("#ff5668"))
1 -> holder.chapter_card.setBackgroundColor(Color.parseColor("#41d5e2"))
2 -> holder.chapter_card.setBackgroundColor(Color.parseColor("#4d53e0"))
3 -> holder.chapter_card.setBackgroundColor(Color.parseColor("#ff8e36"))
}
}
And it works, it may not be the right way to do it, but it works. However, there is one problem. In the screenshot, you can see that the last item has a border radius. I set the cardCornerRadius value for the element in card_layout.xml And for some reason, when I assign a custom color for an item, this value disappears. This can be seen in the screenshot. The last element with a light green background has a border radius (I did not assign a custom background color value to this element) and the first four elements that have a custom color assigned do not have a border radius.
Please tell me why this is happening and how to fix it. I need to keep the border radius for all elements.
I really glad I fond the error by myself. For setting the item background color I used setBackgroundColor but it's not correct in my case. In my case I have to use setCardBackgroundColor because I setting background for the CardView. Now my code looks:
override fun onBindViewHolder(holder: ChapterListAdapter.ViewHolder, position: Int) {
holder.chapter_item.text = chapter_titles[position]
holder.chapter_details.text = chapter_descrs[position]
holder.chapter_image.setImageResource(chapter_images[position])
when(position){
0 -> {
holder.chapter_card.setCardBackgroundColor(Color.parseColor("#ff5668"))
}
1 -> {
holder.chapter_card.setCardBackgroundColor(Color.parseColor("#41d5e2"))
}
2 -> {
holder.chapter_card.setCardBackgroundColor(Color.parseColor("#4d53e0"))
}
3 -> {
holder.chapter_card.setCardBackgroundColor(Color.parseColor("#ff8e36"))
}
}
}
And it works well.

RecyclerView custom LayoutManager remove unwanted views

I have a custom LayoutManager (inherited from LinearLayoutManager) that needs to calculate the item width of each child and remove all children from RecyclerView that has no space for them to appear.
Sample code (edited V2):
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: RecyclerView.State) {
super.onLayoutChildren(recycler, state)
// skip if orientation is vertical, for now we only horizontal custom menu
if (orientation == RecyclerView.VERTICAL) return
// skip if adapter has no items
if (itemCount == 0) return
var totalItemWidth = 0
var totalItemsCanFit = 0
// calculate menu item width and figure out how many items can fit in the screen
for (i in 0 until childCount) {
getChildAt(i)?.let { childView ->
totalItemWidth += getDecoratedMeasuredWidth(childView)
}
if (screenWidth > totalItemWidth) {
totalItemsCanFit++
}
}
// if all items can fit, do nothing and show the whole menu
if (childCount > totalItemsCanFit) {
// remove child views that have no space on screen
for (i in childCount - 1 downTo totalItemsCanFit) {
removeAndRecycleViewAt(i, recycler)
}
}
}
I have 2 questions:
Is the sample code above the correct way to approach this problem?
How can I add a 3-dot icon at the end after seeing that not all items could fit?
EDIT:
To clarify, what I am trying to achieve is a popup menu backed by a RecyclerView. The menu has no item limit, instead it should calculate each item width and remove all items that have no space. Also, add a 3-dot menu item as a more option at the end.
Regarding your first question:
See if addDisappearingView(View child) could help you,
according to the documentation:
To be called only during onLayoutChildren(Recycler, State) to add a
view to the layout that is known to be going away, either because it
has been removed or because it is actually not in the visible portion
of the container but is being laid out in order to inform RecyclerView
in how to animate the item out of view.
As for the second question - you simply need to implement a 'load more' feature to your recyclerView. How you'll implement this is up to your needs/design (if you want a button or auto-scroll...).
There are many tutorials for that, for example: https://androidride.com/android-recyclerview-load-more-on-scroll-example/ .

Override width to the half of the screen

I am developing a chat and i use Recyclerview to show the messages. When the message is an image i use Glide to display it. Using glide i use the override function to show the image with specific dimensions. However this does not work very good because not all the phones have the same resolution and size.
bitmap?.let {
//meaning the image is landscape view
if (bitmap.width > bitmap.height)
Glide.with(Application.instance)
.load(fileInfo.localUri)
.apply(RequestOptions().override(500, 250))
.into(holder.sntImageView)
else
Glide.with(Application.instance)
.load(fileInfo.localUri)
.apply(RequestOptions().override(250, 500))
.into(holder.sntImageView)
holder.sendingProgress.visibility = View.GONE
holder.sntBubble.visibility = View.GONE
holder.sntImageView.visibility = View.VISIBLE
} ?: run {
holder.sntImageView.visibility = View.GONE
holder.sendingProgress.visibility = View.GONE
holder.sntBody.text = "Unable to decode image"
}
So my question is how to use Glide so that the image has almost the half of the screen instead of 500, 250...?
You can start by knowing the with of your container, like recyclerviews width:
container.width
This will give you the width in pixels. After that you can apply a division to get the pixels at half:
val yourViewWidth = container.width / 2
That's how you make it to half of the container.
And then you just have to pass that value to Glide.
If you want to get total device width, you can get the pixels like this accepted answer and then apply the same division criteria.
Note that if you are in a recyclerview and want to know the size of a child view, you may need to wait the layout to be ready in order to have access to its parameters. That can be done with the ViewTreeObserver.OnGlobalLayoutListener, And do your operations inside onGlobalLayout. It will be triggered when the view is ready and you'll be able to operate there.
Hope it helps, happy coding!

ScrollViewer and SetHorizontalOffset

I use scrollviewer in my windows store application, but after calling SetHorizontalOffset function some times scrollviewer doesn't change horizontal scrolling. The same thing with vertical scrolling. Does anybody know how to work with it? May be scrollviewer scroll only for visibility for offset (I mean that if it's see that user can see offset its doesn't scroll at all)
It appears the ScrollViewer's offset lags by one frame.
If the below code is run every frame, diffPrevDesiredActual is always 0. That is to say, the value provided by ChangeView does not take affect immediately.
...
var scrollPosition = /*some new value*/;
MyScrollViewer.ChangeView(null, scrollPosition, null, true);
var current = MyScrollViewer.VerticalOffset;
var diffDesiredActual = scrollPosition - current;
var diffPrevDesiredActual = previous - current;
previous = scrollPosition;
...
private double previous;
If nothing on your screen is animating when you change the scroll offset, then it is possible the ScrollViewer won't show the new value until something triggers the draw of a new frame. To test this hypothesis, try adding an infinite animation (eg ProgressRing) to ensure frames are constantly being drawn.