I want to display items in a horizontal list using RecyclerView. At a time, only 3 items will be displayed. 1 in the middle and the other 2 on the side, below is an image of what I'm trying to achieve:
I'm using LinearSnapHelper which centers an item all of the time. When an item is moved away from the center I would like the opacity to progessively change from 1f to 0.5f.
Here is the below code which I've written to help:
class CustomRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) {
private var itemBoundsRect: Rect? = null
init {
itemBoundsRect = Rect()
addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
calculateVisibility()
}
})
}
private fun calculateVisibility() {
val linearLayoutManger: LinearLayoutManager = layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
var indexes: MutableList<Int> = mutableListOf()
for (i in firstVisibleItem..lastVisibleItem) {
indexes.add(i)
val item: View = layoutManager?.findViewByPosition(i) ?: continue
item.getGlobalVisibleRect(itemBoundsRect)
var itemSize = layoutManager!!.findViewByPosition(i)!!.width
var visibleSize = 0
if (indexes.size == 1) {
visibleSize = itemBoundsRect!!.right
} else {
visibleSize = itemBoundsRect!!.right - itemBoundsRect!!.left
}
var visibilty = visibleSize * 100 / itemSize
if (visibilty > 0) {
visibilty = 100 - visibilty
}
val viewHolder = findViewHolderForLayoutPosition(i)
viewHolder!!.itemView.alpha = (100 - visibilty).toFloat() / 100f
}
}
}
It doesn't work as expected as the opacity changes at the wrong time. The image below demonstrates this better. I expect the opacity to progressively begin to change when the item edges come out of the red box. However, it only starts when the item reaches the yellow edges.
Is there a way to achieve this effect?
Thank you :)
Your code for calculateVisibility() is looking at global position when looking at the relative position within the RecyclerView is sufficient. Maybe there is more to the code than you posted, but try the following. This code looks at the x position of each visible view and calculates the alpha value as a function of displacement from the center of the RecyclerView. Comments are in the code.
private fun calculateVisibility(recycler: RecyclerView) {
val midRecycler = recycler.width / 2
val linearLayoutManger: LinearLayoutManager = recycler.layoutManager as LinearLayoutManager
val firstVisibleItem = linearLayoutManger.findFirstVisibleItemPosition()
val lastVisibleItem = linearLayoutManger.findLastVisibleItemPosition()
for (i in firstVisibleItem..lastVisibleItem) {
val viewHolder = recycler.findViewHolderForLayoutPosition(i)
viewHolder?.itemView?.apply {
// This is the end of the view in the parent's coordinates
val viewEnd = x + width
// This is the maximum pixels the view can slide left or right until it disappears.
val maxSlide = (midRecycler + width / 2).toFloat()
// Alpha is determined by the percentage of the maximum slide the view has moved.
// This assumes a linear fade but can be adjusted to fade in alternate ways.
alpha = 1f - abs(maxSlide - viewEnd) / maxSlide
Log.d("Applog", String.format("pos=%d alpha=%f", i, alpha))
}
}
}
The foregoing assumes that sizes remain constant.
if you need the center View, you can call
View view = snapHelper.findSnapView(layoutManagaer);
once you have the View, you should be able to get the position on the dataset for that View. For instance using
int pos = adapter.getChildAdapterPosition(view)
And then you can update the center View opacity and invoke
adapter.notifyItemChanged(pos);
Related
I have implemented a "page peek" feature for my ViewPager2:
private fun setViewPager() {
inventoryVp?.apply {
clipToPadding = false // allow full width shown with padding
clipChildren = false // allow left/right item is not clipped
offscreenPageLimit = 2 // make sure left/right item is rendered
}
inventoryVp?.setPadding(Utility.dpToPx(25), 0, Utility.dpToPx(25), 0)
val pageMarginPx = Utility.dpToPx(6)
val marginTransformer = MarginPageTransformer(pageMarginPx)
inventoryVp?.setPageTransformer(marginTransformer)
}
Doing this I am able to view a portion of the previous and next page. But first and last page show a bigger white space because there's no other page in this direction to show.
How can I set a different padding for the first and last page?
I solved it using ItemDecoration.
class CartOOSVPItemDecoration(val marginStart: Int,
val marginEnd: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.getChildAdapterPosition(view) == 0) {
outRect.left = marginStart
}
else if(parent.getChildAdapterPosition(view) == ((parent.adapter?.itemCount ?: 0) - 1)) {
outRect.right = marginEnd
}
}
}
inventoryVp?.addItemDecoration( CartOOSVPItemDecoration(Utility.dpToPx(-9), Utility.dpToPx(-9)))
I want to resize the width of My button from Kotlin code.
I tried this
private var viewPagerPageChangeListener: ViewPager.OnPageChangeListener = object : ViewPager.OnPageChangeListener {
override fun onPageSelected(position: Int) {
addBottomDots(position)
if (position == layouts!!.size - 1) {
btnNext!!.text = getString(R.string.start)
btnNext!!.layoutParams = RelativeLayout.LayoutParams(315, 44)
btnSkip!!.visibility = View.GONE
} else {
btnNext!!.text = getString(R.string.next)
btnSkip!!.visibility = View.VISIBLE
}
}
I am using this in the View Pager.
After running my application, button is disappeared from the screen.
You can use View's scaleX and scaleY if you just want to simply stretch the view. Getting the actual width and height is a bit complicated as you have to take into account listening to a ViewTreeObserver
I am looking for a way to programmatically slowly scroll a RecyclerView so as to bring a certain element targetPosition exactly in the middle of the screen. Note that all my items in the RecylerView have the same height by design. The RecyclerView is vertical. I am programming in Kotlin and AndroidX
I have tried:
smoothScrollToPosition(targetPosition)
It does the scrolling slowly (I can also control the speed by extending the LinearLayoutManager and overriding calculateSpeedPerPixel() - see How can i control the scrolling speed of recyclerView.smoothScrollToPosition(position)), however I can't control the exact location of the list item on the screen after the scrolling. All I know is it will be fully visible on the screen.
I have tried:
scrollToX(0, targetPosition * itemHeight - screenHeight / 2)
It gives me the error message: "W/RecyclerView: RecyclerView does not support scrolling to an absolute position. Use scrollToPosition instead"
I have also tried replacing the RecyclerView by a LinearLayoutManager which contains all the items as children, and translate the LinearLayoutManager in the view, but I don't get the children initially outside of the screen to draw.
Is there a solution to this issue?
You can calculate the smoothScrollToPosition's targetPosition based on your actual target position and the number of visible items.
A quick POC on how to do that:
val scrollToPosition = 50
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstPosition = layoutManager.findFirstVisibleItemPosition()
val lastPosition = layoutManager.findLastVisibleItemPosition()
val visibleItems = lastPosition - firstPosition + 1
if (firstPosition < scrollToPosition) {
recyclerView.smoothScrollToPosition(scrollToPosition + (visibleItems / 2))
} else {
recyclerView.smoothScrollToPosition(scrollToPosition - (visibleItems / 2))
}
If you want more precise results that the item should be exactly at the middle of the screen, you can use the heights of the item (since its fixed) and the height of the RecyclerView, then calculate the offset to scroll. And call:
recyclerView.scrollBy(dx, dy)
Or:
recyclerView.smoothScrollBy(dx, dy)
Thank you #Bob. I missed scrollBy(). Following your advice, here is the code that worked for me.
class RecyclerViewFixedItemSize : RecyclerView {
var itemFixedSize : Int = 0
constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) {
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
}
constructor(context: Context) : super(context) {
}
fun smoothScrollToPositionCentered(position: Int) {
// Find the fixed item size - once for all
if (itemFixedSize == 0) {
val ll = layoutManager as LinearLayoutManager
val fp = ll.findFirstCompletelyVisibleItemPosition()
val fv = ll.getChildAt(fp)
itemFixedSize = fv!!.height
}
// find the recycler view position and screen coordinates of the first item (partially) visible on screen
val ll = layoutManager as LinearLayoutManagerFixedItemSize
val fp = ll.findFirstVisibleItemPosition()
val fv = ll.getChildAt(0)
// find the middle of the recycler view
val dyrv = (top - bottom ) / 2
// compute the number of pixels to scroll to get the view position in the center
val dy = (position - fp) * itemFixedSize + dyrv + fv!!.top + itemFixedSize / 2
smoothScrollBy(0, dy)
}
}
When scrolling in a RecyclerView, I want to achieve something like this:
Where the center item is scaled bigger than the other items. I've found a way to scale all the items, or what is called a Carousel Effect. But that is not what I want. I want to scale only the center item, and keep the other items their default size.
this is what I've found on other answers:
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val layoutManager = recyclerView.layoutManager!!
val midpoint = layoutManager.width / 2f
val distance = shrinkDistance * midpoint
for (i in 0 until layoutManager.childCount) {
val child = layoutManager.getChildAt(i)!!
val childMidpoint = (layoutManager.getDecoratedRight(child) + layoutManager.getDecoratedLeft(child)) / 2f
val d = Math.min(distance, Math.abs(midpoint - childMidpoint))
val scale = 1 + -shrinkAmount * d / distance
child.scaleX = scale
child.scaleY = scale
}
}
})
}
As I said, this scales all the visible items, where I only want to zoom the center item.
You could give the middle child different scaling.
Calculate the middle child from childCount and use a different scaling for it than the others.
I'm using data binding to setup a RecyclerView. Here is the binding adapter:
fun setRecyclerDevices(recyclerView: RecyclerView, items: List<Device>, itemBinder: MultipleTypeItemBinder,
listener: BindableListAdapter.OnClickListener<Device>?) {
var adapter = recyclerView.adapter as? DevicesBindableAdapter
if (adapter == null) {
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
recyclerView.addItemDecoration(SpaceItemDecorator(left = 15, top = 15, right = 15, bottom = 15))
adapter = DevicesBindableAdapter(items, itemBinder)
adapter.setOnClickListener(listener)
recyclerView.adapter = adapter
} else {
adapter.setOnClickListener(listener)
adapter.setItemBinder(itemBinder)
adapter.setItems(items)
}
}
getSpanSizeFromScreenWidth needs the recycler's width to do some calculation. But it always returns 0.
I tried to apply a ViewTreeObserver like this:
recyclerView.viewTreeObserver.addOnGlobalLayoutListener(object: ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
}
})
Or use post like this:
recyclerView.post({
val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = SpannedGridLayoutManager.Orientation.VERTICAL,
spans = getSpanSizeFromScreenWidth(recyclerView.context, recyclerView))
recyclerView.layoutManager = spannedGridLayoutManager
})
Code of getSpanSizeFormScreenWidth:
private fun getSpanSizeFromScreenWidth(context: Context, recyclerView: RecyclerView): Int {
val availableWidth = recyclerView.width.toFloat()
val px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300f, context.resources.displayMetrics)
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 15f, context.resources.displayMetrics)
return Math.max(1, Math.floor((availableWidth / (px + margin)).toDouble()).toInt()) * DevicesBindableAdapter.WIDTH_UNIT_VALUE
}
But it still returns 0 despite my RecyclerView being displayed on the screen (not 0).
Any ideas?
In inspecting the code, it appears that your RecyclerView may actually be fine, but your logic in getSpanSizeFromScreenWidth may not be.
It looks like this: Math.floor((availableWidth / (px + margin)).toDouble()).toInt() will always be 0 when availableWidth is less than (px + margin). This will then cause getSpanSizeFromScreenWidth to return 0.
Breaking it down:
Math.floor - rounds a double down to a whole number
availableWidth / (px + margin) - will be a low number (a fraction of availableWidth)
Therefore, you're going to get 0 at times especially on smaller screens and/or smaller density screens.
Does that make sense? May not be this issue, but I'd start there. It's hard to tell you exactly the issue without knowing the whole context, but that's likely your issue.
If that is not your issue, could you say what your value is for availableWidth, px, and margin during execution?