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

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.

Related

Jetpack compose: Top App Bar pinnedScrollBehavior not changing color on scroll from text focus events

Okay, so this is a bit of a specific issue, but hopefully someone understands what's happening:
I'm using a Jetpack Compose Material 3 CenterAlignedTopAppBar, with TopAppBarDefaults.pinnedScrollBehavior to make it so that the color of the appbar changes when I scroll down on nested content.
It works! In most cases. However, one of my screens has a large text field, that when clicked causes the content to scroll down by itself (to focus on the text field)—and that seems to confuse the appbar, which doesn't change color. It will only change color if I manually scroll up and down.
Relevant code:
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
Scaffold(
contentWindowInsets = EmptyWindowInsets,
modifier = Modifier
.fillMaxHeight()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
// just a wrapper around CenterAlignTopAppBar
StandardTopAppBar(scrollBehavior = scrollBehavior)
},
content = { innerPadding ->
// I've also tried with LazyColumn and see the same behavior
Column(
Modifier
.padding(innerPadding)
.padding(start = 10.dp, end = 10.dp)
.fillMaxHeight()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
tl;dr; manually scrolling changes the appbar color, but scrolling caused by clicking into a text field that scrolls into view does not. Any idea how I could fix this?
Probably not quite what you are looking for unfortunately, but I had a similar problem. My TopAppBar color was also stuck, but when navigating between different screens. I fixed it by assigning the TopAppBar a different scrollBehaviour depending on the screen.
val scrollBehavior1 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val scrollBehavior2 = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
In TopAppBar arguments:
scrollBehavior =
when (currentRoute) {
Screens.ExampleScreen1.route -> scrollBehavior1
Screens.ExampleScreen2.route -> scrollBehavior2
else -> null
}
Hopefully this helps you in one way or another.

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

How to change the fonts of all items on recyclerview runtime

I wanted to change font family of items on a recycler view every time I click a button.
So I coded like below.
rbAritaBuri = view.findViewById(R.id.rb_aritaBuri)
rbCafe24 = view.findViewById(R.id.rb_cafe24SurroundAir)
rbAritaBuri.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")
}
rbCafe24.setOnClickListener {
rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/cafe24_surround_air.ttf")
}
But it changes only the font family of the first item of the recycler view.
Is there a way to change fonts of them all together runtime? And please tell me why the code I wrote doesn't work right.
Thank you.
If I were in your position, I would:
Put your font changing calls inside of onBindViewHolder(). If you have to, you could put a bool in there like buttonClicked and link its value to your buttons.
Come up with a good way to force a call to onBindViewHolder(). Sometimes notifyDataSetChanged() is enough. But in some cases, you might have to remove the adapter by setting it to null and then reset the adapter to its original value.
Place that logic from step 2 inside of your buttons' onClick()s.
Edit:
What I mean is, create a var inside the class with the most exterior scope, so outside of oncreate().
var textChoice=""
Now use your buttons to change that var.
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
}
Now inside your onBindViewHolder(), make the font switch.
when (fontChoice){
"fonts/arita_buri.otf"->{ rv_work_preview.tv_work_content.typeface = Typeface.createFromAsset(requireActivity().assets, "fonts/arita_buri.otf")}
//and so on and so forth for all of your fonts
Now when you want to show the change, call notifyDatasetChanged(). I think maybe the best place to do that would be inside of your buttons. So maybe you'd actually have:
rbAritaBuri.setOnClickListener {
textChoice="fonts/arita_buri.otf"
<The name of your recyclerview adapter>.notifyDatasetChanged()
}
Here is how I solved it, thanks to D. Kupra:
class SampleWorkAdapter(private val context: Context) :
RecyclerView.Adapter<SampleWorkAdapter.ViewHolder>() {
var selectedFont = EditTextActivity.HAMBAK_SNOW
First, I assigned the default font Hambak_snow to selectedFont, type String.
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
fun changeFont(font: String) {
CustomFontHelper.setCustomFont(content, font, itemView.context)
} ...
}
Then I wrote a function to be called on onBindViewHolder to change font-family of textview, using custom typeface. https://stackoverflow.com/a/16648457/15096801 This post helped a lot.
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
...
viewHolder.changeFont(selectedFont)
...
}
Now, replaceFont will be called when the variable selectedFont get changed and adapter.notifyDatasetChanged() is called on an activity, like this:
rbMapoFlowerIsland.setOnClickListener {
sampleWorkAdapter.selectedFont = EditTextActivity.MAPO_FLOWER
sampleWorkAdapter.notifyDataSetChanged()
}

offset set with getItemOffsets disappears after scroll

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.

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/ .