Problem:
It seems when I have a specific combination of views and setVisibility between Visible and Gone the RecyclerView does not update properly after initial load. I have a RelativeLayout->ConstraintLayout->Constraint Group(with visibility dynamically set). The view within the constraint group is not updating properly.
Example Use Case of Problem:
So the linked code below it will show a search view at the top. The initial state of empty shows the view properly(With the search icon showing). Then if you type "T" then the search icon will disappear(it shouldn't). So if you either delete the T or type the next letter "E" then it shows again. Also if you delete all search text and type T again it will show.
View Code:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.davidcorrado.myapplication.PlayerVM" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="#+id/playerCell_main_layout"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_alignParentTop="true">
<android.support.constraint.Group
android:id="#+id/playerCell_status_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{vm.showingStatusIcons}"
app:constraint_referenced_ids="playerCell_lineup_status_image" />
<ImageView
android:id="#+id/playerCell_lineup_status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="#drawable/ic_search" />
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
</layout>
Quirks that might help:
1) If I move the visibility line to the relativeLayout or the ConstraintLayout the things seem to work
2) If I remove the RelativeLayout view things seem to work.
See the below for the full code example:
https://github.com/DavidCorrado/LayoutIssue
As far as I can tell, the reason the search icon disappears is due to the call to ObservableList.clear() then ObservableList.addAll(*)
So the disappear happens on the clear, then the reappear happens on the addAll.
As the callbacks in the adapter kick in on both calls, my hunch is it animates the view to disappear, then animates to show when the addAll is triggered.
I have verified this by not actioning a 'List.clear()' and instead in your textChange listener simply adding more views to the list.
I'm not sure how you would clear and add items to the list without animating the clear, perhaps there are some settings you can toggle in the RecyclerAdapter to ignore the remove of Views or not animate the entry of Views?
My adjusted code in your callback for class PlayersListVM
class PlayersListVM {
val filteredVms: ObservableList<ViewModel> = ObservableArrayList<ViewModel>()
val showing = PlayerVM(isShowing = true)
val missing = PlayerVM()
init {
filteredVms.add(showing)
}
fun onQueryChanged(newText: String) {
if (filteredVms.size % 2 == 1) filteredVms.add(missing)
else filteredVms.add(showing)
}
}
Related
I'm trying to programmatically scroll to a particular item within my RecyclerView which is nested within a NestedScrollView.
The Problem
The NestedScrollView scrolls to the complete bottom rather than the desired item.
Note: It works correctly when desired item is the 2nd item, probably since that item is visible in the screen.
What I've tried
I've searched through a dozen solutions from StackOverFlow and came up with the function below.
I've tried:
binding.variantsRecyclerView.getChildAt()
binding.variantsRecyclerView.findViewWithTag()
binding.variantsRecyclerView.findViewHolderForAdapterPosition()
All these do return the correct item, (I know since the edit text within that item is focused as coded) however, the NestedScrollView does not scroll correctly to that item. It is almost always scrolling to the bottom. Sometimes however it scrolls to somewhere in between the required item instead of it's start. The only time this works is when the item is either the 1st or 2nd item. (As stated before)
private fun scrollToPosition(position: Int) {
if (position in 0 until variantsAdapter.itemCount) {
val view = binding.variantsRecyclerView.findViewWithTag<ConstraintLayout>("pos_$position")
if (view != null) {
val height = binding.nestedScrollView.height
val target = view.y.toInt()
binding.nestedScrollView.postDelayed({
binding.nestedScrollView.smoothScrollTo(0, target)
view.requestFocus()
}, 200)
}
}
}
My XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#eff1f4">
<LinearLayout>...</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="#+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constrainedHeight="true"
app:layout_constraintVertical_bias="0"
app:layout_constraintBottom_toTopOf="#+id/btnNextLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbarLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="#dimen/ui_10_dp"
android:layout_marginEnd="#dimen/ui_10_dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/variantsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="12dp"
android:paddingBottom="12dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="#layout/sell_variant_row_item" />
<androidx.constraintlayout.widget.ConstraintLayout>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<LinearLayout>...</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
My Understanding
After debugging, I found out that the NestedScrollView height is lesser than the y co-ordinate of the desired item. Hence it scrolls to the bottom of the view instead of the desired item. My understanding could be completely wrong and if so, please correct me.
I resolved this with a really simple fix.
private fun scrollToPosition(position: Int) {
if (position in 0 until variantsAdapter.itemCount) {
val view = binding.variantsRecyclerView.getChildAt(position)
if (view != null) {
val target = binding.variantsRecyclerView.top + view.top
binding.nestedScrollView.scrollY = target
}
}
}
All I wanted was to get the desired item within the RecyclerView to the top of the screen.
I'm seeing this strange behavior and couldn't find anything similar to this.
So I have a parent Activity and inside is a Fragment, which I'm including in parent via include element and then in parent's onCreate, create Fragment and replace it with this include layout (Tell me if this is a right way? I was using FrameLayout but then switched to include and defined an id to it).
Activity
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.sourcey.materiallogindemo.CustomerDetailActivity"
tools:ignore="MergeRootFrame">
<com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.appbar.MaterialToolbar />
</com.google.android.material.appbar.AppBarLayout>
<include
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/app_bar"
app:layout_constraintBottom_toTopOf="#id/layout_bottom_bar"
layout="#layout/fragment_customer_detail" />
<androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.bottomappbar.BottomAppBar>
</com.google.android.material.bottomappbar.BottomAppBar>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context="com.sourcey.materiallogindemo.CustomerDetailFragment"
android:layout_height="match_parent"
android:layout_width="match_parent">
<!-- THIS IS THE CULPRIT -->
<com.google.android.material.button.MaterialButton
android:id="#+id/btn_update_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButton />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/sku_list"
app:layoutManager="LinearLayoutManager"
tools:context="com.sourcey.materiallogindemo.CustomerDetailFragment"
tools:listitem="#layout/fragment_s_k_u_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment is inflated correctly but when I do this inside onCreateView
rootView.btn_update_position.setOnClickListener {
// ... log something
}
and press the Button, it doesn't do anything? Even though most findings were led to this suggestion that I should inflate the view and then set onClickListener.
I also tried doing these
rootView.findViewById<MaterialButton>(R.id.btn_update_position).setOnClickListener {
// ... log something
}
and
val button = rootView.findViewById<MaterialButton>(R.id.btn_update_position)
button.setOnClickListener {
// ... log something
}
but none of them works.
I also tried above approaches in onViewCreated to see if maybe I was not getting the reference but no errors were thrown and no reaction was coming.
Only thing that works is this
activity?.findViewById<MaterialButton>(R.id.btn_update_position)
?.setOnClickListener {
// ... log something
}
I'm trying to understand why this happens? Could this be the issue of using include the Fragment?
NOTE I'm not a pro in android just do hobby work in it so don't know very deeply about it.
EDIT As you can see I have a RecyclerView in Fragment layout, I'm inflating the layout and then setting its adapter items which seems to work fine opposed to button.
rootView.sku_list.adapter = Adapter()
I'm bit confused about what you want to do here
First,<include> doesn't create new view, it just include the xml into the parent xml file so basically it still on activity and you need activity to findViewById
Second, about your question what different between FrameLayout and <include>.
With <include> like i said above, it just add xml file to the parent file, the main usage is for re-use layout (you can include it anywhere) .
With FrameLayout, from official doc : "FrameLayout is designed to block out an area on the screen to display a single item". E.g : you want your layout have a header and footer for all screen, only the middle part change so place a frame layout at middle then load different view for each screen, because that flexibility frame layout usually use for display fragment (you can google how to use frame layout for more details)
I am trying to create a recyclerview that grows in size with additional items until a certain max height and then fades away. I understand that Constraint layout is the right way to go here and I swear this was already working a month or so ago and then the Recyclerview stopped caring about its constraint and being visible beyond 280dp (see picture). here is my code. I am certain this was already working, I dont know if google changed something to implementation "androidx.constraintlayout:constraintlayout:2.0.0-beta4" or if I am slowly going mad. Maybe someone knows how to fix this. maybe its down to the implementation being beta. any help will be much appreciated.
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
tools:context=".MainActivity"
tools:showIn="#layout/activity_tilemap"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintHeight_default="wrap"
app:layout_constraintHeight_max="280dp"
app:layout_constrainedHeight="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:fadingEdge="horizontal"
android:fadingEdgeLength="30dp"
android:fillViewport="true"
android:requiresFadingEdge="vertical"
android:id="#+id/rv_comms"
android:padding="0dp"
android:clipToPadding="false" />
</androidx.constraintlayout.widget.ConstraintLayout>
I just found the culpable code. turns out the recording buttons in my adapter are to blame. they included the following method that enabled the button to overflow the views of other adapter items (when pressed the button will grow like in whatsapp, see image bellow)
public void setClip(View v) {
if (v.getParent() == null) {
return;
}
if (v instanceof ViewGroup) {
((ViewGroup) v).setClipChildren(false);
((ViewGroup) v).setClipToPadding(false);
}
if (v.getParent() instanceof View) {//this part is to blame
setClip((View) v.getParent());
}
}
the recursive nature of the method (seen already commented out in the last 4 lines of the method) basically set clipToPadding and clipChildren to false for the entire hierarchy of views from the record button upwards wich lead to the weird overflow. I ended up manually setting clipToPadding and clipChildren to false for only those views that were concerned with the adapter (this allowed me to retain the cool recording button overflow animation without having the fading edge issue) and now it looks pretty (see below).
I have seen 2 approaches for setting Visibility in XML using Databinding
<variable
name="vm"
type="com.example.myapp.viewmodel.MyViewmodel" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
style="#style/ProgressBarMediumWhite"
android:visibility="#{vm.showLoader ? View.VISIBLE : View.GONE}" />
<variable
name="vm"
type="com.example.myapp.viewmodel.MyViewmodel" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
style="#style/ProgressBarMediumWhite"
android:visibility="#{vm.showLoader}" />
I want to know which one is the better approach or both seems to be good.
How bad it is to Import android view in to the XML file.
The first one is the better.
Checks if showLoader is true then sets the views visibility to visible else gone.
The second one is passing a boolean showLoader as the view's visibility which should end up throwing an error.
Both approaches are the same, there is no difference.
However, there is one more way to create binding adapter and use it in xml layout files for changing visibility.
For example:
Create BindingAdapters.kt file and put this code in it
#BindingAdapter("goneUnless")
fun goneUnless(view: View, visible: Boolean) {
view.visibility = if (visible) VISIBLE else GONE
}
Now you can use this binding adapter like this
<ProgressBar
style="#style/ProgressBarMediumWhite"
app:goneUnless="#{vm.showLoader}" />
In most cases, both approaches work just fine.
However, when you have to make a choice like this, always choose the option that does not involve passing a view as a parameter. Unexpected events from the source (where the parameter comes from) might cause your app to misbehave or cause the view to render wrongly.
Therefore, the first approach seems to be more reliable.
android:visibility="#{vm.showLoader ? View.VISIBLE : View.GONE}"
I have a chat app and i'm trying to ensure that when the user opens the screen the items display from the bottom with the most recent right above the input area. I've used a custom view for the input area and both the recycler and the custom view are within a ConstraintLayout.
My problem is that when I load items into the list, if the number of items is greater than the size of the recycler, it will not fully show the last item. When I make the input area visibility = Gone then the items display properly at the bottom. It's almost like the recyclers LinearLayoutManager thinks that the height of the Recycler is of the screen without the input field. I've manually printed out the size of the views and used layout inspector to ensure that the recycler is indeed drawn in the correct location (above the input and below the navigation bar).
What could be causing such an issue? I should note that whenever you click on a Linkified text in a chat bubble that the list scrolls a small amount equal to the offset that's incorrect when you open the screen. Clearly something is not measuring right here and not sure where to begin.
I should also note that if I try to add a post with smoothScroll it will go to the end of the list but then whenever a new item appears in the list from sending a message the items above the most recently added one seem to jump up a little with an unnecessary animation. It's like the last item in the list is in some special state?
if you're curious this is my scrolling function:
private fun scrollToFirstItem(dueToKeyboard: Boolean = false) {
val stackingFromEnd = (recyclerView.layoutManager as LinearLayoutManager).stackFromEnd
if (stackingFromEnd) {
val firstPosition = recyclerView.adapter?.itemCount?: 0 - 1
if (dueToKeyboard ) {
recyclerView.scrollBy(0, Integer.MAX_VALUE)
} else {
recyclerView.scrollToPosition(firstPosition)
}
recyclerView.post { recyclerView.smoothScrollToPosition(firstPosition) }
} else {
recyclerView.scrollToPosition(0)
}
}
And my xml for my fragment:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="#+id/searchView"
layout="#layout/compose_new_message_include"/>
<com.airbnb.epoxy.EpoxyRecyclerView
android:id="#+id/conversationEpoxyRV"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="#id/searchView"
app:layout_constraintBottom_toTopOf="#id/composeView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
tools:listitem="#layout/conversation_item_inbound"/>
<include layout="#layout/conversation_pip_view"
android:id="#+id/selectedMediaContainer"/>
<****.ComposeView
android:id="#+id/composeView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Any help is appreciated, I'm so lost..
To me this feels like more of an issue in the layout rather than in the scrolling function. I hope that this can be resolved easily if you use a Relative Layout instead of a Linear Layout. So in case if it may be helpful, i'll add my solution below using a Relative Layout.
<RelativeLayout
android:id="#+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycle_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="#id/input_field">
</androidx.recyclerview.widget.RecyclerView>
<EditText
android:id="#+id/input_field"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"/>
</RelativeLayout>
So in case if it is not clear what i have done, an EditText element is set to align to the bottom of parent which is a Relative Layout. Then a RecyclerView is added so that RecylerView will be always constraint above the EditText but not overlapped.
Everything looks fine, except the restrictions you added to recycleview design, I see that you are setting the recycleview to top of searchView app:layout_constraintTop_toBottomOf="#id/searchView" while the searchView view is not restricted as should it be.
It's better when using ConstraintLayout to restrict every view inorder avoid unexpected behaviors, because every view has a relation with other view will be effected with other view (x, y), therefore your searchView should be look like:
<include
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
layout="#layout/compose_new_message_include"/>
Give the size of arraylist of your data to smoothScrollToPosition
var linearLayoutManager = LinearLayoutManager(context)
recyclerView.layoutManager=linearLayoutManager
recyclerView.adapter=adapter
linearLayoutManager.setStackFromEnd(true)
adapter.notifyDataSetChanged()
recyclerView.smoothScrollToPosition(dataMsgs.size())
I had a similar problem some years back and I solved it with scrollToPositionWithOffset (support library). Of course this was before constraint was used....
I kept my newer items at the bottom, too. The code I used for scrolling after inserting an item was:
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(getItemCount() - 1, 0);
To scroll and adjust the awkward positioning after removing an item, I used:
View vTopCard = layoutManager.getChildAt(0);
// Get its adapter position
final int topCardPos = layoutManager.getPosition(vTopCard);
// Get top of card position
int topOffset = layoutManager.getDecoratedTop(vTopCard);
int cardMargin = (int) getResources().getDimension(R.dimen.card_vertical_margin);
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(topCardPos, topOffset - cardMargin);
The getDecoratedTop helps with the "bounce" and final positioning, as does factoring the vertical margin.
I hope this helps (at least part of your issue)! Like I said, this is old code (when I was learning to program Android and Java at the same time), so if I left something out, let me know and I'll reexamine the app's code in more detail (though, I'll have to find the time).
Good luck!