Recyclerview multiple item selector - android

I'm trying to create a calendar view for a reservation app. I need to show to the user which days are already in use.
For this i like to create a selector between continuous days like this:
For the calendar view i created a RecyclerView using java.util.Calendar as datasource, every day is a ViewHolder.
Adapter:
class CalendarAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var list = emptyArray<CalendarItem>()
override fun getItemViewType(position: Int): Int {
return list[position].viewType?.asInt ?: super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
ViewType.CURRENT_DAY.asInt -> {
val binding = ViewCurrentDayBinding.inflate(inflater, parent, false)
CurrentDayHolder(binding)
}
ViewType.DAY_OF_MONTH.asInt -> {
val binding = ViewDayOfMonthBinding.inflate(inflater, parent, false)
DayOfMonthHolder(binding)
}
ViewType.DAY_OF_WEEK.asInt -> {
val binding = ViewDayOfWeekBinding.inflate(inflater, parent, false)
DayOfWeekHolder(binding)
}
ViewType.SELECTED_DAY.asInt -> {
val binding = ViewSelectedDayBinding.inflate(inflater, parent, false)
SelectedDayHolder(binding)
}
ViewType.MOCK.asInt -> {
val binding = ViewMockDayBinding.inflate(inflater, parent, false)
MockDayHolder(binding)
}
else -> {
val binding = ViewMockDayBinding.inflate(inflater, parent, false)
MockDayHolder(binding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (getItemViewType(position)) {
ViewType.CURRENT_DAY.asInt -> {
(holder as CurrentDayHolder).bind(list[position], position, callback)
}
ViewType.DAY_OF_MONTH.asInt -> {
(holder as DayOfMonthHolder).bind(list[position], position, callback)
}
ViewType.DAY_OF_WEEK.asInt -> {
(holder as DayOfWeekHolder).bind(list[position])
}
ViewType.SELECTED_DAY.asInt -> {
(holder as SelectedDayHolder).bind(list[position], position, callback)
}
ViewType.MOCK.asInt -> {
(holder as MockDayHolder).bind(list[position])
}
}
}
override fun getItemCount(): Int {
return list.size
}
private val callback: (index: Int) -> Unit = {
// find current selected index and unselect
val currentSelectedIndex =
list.indices.find { el -> list[el].viewType == ViewType.SELECTED_DAY }
if (currentSelectedIndex != null) {
list[currentSelectedIndex].viewType = list[currentSelectedIndex].defaultViewType
notifyItemChanged(currentSelectedIndex)
}
// select the new index
list[it].viewType = ViewType.SELECTED_DAY
notifyItemChanged(it)
}
}
ViewHolder:
class CurrentDayHolder(var binding: ViewCurrentDayBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
calendarItem: CalendarItem,
index: Int,
callback: (index: Int) -> Unit
) {
binding.day.text = calendarItem.label
binding.root.setOnClickListener {
callback.invoke( index)
}
}
}
The full project is avaible on GitHub
How can i archive my goal?
I also thought not to use a RecyclerView and create a custom view directly, with the obvious complexities of the case.
I'm sure there is a way to do this with the RecyclerView as well

With a RecyclerView you'd need to customise the item layout so you can display the different styles (normal item, start of selection, middle of selection, end of selection, all the stripey versions of those) and then calculate the state of each item in onBindViewHolder so you can style it correctly, e.g. by having different background drawables you can switch between. But since these are all separate views, you might have trouble getting those stripes to line up correctly where one view ends and the adjacent one starts.
Also you could just use a GridLayout or something for this - no need for a RecyclerView when you're displaying all the items at once. You might want to consider a custom view with a grid/table where each item is a TextView or a borderless Button (better!), where you have another View layer on top which is a custom view that draws the selections.
But since you'd have to draw the text as well (e.g. the white date over the orange highlight) you might find it easier to just make the whole thing as a custom view, where you're positioning all the text yourself. That's one of the benefits of custom views - you get more control over how it draws itself. You could always try subclassing an existing calendar widget! Use the work that's already been done

I solved the problem using your suggestions: I managed a state for each element of the recyclerview indicating the selection of start, destination and the intermediate value when I create the datasource of the elements. then in each viewholder I change the corresponding background.
Full solution available on the example code in the question.
Here a screen of the result.
thanks everyone for the help

Related

Data disappears when scrolling in recycler view

Good day. So I currently have data in my recycler view. It is for now only static data. I still have to do the code where I import. My problem however is I have a button that changes the background of a text view. This happens in my adapter. And when I scroll through my list the bg color change gets reverted back to what it was before the button click. I have read a lot of similar problems but could not really find one that explains clearly or work for me. From what I read the data gets reset to the static data because it is currently happening in my onBindViewHolder and I think this changes the data on every new data read(scrolling). I read that I should create a link or a listener and then call it. But It does not make sense to me because if a link is called the same amount of times as the code is executed then it will be the same will it not. Maybe having a condition listener but not sure if this is the way to go.
I am somewhat new to android and kotlin. Have been working with it for a month now. I dont know everything I am doing but I got given a deadline. So sadly there was no time to go and learn the basics. Thank you for any and all help. Please let me know if you need any additional code/information
my adapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RowViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.table_list_item, parent, false)
return RowViewHolder(itemView)
}
private fun setHeaderBg(view: View) {
view.setBackgroundResource(R.drawable.table_header_cell_bg)
}
private fun setContentBg(view: View) {
view.setBackgroundResource(R.drawable.table_content_cell_bg)
}
override fun onBindViewHolder(holder: RowViewHolder, position: Int) {
// (TableViewAdapter.DataviewHolder) .bind()
val rowPos = holder.adapterPosition
if (rowPos == 0) {
// Header Cells. Main Headings appear here
holder.itemView.apply {
setHeaderBg(txtWOrder)
setHeaderBg(txtDElNote)
setHeaderBg(txtCompany)
// setHeaderBg(txtAddress)
setHeaderBg(txtWeight)
setHeaderBg(txtbutton1)
setHeaderBg(txtbutton2)
setHeaderBg(txttvdone)
txtWOrder.text = "WOrder"
txtDElNote.text = "DElNote"
txtCompany.text = "Company"
// txtAddress.text = "Address"
txtWeight.text = "Weight"
txtbutton1.text = "Delivered"
txtbutton2.text = "Exception"
txttvdone.text = ""
}
} else {
val modal = Tripsheetlist[rowPos - 1]
holder.itemView.apply {
setContentBg(txtWOrder)
setContentBg(txtDElNote)
setContentBg(txtCompany)
// setContentBg(txtAddress)
setContentBg(txtWeight)
setContentBg(txtbutton1)
setContentBg(txtbutton2)
setContentBg(txttvdone)
txtWOrder.text = modal.WOrder.toString()
txtDElNote.text = modal.DElNote.toString()
txtCompany.text = modal.Company.toString()
// txtAddress.text = modal.Address.toString()
txtWeight.text = modal.Weight.toString()
txtbutton1.text = modal.Button1.toString()
txtbutton2.text = modal.Button2.toString()
txttvdone.text = modal.tvdone.toString()
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
txttvdone.setBackgroundResource(R.color.green)
txttvdone.setText("✓")
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
txttvdone.setBackgroundResource(R.color.orange)
txttvdone.setText("x")
}
}
}
class RowViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){
val txttvdone:TextView = itemView.findViewById<TextView>(R.id.txttvdone)
val txtbutton1:Button = itemView.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
} class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view){
var txtbutton1 = view.findViewById<Button>(R.id.txtbutton1)
val txtbutton2:Button = itemView.findViewById<Button>(R.id.txtbutton2)
var txttvdone = view.findViewById<TextView>(R.id.txttvdone)
}
I tried (TableViewAdapter.DataviewHolder) .bind() doing this and creating another class as I saw that was done in another thread(Why do values ​disappear after scrolling in Recycler View?) Its a lot like my problem. I just can't seem to implement his solution to make mine work. ( don't understand his solution fully)
//I am also aware that I am using android extensions which will expire at the end of the year. But for now it works and once I have the code up and running I will start to move over to the newer versions of kotlin.
A RecyclerView, as its name implies, will recycle the views when they go off screen. This means that when the view for an item comes into view, it gets recreated and the onBindViewHolder() is called to fill in the details.
Your onClickListener inside your adapter changes the background of one of the subviews for your cell view. However, that cell will be redrawn if it leaves the screen and comes back.
To get around this, your onClickListener should be changing a property on the data item, and your onBindViewHolder should check that property to determine what background color to display for the subview:
enum class DataState {
Unselected,
Success,
Failure
}
data class DataItem(var state: DataState = DataState.Unselected)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
var dataItems: List<DataItem> = emptyList()
fun updateData(data: List<DataItem>) {
dataItems = data
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val dataItem = dataItems[position]
holder.txttvdone.apply {
setBackgroundResource(when (dataItem.state) {
DataState.Unselected -> android.R.color.transparent
DataState.Success -> R.color.green
DataState.Failure -> R.color.orange
})
text = when (dataItem.state) {
DataState.Unselected -> ""
DataState.Success -> "✓"
DataState.Failure -> "x"
}
}
holder.apply {
txtbutton1.setOnClickListener {
Log.e("Clicked", "Successful delivery")
dataItem.state = DataState.Success
notifyDataSetChanged()
}
txtbutton2.setOnClickListener {
Log.e("Clicked", "Exception on delivery")
dataItem.state = DataState.Failure
notifyDataSetChanged()
}
}
}
}

Animate single item in RecyclerView on data change

I have complex and generic RecyclerView design and List Adapter.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context)
val binding: ViewDataBinding =
DataBindingUtil.inflate(layoutInflater, viewType, parent, false)
return object : BaseViewHolder(binding) {
override fun bindData(position: Int) {
val model = getItem(position).data
itemBinding.setVariable(BR.model, model)
viewModel?.let {
itemBinding.setVariable(BR.viewModel, it)
}
}
}
}
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
holder.run {
bindData(position)
if (itemBinding.hasPendingBindings()) {
itemBinding.executePendingBindings()
}
}
}
It has RecyclerView inside RecyclerView as item and handle multi layout by itself. I update list and items with databinding adapters. When I need to update single item; I search all tree in LiveData list, modify value and post value updated list to LiveData again.
I want to update each view with animation(item inside of RecyclerView inside of RecyclerView) when it's value changed.
here is my update code;
#BindingAdapter("setTransactionBgAnimation")
fun View.setTransactionBgAnimation(ratio: Double?) {
ratio?.let { value ->
val colorAnim = ObjectAnimator.ofInt(
this, "backgroundColor", getEvaluateColor(context, value), Color.WHITE
)
colorAnim.duration = 500
colorAnim.repeatCount = 1
colorAnim.start()
val alphaAnim = ObjectAnimator.ofFloat(
this, "alpha", 0.40f, 0.0f
)
alphaAnim.duration = 500
alphaAnim.repeatCount = 1
alphaAnim.start()
}
}
When value updated; it has called from all views for each change.
I tried to give unique tag to view and check tag in binding adapter but it is not worked for me.
I solve the problem with not -so clean- way.
First of all; animation was called for every visible item's count for each row, I fix it by controlling with giving view tag with changing value and check that tag that is same with new value.
After first fix, only really changed item animated but it animates multiple times. It was causing because of ObjectAnimator's backgroundColor animations. I have no idea why did I even change backgroundColor with animation. I remove it and multiple flickering animation fixed too.
For better understanding please see my code part
fun View.setTransactionBgAnimation(ratio: Double?) {
if (tag != ratio.toString()) {
ratio?.let { value ->
setBackgroundColor(getEvaluateColor(context, value))
val alphaAnim = ObjectAnimator.ofFloat(
this, "alpha", 0.40f, 0.0f
)
alphaAnim.duration = 500
alphaAnim.start()
}
tag = ratio.toString()
}
}

Recycler view post Item deletion, has deleted views overlapping other views

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.

Lists pass by value?

I'm trying to make a copy of my list that contains arraylists inside of it, but when I edit the copied value, the original values gets changed, which means am passing my ref, I tried all sorts of methods like using the copy method for each item, or creating a list/mutable list from the original but it didn't work, so my question is how do you pass a value in kotlin instead of ref?
I made the original as val and its field too.
class FAQAdapter(val faqModel: MutableList<FAQSection>) : RecyclerView.Adapter<FAQAdapter.ViewHolder>() {
val faqOriginal: List<FAQSection>
var faqSectionsCopy: MutableList<FAQSection>
init {
faqOriginal = faqModel
faqSectionsCopy = faqModel.toMutableList()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.faq_section_item, parent, false))
}
override fun getItemCount(): Int {
return faqSectionsCopy.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.faqSectionHeading.text =
faqSectionsCopy.get(holder.adapterPosition).sectionheader
holder.questionsRecyclerView.layoutManager =
LinearLayoutManager(holder.itemView.context,
LinearLayoutManager.VERTICAL, false)
holder.questionsRecyclerView.adapter =
FAQQuestionsAdapter(faqSectionsCopy.get(holder.adapterPosition)
.faqQuestions)
holder.questionsRecyclerView.setHasFixedSize(true)
holder.questionsRecyclerView.minimumHeight = convertDpToPx(holder.itemView.context, 88) * faqSectionsCopy.get(holder.adapterPosition).faqQuestions.size
}
fun filter(text: String) {
var text = text.trim().toLowerCase()
// faqSectionsCopy.clear()
if (text.isEmpty()) {
// faqSectionsCopy = faqModel as ArrayList<FAQSection>
} else {
text = text.toLowerCase()
faqSectionsCopy.map {
faqSectionsCopy[0].faqQuestions = it.faqQuestions.filter { it.question.contains(text) } as ArrayList<FAQQuestion>
}
}
notifyDataSetChanged()
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var questionsRecyclerView = itemView.faqQuestionsRecyclerView
var faqSectionHeading = itemView.faqHeading
}
}
You cannot pass data by value in Kotlin or Java. Instead you should ensure to work on a copy when you retrieve the data:
fun withList(listOfLists: List<List<Any>>) {
...
val list = listOfLists[0].toMutableList()
...
}
The toMutableList extension function will then create a mutable copy of the list and therefore the original will not change. As both levels of the parameter are of type List, the values are immutable themself.
Actually every data pass in Java/Kotlin is by value. Except that in this situation you're passing a reference by value but not the list itself.
To pass a copy of list, you can
passed using yourList.toList() which will make a copy of the list and return an immutable list
explicitly create new mutable ArrayList(yourList)

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.

Categories

Resources