I found a lot of examples for swipe detection on a recyclerview item. But I try to detect right/left swipe on the whole view.
There are also examples like this Android: How to handle right to left swipe gestures
on how to detect swipe on views like layout container. But attaching this touch listener to the recyclerview is not working.
So how can I detect left / right swipe on a whole recyclerview?
Edit for SmartSwipe Lib:
I have tried to add the https://github.com/luckybilly/SmartSwipe Lib because the effect of swipe looks really nice.
But if I try to wrap it to the recyclerview I see no data only the swipe effect:
SmartSwipe.wrap(binding.recyclerView).addConsumer(BezierBackConsumer())
.enableHorizontal()
.addListener(object : SimpleSwipeListener() {
override fun onSwipeOpened(wrapper: SmartSwipeWrapper, consumer: SwipeConsumer, direction: Int) {
}
})
Attaching it to the constraint layout container has no effect at all.
Edit 2:
Attaching an onTouchListener to the view doesn't work for the recyclerview:
view.setOnTouchListener(object : OnHorizontalSwipeListener(requireContext()) {
override fun onRightSwipe() {
println("Swipe right")
}
override fun onLeftSwipe() {
println("Swipe left")
}
})
Attaching it to the Recyclerview leads to an exception:
binding.recyclerView.setOnTouchListener.....
E/MessageQueue-JNI: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter e1
at ui.OnHorizontalSwipeListener$GestureListener.onFling(Unknown Source:2)
Edit 3: Seems to work if I add null check to the listener class like this:
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
var result = false
try {
if (e1 != null && e2 != null) {
val diffY = e2.y - e1.y
val diffX = e2.x - e1.x
....
But starts working only after some scrolling. trying to swipe on a new initialized fragment doesn't work.
Related
I'm facing a problem on recyclerview when developing an android app on TV
I have a list of reviews I'd like to scroll through
when scrolling to the second review, i have some logic to resize the layout to take the entire screen, however, the second review is not centered when scrolling down. all the other items after the second review are centered because im overriding
override fun calculateDtToFit(
viewStart: Int,
viewEnd: Int,
boxStart: Int,
boxEnd: Int,
snapPreference: Int
): Int {
return boxStart + (boxEnd - boxStart) / 2 - (viewStart + (viewEnd - viewStart) / 2)
}
Scrolling up works perfectly with all the reviews centered
Since only the scrolling down behavior was odd, I tried adding a scroll listener and using scrollToPositionWithOffset to scroll the second review to a specific position when scrolling down, and kept the scrolling up logic untouched. somehow it ended up messing with scrolling up anyway, when scrolling up to the second review, recycler view somehow loses focus
code to identify scroll direnction
var firstVisibleInListview = layoutManager.findFirstVisibleItemPosition()
var isScrollingUp = false
private val reviewsListScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
val currentFirstVisible = layoutManager.findFirstVisibleItemPosition()
isScrollingUp = currentFirstVisible > firstVisibleInListview
firstVisibleInListview = currentFirstVisible;
}
}
init {
listRecyclerView.addOnScrollListener(reviewsListScrollListener)
}
code to control scrolling behavior
private fun getScrollToPositionCallback(position: Int): (View, Boolean) -> Unit {
return { view, hasFocus ->
if (hasFocus) {
picksViewModel.selectReviewPosition(position)
if (position == 2) {
if (!isScrollingUp) {
listRecyclerView.setPadding(0,listRecyclerView.paddingBottom,
0, listRecyclerView.paddingBottom)
layoutManager.scrollToPositionWithOffset(position, 300)
}
} else if (position == 1) {
listRecyclerView.setPadding(0,0, 0, listRecyclerView.paddingBottom)
}
listRecyclerView.scrollToUserReviewPosition(position)
}
}
}
I then tried adding onKeyListner to the items and requireFocus to the second review when I'm pressing the up key on the third review, but it immediately skip past the second review and jumps to the first review.
So the problem seems to be the second review can't be focused on after I attached the onScrollListener, how do I fix that?
I have a RecyclerView that contains TextViews. The number of TextViews can vary and the size of them vary as well and can be dynamically changed.
When the user scrolls to a certain position within the list and exits app, I want to be able to return to that exact position in the next session.
To do this, I need to know how many pixels have scrolled past from where the current TextView in view started and where the current position of the scroll is. For example, if the user has the 3rd TextView in view and scrolls 100 pixels down from where that TextView started, I will be able to return to this spot with scrollToPositionWithOffset(2, 100). If the TextView changes size (due to font changes), I can also return to the same spot by calculating the percentage of offset using the TextView's height.
Problem is, I cannot get the offset value in any accurate manor.
I know I can keep a running calculation on the Y value scrolled using get scroll Y of RecyclerView or Webview, but this does not give me where the TextView actually started. I can listen to when the user scrolled past the start of any TextView and record the Y position there but this will be inaccurate on fast scrolling.
Is there a better way?
Don't use position in pixels, use the index of the view. Using layout manager's findFirstVisibleItemPosition or findFirstCompletelyVisibleItemPosition.
That's a very popular question, although it may be intuitive to think and search for pixels not index.
Get visible items in RecyclerView
Find if the first visible item in the recycler view is the first item of the list or not
how to get current visible item in Recycler View
A good reason to not trust pixels is that it's not useful on some situations where index is, like rotating the screen, resizeing/splitting the app size to fit other apps side by side, foldable phones, and changing text / screen resolution.
I solved this by converting to a ListView:
lateinit var adapterRead: AdapterRead // Custom Adapter
lateinit var itemListView: ListView
/*=======================================================================================================*/
// OnViewCreated
itemListView = view.findViewById(R.id.read_listview)
setListView(itemListView)
// Upon entering this Fragment, will automatically scroll to saved position:
itemListView.afterMeasured {
scrollToPosition(itemListView, getPosition(), getOffset())
}
itemListView.setOnScrollListener(object : AbsListView.OnScrollListener {
private var currentFirstVisibleItem = 0
var offset = 0
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// When scrolling stops, will save the current position and offset:
if(scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
offset = if(itemListView.getChildAt(0) == null) 0 else itemListView.getChildAt(0).top - itemListView.paddingTop
saveReadPosition(getReadPosition(itemListView), offset)
}
}
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
currentFirstVisibleItem = firstVisibleItem
}
})
/*=======================================================================================================*/
// Thanks to https://antonioleiva.com/kotlin-ongloballayoutlistener/ for this:
inline fun <T : View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if(measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
/*=======================================================================================================*/
fun setListView(lv: ListView) {
adapterRead = AdapterRead(list, context!!)
lv.apply {this.adapter = adapterRead}
}
/*=======================================================================================================*/
fun scrollToPosition(lv: ListView, position: Int, offset: Int) {
lv.post { lv.setSelectionFromTop(position, offset) }
}
/*=======================================================================================================*/
fun saveReadPosition(position: Int, offset: Int) {
// Persist your data to database here
}
/*=======================================================================================================*/
fun getPosition() {
// Get your saved position here
}
/*=======================================================================================================*/
fun getOffse() {
// Get your saved offset here
}
I'm trying to implement the android library SelectionTracker which allows to select items in a recyclerView.
Everything works fine except that when I click outside of an
item (which is in a grid layout), the all selection is cleared.
I actually have found the code which calls the clearSelection(). It's on the line 78 of the class TouchInputHandler.
It then calls the line 64 of ItemDetailsLookup which returns false because the touch event didn't occurred on an item.
I was wondering if anyone have found a workaround to prevent this behavior, because I didn't found any option in the documentation.
It's a gridLayout so it is quite "normal" to have space between items and I don't want my users to clear the selection because they have touch the side of an item.
This is my solution, based on that if we have predefined ItemDetail that will be used as "this is not the view you can select".
First, inside your ItemDetailsLookup instead of returning null you can pass single item with distinguish data that will make sure there is no name/position collision with any other data you can have
class AppItemDetailsLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
val view = rv.findChildViewUnder(e.x, e.y) ?: return EMPTY_ITEM
return (rv.getChildViewHolder(view) as AppItemViewHolder).getItemDetails()
}
object EMPTY_ITEM : ItemDetails<String>() {
override fun getSelectionKey(): String? = "empty_item_selection_key_that_should_be_unique_somehow_that_is_why_i_made_it_so_long"
override fun getPosition(): Int = Integer.MAX_VALUE
}
}
And then when you are creating SelectionTracker with builder, instead of using standard predicate (default is SelectionPredicates.createSelectAnything()) you make your own that will notify that this EMPTY_ITEM cannot be selected
.withSelectionPredicate(object : SelectionTracker.SelectionPredicate<String>() {
override fun canSelectMultiple(): Boolean = true
override fun canSetStateForKey(key: String, nextState: Boolean): Boolean =
key != AppItemDetailsLookup.EMPTY_ITEM.selectionKey
override fun canSetStateAtPosition(position: Int, nextState: Boolean): Boolean =
position != AppItemDetailsLookup.EMPTY_ITEM.position
})
I tested it with LinearLayoutManger, the selection was deselecting all items once i clicked outside any of them (my items did not had spacing decoration, but there were so few of them that i was seeing empty under last item)
I have my own custom view that extends RecyclerView and I use it to render a document with multiple pages. Each item in the adapter is a page.
I want to add the ability to zoom and pan the entire document (not each page individually).
I have tried following the Android guide here but I still can't get both zoom and pan to work properly.
My current approach is this:
Have a GestureDetector to detect pan gestures
Have a ScaleGestureDetector to detect pinch gestures
When each of these detectors register events, save the information regarding the current scale factor and the pan, call invalidate() and modify the Canvas in the drawChild method appropriately.
It is the last step that I'm unsure how to implement properly.
I'm using this data class to keep track of the view's current pan and zoom:
private data class PanAndZoom(
var scaleFactor: Float,
var focusX: Float,
var focusY: Float,
var panX: Float ,
var panY: Float
)
// field in my custom view initialised like this:
private val panAndZoom = PanAndZoom(1f, 0f, 0f, 0f, 0f)
and here is how I update the values when gestures are received:
private val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
panAndZoom.scaleFactor *= detector.scaleFactor
panAndZoom.focusX = detector.focusX
panAndZoom.focusY = detector.focusY
invalidate()
return true
}
}
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
panAndZoom.panX -= distanceX
panAndZoom.panY -= distanceY
invalidate()
return true
}
}
I override two methods in my custom view, one to pass touch events to my gesture detectors and one to draw child view:
override fun onTouchEvent(e: MotionEvent): Boolean {
gestureDetector.onTouchEvent(e)
scaleGestureDetector.onTouchEvent(e)
return super.onTouchEvent(e)
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
val save = canvas.save()
canvas.scale(panAndZoom.scaleFactor, panAndZoom.scaleFactor, panAndZoom.focusX, panAndZoom.focusY)
canvas.translate(panAndZoom.panX, panAndZoom.panY)
val result = super.drawChild(canvas, child, drawingTime)
canvas.restoreToCount(save)
return result
}
However this doesn't give the expected result, namely:
I am able to overscroll outside the RV altogether (I realise I'm not restricting my pan values, but how do I do that)?
Pan seems to be going very fast when zoomed in (how do I scale my pan values so that they respect the scale factor)?
If I am on page 2 of the document, and I zoom in, then pan to page 3, the layout manager doesn't realise that the view was actually scrolled (meaning, calling findFirstVisibleItem() will return the second page when it isn't actually visible.
Most likely related to the point above: I can pan past a page, and because the layout manager doesn't realise that another view is now displayed it doesn't render it (because it still thinks that view is hidden from view and so it optimises by not rendering it).
I have a LineChart contained within a ScrollView. When the chart is long enough for it to be necessary for a user to scroll to see it in its entirety, the drag features become unresponsive. Drag gestures only register when I hold my finger down for a short period of time in the chart bounds and then drag.
I have tried using the requestDisallowInterceptTouchEvent method that should prevent the chart's parents from intercepting the touch events (but this doesn't solve anything). I've also tried directly passing MotionEvents registered by the ScrollView straight to the chart/translating drag gestures to translateY calls but this doesn't do what I thought it would.
Note: Zooming continues to work perfectly.
Also, this is not an issue when the graph fits in the original window or when it's placed in any view that is not a ScrollView. I have considered getting rid of the ScrollView but it's a pretty necessary feature in my project.
Any ideas on why this could be happening would be appreciated!
Edit: the LineChart has a fixed height
This question is kind of old but I run into the same problem recently and to solve it, I did the following.
Added a transparent view over the graph on the xml.
Set setOnTouchListener on the view
ex.
clickInterceptorGraph.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return onTouchActionHandler(v, event);
}
});
On the MotionEvent Action Down, I'm setting requestDisallowInterceptTouchEvent to true and returning false so the event won't be consumed, so while the user is pressing on the transparent image over the graph requestDisallowInterceptTouchEvent will be true and it will disable the Touch Event on the scrollview which will disable the scroll and when you release the touch the scrollview will work normally again.
ex.
protected boolean onTouchActionHandler(View v, MotionEvent event){
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Disallow RecyclerView to intercept touch events.
scrollView.requestDisallowInterceptTouchEvent(true);
Log.e(TAG, "onTouchActionHandler: ACTION_DOWN" );
// Disable touch on transparent view
return false;
default:
return true;
}
}
*** You could do everything on the setOntouchListener but in my case I need to reuse the requestDisallowInterceptTouchEvent.
Similar approach as Ruan_Lopes mentioned, you just don't need a transparent overlay view. You can also use chart gesture listener to intercept touches the same way.
lineChart.onChartGestureListener = object : OnChartGestureListener {
override fun onChartGestureEnd(
me: MotionEvent?,
lastPerformedGesture: ChartTouchListener.ChartGesture?
) = Unit
override fun onChartFling(
me1: MotionEvent?,
me2: MotionEvent?,
velocityX: Float,
velocityY: Float
) = Unit
override fun onChartSingleTapped(me: MotionEvent?) = Unit
override fun onChartGestureStart(
e: MotionEvent?,
lastPerformedGesture: ChartTouchListener.ChartGesture?
) = recyclerView.requestDisallowInterceptTouchEvent(true)
override fun onChartScale(me: MotionEvent?, scaleX: Float, scaleY: Float) = Unit
override fun onChartLongPressed(me: MotionEvent?) = Unit
override fun onChartDoubleTapped(me: MotionEvent?) = Unit
override fun onChartTranslate(me: MotionEvent?, dX: Float, dY: Float) = Unit
}