Swipe to Recycler View Item until displayed - android-recyclerview

I have a nested Recycler lists which don't have ~~scrolling~~ & nestedScrollingEnabled=false. I'm attempting to swipe up and click on the recycler item by it's text. Having issues with determining when to swipe and how far.
UPDATE: This may have scrolling, I may need to specify the ViewHolder of the Item with text instead of the view with text... Experimenting...
parent_recycler_list
recycler_list
List item A
List item B
recycler_list
List item A
List item B
So far I am able to find the item and try to click on it:
Espresso.onView(
CoreMatchers.allOf(
ViewMatchers.withId(R.id.recycler_list),
ViewMatchers.hasDescendant(recyclerViewItemWithText(text))
)
).perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
recyclerViewItemWithText(text),
ViewActions.click()
)
)
fun recyclerViewItemWithText(text: String) = object : BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("Searching for text with: $text")
}
override fun matchesSafely(item: View?): Boolean {
val views = ArrayList<View>()
item?.findViewsWithText(views, text, View.FIND_VIEWS_WITH_TEXT)
return when (views.size) {
1 -> true
else -> false
}
}
}
This only works by it self when the list item is displayed.
I have tried to swipe until the view item is displayed:
Espresso.onView(ViewMatchers.withId(R.id.parent_recycler_list)).perform(
ViewActions.repeatedlyUntil(
ViewActions.swipeUp(),
Matchers.allOf(
ViewMatchers.hasDescendant(ViewMatchers.withText(text)),
isCompletelyDisplayed()
), 10
)
)
This will always swipe at least once... and can swipe past the view item I'm looking for.
Is there a way I can be more precise in when and how far to swipe?
I'm a bit of a novice still and don't know much about custom swipe actions on view holders. Thanks
When trying to use nestedScrollTo()
java.lang.RuntimeException: Action will not be performed because the
target view does not match one or more of the following constraints:
(view has effective visibility=VISIBLE and is descendant of a: (is
assignable from class: class androidx.core.widget.NestedScrollView))

You can simply use ViewActions.scrollTo() if your nested recycler views do not have nested scrolling enabled, but you'll need to tweak the action first because it does not support NestedScrollView:
fun nestedScrollTo(): ViewAction = object : ViewAction {
private val scrollTo = ViewActions.scrollTo()
override fun getConstraints(): Matcher<View> {
return Matchers.allOf(
ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE),
ViewMatchers.isDescendantOfA(Matchers.anyOf(ViewMatchers.isAssignableFrom(NestedScrollView::class.java))))
}
override fun getDescription(): String = scrollTo.description
override fun perform(uiController: UiController, view: View) = scrollTo.perform(uiController, view)
}
Then use the new custom action to scroll, for example:
onView(withText("query")).perform(nestedScrollTo(), click())
Avoid using swipe in this use case if possible, they can be unreliable at times.

6 months later I figured out a bit more about Recycler lists and that I was trying to use them wrong, or at least figured out which Recycler actions work (some don't seem to work at all). This had nothing to do with Nested scrolling even though I have nested recycler lists.
Needed to use a swipe up action as a back up for when the list doesn't exist in the hierarchy.
Also there is a Potential infinite loop.
fun tapRecyclerItem(titleText: String) {
val parentList by lazy { onView(withId(R.id.parent_recycler_list)) }
try {
//Try to scroll to the item in the child list
onView(allOf(
withId(R.id.recycler_list),
hasDescendantWithText(titleText)
)).perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendantWithText(titleText),
ViewActions.scrollTo()
)
)
// Tap the title
onView(allOf(withId(R.id.title), withText(titleText))).tap()
} catch (ex: NoMatchingViewException) {
// Swipe up and try again
parentList.perform(swipeCenterUp())
tapRecyclerItem(titleText)
}
}
fun hasDescendantWithText(text: String): Matcher<View> {
return Matchers.allOf(
hasDescendant(withText(text)),
withEffectiveVisibility(VISIBLE)
)
}
fun swipeCenterUp(): ViewAction? {
return ViewActions.actionWithAssertions(
GeneralSwipeAction(
Swipe.FAST,
GeneralLocation.CENTER,
GeneralLocation.TOP_CENTER,
Press.FINGER
)
)
}

Related

JetPack Compose onClick function

Please try to tolerate my inexperience.
I'm practicing Jetpack Compose navigation. I making an app that displays a set of card in a LazyHorizontalGrid format, and when clicked it navigates to a screen (destination) containing details of the card, depending on which card is clicked.
Here's what I mean:
I want to navigate to a screen (destination) that describe a particular "Favorite Collections" when clicked, depending on which is clicked.
I was following a tutorial on youtube, but I got lost when he was using Companion Object on a new activity (i.e having multiple activity in a single project and navigating by intent), since that's not recommended I'm not doing that, so I don't know where to put my Companion Object.
RecyclerView in Jetpack Compose - Jetpack Compose For Beginners #7
As I get errors when I try to put the Companion Object in the file of the Screen I'm trying to navigate to, error; Modifier 'companion' is not applicable inside 'file', or In a Composable in the file, then I get this error; Unresolved reference: companion.
This is the screen I'm trying to navigate to (let's call it; DetailScreen);
#Composable
fun DisplayHomeInfo(){
WelcomeText()
HomeInfoDetails(homeInfo = )
}
#Composable
fun WelcomeText() {
Text(
text = "Welcome, to Home Information",
style = MaterialTheme.typography.h3,
modifier = Modifier.padding(horizontal = 12.dp, vertical = 18.dp)
)
}
#Composable
fun HomeInfoDetails(homeInfo: HomeInfo) {
Card(
modifier = Modifier
.padding(10.dp)
.clip(CircleShape),
elevation = 10.dp,
) {
....
This is the screen I'm trying to navigate from;
#Composable
fun HomeScreen(onHomeCardClick: () -> Unit) {
HomeContentScreen(onHomeCardClick = onHomeCardClick)
}
....
Data class I want to load on the detail screen;
data class HomeInfo(
val id: Int,
val title: String,
val sex: String,
val age: Int,
val description: String,
val homeInfoImageId: Int = 0
)
And this is the Object I want to match with the data;
object HomeInfoModel {
val homeInfoModelList = listOf(
HomeInfo(
id = 1,
title = "Monty",
sex = "Male",
age = 14,
description = "Monty enjoys chicken treats and cuddling while watching Seinfeld.",
homeInfoImageId = R.drawable.ab1_inversions
),
...
)
}
To summarize everything, MY QUESTION is how do I pass the data to the detailScreen, and show each card details, depending on the card clicked.
Please I understand my explanation is not very clear, and I'll be more than happy to clarify anything.
No information is too small, I will appreciate any help. Thanks a lot in advance.

Composable Focus

I have a project where we display a graph, this graph is in an Box which is scrollable.
By opening the view, we need to center the root node which causes the problem currently.
Determining the position and setting the values of the states is currently done the following way:
.onGloballyPositioned { coordinates -> scrollBy = coordinates.positionInParent().y - dpstate!!.scrollState.firstVisibleItemScrollOffset
}
.onFocusChanged {
if(it.isFocused ){
print("is focused")
scope.launch { dpstate!!.scrollState.animateScrollBy(scrollBy)
dpstate!!.offsetState.value = Offset(leftX.toFloat(),dpstate!!.offsetState.value.y)
}
}
}
in the modifier of the box.
The state dpstate is an instance of the following:
data class DisplayState(
val scrollState: LazyListState,
val scaleState: MutableState<Float>,
val offsetState: MutableState<Offset>,
val editState: MutableState<Boolean>,
val showInfo:Map<Int, MutableState<Boolean>>,)
Important is, that I need to center by opening it, not by clicking a button.
My try was in the calling code of all of this the following code:
DisposableEffect(Unit){
com.github.tukcps.appel.ui.rendering.focusRequester!!.requestFocus()
onDispose { }
}
There is no exception, it just don't do anything, thanks for your help.

Android Jetpack Compose NumberPicker Widget Equivalent

What is the recommended solution for creating a NumberPicker Widget in Jetpack Compose? Similar to the image below. I am able to create an NumberPicker using an AndroidView within my composable but the view does not seem to allow flings or snap to position. Btw the UI below shows three NumberPickers placed in a row. It is not supposed to represent a DatePicker
By coincidence I've implemented a screen like that last week.
I can't share the whole code here, but basically what I did was:
Create a layout with a DatePicker (res/layout/date_picker.xml).
<DatePicker xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/datePicker"
android:theme="#style/DatePickerStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:calendarViewShown="false"
android:datePickerMode="spinner" />
Then, use it in your composable function.
#Composable
fun DatePicker(
onDateSelected: (Date) -> Unit
) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
val view = LayoutInflater.from(context).inflate(R.layout.date_picker, null)
val datePicker = view.findViewById<DatePicker>(R.id.datePicker)
val calendar = Calendar.getInstance() // show today by default
datePicker.init(
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
) { _, year, monthOfYear, dayOfMonth ->
val date = Calendar.getInstance().apply {
set(year, monthOfYear, dayOfMonth)
}.time
onSelectedDateUpdate(date)
}
datePicker
}
)
}
Finally, use it in a ModalBottomSheetLayout
Editing my answer... Using a NumberPicker working as well...
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
NumberPicker(context).apply {
setOnValueChangedListener { numberPicker, i, i2 -> }
minValue = 0
maxValue = 50
}
}
)
Here is the result.
I know maybe you are not looking for something like this. But since there is no such widget in compose yet and compose is all about making your way easier to build your own component. So Apart from android.widget NumberPicker, you can make something like this one. You can change the visualization more like the NumberPicker widget and add your callback and stuff.
Have you checked this one on github? ComposeNumberPicker.Kt
We are using this library in our compose project for number picker widget.
https://github.com/ChargeMap/Compose-NumberPicker

Drag gestures inside scrollable control

I've made a custom control based around a Canvas. It uses two .pointerInput modifiers, one to detect click and one to detect drag so the user can toggle a column of 50 buttons, either by clicking on one at a time or dragging across a number of them to set them all at once. It works well, and now I'd like to have a horizontally scrollable Row containing a number of these. The immediate problem is that the Row, when the .horizontalScroll modifier is applied, swallows vertical movement as well, and even taps, so although I can scroll through a number of controls I'm no longer able to interact with them.
The only example I can find that's similar is the nested scrolling in the Gestures documentation, but that's between two controls using scrolling, and although the outer control is clearly not preventing the inner control receiving events it's not clear how to apply it in my case.
Without pasting huge quantities of code, I'm defining the Row by
#Composable
fun ScrollBoxes() {
Row(
modifier = Modifier
.background(Color.LightGray)
.fillMaxSize()
.horizontalScroll(rememberScrollState())
) {
repeat(20) {
Column {
Text(
"Item $it", modifier = Modifier
.padding(2.dp)
.width(500.dp)
)
JetwashSlide(
model = JetwashSlideViewModel()
)
}
}
}
}
and the modifier of the Canvas is my custom control is set up as
modifier
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { ... }
},
onDragEnd = { ... },
onDrag = { change, dragAmount ->
change.consumeAllChanges();
...
}
}
)
}
.pointerInput(Unit) {
detectTapGestures(
onPress = { it ->
...
}
)
}
A crude approach would be to have a scrollable row of labels and use the presently selected label to determine which full width custom control is presently visible. This wouldn't be as aesthetically pleasing as having the controls scrolling horizontally. Does anyone have an idea how this could be achieved?
Ok, my starting point was this tutorial on how to make a scrolling pager from scratch;
https://fvilarino.medium.com/creating-a-viewpager-in-jetpack-compose-332d6a9181a5
Using that, you can get a horizontally scrolling row of instances of a control. The problem is that I wanted horizontal swipes to be handled by the pager, and taps and vertical swipes to be handled by the custom control, and although the custom control gets the gestures if made over the control, it couldn't pass them up to the pager if they were horizontal swipes, so you could only scroll using the gaps between the custom controls. I expected that the control could use .detectVerticalDragGestures and .detectTapGestures and the pager could use .detectHorizontalDragGestures, but it wasn't that simple.
I ended up pulling code from the Jetpack source into my own code so I could modify it to produce my own gesture detector which captures vertical scroll and tap events but does not capture horizontal scroll;
suspend fun PointerInputScope.detectVerticalDragOrTapGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
onClick: ((Offset) -> Unit) = { },
model: JetwashSlideViewModel
) {
forEachGesture {
awaitPointerEventScope {
model.inhibitGesture = false
val down = awaitFirstDown(requireUnconsumed = false)
//val drag = awaitVerticalTouchSlopOrCancellation(down.id, onVerticalDrag)
val drag = awaitTouchSlopOrCancellation(
down.id
) { change: PointerInputChange, dragAmount: Offset ->
if (abs(dragAmount.y) > abs(dragAmount.x)) {
//This is a real swipe down the slide
change.consumeAllChanges()
onVerticalDrag(change, dragAmount.y)
}
}
if (model.inhibitGesture) {
onDragCancel()
} else {
if (drag != null) {
onDragStart.invoke(drag.position)
if (
verticalDrag(drag.id) {
onVerticalDrag(it, it.positionChange().y)
}
) {
onDragEnd()
} else {
onDragCancel()
}
} else {
//click.
if (!model.inhibitGesture)
onClick(down.position)
}
}
}
}
}
Now in the pager, it was using .horizontalDrag. This is fine if you can actually drag purely horizontally or purely vertically, but you can't and swipes that were intended to be vertical on the inner control often had a tiny bit of horizontal motion which caused the pager to intercept it. So in the pager, I also had to copy and modify some code to make my own .horizontalDrag;
suspend fun AwaitPointerEventScope.horizontalDrag(
pointerId: PointerId,
onDrag: (PointerInputChange) -> Unit
): Boolean = drag(
pointerId = pointerId,
onDrag = onDrag,
motionFromChange = { if (abs(it.positionChangeIgnoreConsumed().x) > 10) it.positionChangeIgnoreConsumed().x else 0f },
motionConsumed = { it.positionChangeConsumed() }
)
This only triggers if the horizontal component of the movement is larger than 10px.
Finally, since a horizontal scroll may also have some element of vertical which could affect the inner controls, in my onPagerScrollStart and onPagerScrollFinished callbacks I set and clear a flag in the model, inhibitGesture which causes the inner control to disregard gestures that it happens to get while the pager is in the process of being scrolled.

ItemDecoration bottom spacing not updating on adding a new Item

I have a SpacingDecoration for my recyclerview which will add some extra spacing after the last item in the list.
Here is my Spacing Decoration
class SpacingDecoration(val context:Context):RecyclerView.ItemDecoration() {
private val twelveDp=getPixelValue(12)
private val seventyDp=getPixelValue(70)
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val dataCount=parent.adapter!!.itemCount
val viewPosition=parent.getChildAdapterPosition(view)
outRect.left=twelveDp
outRect.right=twelveDp
when(viewPosition){
0->{
outRect.top=twelveDp
outRect.bottom=twelveDp/2
return
}
dataCount-1 ->{
outRect.top=twelveDp/2
outRect.bottom=seventyDp
return
}
else->{
outRect.top=twelveDp/2
outRect.bottom=twelveDp/2
}
}
}
private fun getPixelValue(Dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
Dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
}
This works nicely.
But the problem starts when a new item is added at the bottom of the list.
I am using DiffUtils to update the list.
When the list updates and adds a new item to the list, it adds the new item after the bottom spacing of seventy Dp.
Hope u understand my problem.
I want to add the last new item so that the spacing between the last item and the item before it reduces to twelveDp.
Please help as I am just a beginner
I found this answer helped: https://stackoverflow.com/a/62002161/9901599
Basically, call RecyclerView.invalidateItemDecorations() once you have refreshed your data. Since I was using ListAdapter, I found that I had to put this line in the callback after the list actually updates, like this:
myAdapter.submitList(newList) {
// this callback runs when the list is updated
myRecyclerView.invalidateItemDecorations()
}