I had a issue with PositionalDataSource, were loadRange not getting called once i invalidate the data source, this issue happens only if i add the item decoration for my recyclerview for sticky header
Without item decoration for recyclerview works fine
Here is my datasource
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<SomeData>) {
getData(0, params.requestedLoadSize)
.doOnSubscribe {
loading.set(true)
}
.map {
callback.onResult(it.data, 0)
}
.doOnComplete {
loading.set(false)
}
.subscribe()
.addTo(disposal)
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<SomeData>) {
getData(params.startPosition, params.loadSize)
.doOnSubscribe {
loading.set(true)
}
.map {
callback.onResult(it.data)
}
.doOnComplete {
loading.set(false)
}
.subscribe()
.addTo(disposal)
}
I invalidate the datasource from getData(int,int)
Here is my sticky header item decoration class
class StickyHeaderItemDecoration constructor(listener : StickyHeader) : RecyclerView.ItemDecoration() {
private val mListener: StickyHeader = listener
private var mStickyHeaderHeight = 0
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(canvas, parent, state)
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val headerPos = mListener.getHeaderPositionFor(topChildPosition)
val currentHeader = getHeaderViewForItem(headerPos, parent)
fixLayoutSize(parent, currentHeader)
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint, headerPos)
if (childInContact != null && mListener.isHeaderView(
parent.getChildAdapterPosition(
childInContact
)
)
) {
moveHeader(canvas, currentHeader, childInContact)
return
}
drawHeader(canvas, currentHeader)
}
private fun getHeaderViewForItem(headerPosition: Int, parent: RecyclerView): View {
val layoutResId = mListener.getHeaderLayoutIdFor(headerPosition)
val header =
LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
// mListener.bindHeaderData(header, headerPosition)
return header
}
private fun drawHeader(c: Canvas, header: View) {
c.save()
c.translate(0F, 0F)
header.draw(c)
c.restore()
}
private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
c.save()
c.translate(0F, (nextHeader.top - currentHeader.height)*1.0F)
currentHeader.draw(c)
c.restore()
}
private fun getChildInContact(
parent: RecyclerView,
contactPoint: Int,
currentHeaderPos: Int
): View? {
var childInContact: View? = null
for (i in 0 until parent.childCount) {
var heightTolerance = 0
val child = parent.getChildAt(i)
//measure height tolerance with child if child is another header
if (currentHeaderPos != i) {
val isChildHeader =
mListener.isHeaderView(parent.getChildAdapterPosition(child))
if (isChildHeader) {
heightTolerance = mStickyHeaderHeight - child.height
}
}
//add heightTolerance if child top be in display area
var childBottomPosition: Int
childBottomPosition = if (child.top > 0) {
child.bottom + heightTolerance
} else {
child.bottom
}
if (childBottomPosition > contactPoint) {
if (child.top <= contactPoint) { // This child overlaps the contactPoint
childInContact = child
break
}
}
}
return childInContact
}
/**
* Properly measures and layouts the top sticky header.
* #param parent ViewGroup: RecyclerView in this case.
*/
private fun fixLayoutSize(
parent: ViewGroup,
view: View
) { // Specs for parent (RecyclerView)
val widthSpec =
View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec =
View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Specs for children (headers)
val childWidthSpec = ViewGroup.getChildMeasureSpec(
widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width
)
val childHeightSpec = ViewGroup.getChildMeasureSpec(
heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height
)
view.measure(childWidthSpec, childHeightSpec)
view.layout(
0,
0,
view.measuredWidth,
view.measuredHeight.also { mStickyHeaderHeight = it }
)
}
}
Can anyone guess, what may be the issue ?
Related
I am unable to used pagination in HeterogeneousRecyclerview.it contents horizontal and vertical recycleview. I want to use pagination in VerticalRecycleview endless. When I try to do that it will not satisfy conditon.I will return visibleItemCount,totalItemCount and firstVisibleItemPosition that will not help to load more data. Please suggest me.
Recycle view:
val recyclerView = findViewById<RecyclerView>(R.id.rvMain)
val adapter = MainAdapter(this, getObject())
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
recycleViewScroll(recyclerView)
private fun getObject(): ArrayList<Any> {
jsonHelper?.readHorizontalItemsJSON()?.get(0)?.let { objects.add(it) }
jsonHelper?.readVerticalItemsJSON()?.get(0)?.let { objects.add(it) }
jsonHelper?.readHorizontalItemsJSON()?.get(0)?.let { objects.add(it) }
return objects as ArrayList<Any>
}
private fun recycleViewScroll(recyclerView: RecyclerView) {
recyclerView.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val visibleItemCount = recyclerView.layoutManager!!.childCount
val totalItemCount = recyclerView.layoutManager!!.itemCount
val firstVisibleItemPosition =
(recyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
val lastpositionView = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
//val lastpositionView = (recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
println("dsfdsfdsfdfd----->" + visibleItemCount + "," + totalItemCount + "," + firstVisibleItemPosition!! +","+lastpositionView)
if (!isLoading && !isLastPage) {//
if (visibleItemCount + firstVisibleItemPosition!! >= totalItemCount && firstVisibleItemPosition!! >= 0 && totalItemCount >= PAGE_SIZE) {
loadMoreItems()
}
}
}
})
}
}`
Adapter:
class MainAdapter(private val context: Context, private val items: ArrayList<Any>?=null) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val jsonHelper = JsonHelper(context)
//this method returns the number according to the Vertical/Horizontal object
override
fun getItemViewType(position: Int): Int {
if (items?.get(position) is HorizontalModel)
return HORIZONTAL
return if (items?.get(position) is VerticalModel) VERTICAL else -1
}
override
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(context)
val view: View
val holder: RecyclerView.ViewHolder
when (viewType) {
HORIZONTAL -> {
view = inflater.inflate(R.layout.layout_horizontal_recycler_view, parent, false)
holder = HorizontalViewHolder(view)
}
VERTICAL -> {
view = inflater.inflate(R.layout.layout_vertical_recycler_view, parent, false)
holder = VerticalViewHolder(view)
}
else -> {
view = inflater.inflate(R.layout.layout_horizontal_recycler_view, parent, false)
holder = HorizontalViewHolder(view)
}
}
return holder
}
override
fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == HORIZONTAL)
horizontalView(holder as HorizontalViewHolder)
else if (holder.itemViewType == VERTICAL)
verticalView(holder as VerticalViewHolder)
}
private fun horizontalView(holder: HorizontalViewHolder) {
val adapter = HorizontalAdapter(context, jsonHelper.readHorizontalItemsJSON()!!)
holder.recyclerView.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
holder.recyclerView.adapter = adapter
}
private fun verticalView(holder: VerticalViewHolder) {
val adapter = VerticalAdapter(context, jsonHelper.readVerticalItemsJSON()!!)
holder.recyclerView.layoutManager = LinearLayoutManager(context)
holder.recyclerView.adapter = adapter
}
override
fun getItemCount(): Int {
return items?.size ?:0
}
inner class HorizontalViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var recyclerView: RecyclerView = itemView.findViewById(R.id.rvHorizontal)
}
inner class VerticalViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
internal var recyclerView: RecyclerView = itemView.findViewById(R.id.rvVertical)
}
}
I have recyclerview with horizontal scroll and it snaps by default with LinearSnapHelper or PagerSnapHelper to the center of item. I want to snap to the left side of the each item. Is it possible?
You can easily extend PagerSnapHelper to align items by left side. There is one trick only needed with last item.
My solution is:
class AlignLeftPagerSnapHelper : PagerSnapHelper() {
private var horizontalHelper: OrientationHelper? = null
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
return getStartView(layoutManager as LinearLayoutManager, getHorizontalHelper(layoutManager))
}
private fun getStartView(layoutManager: LinearLayoutManager, helper: OrientationHelper): View? {
val firstVisibleChildPosition = layoutManager.findFirstVisibleItemPosition()
val lastCompletelyVisibleChildPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val lastChildPosition = layoutManager.itemCount - 1
if (firstVisibleChildPosition != RecyclerView.NO_POSITION) {
var childView = layoutManager.findViewByPosition(firstVisibleChildPosition)
if (helper.getDecoratedEnd(childView) < helper.getDecoratedMeasurement(childView) / 2) {
childView = layoutManager.findViewByPosition(firstVisibleChildPosition + 1)
} else if (lastCompletelyVisibleChildPosition == lastChildPosition) {
childView = layoutManager.findViewByPosition(lastChildPosition)
}
return childView
}
return null
}
override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray =
intArrayOf(distanceToStart(targetView, getHorizontalHelper(layoutManager)), 0)
override fun findTargetSnapPosition(
layoutManager: RecyclerView.LayoutManager,
velocityX: Int,
velocityY: Int
): Int {
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val currentPosition = layoutManager.getPosition(currentView)
return if (velocityX < 0) {
(currentPosition - 1).coerceAtLeast(0)
} else {
(currentPosition + 1).coerceAtMost(layoutManager.itemCount - 1)
}
}
private fun distanceToStart(targetView: View, helper: OrientationHelper): Int =
helper.getDecoratedStart(targetView) - helper.startAfterPadding
private fun getHorizontalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (horizontalHelper == null) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return horizontalHelper!!
}
}
As above pic show, The items will be misplaced momently while i refresh.
smartRefresh.setOnRefreshListener {
mViewModel.observeComposition(ConnectivityUtil.isNetworkReachable(this),mLabelId.toInt(),lifecycleScope).observe(this, Observer {
mCompositionAdapter.submitList(it)
smartRefresh.finishRefresh()
})
}
My Adapter
class CompositionsAdapter(context: Context) : PagedListAdapter<CompositionAbstract, CompositionsAdapter.ViewHolder>(CompositionAbstractDiffCallback()) {
private val mScreenWidth by lazy {
val displayMetrics = DisplayMetrics()
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
displayMetrics.widthPixels
}
val width by lazy {
(mScreenWidth - 15.px * 3) / 2
}
val height by lazy {
val srcWidth = 165f.px
val srcHeight = 155f.px
width * (srcHeight / srcWidth)
}
class ViewHolder(private val binding: HomepageItemGroupCompositionBinding)
: RecyclerView.ViewHolder(binding.root) {
fun bind(item: CompositionAbstract) {
binding.apply {
composition = item.briefComposition
itemView.setOnClickListener {
ARouter.getInstance().build(HomePageArouterConstants.PATH_AUDIO_PLAYER).withString(HomePageArouterConstants.KEY_ID, item.briefComposition.id).navigation()
}
executePendingBindings()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = HomepageItemGroupCompositionBinding.inflate(LayoutInflater.from(parent.context))
binding.imageView.layoutParams = LinearLayout.LayoutParams(width, height.toInt())
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
getItem(position)?.let {
holder.bind(it)
}
}
}
My Itemdecoration
class GridSpacingItemDecoration(private val spanCount: Int,
private val verticalSpacing: Int,
private val topSpacing: Int
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view)
if (position / spanCount == 0) {
outRect.top = topSpacing
}
if (position % spanCount == 1) {
outRect.left = 7.px
}
outRect.bottom = verticalSpacing
}
}
My diffUtil
class CompositionAbstractDiffCallback : DiffUtil.ItemCallback<CompositionAbstract>() {
override fun areItemsTheSame(oldItem: CompositionAbstract, newItem: CompositionAbstract): Boolean {
return oldItem.briefComposition.id == newItem.briefComposition.id
}
override fun areContentsTheSame(oldItem: CompositionAbstract, newItem: CompositionAbstract): Boolean {
return oldItem == newItem
}
}
Any ideas for this problem? Thanks for first!
I am trying to create a method that will listen to the item that is clicked and if the category is clicked, it should open the activity selected. I have a Grid Layout setup, but I am not really sure how I would set an onClickListener or an onRecyclerViewItemClick for this purpose. I do have a pseudocode that I believe could be implemented, but would need guidance for this.
Here is my MainActivity.kt:
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
private lateinit var appBarConfiguration: AppBarConfiguration
private var mAuthListener : FirebaseAuth.AuthStateListener? = null
lateinit var myApi: IMyAPI
lateinit var txt_user_name:TextView
lateinit var txt_email_address:TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
myApi = RetrofitClient.getInstance().create(IMyAPI::class.java)
Log.d("__MAIN__", "OnCreate")
}
private fun initView() {
recycler_view.layoutManager = GridLayoutManager(this, 2)
recycler_view.addItemDecoration(GridItemDecoration(10, 2))
val categoryListAdapter = CategoryListGridRecyclerAdapter()
recycler_view.adapter = categoryListAdapter
categoryListAdapter.setProductList(generateDummyData())
val navView: NavigationView = findViewById(R.id.nav_view)
navView.setNavigationItemSelectedListener(this)
navView.bringToFront()
}
private fun generateDummyData(): List<Category> {
val listOfCategory = mutableListOf<Category>()
var categoryModel = Category(1, "Products", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
categoryModel = Category(2, "Food", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
categoryModel = Category(3, "Technology", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
categoryModel = Category(4, "News", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
categoryModel = Category(5, "Economy", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
categoryModel = Category(6, "Sports", R.drawable.kumo_logo, 1.90)
listOfCategory.add(categoryModel)
return listOfCategory
}
//Pseudocode:
/*override fun onRecyclerViewItemClick(category: Category, position: Int)
{
lateinit var catClick: Intent
if(category.id == 1)
{
catClick = Intent(this, ShowProducts::class.java)
}
startActivity(catClick) //start activity selected
}*/
override fun onNavigationItemSelected(item: MenuItem):Boolean {
when (item.itemId) {
R.id.nav_sign_out -> {
Log.d("Testing logout ", "user")
FirebaseAuth.getInstance().signOut()
startActivity(Intent(this#MainActivity, LoginActivity::class.java))
finish()
}
}
return true
}
}
GridItemDecoration.kt:
class GridItemDecoration(gridSpacingPx: Int, gridSize: Int) : RecyclerView.ItemDecoration() {
private var mSizeGridSpacingPx: Int = gridSpacingPx
private var mGridSize: Int = gridSize
private var mNeedLeftSpacing = false
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val frameWidth = ((parent.width - mSizeGridSpacingPx.toFloat() * (mGridSize - 1)) / mGridSize).toInt()
val padding = parent.width / mGridSize - frameWidth
val itemPosition = (view.getLayoutParams() as RecyclerView.LayoutParams).viewAdapterPosition
if (itemPosition < mGridSize) {
outRect.top = 0
} else {
outRect.top = mSizeGridSpacingPx
}
if (itemPosition % mGridSize == 0) {
outRect.left = 0
outRect.right = padding
mNeedLeftSpacing = true
} else if ((itemPosition + 1) % mGridSize == 0) {
mNeedLeftSpacing = false
outRect.right = 0
outRect.left = padding
} else if (mNeedLeftSpacing) {
mNeedLeftSpacing = false
outRect.left = mSizeGridSpacingPx - padding
if ((itemPosition + 2) % mGridSize == 0) {
outRect.right = mSizeGridSpacingPx - padding
} else {
outRect.right = mSizeGridSpacingPx / 2
}
} else if ((itemPosition + 2) % mGridSize == 0) {
mNeedLeftSpacing = false
outRect.left = mSizeGridSpacingPx / 2
outRect.right = mSizeGridSpacingPx - padding
} else {
mNeedLeftSpacing = false
outRect.left = mSizeGridSpacingPx / 2
outRect.right = mSizeGridSpacingPx / 2
}
outRect.bottom = 0
}
}
CategoryListGridRecyclerAdapter.kt
class CategoryListGridRecyclerAdapter:RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var listOfCategory = listOf<Category>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CategoryListViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.product_row, parent, false))
}
override fun getItemCount(): Int = listOfCategory.size
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
val productViewHolder = viewHolder as CategoryListViewHolder
productViewHolder.bindView(listOfCategory[position])
}
fun setProductList(listOfCategory: List<Category>) {
this.listOfCategory = listOfCategory
notifyDataSetChanged()
}
}
You can do something like this:
lateinit var mOnItemClickListener: (category: Category,pos: Int)-> Unit
fun setOnItemClickListener(callback: (category: Category,pos: Int)-> Unit){
mOnItemClickListener = callback
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CategoryListViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.product_row, parent, false))
.listen {pos,_ ->
if(::mOnItemClickListener.isInitialized){
mOnItemClickListener(listOfCategory[pos],pos)
}
}
}
//I took this code from a post a while back, don't know the author
fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
itemView.setOnClickListener {
event.invoke(adapterPosition, itemViewType)
}
return this
}
On MainActivity
categoryListAdapter.setOnItemClickListener { category, pos ->
...
}
I'd like to headers in a list using the android.arch.paging components.
Usually this would easy enough, just add different types for a RecyclerView adapter to handle and create a new list from items including headers.
But with the paging components I'm essentially handing the PagedList a PositionalDataSource from an SQL query. Is it possible to interrupt this and add header types?
How I ended up solving this for anyone interested.
when binding the data I pass in the previous item as well
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val previousItem = if (position == 0) null else getItem(position - 1)
holder.bind(item, previousItem)
}
Every view then sets a header, which is only made visible if the previous item doesn't have the same header.
val previousHeader = previousItem?.name?.capitalize().first()
val header = item?.name?.capitalize()?.first()
view.cachedContactHeader.text = header
view.cachedContactHeader.isVisible = previousHeader != header
Update 24/01/20
Since answering this I have changed to using a custom ItemDecoration
class StickyHeaderDividerItem(
context: Context,
private val stickyHeaderCallbacks: StickyHeaderCallbacks
) : ItemDecoration() {
private val headerHeight = context.resources.getDimensionPixelOffset(R.dimen.sticky_header_height)
private var headerView: View? = null
private var headerTextView: TextView? = null
private var headerEndTextView: TextView? = null
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
if (position >= 0 && stickyHeaderCallbacks.isHeader(position)) {
outRect.top = headerHeight
}
}
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: State) {
super.onDrawOver(canvas, parent, state)
if (headerView == null) {
headerView = (parent.inflate(R.layout.list_item_sticky_header)).apply {
fixLayoutSize(this, parent)
}
headerTextView = headerView?.headerText
headerEndTextView = headerView?.headerEndText
}
var previousHeader = ""
parent.children.toList().forEach { child ->
val position = parent.getChildAdapterPosition(child)
val headerTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitle).orEmpty()
val headerEndTitle = position.takeIf { it >= 0 }?.let(stickyHeaderCallbacks::headerTitleEnd)
headerTextView?.text = headerTitle
headerEndTextView?.text = headerEndTitle
if ((previousHeader != headerTitle) || (position >= 0 && stickyHeaderCallbacks.isHeader(position))) {
headerView?.let { drawHeader(canvas, child, it) }
previousHeader = headerTitle
}
}
}
private fun drawHeader(canvas: Canvas, child: View, headerView: View) {
canvas.run {
save()
translate(0F, maxOf(0, child.top - headerView.height).toFloat())
headerView.draw(this)
restore()
}
}
private fun fixLayoutSize(view: View, parent: ViewGroup) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingStart + parent.paddingEnd, view.layoutParams.width)
val childHeight = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
interface StickyHeaderCallbacks {
fun isHeader(itemPosition: Int): Boolean
fun headerTitle(itemPosition: Int): String
fun headerTitleEnd(itemPosition: Int): String? = null
}