fix height of scrollbar in nested recyclerview android - android

I have a horizontal Recyclerview inside another horizontal Recyclerview. Now I want to show the scrollbar. But the height of the scrollbar is changing as I scroll. I want to have a fixed height of the scrollbar as I scroll. How can I achieve that? I have a custom LinearLayoutManager. But it is not working properly. Any better solution to this problem?
class CoolLayoutManager(context: Context,orientation: Int,
reverseLayout: Boolean) : LinearLayoutManager(context,orientation,
reverseLayout) {
init {
isSmoothScrollbarEnabled = true
}
override fun computeHorizontalScrollExtent(state: State): Int {
val count = childCount
return if (count > 0) {
SMOOTH_VALUE * 3
} else 0
}
override fun computeHorizontalScrollRange(state: State): Int {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
override fun computeHorizontalScrollOffset(state: State): Int {
val count = childCount
if (count <= 0) {
return 0
}
if (findLastCompletelyVisibleItemPosition() == itemCount - 1) {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
val widthOfScreen: Int
val firstPos = findFirstVisibleItemPosition()
if (firstPos == RecyclerView.NO_POSITION) {
return 0
}
val view = findViewByPosition(firstPos) ?: return 0
// Top of the view in pixels
val top = getDecoratedTop(view)
val width = getDecoratedMeasuredWidth(view)
widthOfScreen = if (width <= 0) {
0
} else {
Math.abs(SMOOTH_VALUE * top / width)
}
return if (widthOfScreen == 0 && firstPos > 0) {
SMOOTH_VALUE * firstPos - 1
} else SMOOTH_VALUE * firstPos + widthOfScreen
}
companion object {
private const val SMOOTH_VALUE = 100
}
}
Here is the screenshot of the layout:

Related

How to snap to the left side of the item in RecyclerView

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!!
}
}

SnapHelper issue with first and Last item

I am using Recyclerview with PageSnapHelper to create an Image carousel.
First item - Not Centered
The first Item is not centered and Subsequent Items should be centered, I have achieved this using item decorator. RecyclerView is inside nested scrollview.
Issue:
Scrolling is not smooth, I have override findTargetSnapPosition, It is scrolling 2 items for the first fling.
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int, velocityY: Int): Int {
if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
return RecyclerView.NO_POSITION
}
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val layoutManager = layoutManager as LinearLayoutManager
val position1 = layoutManager.findFirstVisibleItemPosition()
val position2 = layoutManager.findLastVisibleItemPosition()
var currentPosition = layoutManager.getPosition(currentView)
if (velocityX > 500) {
currentPosition = position2
} else if (velocityX < 500) {
currentPosition = position1
}
return if (currentPosition == RecyclerView.NO_POSITION) {
RecyclerView.NO_POSITION
} else currentPosition
}
If i got you right, you need to override LinearSnapHelper instead, cause your item views are not full screened. For achieving focusing on first/last items you need to override findSnapView next way(note that this snippet only applicable when RecyclerView.layoutmanager is LinearLayoutManager):
fun RecyclerView.setLinearSnapHelper(isReversed: Boolean = false) {
object : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
val firstVisiblePosition = (layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
val lastVisiblePosition = layoutManager.findLastCompletelyVisibleItemPosition()
val firstItem = 0
val lastItem = layoutManager.itemCount - 1
return when {
firstItem == firstVisiblePosition -> layoutManager.findViewByPosition(firstVisiblePosition)
lastItem == lastVisiblePosition -> layoutManager.findViewByPosition(lastVisiblePosition)
else -> super.findSnapView(layoutManager)
}
}
}.apply { attachToRecyclerView(this#setLinearSnapHelper) }
}

RecyclerView SnapHelper fails to show first/last items

I have a RecyclerView which is attached to a LinearSnapHelper to snap to center item. When I scroll to the first or last items, these items are not fully visible anymore. This problem is shown in the following image. How to solve it?
This issue happens when center of item which is next to the first/last is closer to the center of container. So, we should make some changes on snapping functionality to ignore this case. Since we need some fields in LinearSnapHelper class, we can copy its source code and make change on findCenterView method as following:
MyLinearSnapHelper.kt
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.aminography.view.component
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.OrientationHelper
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.SnapHelper
import android.view.View
/**
* Implementation of the [SnapHelper] supporting snapping in either vertical or horizontal
* orientation.
*
*
* The implementation will snap the center of the target child view to the center of
* the attached [RecyclerView]. If you intend to change this behavior then override
* [SnapHelper.calculateDistanceToFinalSnap].
*/
class MyLinearSnapHelper : SnapHelper() {
// Orientation helpers are lazily created per LayoutManager.
private var mVerticalHelper: OrientationHelper? = null
private var mHorizontalHelper: OrientationHelper? = null
override fun calculateDistanceToFinalSnap(
layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
val out = IntArray(2)
if (layoutManager.canScrollHorizontally()) {
out[0] = distanceToCenter(layoutManager, targetView,
getHorizontalHelper(layoutManager))
} else {
out[0] = 0
}
if (layoutManager.canScrollVertically()) {
out[1] = distanceToCenter(layoutManager, targetView,
getVerticalHelper(layoutManager))
} else {
out[1] = 0
}
return out
}
override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager, velocityX: Int,
velocityY: Int): Int {
if (layoutManager !is RecyclerView.SmoothScroller.ScrollVectorProvider) {
return RecyclerView.NO_POSITION
}
val itemCount = layoutManager.itemCount
if (itemCount == 0) {
return RecyclerView.NO_POSITION
}
val currentView = findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
val currentPosition = layoutManager.getPosition(currentView)
if (currentPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION
}
val vectorProvider = layoutManager as RecyclerView.SmoothScroller.ScrollVectorProvider
// deltaJumps sign comes from the velocity which may not match the order of children in
// the LayoutManager. To overcome this, we ask for a vector from the LayoutManager to
// get the direction.
val vectorForEnd = vectorProvider.computeScrollVectorForPosition(itemCount - 1)
?: // cannot get a vector for the given position.
return RecyclerView.NO_POSITION
var vDeltaJump: Int
var hDeltaJump: Int
if (layoutManager.canScrollHorizontally()) {
hDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getHorizontalHelper(layoutManager), velocityX, 0)
if (vectorForEnd.x < 0) {
hDeltaJump = -hDeltaJump
}
} else {
hDeltaJump = 0
}
if (layoutManager.canScrollVertically()) {
vDeltaJump = estimateNextPositionDiffForFling(layoutManager,
getVerticalHelper(layoutManager), 0, velocityY)
if (vectorForEnd.y < 0) {
vDeltaJump = -vDeltaJump
}
} else {
vDeltaJump = 0
}
val deltaJump = if (layoutManager.canScrollVertically()) vDeltaJump else hDeltaJump
if (deltaJump == 0) {
return RecyclerView.NO_POSITION
}
var targetPos = currentPosition + deltaJump
if (targetPos < 0) {
targetPos = 0
}
if (targetPos >= itemCount) {
targetPos = itemCount - 1
}
return targetPos
}
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
if (layoutManager.canScrollVertically()) {
return findCenterView(layoutManager, getVerticalHelper(layoutManager))
} else if (layoutManager.canScrollHorizontally()) {
return findCenterView(layoutManager, getHorizontalHelper(layoutManager))
}
return null
}
private fun distanceToCenter(layoutManager: RecyclerView.LayoutManager,
targetView: View, helper: OrientationHelper): Int {
val childCenter = helper.getDecoratedStart(targetView) + helper.getDecoratedMeasurement(targetView) / 2
val containerCenter: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
return childCenter - containerCenter
}
/**
* Estimates a position to which SnapHelper will try to scroll to in response to a fling.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The [OrientationHelper] that is created from the LayoutManager.
* #param velocityX The velocity on the x axis.
* #param velocityY The velocity on the y axis.
*
* #return The diff between the target scroll position and the current position.
*/
private fun estimateNextPositionDiffForFling(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper, velocityX: Int, velocityY: Int): Int {
val distances = calculateScrollDistance(velocityX, velocityY)
val distancePerChild = computeDistancePerChild(layoutManager, helper)
if (distancePerChild <= 0) {
return 0
}
val distance = if (Math.abs(distances[0]) > Math.abs(distances[1])) distances[0] else distances[1]
return Math.round(distance / distancePerChild)
}
/**
* Return the child view that is currently closest to the center of this parent.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The relevant [OrientationHelper] for the attached [RecyclerView].
*
* #return the child view that is currently closest to the center of this parent.
*/
private fun findCenterView(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper): View? {
// ----- Added by aminography
if (layoutManager is LinearLayoutManager) {
if (layoutManager.findFirstCompletelyVisibleItemPosition() == 0) {
return layoutManager.getChildAt(0)
} else if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.itemCount - 1) {
return layoutManager.getChildAt(layoutManager.itemCount - 1)
}
}
// -----
val childCount = layoutManager.childCount
if (childCount == 0) {
return null
}
var closestChild: View? = null
val center: Int = if (layoutManager.clipToPadding) {
helper.startAfterPadding + helper.totalSpace / 2
} else {
helper.end / 2
}
var absClosest = Integer.MAX_VALUE
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val childCenter = helper.getDecoratedStart(child) + helper.getDecoratedMeasurement(child) / 2
val absDistance = Math.abs(childCenter - center)
/** if child center is closer than previous closest, set it as closest */
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
}
}
return closestChild
}
/**
* Computes an average pixel value to pass a single child.
*
*
* Returns a negative value if it cannot be calculated.
*
* #param layoutManager The [RecyclerView.LayoutManager] associated with the attached
* [RecyclerView].
* #param helper The relevant [OrientationHelper] for the attached
* [RecyclerView.LayoutManager].
*
* #return A float value that is the average number of pixels needed to scroll by one view in
* the relevant direction.
*/
private fun computeDistancePerChild(layoutManager: RecyclerView.LayoutManager,
helper: OrientationHelper): Float {
var minPosView: View? = null
var maxPosView: View? = null
var minPos = Integer.MAX_VALUE
var maxPos = Integer.MIN_VALUE
val childCount = layoutManager.childCount
if (childCount == 0) {
return INVALID_DISTANCE
}
for (i in 0 until childCount) {
val child = layoutManager.getChildAt(i)
val pos = layoutManager.getPosition(child!!)
if (pos == RecyclerView.NO_POSITION) {
continue
}
if (pos < minPos) {
minPos = pos
minPosView = child
}
if (pos > maxPos) {
maxPos = pos
maxPosView = child
}
}
if (minPosView == null || maxPosView == null) {
return INVALID_DISTANCE
}
val start = Math.min(helper.getDecoratedStart(minPosView),
helper.getDecoratedStart(maxPosView))
val end = Math.max(helper.getDecoratedEnd(minPosView),
helper.getDecoratedEnd(maxPosView))
val distance = end - start
return if (distance == 0) {
INVALID_DISTANCE
} else 1f * distance / (maxPos - minPos + 1)
}
private fun getVerticalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (mVerticalHelper == null || mVerticalHelper!!.layoutManager !== layoutManager) {
mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager)
}
return mVerticalHelper!!
}
private fun getHorizontalHelper(
layoutManager: RecyclerView.LayoutManager): OrientationHelper {
if (mHorizontalHelper == null || mHorizontalHelper!!.layoutManager !== layoutManager) {
mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager)
}
return mHorizontalHelper!!
}
companion object {
private const val INVALID_DISTANCE = 1f
}
}
I know I am late but I want to suggest an simple solution written in Java code:
Create CustomSnapHelper class:
public class CustomSnapHelper extends LinearSnapHelper {
#Override
public View findSnapView(RecyclerView.LayoutManager layoutManager) {
if(layoutManager instanceof LinearLayoutManager){
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
if(needToDoSnap(linearLayoutManager)==false){
return null;
}
}
return super.findSnapView(layoutManager);
}
public boolean needToDoSnap(LinearLayoutManager linearLayoutManager){
return linearLayoutManager.findFirstCompletelyVisibleItemPosition()!=0&&linearLayoutManager.findLastCompletelyVisibleItemPosition()!=linearLayoutManager.getItemCount()-1;
}
}
Attach an object of CustomSnapHelper to the recycler view:
CustomSnapHelper mSnapHelper = new CustomSnapHelper();
mSnapHelper.attachToRecyclerView(mRecyclerView);
I tried to implement a simple solution. Basically I checked if the first/last items are completely visible. If so, we don't need to perform the snap. See the solution below:
class CarouselSnapHelper : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
val linearLayoutManager = layoutManager as? LinearLayoutManager
?: return super.findSnapView(layoutManager)
return linearLayoutManager
.takeIf { isValidSnap(it) }
?.run { super.findSnapView(layoutManager) }
}
private fun isValidSnap(linearLayoutManager: LinearLayoutManager) =
linearLayoutManager.findFirstCompletelyVisibleItemPosition() != 0 &&
linearLayoutManager.findLastCompletelyVisibleItemPosition() != linearLayoutManager.itemCount - 1
}
I found a less invasive answer:
private class PagerSelectSnapHelper : LinearSnapHelper() {
override fun findSnapView(layoutManager: RecyclerView.LayoutManager): View? {
// Use existing LinearSnapHelper but override when the itemDecoration calculations are off
val snapView = super.findSnapView(layoutManager)
return if (!snapView.isViewInCenterOfParent(layoutManager.width)) {
val endView = layoutManager.findViewByPosition(layoutManager.itemCount - 1)
val startView = layoutManager.findViewByPosition(0)
when {
endView.isViewInCenterOfParent(layoutManager.width) -> endView
startView.isViewInCenterOfParent(layoutManager.width) -> startView
else -> snapView
}
} else {
snapView
}
}
private fun View?.isViewInCenterOfParent(parentWidth: Int): Boolean {
if (this == null || width == 0) {
return false
}
val parentCenter = parentWidth / 2
return left < parentCenter && parentCenter < right
}
}

RecyclerView with offsets and PagerSnapHelper not clipping correctly

I´ve two RecyclerViews with two ItemDecoration each that adds a fixed offset to the first and last items.
StartOffsetItemDecoration.kt
class StartOffsetItemDecoration : RecyclerView.ItemDecoration {
private var mOffsetPx: Int = 0
private var mOffsetDrawable: Drawable? = null
private var mOrientation: Int = 0
/**
* Constructor that takes in the size of the offset to be added to the
* start of the RecyclerView.
*
* #param offsetPx The size of the offset to be added to the start of the
* RecyclerView in pixels
*/
constructor(offsetPx: Int) {
mOffsetPx = offsetPx
}
/**
* Constructor that takes in a {#link Drawable} to be drawn at the start of
* the RecyclerView.
*
* #param offsetDrawable The {#code Drawable} to be added to the start of
* the RecyclerView
*/
constructor(offsetDrawable: Drawable) {
mOffsetDrawable = offsetDrawable
}
/**
* Determines the size and location of the offset to be added to the start
* of the RecyclerView.
*
* #param outRect The [Rect] of offsets to be added around the child view
* #param view The child view to be decorated with an offset
* #param parent The RecyclerView onto which dividers are being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (parent.getChildAdapterPosition(view) > 0) {
return
}
mOrientation = (parent.layoutManager as LinearLayoutManager).orientation
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
if (mOffsetPx > 0) {
outRect.left = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.left = mOffsetDrawable!!.intrinsicWidth
}
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
if (mOffsetPx > 0) {
outRect.top = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.top = mOffsetDrawable!!.intrinsicHeight
}
}
}
/**
* Draws horizontal or vertical offset onto the start of the parent
* RecyclerView.
*
* #param c The [Canvas] onto which an offset will be drawn
* #param parent The RecyclerView onto which an offset is being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mOffsetDrawable == null) {
return
}
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawOffsetHorizontal(c, parent)
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
drawOffsetVertical(c, parent)
}
}
private fun drawOffsetHorizontal(canvas: Canvas, parent: RecyclerView) {
val parentTop = parent.paddingTop
val parentBottom = parent.height - parent.paddingBottom
val parentLeft = parent.paddingLeft
val offsetDrawableRight = parentLeft + mOffsetDrawable!!.intrinsicWidth
mOffsetDrawable?.setBounds(parentLeft, parentTop, offsetDrawableRight, parentBottom)
mOffsetDrawable?.draw(canvas)
}
private fun drawOffsetVertical(canvas: Canvas, parent: RecyclerView) {
val parentLeft = parent.paddingLeft
val parentRight = parent.width - parent.paddingRight
val parentTop = parent.paddingTop
val offsetDrawableBottom = parentTop + mOffsetDrawable!!.intrinsicHeight
mOffsetDrawable?.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom)
mOffsetDrawable?.draw(canvas)
}
}
EndOffsetItemDecoration.kt
class EndOffsetItemDecoration: RecyclerView.ItemDecoration {
private var mOffsetPx: Int = 0
private var mOffsetDrawable: Drawable? = null
private var mOrientation: Int = 0
/**
* Constructor that takes in the size of the offset to be added to the
* start of the RecyclerView.
*
* #param offsetPx The size of the offset to be added to the start of the
* RecyclerView in pixels
*/
constructor(offsetPx: Int) {
mOffsetPx = offsetPx
}
/**
* Constructor that takes in a {#link Drawable} to be drawn at the start of
* the RecyclerView.
*
* #param offsetDrawable The {#code Drawable} to be added to the start of
* the RecyclerView
*/
constructor(offsetDrawable: Drawable) {
mOffsetDrawable = offsetDrawable
}
/**
* Determines the size and location of the offset to be added to the end
* of the RecyclerView.
*
* #param outRect The [Rect] of offsets to be added around the child view
* #param view The child view to be decorated with an offset
* #param parent The RecyclerView onto which dividers are being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val itemCount = state.itemCount
if (parent.getChildAdapterPosition(view) != itemCount - 1) {
return
}
mOrientation = (parent.layoutManager as LinearLayoutManager).orientation
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
if (mOffsetPx > 0) {
outRect.right = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.right = mOffsetDrawable!!.intrinsicWidth
}
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
if (mOffsetPx > 0) {
outRect.bottom = mOffsetPx
} else if (mOffsetDrawable != null) {
outRect.bottom = mOffsetDrawable!!.intrinsicHeight
}
}
}
/**
* Draws horizontal or vertical offset onto the end of the parent
* RecyclerView.
*
* #param c The [Canvas] onto which an offset will be drawn
* #param parent The RecyclerView onto which an offset is being added
* #param state The current RecyclerView.State of the RecyclerView
*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mOffsetDrawable == null) {
return
}
if (mOrientation == LinearLayoutManager.HORIZONTAL) {
drawOffsetHorizontal(c, parent)
} else if (mOrientation == LinearLayoutManager.VERTICAL) {
drawOffsetVertical(c, parent)
}
}
private fun drawOffsetHorizontal(canvas: Canvas, parent: RecyclerView) {
val parentTop = parent.paddingTop
val parentBottom = parent.height - parent.paddingBottom
val lastChild = parent.getChildAt(parent.childCount - 1)
val lastChildLayoutParams = lastChild.layoutParams as RecyclerView.LayoutParams
val offsetDrawableLeft = lastChild.right + lastChildLayoutParams.rightMargin
val offsetDrawableRight = offsetDrawableLeft + mOffsetDrawable!!.intrinsicWidth
mOffsetDrawable?.setBounds(offsetDrawableLeft, parentTop, offsetDrawableRight, parentBottom)
mOffsetDrawable?.draw(canvas)
}
private fun drawOffsetVertical(canvas: Canvas, parent: RecyclerView) {
val parentLeft = parent.paddingLeft
val parentRight = parent.width - parent.paddingRight
val lastChild = parent.getChildAt(parent.childCount - 1)
val lastChildLayoutParams = lastChild.layoutParams as RecyclerView.LayoutParams
val offsetDrawableTop = lastChild.bottom + lastChildLayoutParams.bottomMargin
val offsetDrawableBottom = offsetDrawableTop + mOffsetDrawable!!.intrinsicHeight
mOffsetDrawable?.setBounds(parentLeft, offsetDrawableTop, parentRight, offsetDrawableBottom)
mOffsetDrawable?.draw(canvas)
}
}
I´ve also added a PagerSnapHelper on both RecyclerViews but they don't seem to work well with the offset given. I think I may be missing some configuration or maybe making the PageSnapHelper aware of the offset so it can calculate the right snapping for the first and the last elements.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_anomaly_solution_hours)
hours.layoutManager = LinearLayoutManager(
this#AnomalySolutionHoursActivity, LinearLayoutManager.VERTICAL, false
)
minutes.layoutManager = LinearLayoutManager(
this#AnomalySolutionHoursActivity, LinearLayoutManager.VERTICAL, false
)
hours.setHasFixedSize(true)
minutes.setHasFixedSize(true)
hours.addItemDecoration(StartOffsetItemDecoration(calculateOffset().toInt()))
hours.addItemDecoration(EndOffsetItemDecoration(calculateOffset().toInt()))
minutes.addItemDecoration(StartOffsetItemDecoration(calculateOffset().toInt()))
minutes.addItemDecoration(EndOffsetItemDecoration(calculateOffset().toInt()))
val hoursSnapHelper = PagerSnapHelper()
val minutesSnapHelper = PagerSnapHelper()
hours.adapter = TimeAdapter((0..10).toList().toTypedArray(), this#AnomalySolutionHoursActivity,
object: TimeAdapter.OnItemClickListener {
override fun onItemClick(unit: Int) {
}
})
hoursSnapHelper.attachToRecyclerView(hours)
minutes.adapter = TimeAdapter((0..60).toList().toTypedArray(), this#AnomalySolutionHoursActivity,
object: TimeAdapter.OnItemClickListener {
override fun onItemClick(unit: Int) {
}
})
minutesSnapHelper.attachToRecyclerView(minutes)
}
private fun calculateOffset(): Float {
val listHalfHeight = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250f, resources.displayMetrics)) / 2
val listItemHalfHeight = (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48f, resources.displayMetrics)) / 2
return listHalfHeight - listItemHalfHeight
}
}
Has someone been able to make a PagerSnapHelper work with a RecyclerView containing offsets? LinearSnapHelper seems to work better than PagerSnapHelper but still can't snap the first and the last items.
Thanks!
UPDATE
Made both list height up to 500dp but nothing changed.
UPDATE 2
Added an empty item to the first and last positions and now it work pretty good. Just have to keep on mind those items when calculating positions with sizes.

Android ListView current scroll location Y pixels

I'm trying to detect when a list view is scrolled beyond certain fixed threshold in pixels (half way through the first item). Unfortunately listview's getScrollY() seems to always return 0 instad of the scroll position. Is there any way to get the actual scroll location by pixel?
Here's the code I tried to use but as said it only returns 0.
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
Log.d("scroll", "scroll: " + getListView().getScrollY());
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == 0)
Log.d("scroll", "scrolling stopped");
}
});
There is no notion of Y scroll for a ListView in Android simply because the total height of the content is unknown. Only the height of the displayed content is known.
However it is possible to get the current position/Y scroll of a visible item using the following hack:
getListView().getChildAt(0).getTop();
A refactor code of Malachiasz's answer. This function is used for dynamic row's height.
Call onScroll listener
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mCardsListView.getChildCount() > 0) {
int scrollY = getScrollY();
}
}
});
getScrollY function
private Dictionary<Integer, Integer> mListViewItemHeights = new Hashtable<Integer, Integer>();
private int getScrollY() {
View child = mCardsListView.getChildAt(0); //this is the first visible row
if (child == null) return 0;
int scrollY = -child.getTop();
mListViewItemHeights.put(mCardsListView.getFirstVisiblePosition(), child.getHeight());
for (int i = 0; i < mCardsListView.getFirstVisiblePosition(); ++i) {
Integer hei = mListViewItemHeights.get(i);
//Manual add hei each row into scrollY
if (hei != null)
scrollY += hei;
}
return scrollY;
}
You need two things to precisely define the scroll position of a listView:
To get current position:
int firstVisiblePosition = listView.getFirstVisiblePosition();
int topEdge=listView.getChildAt(0).getTop(); //This gives how much the top view has been scrolled.
To set the position:
listView.setSelectionFromTop(firstVisiblePosition,0);
// Note the '-' sign...
listView.scrollTo(0,-topEdge);
You can try implementing OnTouchListener and override its onTouch(View v, MotionEvent event) and get the x and y using event.getX() and event.getY(). I had just created a demo for swipe on ListView row that will enable a delete button for deleting a particular item from the ListView. You can check the source code from my github as ListItemSwipe.
Maybe it will be useful for someone. Code of previous answer's will not work when the list is scrolled fast. Because in this case firstVisiblePosition may be change irregular. In my code I do not use an array to store positions
fun setOnTouchScrollListener(lv: AbsListView, onTouchScrollListener: (isDown: Boolean, offset: Int?, isScrollWorking: Boolean) -> Unit) {
var lastItemPosition: Int = -1
var lastItemTop: Int? = null
var lastItemHeight: Int? = null
var lastScrollState: Int? = AbsListView.OnScrollListener.SCROLL_STATE_IDLE
lv.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
val child = lv.getChildAt(0)
var offset: Int? = null
var isDown: Boolean? = if (firstVisibleItem == lastItemPosition || lastItemPosition == -1) null else firstVisibleItem > lastItemPosition
val dividerHeight = if (lv is ListView) lv.dividerHeight else 0
val columnCount = if (lv is GridView) lv.numColumns else 1
if (child != null) {
if (lastItemPosition != -1) {
when (firstVisibleItem - lastItemPosition) {
0 -> {
isDown = if (lastItemTop == child.top) null else lastItemTop!! > child.top
offset = Math.abs(lastItemTop!! - child.top)
}
columnCount -> offset = lastItemHeight!! + lastItemTop!! - child.top + dividerHeight
-columnCount -> offset = child.height + child.top - lastItemTop!! + dividerHeight
}
}
lastItemPosition = firstVisibleItem
lastItemHeight = child.height
lastItemTop = child.top
if (isDown != null && (offset == null || offset != 0)
&& lastScrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
onTouchScrollListener(isDown, offset, true)
}
}
}
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
lastScrollState = scrollState
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
onTouchScrollListener(true, 0, false)
lastItemPosition = -1
lastItemHeight = null
lastItemTop = null
}
}
})
}
Then we can use when a listview is scrolled beyond threshold
private var lastThresholdOffset = 0
private var thresholdHeight = some value
setOnTouchScrollListener(listView) { isDown: Boolean, offset: Int?, isScrollWorking: Boolean ->
val offset = offset ?: if (isDown) 0 else thresholdHeight
lastThresholdOffset = if (isDown) Math.min(thresholdHeight, lastThresholdOffset + offset)
else Math.max(0, lastThresholdOffset - offset)
val result = lastThresholdOffset > thresholdHeight
}
isScrollWorking - can be used for animation

Categories

Resources