I already tried this solution here, but unfortunately doesnt work in my scenario.
Ill keep it simple: I have multiple viewHolders with multiple animations for a chat App,
Since I have no touch listeners to register the adapter position of the typing indicators, I have:
In my CustomAdapter
private var typingIndicatorAdapterPosition: Int = -1
private var inlineErrorAdapterPosition: Int = -1
In my onBindViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
...
USER_REQUEST_TEXT -> {
val userRequestViewHolder = (holder as UserRequestViewHolder)
configUserRequestViewHolder(userRequestViewHolder, position)
userRequestViewHolder.setIsRecyclable(false)
}
TYPE_INDICATOR -> {
val typingIndicatorViewHolder = (holder as TypingIndicatorViewHolder)
configTypingIndicatorViewHolder(typingIndicatorViewHolder, position)
typingIndicatorAdapterPosition = typingIndicatorViewHolder.layoutPosition
typingIndicatorViewHolder.setIsRecyclable(true)
}
INLINE_ERROR -> {
val inlineErrorViewHolder = (holder as InlineErrorViewHolder)
configInlineErrorViewHolder(inlineErrorViewHolder, position)
inlineErrorAdapterPosition = inlineErrorViewHolder.layoutPosition
inlineErrorViewHolder.setIsRecyclable(true)
}
}
}
my adapter code for deletion :
fun removeTypingIndicator() {
if(typingIndicatorAdapterPosition > 0) {
if(messageContainerList[typingIndicatorAdapterPosition].messageType == TYPE_INDICATOR) {
messageContainerList.removeAt(typingIndicatorAdapterPosition)
notifyItemRemoved(typingIndicatorAdapterPosition)
notifyItemRangeChanged(typingIndicatorAdapterPosition, itemCount - 1)
typingIndicatorAdapterPosition = -1
}
}
}
Note - I do not prefer notifyDataSetChanged() etc. as it cancels the animations.
here are some screen shots:
This line looks incorrect:
notifyItemRangeChanged(typingIndicatorAdapterPosition, itemCount - 1)
Assuming you're trying to change all the items from the typing indicator position through the end of the list, it looks like you're trying to use it as (start, end), but the parameters are actually (start, itemCount), with itemCount being the number of items changed at that index. See the documentation here for more information.
Related
I have a list of the RecyclerView. And I made a swipe removal. Then I made a Snackbar in MainActivity to undo the removal:
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
adapter.removeItem(
viewHolder.absoluteAdapterPosition
)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(
viewHolder.absoluteAdapterPosition)
}
show()
}
}
}
}
}
Code in adapter:
fun removeItem(pos: Int) {
listArray.removeAt(pos)
notifyItemRemoved(pos)
}
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos])
notifyItemInserted(pos)
}
And when I make the undo operation, my app stops, and I see this in a Logcat:
java.lang.ArrayIndexOutOfBoundsException: length=10; index=-1
at java.util.ArrayList.get(ArrayList.java:439)
at com.example.databaselesson.recyclerView.ExpensesAdapter.restoreItem(ExpensesAdapter.kt:79)
at com.example.databaselesson.MainActivity2$onSwipe$1.onSwiped$lambda-1$lambda-0(MainActivity2.kt:391)
at com.example.databaselesson.MainActivity2$onSwipe$1.$r8$lambda$AhJR3pu-3ynwFvPp66LdaLyFdB0(Unknown Source:0)
at com.example.databaselesson.MainActivity2$onSwipe$1$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
Please, help
If you need more code, please, write, and I will send you it
When you delete the item and do notifyItemRemoved, the ViewHolder being used to display that item is removed from the list. Since it's not displaying anything, its absoluteAdapterPosition is set to NO_POSITION, or -1:
Returns int
The adapter position of the item from RecyclerView's perspective if it still exists in the adapter and bound to a valid item. NO_POSITION if item has been removed from the adapter, notifyDataSetChanged has been called after the last layout pass or the ViewHolder has already been recycled.
So when you tap your UNDO button, that viewholder is going to return -1, which is not a valid index for your data list!
You should probably store the actual position you're removing:
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
// get the position first, and store that value
val position = viewHolder.absoluteAdapterPosition
when (direction) {
ItemTouchHelper.RIGHT -> {
// using the position we stored
adapter.removeItem(position)
// you don't have to use apply here if you don't want - it's designed
// to be chained (fluent interface where each call returns the Snackbar)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
// using that fixed position value again
.setAction("Undo") { adapter.restoreItem(position) }
.show()
}
}
}
This way you're removing a specific item position, and if the undo button is hit, you use the same position value to restore it. You're not relying on the state of the ViewHolder that was being used.
Also this:
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos])
notifyItemInserted(pos)
}
doesn't seem to restore anything? It just inserts a copy of item pos at the same position. Since your removeItem actually deletes the item from the list, there's no way to get it back unless you store it somewhere. You could have a lastDeletedItem variable that you update in removeItem that restoreItem restores:
var lastDeletedItem: Item? = null
fun removeItem(pos: Int) {
// store the deleted item
lastDeletedItem = listArray[pos]
listArray.removeAt(pos)
notifyItemRemoved(pos)
}
fun restoreItem(pos: Int) {
// restore the last thing that was deleted at this position
lastDeletedItem?.let {
listArray.add(pos, it)
notifyItemInserted(pos)
}
}
But then you have the item that was deleted in one place, and the position in another (the snackbar lambda) so you might want to just put them both together - store the lastDeletedPosition in removeItem and reference that in restoreItem (don't pass pos in), or make restoreItem take a pos and item and fetch the item in your swipe callback, when you store the current adapter position
There are two issues here.
1st: Call viewHolder.absoluteAdapterPosition after notifyItemRemoved shall return -1
This match the exception in your Logcat since it is telling you that you are trying to get index=-1 from listArray.
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
adapter.removeItem(
viewHolder.absoluteAdapterPosition //<==Let's say position return 8
)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(
viewHolder.absoluteAdapterPosition) //<==Deselected item so it shall return -1
}
show()
}
}
}
}
}
2nd: You haven't cached the item object so it will fail to retrieve the correct data
// Assume that `listArray` = ["A", "B", "C"], `pos` = 1
fun removeItem(pos: Int) {
listArray.removeAt(pos) = ["A", "C"]
notifyItemRemoved(pos)
}
// `listArray` = ["A", "C"], `pos` = 1 (Assume you get the correct target pos)
fun restoreItem(pos: Int) {
listArray.add(pos, listArray[pos]) //`listArray[1]` = "C", listArray = ["A", "C", "C"]
notifyItemInserted(pos)
}
In order to resolve this, you will need to cache both the position and item object in onSwiped call
val onSwipe = object : OnSwipe(this) {
override fun onSwiped(viewHolder: ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.RIGHT -> {
val cachedPosition = viewHolder.absoluteAdapterPosition // cache position!
val cachedItem = listArray[cachedPosition] // cache item!
adapter.removeItem(cachedPosition)
Snackbar.make(binding.rv, "Deleted", Snackbar.LENGTH_SHORT)
.apply {
setAction("Undo") {
adapter.restoreItem(cachedPosition, cachedItem)
}
show()
}
}
}
}
}
today I was trying to implement custom ads inside a horizontally oriented recycler view.
Everything went fine, till I ran the app and noticed that some of the items inside my MutableList are not displayed (or are being displayed as blank spaces, don't know for sure) and right after every ad (only does that after ads) there's a huge blank space.
I don't know what to do to solve this, I'm not familiar with multiple layouts inside an adapter.
Adapter declaration:
class CardAdapter (val context2: Context, private val Cards:MutableList<Card>) : RecyclerView.Adapter<RecyclerView.ViewHolder>()
This is my ad holder inside the adapter:
inner class HolderNativeAd(itemView: View): RecyclerView.ViewHolder(itemView){
val app_ad_background : ImageView = itemView.findViewById(R.id.ad_icon)
val ad_headline : TextView = itemView.findViewById(R.id.ad_headline)
val ad_description : TextView = itemView.findViewById(R.id.ad_description)
val ad_price : TextView = itemView.findViewById(R.id.ad_price)
val ad_store : TextView = itemView.findViewById(R.id.ad_store)
val call_to_action : CardView = itemView.findViewById(R.id.ad_call_to_action)
val ad_advertiser : TextView = itemView.findViewById(R.id.ad_advertiser)
val nativeAdView : NativeAdView = itemView.findViewById(R.id.nativeAdView)
fun createAD(context : Context){
val adLoader = AdLoader.Builder(context, context.getString(R.string.native_ad_id_test))
.forNativeAd { nativeAd ->
Log.d(TAG, "onNativeAdLoaded: ")
displayNativeAd(this#HolderNativeAd, nativeAd)
}.withNativeAdOptions(NativeAdOptions.Builder().build()).build()
adLoader.loadAd(AdRequest.Builder().build())
}
}
onCreateViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view: View
if(viewType == VIEW_TYPE_CONTENT){
view = LayoutInflater.from(context2).inflate(R.layout.item_card, parent, false)
return HolderCards(view)
}else{
view = LayoutInflater.from(context2).inflate(R.layout.native_ad_card, parent, false)
return HolderNativeAd(view)
}
}
onBindViewHolder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (getItemViewType(position) == VIEW_TYPE_CONTENT) {
val model: Card = Cards[position]
(holder as HolderCards).setCard(model, context2)
} else if (getItemViewType(position) == VIEW_TYPE_AD) {
(holder as HolderNativeAd).createAD(context2)
}
}
getItemViewType
override fun getItemViewType(position: Int): Int {
//logic to display Native Ad between content
if(position != 0) {
return if (position % 2 == 0) {
//after 2 items, show native ad
VIEW_TYPE_AD
} else {
VIEW_TYPE_CONTENT
}
}
return VIEW_TYPE_CONTENT
}
And getitemCount() returns Cards.size
Cards mutable population:
currenctly I have a SingleValueEventListener which grabs the cards and puts them inside a mutableList calling adapter.NotifyItemInserted() for each item.
displayNativeAd (custom method used in the ad holder)
private fun displayNativeAd(holderNativeAd: CardAdapter.HolderNativeAd, nativeAd: NativeAd) {
/* Get Ad assets from the NativeAd Object */
val headline = nativeAd.headline
val body = nativeAd.body
val background = nativeAd.icon
val callToAction = nativeAd.callToAction
val price = nativeAd.price
val store = nativeAd.store
val advertiser = nativeAd.advertiser
...
... (checks to see if a val is null or not)
holderNativeAd.nativeAdView.setNativeAd(nativeAd)
}
All right buckle up because this is a long one! It's actually the "adding ads" part that's complicating things here, not the extra ViewHolder type.
You're missing items because you're replacing some of them with ads. The total number of items (itemCount) in your Adapter should be the number of cards plus the number of ads you want to display.
Because you're not handling that, you're effectively skipping over items in cards with this code:
override fun getItemViewType(position: Int): Int {
//logic to display Native Ad between content
if(position != 0) {
return if (position % 2 == 0) {
//after 2 items, show native ad
VIEW_TYPE_AD
} else {
VIEW_TYPE_CONTENT
}
}
return VIEW_TYPE_CONTENT
}
You have cards.size number of items, and instead of showing cards[2] you show an ad instead, and cards[2] never gets shown. (Also that code shows an ad every two items btw, position % 2 either produces a 0 or 1, so it loops every second number - you want position % 3 so it's every multiple of three. But there's more to it than that, we'll get to it!)
So you need logic to handle the fact that your data (cards) and your contents (cards + ads) are different:
itemCount needs to include the appropriate number of ads
getItemViewType needs to know if position holds an ad or a card
onBindViewHolder needs to be able to translate position to the appropriate index in cards when displaying a card
Let's lay down the rules first - let's say that you want an ad displayed as every third item, that starts after the first two items, and you're happy to end with an ad, to make things simple.
So the number of ads is just how many groups of 2 there are - integer division will do that:
val adCount = cards.size / 2
The total number of items is that plus the number of cards:
override fun getItemCount() = cards.size + (cards.size / 2)
Working out whether position is a card or an ad is simple enough, it's basically what you already did! Except we need to handle every third item as an ad. We also need to account for the zero-based indexing:
| | |
0 1 2 3 4 5 6 7 8 9
We get ads on 2, 5 and 8. We care about finding multiples of 3 (where the modulo operation returns zero) so we can add 1 to each position. This also eliminates the need to check if position == 0 (that special edge case was a sign your logic wasn't consistent - don't worry I only realised that while writing this!)
fun isCard(position: Int) = (position + 1) % 3 != 0
Note that we're using 3 here because we're dealing with the position in the list which has been padded out with an ad every 2 places. Every 2 items in cards has become 2+1 items in the adapter's content.
Really we should be using a constant, val ITEMS_PER_AD = 2 and deriving another value from that, val AD_FREQUENCY = ITEMS_PER_AD + 1. Avoids magic numbers that are hard to read and work with, and easy to mess up. This is clearer (maybe with better names!) and you can just change ITEMS_PER_AD to change how many there are, and everything else will adjust along with it
Translating from a position to a card is the last bit. You have to account for when a position isn't a valid card, i.e. isCard is false. It's easiest to return null here in that case.
It might help to look at how the translations should work out:
position: 0 1 2 3 4 5 6 7 8 9
card index: 0 1 x 2 3 x 4 5 x 6
Yep it's one of them logic puzzles - what's the pattern in this progression?
The offset is happening every multiple of 3 items, so what if we divide position by 3 and subtract it, removing those offsets?
position: 0 1 2 3 4 5 6 7 8 9
pos / 3: 0 0 0 1 1 1 2 2 2 3
card index: 0 1 x 2 3 x 4 5 x 6
Nice, that looks good! So now, we need to either return null if it's not a card, otherwise fetch the appropriate card from the data set:
fun getCardForPosition(position: Int): Card? {
val offset = position / 3
return if (isCard(position)) cards[position - offset] else null
}
Those are the pieces required to size your list properly, work out if a particular position is a card or an ad, and fetch the appropriate card from your data. Hopefully you can see how to work that into the Adapter methods to work out which itemViewType you need, etc.
You could actually just try to getCardForPosition in onBindViewHolder and if the result is null, display an ad (and cast the ViewHolder you've been passed to the ad one, since that's what you should be getting as they're all using the same functions to determine what's what). Lots of options, the logic around the list is the hard part!
As for the spaces, see if it works when you have everything displaying correctly. It might resolve itself, or it might be a layout issue with your ad items. Make sure their width isn't match_parent or anything. You can always use the Layout Inspector with a running app to see exactly what's happening in the layout on the screen, might give you some clues
I wanted to check I hadn't missed anything so I wrote a basic implementation if it helps:
data class Card(val info: String)
class Adapter(private val cards: List<Card>) : RecyclerView.Adapter<Adapter.MyViewHolder>() {
private fun isCard(position: Int) = (position + 1) % AD_FREQUENCY != 0
private fun getCardForPosition(position: Int): Card? {
val offset = position / AD_FREQUENCY
return if (isCard(position)) cards[position - offset] else null
}
override fun getItemViewType(position: Int) =
if (isCard(position)) CARD_VIEWTYPE else AD_VIEWTYPE
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = ItemViewBinding.inflate(inflater, parent, false)
return if (viewType == AD_VIEWTYPE) MyViewHolder.AdViewHolder(binding)
else MyViewHolder.CardViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val card = getCardForPosition(position)
if (card == null) (holder as MyViewHolder.AdViewHolder).binding.textView.text = "AD"
else (holder as MyViewHolder.CardViewHolder).binding.textView.text = card.info
}
override fun getItemCount() = cards.size + (cards.size / ITEMS_PER_AD)
sealed class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
class AdViewHolder(val binding: ItemViewBinding) : MyViewHolder(binding.root)
class CardViewHolder(val binding: ItemViewBinding) : MyViewHolder(binding.root)
}
companion object {
const val ITEMS_PER_AD = 3
const val AD_FREQUENCY = ITEMS_PER_AD + 1
const val AD_VIEWTYPE = 0
const val CARD_VIEWTYPE = 1
}
}
// set up with
recycler.layoutManager =
LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
recycler.adapter = Adapter(List(32) { Card("Content $it") })
Really simple, just uses the same layout for both ViewHolders with a TextView in it. Fixed size for the layout, no spaces popping up:
Hope it helps!
Yes this works fine, I have similar thing which can help too.
class MyVideoAdapter() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val TAG = "AdsCalled"
companion object {
const val AD_DISPLAY_FREQUENCY = 3
const val ITEM_TYPE = 1
const val AD_TYPE = 0
}
private val adItems: MutableList<NativeAd>
init {
adItems = ArrayList()
}
private var myResult: MyResult? = null
set(value) {
field = value
notifyDataSetChanged()
}
private val itemList get() = myResult?.myVideos?.list?: emptyList()
class ItemHolder(val binding: ItemSingleVideoBinding) : RecyclerView.ViewHolder(binding.root)
class ItemAdHolder(val binding: ItemSingleVideoAdBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
with(binding) {
nativeAdView.iconView = adAppIcon
nativeAdView.headlineView = adHeadline
nativeAdView.advertiserView = adAdvertiser
nativeAdView.priceView = adPrice
nativeAdView.storeView = adStore
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
if (viewType == AD_TYPE)
ItemAdHolder(ItemSingleVideoAdBinding.inflate(
LayoutInflater
.from(parent.context), parent, false))
else
ItemHolder(ItemSingleVideoBinding.inflate(
LayoutInflater
.from(parent.context),parent,false))
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == AD_TYPE) {
val adHolder = holder as ItemAdHolder
var ad: NativeAd? = null
if (adItems.size > position / AD_DISPLAY_FREQUENCY) {
ad = adItems[position / AD_DISPLAY_FREQUENCY]
} else {
val nativeAdOptions =
NativeAdOptions.Builder().setMediaAspectRatio(MediaAspectRatio.LANDSCAPE)
.build()
val builder = AdLoader.Builder(adHolder.binding.root.context,
"ca-app-pub-3940256099942544/2247696110")
val adLoader: AdLoader = builder.forNativeAd { nativeAd ->
ad = nativeAd
adItems.add(nativeAd)
}.withNativeAdOptions(nativeAdOptions)
.withAdListener(object : AdListener() {
override fun onAdFailedToLoad(p0: LoadAdError) {
Log.d(TAG, "onAdFailedToLoad: Failed : ${p0.message}")
}
})
.build()
adLoader.loadAd(AdRequest.Builder().build())
}
ad?.let { nativeAd ->
adHolder.binding.run {
adHeadline.text = nativeAd.headline
adPrice.text = nativeAd.price
adStore.text = nativeAd.store
adAdvertiser.text = nativeAd.advertiser
adAppIcon.setImageDrawable(nativeAd.icon?.drawable)
nativeAdView.setNativeAd(nativeAd)
}
}
} else {
val index = position - position / AD_DISPLAY_FREQUENCY - 1
val item= itemList[index]
val itemHolder = holder as ItemHolder
}
}
}
}
override fun getItemCount() = (itemList.size + adItems.size)
override fun getItemViewType(position: Int): Int {
if (position % AD_DISPLAY_FREQUENCY == 0)
AD_TYPE
else ITEM_TYPE
}
fun clearResult() {
myResult = null
notifyDataSetChanged()
}
fun setResult(myResult : MyResult) {
this.myResult = myResult
notifyDataSetChanged()
}
}
But the main problem here is, what if the Admob failed to load the ads ?
If there's a condition when ads are not loading from the server at that time:
adsItem Size = 0
itemsList Size = 20 (Assume)
AD_DISPLAY_FREQUENCY = 3
So, after every 2 post an Ad will be displayed, and in getItemViewType method we have the modulas function (position%AD_FREQ..)
So. by default it will return the AD_TYPE and the AD Will not be loaded, resulting in empty ItemAdHolder layout inflation. Moreover we will skip the Post Item, as the size of adslist is 0 and we are updating the index for post items, so how to resolve this thing ? I tried checking the adItems size before getting viewType but it's not helping
What I have tried till now is
override fun getItemViewType(position: Int): Int {
return if (position == 0) 0 else
if (position % AD_DISPLAY_FREQUENCY == 0)
AD_TYPE
else ITEM_TYPE
}
override fun getItemCount() = if (itemList .isEmpty()) 0 else (itemList .size + adItems.size)
In bindViewHolder() for ITEM_TYPE case
val index = if (adItems.isNotEmpty()) position - position / AD_DISPLAY_FREQUENCY - 1 else position
I have an arrayList<> of strings and I added 10 strings to it.
private val names: ArrayList<String> = arrayListOf()
These are the strings added
[G.I. Joe: The Rise of Cobra, Creed 2, The Equalizer 2, Ride Along 2, Mission Impossible, Mission Impossible II, Mission Impossible III, Mission Impossible: Ghost Protocol, Mission Impossible: Fallout, Suicide Squad]
I have a recycler view with its adapter as follows:
class MovieSeriesAdapter(
private val movie: MoviesInSeries,
private val movieNameList: ArrayList<String>?,
private val restMoviesPosition: Int,
): RecyclerView.Adapter<MovieSeriesAdapter.ViewHolder>() {
class ViewHolder(binding: SeriesMoviesItemBinding): RecyclerView.ViewHolder(binding.root) {
val mainThing = binding.mainThing
val tvMovieNameAndYear = binding.seriesMovieNameAndYear
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(SeriesMoviesItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (position % 2 != 0) {
holder.mainThing.setBackgroundColor(Color.parseColor("#E8E8E8"))
}
val RestMoviesList = Constants.getRestOfSeriesMovies()
var targetPosition: Int = 0
when (movie.originalMovieName) {
"G.I. Joe: Retaliation" -> targetPosition = 0
"Creed" -> targetPosition = 1
"The Equalizer" -> targetPosition = 2
"Ride Along" -> targetPosition = 3
"Mission Impossible" -> targetPosition = 4
"Suicide Squad" -> targetPosition = 9
"Venom" -> targetPosition = 10
}
val targetMovieName = movieNameList!![targetPosition]
holder.tvMovieNameAndYear.text = "$targetMovieName ($targetMovieDate)"
}
override fun getItemCount(): Int {
return 5
}
The thing is that sometimes I need the recycler view to show 5 items, as in the case below. For Example, I have 5 movies of mission impossible and I want to show them. But the targetPosition is an integer variable of only one number.
I tried creating an array list of only the mission impossible movies but it showed identical 5 items with all of the data in one item.
How do I make it that when I need 5 items to be displayed, each item should get a diffent value from the name array list.
I'll recommend you to directly use the value of position for targetValue, inside onBindViewHolder while setting the value of text.
You should just give the adapter a list of the 5 you want to show.
Also, a lot of the code you wrote doesn't make a lot of sense.
For example
when (movie.originalMovieName) {
You decide here something based on movie.originalMovieName, but this movie is passed in the constructor of the adapter, which already doesn't make sense, but it also means that it will be the same movie for every item.
I checked all the examples, but they don't work after all. As far as I know, even if payload is 'List', String or Int value can go into.
class RecordListAdapter (val context: Context, val layoutManager: LinearLayoutManager, private val canBeEdited: Boolean)
: RecyclerView.Adapter<RecordListAdapter.RecordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var records: ArrayList<Record> = arrayListOf()
// Update ALL VIEW holder
override fun onBindViewHolder(holder: RecordViewHolder, position: Int) {
val current = records[position]
holder.autoCompleteTextView.text = SpannableStringBuilder(current.name)
holder.weightPicker.value = current.weight
holder.setPicker.value = current.set
holder.repsPicker.value = current.reps
if(position == itemCount - 1) holder.addBtn.visibility = View.VISIBLE
else holder.addBtn.visibility = View.GONE
if(canBeEdited) {
if(itemCount == 1) {
holder.deleteBtn.visibility = View.GONE
} else {
holder.deleteBtn.visibility = View.VISIBLE
holder.deleteBtn.setOnClickListener {
records.remove(current)
notifyItemRemoved(position)
}
}
} else
holder.deleteBtn.visibility = View.GONE
}
// Update only part of ViewHolder that you are interested in
override fun onBindViewHolder(holder: RecordViewHolder, position: Int, payloads: MutableList<Any>) {
Log.e("payload", "::$payloads")
if(payloads.isNotEmpty()) {
} else
super.onBindViewHolder(holder,position, payloads)
}
private fun addRecordDefault() {
this.records.add(Record("", 28, 5, 10))
notifyItemInserted(itemCount)
notifyItemRangeChanged(itemCount-1, 2, "PAYLOAD_ADD_BTN")
}
override fun getItemCount() = records.size
}
As above code, I set the Log.e to know whether the value is empty or not. The payload Log.e always say it's null.
E/payload: ::[]
Firstly, it seems you are just adding the item and want to change something in it with payloads right away.
Obviously, when you just add a new one, the whole item has to be drawn, thus no payloads needed.
Only then, when it is already drawn and you want to change some elements, you may use payloads with notifyItemChanged (if one item was changed) or notifyItemRangeChanged (if several items were changed).
Secondly, I am not sure regarding the range you use.
The first argument of notifyItemRangeChanged is the start index.
The second one is how many items you want to change.
Thirdly, it's not clear where do you call the addRecordDefault
So make sure you called notifyItemChanged or notifyItemRangeChanged with payloads.
I have a RecyclerView managed by a LinearlayoutManager, if I swap item 1 with 0 and then call mAdapter.notifyItemMoved(0,1), the moving animation causes the screen to scroll. How can I prevent it?
Sadly the workaround presented by yigit scrolls the RecyclerView to the top. This is the best workaround I found till now:
// figure out the position of the first visible item
int firstPos = manager.findFirstCompletelyVisibleItemPosition();
int offsetTop = 0;
if(firstPos >= 0) {
View firstView = manager.findViewByPosition(firstPos);
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView);
}
// apply changes
adapter.notify...
// reapply the saved position
if(firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop);
}
Call scrollToPosition(0) after moving items. Unfortunately, i assume, LinearLayoutManager tries to keep first item stable, which moves so it moves the list with it.
Translate #Andreas Wenger's answer to kotlin:
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
var offsetTop = 0
if (firstPos >= 0) {
val firstView = manager.findViewByPosition(firstPos)!!
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView)
}
// apply changes
adapter.notify...
if (firstPos >= 0) {
manager.scrollToPositionWithOffset(firstPos, offsetTop)
}
In my case, the view can have a top margin, which also needs to be counted in the offset, otherwise the recyclerview will not scroll to the intended position. To do so, just write:
val topMargin = (firstView.layoutParams as? MarginLayoutParams)?.topMargin ?: 0
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - topMargin
Even easier if you have ktx dependency in your project:
offsetTop = manager.getDecoratedTop(firstView) - manager.getTopDecorationHeight(firstView) - firstView.marginTop
I've faced the same problem. Nothing of the suggested helped. Each solution fix and breakes different cases.
But this workaround worked for me:
adapter.registerAdapterDataObserver(object: RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
if (fromPosition == 0 || toPosition == 0)
binding.recycler.scrollToPosition(0)
}
})
It helps to prevent scrolling while moving the first item for cases: direct notifyItemMoved and via ItemTouchHelper (drag and drop)
I have faced the same problem. In my case, the scroll happens on the first visible item (not only on the first item in the dataset). And I would like to thanks everybody because their answers help me to solve this problem.
I inspire my solution based on Andreas Wenger' answer and from resoluti0n' answer
And, here is my solution (in Kotlin):
RecyclerViewOnDragFistItemScrollSuppressor.kt
class RecyclerViewOnDragFistItemScrollSuppressor private constructor(
lifecycleOwner: LifecycleOwner,
private val recyclerView: RecyclerView
) : LifecycleObserver {
private val adapterDataObserver = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
suppressScrollIfNeeded(fromPosition, toPosition)
}
}
init {
lifecycleOwner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun registerAdapterDataObserver() {
recyclerView.adapter?.registerAdapterDataObserver(adapterDataObserver) ?: return
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun unregisterAdapterDataObserver() {
recyclerView.adapter?.unregisterAdapterDataObserver(adapterDataObserver) ?: return
}
private fun suppressScrollIfNeeded(fromPosition: Int, toPosition: Int) {
(recyclerView.layoutManager as LinearLayoutManager).apply {
var scrollPosition = -1
if (isFirstVisibleItem(fromPosition)) {
scrollPosition = fromPosition
} else if (isFirstVisibleItem(toPosition)) {
scrollPosition = toPosition
}
if (scrollPosition == -1) return
scrollToPositionWithCalculatedOffset(scrollPosition)
}
}
companion object {
fun observe(
lifecycleOwner: LifecycleOwner,
recyclerView: RecyclerView
): RecyclerViewOnDragFistItemScrollSuppressor {
return RecyclerViewOnDragFistItemScrollSuppressor(lifecycleOwner, recyclerView)
}
}
}
private fun LinearLayoutManager.isFirstVisibleItem(position: Int): Boolean {
apply {
return position == findFirstVisibleItemPosition()
}
}
private fun LinearLayoutManager.scrollToPositionWithCalculatedOffset(position: Int) {
apply {
val offset = findViewByPosition(position)?.let {
getDecoratedTop(it) - getTopDecorationHeight(it)
} ?: 0
scrollToPositionWithOffset(position, offset)
}
}
and then, you may use it as (e.g. fragment):
RecyclerViewOnDragFistItemScrollSuppressor.observe(
viewLifecycleOwner,
binding.recyclerView
)
LinearLayoutManager has done this for you in LinearLayoutManager.prepareForDrop.
All you need to provide is the moving (old) View and the target (new) View.
layoutManager.prepareForDrop(oldView, targetView, -1, -1)
// the numbers, x and y don't matter to LinearLayoutManager's implementation of prepareForDrop
It's an "unofficial" API because it states in the source
// This method is only intended to be called (and should only ever be called) by
// ItemTouchHelper.
public void prepareForDrop(#NonNull View view, #NonNull View target, int x, int y) {
...
}
But it still works and does exactly what the other answers say, doing all the offset calculations accounting for layout direction for you.
This is actually the same method that is called by LinearLayoutManager when used by an ItemTouchHelper to account for this dreadful bug.