Recyclerview selection: createSelectSingleAnything selects multiple when scrolling - android

I'm trying to create a horizontal recyclerview with a single selection. The code below works but if you scroll, then 2 or more objects can be selected, even though I specify the single selection. I guess it's because the onBindViewHolder is not called if the item is off the screen but when I call the notifyDataSetChanged the behavior is still the same. The callbacks are correct, but the selector background, shows behind multiple objects.
class CategoryAdapter(private val listener: CategoryListener) :
ListAdapter<CategoryListing, CategoryAdapter.ViewHolder>(CategoryListing.DIFF_COMPARATOR) {
init {
setHasStableIds(true)
}
var tracker: SelectionTracker<Long>? = null
...
override fun getItemId(position: Int): Long = position.toLong()
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
tracker?.let {
if (!it.hasSelection() && position == 0) {
it.select(0)
}
}
holder.bind(
currentList[position],
listener,
tracker?.isSelected(position.toLong()) ?: false
)
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(
category: CategoryListing,
listener: CategoryListener,
isSelected: Boolean
) {
if (isSelected) selector.show() else selector.hide()
itemView.setOnClickListener {
tracker?.clearSelection() // this doesn't seem to do anything
// notifyDataSetChanged() // neither this
// tracker?.select(adapterPosition.toLong()) // or this
listener.onItemClicked(category, adapterPosition)
}
}
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition() = adapterPosition
override fun getSelectionKey() = itemId
}
}
Fragment:
rv_categories.adapter = categoryAdapter
rv_categories.setHasFixedSize(true)
selectionTracker = SelectionTracker.Builder<Long>(
CardsFragment::javaClass.name,
rv_categories,
CategoryItemKeyProvider(rv_categories),
CategoryDetailsLookup(rv_categories),
StorageStrategy.createLongStorage()
).withSelectionPredicate(SelectionPredicates.createSelectSingleAnything()).build()
categoryAdapter.tracker = selectionTracker
selectionObserver = CategorySelectionObserver(selectionTracker) { selectedPosition ->
onSelectionChanged(selectedPosition)
}
selectionTracker.addObserver(selectionObserver)
...
override fun onItemClicked(category: CardCategoryListing, position: Int) {
// load the data for that category
}
private fun onSelectionChanged(selectedItemPosition: Long) {
rv_categories.findViewHolderForAdapterPosition(selectedItemPosition.toInt())?.itemView?.performClick()
}
Can anyone help?

Related

ListAdapter currentList and itemCount not returning updates after filter or submitList

I am implementing filterable list for RecyclerView using ListAdapter with AsyncDifferConfig.Builder that implements Filterable. When searching and no result match, a TextView will be shown.
adapter.filter.filter(filterConstraint)
// Searched asset may not match any of the available item
if (adapter.itemCount <= 0 && adapter.currentList.isEmpty() && filterConstraint.isNotBlank())
logTxtV.setText(R.string.no_data)
else
logTxtV.text = null
Unfortunately the update of filter did not propagate immediately on adapter's count and list.
The adapter count and list is one step behind.
The TextView should be displaying here already
But it only shows after updating it back and the list is no longer empty at this point
I am not sure if this is because I am using AsyncDifferConfig.Builder instead of regular DiffCallback
ListAdapter class
abstract class FilterableListAdapter<T, VH : RecyclerView.ViewHolder>(
diffCallback: DiffUtil.ItemCallback<T>
) : ListAdapter<T, VH>(AsyncDifferConfig.Builder(diffCallback).build()), Filterable {
/**
* True when the RecyclerView stop observing
* */
protected var isDetached: Boolean = false
private var originalList: List<T> = currentList.toList()
/**
* Abstract method for implementing filter based on a given predicate
* */
abstract fun onFilter(list: List<T>, constraint: String): List<T>
override fun getFilter(): Filter {
return object : Filter() {
override fun performFiltering(constraint: CharSequence?): FilterResults {
return FilterResults().apply {
values = if (constraint.isNullOrEmpty())
originalList
else
onFilter(originalList, constraint.toString())
}
}
#Suppress("UNCHECKED_CAST")
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
submitList(results?.values as? List<T>, true)
}
}
}
override fun submitList(list: List<T>?) {
submitList(list, false)
}
/**
* This function is responsible for maintaining the
* actual contents for the list for filtering
* The submitList for parent class delegates false
* so that a new contents can be set
* While a filter pass true which make sure original list
* is maintained
*
* #param filtered True if the list was updated using filter interface
* */
private fun submitList(list: List<T>?, filtered: Boolean) {
if (!filtered)
originalList = list ?: listOf()
super.submitList(list)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
}
RecyclerView Adapter
class AssetAdapter(private val glide: RequestManager, private val itemListener: ItemListener) :
FilterableListAdapter<AssetDataDomain, AssetAdapter.ItemView>(DiffUtilAsset()) {
inner class ItemView(itemView: AssetCardBinding) : RecyclerView.ViewHolder(itemView.root) {
private val assetName = itemView.assetName
private val assetPrice = itemView.assetPrice
private val assetMarketCap = itemView.assetMarketCap
private val assetPercentChange = itemView.assetPercentChange
private val assetIcon = itemView.assetIcon
private val assetShare = itemView.assetShare
// Full update/binding
fun bind(domain: AssetDataDomain) {
with(itemView.context) {
assetName.text = domain.symbol ?: domain.name
bindNumericData(
domain.metricsDomain.marketDataDomain.priceUsd,
domain.metricsDomain.marketDomain.currentMarketcapUsd,
domain.metricsDomain.marketDataDomain.percentChangeUsdLast24Hours
)
if (!isDetached)
glide
.load(
getString(
R.string.icon_url,
AppConfigs.ICON_BASE_URL,
domain.id
)
)
.into(assetIcon)
assetShare.setOnClickListener {
itemListener.onRequestScreenShot(
itemView,
getString(
R.string.asset_info,
domain.name,
assetPercentChange.text.toString(),
assetPrice.text.toString()
)
)
}
itemView.setOnClickListener {
itemListener.onItemSelected(domain)
}
}
}
// Partial update/binding
fun bindNumericData(priceUsd: Double?, mCap: Double?, percent: Double?) {
with(itemView.context) {
assetPrice.text = getString(
R.string.us_dollars,
NumbersUtil.formatFractional(priceUsd)
)
assetMarketCap.text = getString(
R.string.mcap,
NumbersUtil.formatWithUnit(mCap)
)
assetPercentChange.text = getString(
R.string.percent,
NumbersUtil.formatFractional(percent)
)
AppUtil.displayPercentChange(assetPercentChange, percent)
if (NumbersUtil.isNegative(percent))
assetPrice.setTextColor(Color.RED)
else
assetPrice.setTextColor(Color.GREEN)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemView =
ItemView(
AssetCardBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun onBindViewHolder(holder: ItemView, position: Int) {
onBindViewHolder(holder, holder.absoluteAdapterPosition, emptyList())
}
override fun onBindViewHolder(holder: ItemView, position: Int, payloads: List<Any>) {
if (payloads.isEmpty() || payloads[0] !is Bundle)
holder.bind(getItem(position)) // Full update/binding
else {
val bundle = payloads[0] as Bundle
if (bundle.containsKey(DiffUtilAsset.ARG_PRICE) ||
bundle.containsKey(DiffUtilAsset.ARG_MARKET_CAP) ||
bundle.containsKey(DiffUtilAsset.ARG_PERCENTAGE))
holder.bindNumericData(
bundle.getDouble(DiffUtilAsset.ARG_PRICE),
bundle.getDouble(DiffUtilAsset.ARG_MARKET_CAP),
bundle.getDouble(DiffUtilAsset.ARG_PERCENTAGE)
) // Partial update/binding
}
}
// Required when setHasStableIds is set to true
override fun getItemId(position: Int): Long {
return currentList[position].id.hashCode().toLong()
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
isDetached = true
}
override fun onFilter(list: List<AssetDataDomain>, constraint: String): List<AssetDataDomain> {
return list.filter {
it.name.lowercase().contains(constraint.lowercase()) ||
it.symbol?.lowercase()?.contains(constraint.lowercase()) == true
}
}
interface ItemListener {
fun onRequestScreenShot(view: View, description: String)
fun onItemSelected(domain: AssetDataDomain)
}
}
UPDATE:
I can confirm that using DiffCallback instead of AsyncDifferConfig.Builder does not change the behavior and issue. It also seems that currentList is in async thus update on list does not reflect immediately after calling submitList.
I do not know if this is intended behavior but upon overriding onCurrentListChanged the currentList parameter is working correctly.
But the adapter.currentList is behaving like a previousList parameter
When you submit a list to recyclerView, it takes some time to compare items of current list and the previous one (to see if an item is removed, moved or added). so the result is not immediately ready.
you can use a RecyclerView.AdapterDataObserver to be notified of changes in recyclerView (it will tell what happened to items overall, like 5 were added etc)
P.S. if you look at recyclerView source code you will see that the DiffCallBack passed in the constructor, is wrapped in AsyncDifferConfig

position of recyclerview item changes after opening the fragment again

I am using android kotlin. i am working on online shopping app.
in the shopping cart section. all things works fine.adding an item to cart or delete the item or adding quantity and undoing the delete .
when i click on undo button the item is placed in its own position after undoing the deleted item. but when i close the fragment and open it again all item position changes and the item that i clicked first will be first item and so to the end
this is my recycler adapter:
class BasketRecyclerAdapter(val list: MutableList<Model.BasketItem>, val listener: ButtonListener) :
RecyclerView.Adapter<BasketRecyclerAdapter.ViewHolder>() {
lateinit var context: Context
lateinit var localStore: LocalStore // i saved user phone and pass in a shared prefs using this class
inner class ViewHolder(val binding: BasketItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BasketRecyclerAdapter.ViewHolder {
val binding: BasketItemLayoutBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.basket_item_layout,
parent,
false
)
localStore = LocalStore(parent.context)
context = parent.context
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: BasketRecyclerAdapter.ViewHolder, position: Int) {
val currentItem = list[position]
holder.binding.apply {
title.text = currentItem.title
numberPicker.progress = currentItem.quantity
color.setCardBackgroundColor(android.graphics.Color.parseColor(currentItem.color))
colorName.text = currentItem.color_name
price.text =
doubleToStringNoDecimal((currentItem.price * currentItem.quantity).toDouble())
Glide.with(context)
.asBitmap()
.load(currentItem.url)
.into(productImage)
holder.binding.apply {
numberPicker.doOnProgressChanged { numberPicker, progress, formUser ->
listener.onPickerProgressChanged(
numberPicker,
progress,
formUser,
position
)
price.text =
doubleToStringNoDecimal((currentItem.price * progress).toDouble())
}
delete.setOnClickListener {
listener.delete(holder.adapterPosition)
removeItem(holder.adapterPosition, holder)
}
}
}
}
override fun getItemCount(): Int = list.size
fun doubleToStringNoDecimal(d: Double): String? {
val formatter: DecimalFormat = NumberFormat.getInstance(Locale.US) as DecimalFormat
formatter.applyPattern("#,###,###,###")
return formatter.format(d)
}
fun removeItem(position: Int, viewHolder: RecyclerView.ViewHolder) {
val removedItem = list[position]
val removedPosition = position
list.removeAt(position)
notifyItemRemoved(position)
val snackbar = Snackbar.make(
viewHolder.itemView,
"این کالا از سبد خرید حذف شد",
Snackbar.LENGTH_LONG
)
snackbar.setAction("بازگرداندن") {
list.add(removedPosition, removedItem)
notifyItemInserted(removedPosition)
listener.SnackActionClicked(position)
}
snackbar.setActionTextColor(Color.YELLOW);
snackbar.show();
}
interface ButtonListener {
fun onPickerProgressChanged(
numberPicker: NumberPicker,
progress: Int,
fromUser: Boolean,
position: Int
)
fun delete(position: Int)
fun SnackActionClicked(position: Int)
}
}
adapter =
BasketRecyclerAdapter(
items as ArrayList<Model.BasketItem>,
object : BasketRecyclerAdapter.ButtonListener {
override fun onPickerProgressChanged(
numberPicker: NumberPicker,
progress: Int,
fromUser: Boolean,
position: Int
) {
val currentitem = items[position]
viewModel.AddToCart(
localStore.getMobile().toString(),
currentitem.category,
currentitem.title,
currentitem.url,
currentitem.price,
progress,
currentitem.color,
currentitem.color_name,
currentitem.product_id
)
updateWorkerData(
currentitem.title,
currentitem.color,
Model.BasketItem::quantity,
progress
)
viewModel.CalculateTotalPrice(items)
.observe(viewLifecycleOwner) {
binding.price.text = doubleToStringNoDecimal(it)
}
}
override fun delete(position: Int) {
val currentitem = items[position]
viewModel.DeleteItemInCart(
localStore.getMobile().toString(),
currentitem.title,
currentitem.color_name
)
itemList.remove(
Model.BasketItem(
currentitem.id,
localStore.getMobile().toString(),
currentitem.category,
currentitem.title,
currentitem.url,
currentitem.price,
currentitem.quantity,
currentitem.color,
currentitem.color_name,
currentitem.product_id
)
)
if (items.size == 1)
if (isLastVisible()) {
basketpic.visibility = View.VISIBLE
emptyBasketText.visibility = View.VISIBLE
} else {
basketpic.visibility = View.GONE
emptyBasketText.visibility = View.GONE
}
viewModel.CalculateTotalPrice(items)
.observe(viewLifecycleOwner) {
binding.price.text = doubleToStringNoDecimal(it)
}
}
override fun SnackActionClicked(position: Int) {
val currentitem = items[position]
viewModel.AddToCart(
localStore.getMobile().toString(),
currentitem.category,
currentitem.title,
currentitem.url,
currentitem.price,
currentitem.quantity,
currentitem.color,
currentitem.color_name,
currentitem.product_id
)
binding.apply {
basketpic.visibility = View.GONE
emptyBasketText.visibility = View.GONE
}
viewModel.CalculateTotalPrice(items)
.observe(viewLifecycleOwner) {
binding.price.text = doubleToStringNoDecimal(it)
}
}
})
any help will be appreciated
UPDATE
you can see that first one is white and the second is brown but when i delete and undodelete the brown then white and reopen the fragment their position changes-> see second picture
after delete and undo deleteitems(first brown then white) and reopening the fragment
The Problem was that when i Clicked on Delete button the corresponding row in the Mysql was deleted and when i Clicked on Undo button the items that i deleted them
Were added to the table with new id and beacause of this the position of item changed after the fragment reopened
so i set a Snackbar.Callback to snackbar and i said that when it closed on its own then you can delete the item from database(mysql)
this is the code:
val snackbar = Snackbar.make(
viewHolder.itemView,
"این کالا از سبد خرید حذف شد",
Snackbar.LENGTH_LONG
)
snackbar.setAction("بازگرداندن") {
list.add(removedPosition, removedItem)
listener.SnackActionClicked(position)
notifyItemInserted(removedPosition)
}
snackbar.setActionTextColor(Color.YELLOW);
snackbar.addCallback(object : Snackbar.Callback() {
override fun onDismissed(snackbar: Snackbar?, event: Int) {
super.onDismissed(snackbar, event)
if (event == Snackbar.Callback.DISMISS_EVENT_TIMEOUT) {
// Snackbar closed on its own
// Delete item from mysql database
}
}
override fun onShown(snackbar: Snackbar?) {
super.onShown(snackbar)
}
});
snackbar.show();

Implement SearchView inside recyclerview header

I wanted to add a SearchView to my recyclerview. I wanted it to be at the top and scrollable with the items. To achieve this, I created separate adapter for my header and it contains the Searchview as well. Then I used a ConcatAdapter to combine this header adapter with the contents below it.
Initially I want all the items to be visible under the SearchView from _onBoardingState which is a MutableStateFlow and when user searches for a tag then the results for it get added to _onSearch which is also a MutableStateFlow.
I have this MutableStateFlow, _onBoardingState inside my ViewModel that gets the value from Firestore in the init of ViewModel. The number of results is less (~ 20) so there is no pagination implemented and all items get loaded at once.
Now, whenever user wants to search an item by a tag, the SearchView returns a Flow of the typed value and also a Flow that updates about if the SearchView is still open or closed. I used these extension functions for this:
fun SearchView.getQueryTextChangeStateFlow(onSubmit: ()-> Unit): StateFlow<String> {
val query = MutableStateFlow("")
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
onSubmit()
return true
}
override fun onQueryTextChange(newText: String): Boolean {
query.value = newText
return true
}
})
return query
}
fun SearchView.getActiveStateFlow(): StateFlow<Boolean> {
val isOpen = MutableStateFlow(false)
setOnSearchClickListener {
isOpen.value = true
}
setOnCloseListener {
isOpen.value = false
false
}
return isOpen
}
Inside my ViewModel I have
...
private val _onBoardingState: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _onSearch: MutableStateFlow<Model?> = MutableStateFlow(null)
private val _isActive: MutableStateFlow<Boolean> = MutableStateFlow(false)
fun toggleSearchViewState(isActive: Boolean) {
_isActive.value = isActive
}
val cuurentFlow: Flow<Model?> =
_isActive.flatMapLatest { isActive ->
if (isActive) {
_onSearch
} else {
_onBoardingState
}
}
...
Now the issue here is, whenever the recyclerview is scrolled down, the SearchView gets recycled and hence the setOnCloseListener gets called for it. This causes the _isActive value to be set to false by the Header's Adapter so the value of cuurentFlow gets toggled which should not be happening.
I thought of a solution as to set the setOnCloseListener of SearchView inside the header adapter's onViewRecycled() to null, but this didn't help. Below is code for my Header Adapter as well if needed.
class OnBoardingHeaderAdapter(
private val context: Context,
) : RecyclerView.Adapter<OnBoardingHeaderAdapter.HeaderViewHolder>() {
private var queryTextListener: ((StateFlow<String>) -> Unit)? = null
private var searchViewListener: ((StateFlow<Boolean>) -> Unit)? = null
inner class HeaderViewHolder(binding: OnboardingHeaderItemBinding) :
RecyclerView.ViewHolder(binding.root) {
private val root = binding.headerRoot
val search = binding.search
fun bind(headerMetaData: HeaderMetaData) {
root.visibility =
if (headerMetaData.shouldShow)
View.VISIBLE
else
View.GONE
val searchEditText: EditText =
search.findViewById(androidx.appcompat.R.id.search_src_text)
searchEditText.setHintTextColor(context.resources.getColor(R.color.white))
searchEditText.setTextColor(context.resources.getColor(R.color.white))
}
}
fun setQueryTextListener(listener: (StateFlow<String>) -> Unit) {
this.queryTextListener = listener
}
fun setSearchViewListener(listener: (StateFlow<Boolean>) -> Unit) {
this.searchViewListener = listener
}
private val RECYCLER_COMPARATOR = object : DiffUtil.ItemCallback<HeaderMetaData>() {
override fun areItemsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HeaderMetaData, newItem: HeaderMetaData) =
oldItem == newItem
}
val headerDiffer = AsyncListDiffer(this, RECYCLER_COMPARATOR)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val binding = OnboardingHeaderItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return HeaderViewHolder(binding)
}
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
//holder.setIsRecyclable(false)
if (position < 1) {
val header = headerDiffer.currentList[position]
holder.bind(header)
}
queryTextListener?.let {
it(holder.search.getQueryTextChangeStateFlow() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
val view: View = holder.search
imm.hideSoftInputFromWindow(view.windowToken,0)
})
}
searchViewListener?.let {
it(holder.search.getActiveStateFlow())
}
}
override fun getItemCount(): Int = headerDiffer.currentList.size
override fun onViewRecycled(holder: HeaderViewHolder) {
super.onViewRecycled(holder)
holder.search.setOnCloseListener(null)
}
}
I wanted to know what is the best approach to solve this issue, I think even if i use a recyclerview with multiple view types here for the header then still the recycling issue will be there.

How to show all checkboxes in recyclerview?

I want to display all the checkboxes by long press, but only one is displayed
There is a part of my adapter
override fun onBindViewHolder(holder: ViewHoldder, position: Int) {
holder.textView?.text = lstWords[position].engWord
holder.textView2?.text = lstWords[position].transWord
holder.textView3?.text = lstWords[position].rusWord
holder.checkBox?.setOnCheckedChangeListener(object : CompoundButton.OnCheckedChangeListener {
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
val word = Words(
lstWords[position].idWord!!,
lstWords[position].engWord!!,
lstWords[position].transWord!!,
lstWords[position].rusWord!!,
lstWords[position].check!!
)
if (holder.checkBox?.isChecked!!){
words.add(word.idWord.toString())
Log.d("MYTAG", "$words")
}
if (!holder.checkBox?.isChecked!!){
words.remove(word.idWord.toString())
Log.d("MYTAG", "$words")
}
}
})
holder.itemView.setOnLongClickListener {
holder.checkBox?.visibility = CheckBox.VISIBLE
true
}
}
How to change all checkboxes?
Use a property in the adapter class that determines if check boxes should be shown. Call notifyDataSetChanged() to force all item views to be rebound, so it will have a chance to modify each of them.
private var shouldShowCheckBoxes = false
//...
override fun onBindViewHolder(holder: ViewHoldder, position: Int) {
//...
holder.checkBox?.visiblility = if (shouldShowCheckBoxes) View.VISIBLE else View.INVISIBLE
holder.itemView.setOnLongClickListener {
shouldShowCheckBoxes = true
notifyDataSetChanged()
true
}
}

Is it possible to create a class derived of LinearSmoothScroller, which has added a property to use its value in one of overridden methods

I want to have a class derived from LinearSmoothScroller class, in which I could pass a scrolling speed property by a class constructor. I've got a code below, which works and have added some println()'s for tests.
ScrollerManager class:
class ScrollerManager(
val context: Context,
private var millisecondsPerInch: Float
) : LinearSmoothScroller(context) {
init {
println("init millisecsPerInch: $millisecondsPerInch, instance: $this")
}
fun printSpeed() {
println("printSpeed(): $millisecondsPerInch, instance: $this")
}
override fun getHorizontalSnapPreference(): Int {
return LinearSmoothScroller.SNAP_TO_START
}
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
println("calcSpeedPerPixel(..) millisecsPerInch: ${this.millisecondsPerInch}, instance: $this")
// Here I want to use millisecondsPerInch field value, instead of written value 30f:
return 30f / displayMetrics?.densityDpi!!
}
}
Click item listener interface:
interface RecItemClickListener {
fun onClick(view: View, position: Int)
}
An adapter. Context, RecyclerView, and itemList are passed from Actvity.
When recyclerView item is clicked, a scrollToPosition method is called where my SmoothScroller is used.
class AdvancedRecViewHoriAdapter(
val context: Context,
val recyclerView: RecyclerView,
val itemsList: List<String>
) : RecyclerView.Adapter<AdvancedRecViewHoriCustomViewHolder>() {
companion object {
var selectedPosition = RecyclerView.NO_POSITION
}
override fun getItemCount(): Int {
return itemsList.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
: AdvancedRecViewHoriCustomViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
// inflate layout from xml
val cellForRow = layoutInflater.inflate(
R.layout.rec_view_advanced_hori_row, parent, false)
return AdvancedRecViewHoriCustomViewHolder(cellForRow)
}
private fun scrollToPosition(position: Int) {
val smoothScroller = ScrollerManager(context, 30f)
smoothScroller.printSpeed()
smoothScroller.targetPosition = position
recyclerView.layoutManager.startSmoothScroll(smoothScroller)
}
override fun onBindViewHolder(holder: AdvancedRecViewHoriCustomViewHolder, position: Int) {
val itemName = itemsList[position]
itemView.rec_view_adv_hori_row_text_view_id.text = itemName
holder.itemClickListener = object : RecItemClickListener {
override fun onClick(view: View, position: Int) {
selectedPosition = position // Update layout position of clicked item.
scrollToPosition(position)
}
}
}
}
A custom ViewHolder:
class AdvancedRecViewHoriCustomViewHolder(val view: View) :
RecyclerView.ViewHolder(view), View.OnClickListener {
var itemClickListener: RecItemClickListener? = null
init {
view.setOnClickListener(this#AdvancedRecViewHoriCustomViewHolder)
}
override fun onClick(view: View) {
try {
itemClickListener?.onClick(view, adapterPosition)
} catch (exception: NullPointerException) {
Log.e("NullPointerException", "Item view is null")
}
}
}
The problem is that when I click one of the recyclerView items I get system prints like this. As Pawel has given an advice, I've checked and I'm adding addresses of instances. But, prints come from the same instances, so it's not solving this issue.
I/System.out:
calcSpeedPerPixel(..) inchPerMilisecs: 0.0, instance: com.example.danielg.loginviewexercise.ScrollerManager#1c4f5c5
init inchPerMilisecs: 30.0, instance: com.example.danielg.loginviewexercise.ScrollerManager#1c4f5c5
printSpeed(): 30.0, instance: com.example.danielg.loginviewexercise.ScrollerManager#1c4f5c5
The question is:
Why millisecsPerInch doesn’t have the value passed in a ScrollerManager class constructor and its value is 0.0?
I've done some research and have looked inside LinearSmoothScroller class implementation. Maybe the problem is that a calculateSpeedPerPixel method is protected. I've also considered overriding a value of MILLISECONDS_PER_INCH property, but it's final.
Listing from LinearSmoothScroller.java file:
private static final float MILLISECONDS_PER_INCH = 25f;
...
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}

Categories

Resources