Android RecyclerView Make fixed grid layout with combined rows - android

I would like to make a layout like this:
I tried with this code, but the second and third item's height are same as the first item.
val lm = GridLayoutManager(requireContext(), 2, RecyclerView.HORIZONTAL, false)
lm.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (position == 0) 2
else 1
}
}

Related

AdMob , ad item in Recycler View hides some items and creates spaces after the ad

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

Is it posible to change layoutmanager for a different viewtype?

Actually I have two viewtypes, a recyclerview is showing the elements in a gridLayout fashion.
Problem is that one of those elements in the grid should be show horizontally with a linearlayout aspect and the others as a grid.
How can I manage this ?
override fun getItemViewType(position: Int): Int {
return if(position %5 == 0) 1 else 0
}
This is my recyclerview
binding.rv_mylist.adapter = TestAdapter()
binding.rv_mylist.layoutManager = GridLayoutManager(requireContext(), 3)
in conclusion, what I need is that viewtype 1 shows with a LinearLayoutManager and viewtype 0 with a GridLayoutManager
You can achieve this by modify the spanSizeLookup of the GridLayoutManager, i.e.
layoutManager = GridLayoutManager(activity, 2).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return when(adapter.getItemViewType(position)) {
Type.Linear -> 1
Type.Grid -> 2
else -> 1
}
}
})
}
recyclerView.layoutManager = layoutManager

How to change the spancount for every viewholder with conditions other than position in android

I'm trying to change the spanCount for every viewHolder item in the Recyclerview according to the condition of the data in the items of the recyclerview.Currently, I'm changing the spanCount with the position. But How am I able to change the span of each Items(Viewholder) according to the conditions other than position? For example I want to do like if (type == Item.type) return 1
Some examples or hints would be lovely! I would love to hear from you!
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
return if (adapter.isMainHead(position) && tabIndex == 0) {
gridLayoutManager.spanCount
} else {
1
}
}
}
Hope this example will help you.
(layoutManager as GridLayoutManager).setSpanSizeLookup(object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
when (adapter.getItemViewType(position)) {
CalendarAdapter.TYPE_HEADER -> return 7
CalendarAdapter.TYPE_ITEM -> return 1
CalendarAdapter.TYPE_SPAN -> return dataArrayList[position].myVar.toInt()
else -> return -1
}
}
})

RecyclerView adds a "empty" layout item and when I click it the app crashes

I didn't add any data to my RecyclerView but it shows a empty box (the one I styled in the layouts for my data) anyways. It crashes with this errormessage
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
Here is my customAdapter:
class CustomAdapterExercise(var exerciseList: ArrayList<Exercise>, val addList: ArrayList<textAdd>) : RecyclerView.Adapter<CustomAdapterExercise.ViewHolder>() {
val typeAdd = 0
val typeExercise = 1
override fun getItemViewType(position: Int): Int {
if (position == exerciseList.size + 1) {
return typeAdd
}
else{
return typeExercise
}
}
//this method is returning the view for each item in the list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapterExercise.ViewHolder {
if (viewType == typeExercise) {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.exercise_layout, parent, false)
return ViewHolder(itemView)
} else {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.add_layout, parent, false)
return ViewHolder(itemView)
}
}
//this method is binding the data on the list
override fun onBindViewHolder(holder: CustomAdapterExercise.ViewHolder, position: Int) {
if (holder.itemViewType == typeAdd) {
holder.bindAdd(addList[0])
}
else{
if(position != exerciseList.size){
holder.bindItems(exerciseList[position])
}
}
}
//this method is giving the size of the list
override fun getItemCount(): Int {
return exerciseList.size + 2
}
//the class is hodling the list view
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bindItems(Exercise: Exercise) {
var exerciseAmount = itemView.findViewById<TextView>(R.id.exerciseAmount)
if(exerciseAmount != null){
exerciseAmount.text = Exercise.exAmount
}
}
fun bindAdd(textAdd: textAdd){
val addText = itemView.findViewById<TextView>(R.id.addText)
if(addText != null){
addText.text = textAdd.textAdd
}
}
}
}
Even if I add some data it still produces a empty box there and I don't get why.
I wonder how can I stop it from producing a empty box always?
These are issues with calculating the index in RecyclerView:
In getItemCount it should be + 1, instead of + 2, as it only needs to add one additional item for add button.
In getItemViewType position at the end of the list if list length, rather than list lenght +1. This is because position is 0-indexed. So, for example, if you have 5 items, positions 0-4 will be your exercise items, and then position 5 (position == exerciseList.size) will be an add item.
Adding logs in getItemViewType for position and generated view type is helpful for debugging, as it shows which positions are calculated incorrectly very quickly.

GridLayoutManager with different column count per row

I'm trying to build a RecyclerView with a GridLayoutManager which has a variable column count per row, something like this:
The sum of the width of all items in the same row will always be the screen width.
I tried to re-organize the list of items, grouping them by list of rows, and then inflating a LinearLayout per row. It didn't work quite well.
So I'm stuck and out of ideas. Any help would be really appreciated
You can use GridLayoutManager. To have different column count in row you have to override setSpanSizeLookup.
Example:
//spanCount = 3 (just for example)
GridLayoutManager gridLayoutManager = new GridLayoutManager(getAppContext(), spanCount);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int position) {
//define span size for this position
//some example for your first three items
if(position == item1) {
return 1; //item will take 1/3 space of row
} else if(position == item2) {
return 2; //you will have 2/3 space of row
} else if(position == item3) {
return 3; //you will have full row size item
}
}
});
I code sample above I just show have you can change item size. Pay attention that spanSize <= spanCount.
I have similar situation and think it is a good choice to use kotlin: sealed class. You may set any spanSizeLookup for every item in your adapter list.
Example of setup spanSizeLookup for GridLayoutManager:
val spanCount = 2
val layoutManager = GridLayoutManager(context, spanCount)
layoutManager.spanSizeLookup = object : SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val item = adapter.getItemList().getOrNull(position) ?: return spanCount
return when (item) {
is ProfileItem.Header -> spanCount
is ProfileItem.Status -> spanCount
is ProfileItem.Progress -> spanCount
is ProfileItem.Empty -> spanCount
is ProfileItem.Item -> spanCount / 2
is ProfileItem.Load -> spanCount / 2
}
}
}
My sealed class:
sealed class ProfileItem {
object Header : ProfileItem()
data class Status(var content: UserItem.User?) : ProfileItem()
object Progress : ProfileItem()
object Empty : ProfileItem()
data class Item(val content: RecordItem.Record) : ProfileItem()
object Load : ProfileItem()
}

Categories

Resources