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.
Related
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);
I have recyclerView inside NestedScrollView. I want to calculate speed to recyclerview but it is not working inside nestedScrollView.
This is code to calculate scroll speed which is working without NestedScrollView. I want to make it work with NestedScrollView also.
I have set nested scrolling false but its not working
recyclerView.setNestedScrollingEnabled(false)
Class:
abstract class ScrollSpeedRecycleViewScrollListener(private val maxScrollSpeedForAdInjection: Int) :
RecyclerView.OnScrollListener() {
var currentScrollSpeed: Int = 0
private var previousFirstVisibleItem = 0
private var previousEventTime: Long = 0
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val firstItemPosition = layoutManager.findFirstVisibleItemPosition()
if (previousFirstVisibleItem != firstItemPosition) {
}
}
}
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> {
if (currentScrollSpeed > maxScrollSpeedForAdInjection) {
listNeedsRefresh()
}
currentScrollSpeed = 0
}
}
}
}
The concept of speed involves distance covered over a certain period of time. You can get the values scrolled (distance) using the dx or dy in the onScrolled method. To get time you would need to get some timestamps at the point where you're getting the distance values and then use a calculation of speed = distance / time. But your also going to need calculate the difference between calls of onScrolled. so it'll actually end up as
speed = (distance2 - distance1) / (time2 - time1)
The problem you're going to have here is that the method will be getting called loads when the user is scrolling fast, and the calculation will need to be done each time and that will have a detrimental effect on the smoothness of the scroll.
I'm willing to bet there is a better way to overcome your problem.
First of all let me show you an image what i am trying achieve exactly :
Now as of above gif i need to build it with recyclerview.
Fit only 5 items at a time on screen.
Center and other 4 items will be scaled as shown in image.
I have tried with custom layout manager like below:
private val shrinkAmount = 0.3f
private val shrinkDistance = 1f
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == VERTICAL) {
val scrolled = super.scrollVerticallyBy(dy, recycler, state)
if (isScaleView()) {
val midpoint = height / 2f
val d0 = 0f
val d1 = shrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - shrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedBottom(child!!) + getDecoratedTop(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
}
return scrolled
} else {
return 0
}
}
But i am getting output as follows:
How can i achive exactly like above gif ?
What I'm trying to do.
Create a simple carousel with RecyclerView.
Problem
Initially the view is not snap to center and the view is not getting the style I intended to.(i.e, the item which is fully visible should be bigger than other, when scroll by finger it works fine)
When scroll programmatically the view is not getting snap effect like it does when scroll with finger.
See the attached gif below for example.
Question
How to have the style as intended (i.e the fully visible item is bigger) when started.
How to get the style when scroll to button is click. (It scrolls to correct position the only problem is not getting the style as intended and its not snap to center)
Full code here on github
Here's the code for custom LayoutManager
open class CarouselLayoutManager(
context: Context,
orientation: Int,
reverseLayout: Boolean
) : LinearLayoutManager(context, orientation, reverseLayout) {
private val mShrinkAmount = 0.15f
private val mShrinkDistance = 0.9f
override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {
scrollVerticallyBy(0, recycler, state)
super.onLayoutChildren(recycler, state)
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == LinearLayoutManager.HORIZONTAL) {
val scrolled = super.scrollHorizontallyBy(dx, recycler, state)
val midpoint = width / 2f
val d0 = 0f
val d1 = mShrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - mShrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedRight(child) + getDecoratedLeft(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
return scrolled
} else {
return 0
}
}
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int {
val orientation = orientation
if (orientation == LinearLayoutManager.VERTICAL) {
val scrolled = super.scrollVerticallyBy(dy, recycler, state)
val midpoint = height / 2f
val d0 = 0f
val d1 = mShrinkDistance * midpoint
val s0 = 1f
val s1 = 1f - mShrinkAmount
for (i in 0 until childCount) {
val child = getChildAt(i)
val childMidpoint = (getDecoratedBottom(child) + getDecoratedTop(child)) / 2f
val d = Math.min(d1, Math.abs(midpoint - childMidpoint))
val scale = s0 + (s1 - s0) * (d - d0) / (d1 - d0)
child.scaleX = scale
child.scaleY = scale
}
return scrolled
} else {
return 0
}
}
}
Finally I have solved the problem by using this libraries/examples
DiscreteScrollView
android-viewpager-transformers
Here is the final result.
For full code see Carousel Demo
call scrollVerticallyBy(0, recycler, state) in onLayoutCompleted() method
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?