Negative offset causes RecyclerView item to disappear - android-recyclerview

In my RecyclerView, I have a header image for each group of CardView. The image extends below the first card view to create a nice visual effect. I achieved this by creating an image header view holder, and set the offset of the first CardView in each group to be negative (using ItemDecoration). However, as you can see at the bottom of the screen, the CardView disappears before it goes completely offscreen.
My custom item decoration
class OffsetItemDecoration(val offset: Int, val itemPositions: Array<Int>) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
val layoutManager = parent.layoutManager
if (!itemPositions.contains(position) || layoutManager !is LinearLayoutManager) {
return
}
outRect.top = offset
}
}
In my MainActivity:
val itemOffset = OffsetItemDecoration(-150, arrayOf(1, 5))
recycler_view.addItemDecoration(itemOffset)
Recommendations of other ways to achieve the same effect would be appreciated as well

Related

How do I use ViewHolder to get a reference to items in the RecyclerView?

I am working with the LeDeviceListAdapter class in Android Studio. Specifically, I don't know how to call the ViewHolder for my RecyclerView in the getView function of this class.
override fun getView(i: Int, view: View?, viewGroup: ViewGroup?): View? {
var view: View? = view
val viewHolder: ViewHolder
// General ListView optimization code.
if (view == null) {
view = mInflator.inflate(R.layout.listitem_device, null)
viewHolder = ItemAdapter.ItemViewHolder(view)
viewHolder.device_name = view.findViewById(R.id.device_address) as TextView
viewHolder.device_address = view.findViewById(R.id.device_name) as TextView
view.setTag(viewHolder)
} else {
viewHolder = view.tag as ViewHolder
}
val device = mLeDevices[i]
val deviceName = device.name
if (deviceName != null && deviceName.length > 0) viewHolder.deviceName.setText(deviceName) else viewHolder.deviceName.setText(
R.string.unknown_device
)
viewHolder.deviceAddress.setText(device.address)
return view
}
I am getting an error where it says...
viewHolder = ViewHolder( )
The error says I can't instantiate an abstract class. I have tried to extend the ViewHolder class, and I have tried to access the ViewHolder through the RecyclerView.
How can I access the ViewHolder from this class?
Let me know if you need to see more of my project or hear more specifics.
UPDATE: I moved these two lines...
view = mInflator.inflate(R.layout.listitem_device, null)
viewHolder = ItemAdapter.ItemViewHolder(view)
...to the top of the function, and it resolved my problem with the ViewHolder.
For some reason, though, the line that says...
if (deviceName != null && deviceName.length > 0)
viewHolder.deviceName.setText(deviceName)
...cannot access the deviceName property of the ViewHolder the same way that I could above.

RecyclerView and notifyDataSetChanged LongClick mismatch

I'm having a weird problem with notifyDataSetChanged() in my Recycler Adapter. If I keep 5 items in an array the code works fine and I can check the checkbox at the item I LongClick, but when I add 5 items or more to the array other checkboxes get checked in my list.
I am using a boolean to toggle between VISIBLE and GONE on the checkboxes when the user LongClicks as well.
Here is my code:
class RecyclerAdapter(private val listActivity: ListActivity) : RecyclerView.Adapter<RecyclerAdapter.Holder>() {
lateinit var binding: ActivityListItemRowBinding
var checkboxesVisibility = false
val dummyArrayWorks = arrayOf("000", "111", "222", "333", "444")
val dummyArrayFails = arrayOf("000", "111", "222", "333", "444", "555")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
binding = ActivityListItemRowBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return Holder(binding)
}
override fun getItemCount(): Int = dummyArrayFails.size
#SuppressLint("NotifyDataSetChanged")
override fun onBindViewHolder(holder: Holder, position: Int) {
val item = dummyArrayFails[position]
holder.binding.checkbox.visibility = if (checkboxesVisibility) VISIBLE else GONE
holder.bindItem(item)
holder.itemView.setOnLongClickListener {
if (!checkboxesVisibility) {
checkboxesVisibility = true
holder.binding.checkbox.isChecked = true
notifyDataSetChanged()
true
} else {
false
}
}
holder.itemView.setOnClickListener {
if (!checkboxesVisibility) {
//Some other unrelated code
} else {
holder.binding.checkbox.isChecked = !holder.binding.checkbox.isChecked
notifyDataSetChanged()
}
}
}
class Holder(internal val binding: ActivityListItemRowBinding) : RecyclerView.ViewHolder(binding.root) {
var item = String()
fun bindItem(item: String) {
this.item = item
binding.itemPlaceHolder.text = item
}
}
}
I should add that when I remove the toggle for the checkboxes, and just show the checkboxes on first load, the clicks match the checkmarks without a problem.
Does anybody have any idea of what is going on? All help will be much appreciated!
The problem is you're holding your checked state in the ViewHolder itself - you're toggling its checkbox on and off depending on clicks, right?
The way a RecyclerView works is that instead of having a ViewHolder for every single item (like a ListView does), it only creates a handful of them - enough for what's on screen and a few more for scrolling - and recycles those, using them to display different items.
That's what onBindViewHolder is about - when it needs to display the item at position, it hands you a ViewHolder from its pool and says here you go, use that to display this item's details. This is where you do things like setting text, changing images, and setting things like checkbox state to reflect that particular item.
What you're doing is you're not storing the item's state anywhere, you're just setting the checkbox on the view holder. So if you check it, every item that happens to be displayed in that reusable holder object will have its box ticked. That's why you're seeing it pop up on other items - that checked state has nothing to do with the items themselves, just which view holder they all happen to use because of their position in the list.
So instead, you need to keep their checked state somewhere - it could be as simple as a boolean array that matches the length of your item list. Then you just set and get from that when binding your data (displaying it). Working with what you've got:
// all default to false
val itemChecked = BooleanArray(items.size)
override fun onBindViewHolder(holder: Holder, position: Int) {
...
// when displaying the data, refer to the checked state we're holding
holder.binding.checkbox.checked = itemChecked[position]
...
holder.itemView.setOnLongClickListener {
...
// when checking the box, update our checked state
// since we're calling notifyDataSetChanged, the item will be redisplayed
// and onBindViewHolder will be called again (which sets the checkbox)
itemChecked[position] = true
// notifyItemChanged(position) is better here btw, just refreshes this one
notifyDataSetChanged()
...
}
}

How to show "Toast" after scrolling down a certain amount in Kotlin

I want to implement a feature in my app where it shows a Toast/message when a user scrolls down the recyclerview. For example when the user is scrolling down the screen, after they pass about 10 items a Toast pops up saying "Tap the home button to go back to the top"
How would I go about doing this?
I don't know if this would work, but you can try doing it in your adapter. like
when (position) {
10 -> Toast.makeText().show
}
or use an if statment.
Again, I don't know for sure if it would work, but I think so.
I think it's probably preferable to base it on distance scrolled instead of which item has appeared on screen most recently, so the threshold for when the message should be shown is not dependent on screen size. It's also best to keep this behavior out of the Adapter because of separation of concerns.
Here's a scroll listener you could use to do this behavior. I think the code is self-explanatory.
open class OnScrolledDownListener(
private val context: Context,
private val thresholdDp: Int,
var resetOnReturnToTop: Boolean = true
): RecyclerView.OnScrollListener() {
private var eventFired = false
private var y = 0
open fun onScrolledDown() {}
open fun onScrolledBackToTop() {}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
y += dy
val yDp = (y / context.resources.displayMetrics.density).roundToInt()
if (yDp >= thresholdDp && !eventFired) {
eventFired = true
onScrolledDown()
} else if (resetOnReturnToTop && yDp == 0 && eventFired) {
eventFired = false
onScrolledBackToTop()
}
}
}
You can subclass and override the two events for when it scrolls down at least a certain amount for the first time (onScrolledDown), and for when it is scrolled back to the top and resets itself (onScrolledBackToTop).
myRecyclerView.addOnScrollListener(object: OnScrolledDownListener(context, 120) {
override fun onScrolledDown() {
showMyMessage()
}
override fun onScrolledBackToTop() {
hideTheMessage()
}
})

Change the background color of a texview from a list in the spinner in android studio with Kotlin

I wanted one more time for your help. I'm starting with the android kotlin programming. how can I change the background color of a textview from a list of colors that I have in a spinner
I have created drawable with the colors but I don't know how to select a list color to change the background color of that textview
val arrayscolor= arrayOf("#FF03DAC5","#FF414141")
spinner.adapter=ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,arryscolor)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long
) {
txtmensaje.setBackgroundColor(Color.parseColor(position.toString()))
}
override fun onNothingSelected(parent: AdapterView<*>?) {
txtmensaje.setText("")
}
}
I want to mention that I don't know how to put that drawable to my color arrays because I put that color by default, I want to add that how to make the user look at the color and not the exaggecimal number
help thanks in advance
What you're doing wrong is that you're converting the position to String and parsing it as a Color which means if first is item is selected, you're parsing 0 to String as first item's position is 0 here :
Color.parseColor(position.toString())
What you've to do is you've to use that position and extract the value on that position from the array or the adapter as :
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long
) {
txtmensaje.setBackgroundColor(Color.parseColor(parent.getItemAtPosition(position).toString()))
//parent is the adapterView and position is the selected item's position.
}
Also, you can directly use the colorArray as
txtmensaje.setBackgroundColor(Color.parseColor(arrayscolor[position].toString()))
You can also do something else which I use in one of my projects :
Save the colors in colors.xml as resources for easy access.
Create a function as :
private fun chooseColorAsPerPosition(context: Context, position: Int): Int {
when (position) {
0 -> return context.resources.getColor(R.color.color1, null)
1 -> return context.resources.getColor(R.color.color2, null)
2 -> return context.resources.getColor(R.color.color3, null)
3 -> return context.resources.getColor(R.color.color4, null)
else -> return context.resources.getColor(R.color.defaultColor, null)
}
return context.resources.getColor(R.color.defaultColor, null)
}
Call the function as in onItemSelected() as :
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
txtmensaje.setBackgroundColor(chooseColorAsPerPosition(this#YourActivity, position))
//pass the context, for activity this or this#YourActivity and position is the selected item's position.
}
This helps when you need these values in multiple places, in multiple Acitivites.

ImageView seem to be recycled as soon they leave the screen in a RecyclerView

I just started using a RecyclerView to display some rows containing an ImageView and a TextView.
Everything is properly displayed when I open the screen. If I scroll fast to the bottom of the list then back up quickly there's no issue.
However, as soon as I scroll down slowly to display the next row and the 1st one isn't displayed anymore scrolling back up will show a black square instead of my bitmap.
I thought onBindViewHolder() would be called again when scrolling up but this only happens after scrolling for 3 rows.
So it looks like the RecyclerView is prefetching some rows
I tried calling setItemPrefetchEnabled(false); on the layoutManager or setInitialPrefetchItemCount(0);
but it doesn't change anything
Any idea what's going on and how to fix this?
I'm doing a poke so the code is minimal here
// Fragment
final RecyclerView view = this.fragmentView.findViewById(R.id.recyclerView);
view.setHasFixedSize(true);
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
//layoutManager.setItemPrefetchEnabled(false);
//layoutManager.setInitialPrefetchItemCount(0);
view.setLayoutManager(layoutManager);
MyAdapter adapter = new MyAdapter(data);
view.setAdapter(adapter);
// ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder {
ImageView thumbnail;
TextView name;
public MyViewHolder(#NonNull View itemView) {
super(itemView);
}
}
// Adapter
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// create a new view
final View view = LayoutInflater.from(parent.getContext()).inflate(R.id.my_layout, parent, false);
final MyViewHolder viewHolder = buildViewHolder(view);
view.setTag(viewHolder);
return viewHolder;
}
#Override
public void onBindViewHolder(MyViewHolder viewHolder, int position) {
final MyData myData = this.data.get(position);
viewHolder.name.setText(myData.getName());
viewHolder.thumbnail.setImageBitmap(getBitmap(myData.getThumbnailPath()));
}
private MyViewHolder buildViewHolder(View view) {
final MyViewHolder viewHolder = new MyViewHolder(view);
viewHolder.name = view.findViewById(R.id.name);
viewHolder.thumbnail = view.findViewById(R.id.thumbnail);
return viewHolder;
}
I didn't understand your question properly, and I also can't comment.
But if your issue is that views are recycled try:
holder.setIsRecyclable(false).
inside onBindViewHolder()
this tells "hey recyclerview, stop recycling the views so much"
It looks like calling
this.view.setItemViewCacheSize(0);
on my RecyclerView fixes the issue, but I don't understand why this is necessary.
I still would want to understand what's happening here