I've spent the last 2 days attempting to triage why my RecyclerView is so is so unbearably slow while scrolling and I've narrowed it down to the ConstraintLayout I'm using for the rows. Using the GPU profiler on android shows green/blueish green bars all the way up to the top of the screen, indicating substantial jank. It's super obvious something is wrong.
Here's what my viewholder looks like:
class MyViewHolder(
override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer {
fun bindTo(item: Item?, clickListener: (Item?) -> Unit) {
text_item_name.text = item?.name
Glide.with(containerView.context).load(item?.url).into(image_item)
containerView.setOnClickListener { clickListener(item) }
}
}
And here's what my layout looks like. Simple as can be:
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:paddingBottom="#dimen/padding"
android:paddingEnd="#dimen/padding_2x"
android:paddingStart="#dimen/padding_2x"
android:paddingTop="#dimen/padding">
<ImageView
android:id="#+id/image_item"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="#dimen/margin_2x"
android:layout_marginStart="#dimen/margin_2x"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/text_coin_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="#dimen/margin_2x"
android:layout_marginStart="#dimen/margin_2x"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="#id/image_item"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
So my question is this: what is wrong with my layout that's causing the jank? Am I using an incorrect LayoutManager? Are the constraints causing overdraws?
If I completely rewrite the layout XML to be LinearLayout based, it's smooth as silk.
Well, I finally found the culprit:
android:layout_height="wrap_content"
If I specify a fixed size for this attribute all is well. The constant calculations to determine view height for each row were likely causing the issues.
Related
I have several TextViews inside ConstraintLayout. The visibility of those TextViews are set at runtime based upon the data availability.
I need to bold the text of the first visible TextView.
I have tried many things but couldn't resolve the issue.
I have tried:
Looping through the child inside the parent layout and
finding the first child and making it bold. This approach always
finds the first child inside the parent layout regardless of it's
visiblility.
I put a check before retrieving the first element if view.visibility
== View.VISIBLE and also I checked view.isShown, both way again returns the first child in the view hierarchy irrespective of the visibility. So, view.visibility always returns View.VISIBLE.
What am I missing and how can I make it work?
Providing sample codes:
XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/rootLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<TextView
android:id="#+id/textView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="First textview"
android:textColor="#color/black"
android:visibility="#{viewModel.textView1Visibility}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Second textview"
android:textColor="#color/black"
android:visibility="#{viewModel.textView2Visibility}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/textView1" />
<TextView
android:id="#+id/textView3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Third textview"
android:textColor="#color/black"
android:visibility="#{viewModel.textView3Visibility}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/textView2" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
CODE
binding.apply {
/** We have three textviews.
* textView1 doesn't have data so we are hiding it.
* textView2 has data so, we need to show it.
* and textView2 will be the first item, we need to bold it as well. */
textView1.visibility = View.GONE
/** looping through the children of root layout and bolding the first visible */
rootLayout.forEach exit# { view ->
if(view.isVisible || view.isShown) {
(view as TextView).typeface = Typeface.DEFAULT_BOLD
return#exit
}
}
}
Your code is looping through all the children of the ConstraintLayout because return#exit acts like a continue and not a true break.
Try the following:
run exit#{
rootLayout.forEach { view ->
if (view.isVisible || view.isShown) {
(view as TextView).typeface = Typeface.DEFAULT_BOLD
return#forEach
}
}
}
There may be a more elegant way to go about this, but the above should work.
Update to code:
rootLayout.children.firstOrNull { view ->
view.isVisible || view.isShown
}?.also { view ->
(view as TextView).typeface = Typeface.DEFAULT_BOLD
}
I will also mention that your code will find the first visible child of the ConstraintLayout child order, but that view may not be the view that one would pick as the first looking at the screen. That is because the view order within the children of ConstraintLayout may differ from the on-screen order due to layout constraints. This may not be a problem for you, but it is something to think about.
In my application I have recyclerView and for this I should use GridLayoutManager and set 2 items per column.
I want set this 2 items fit of screen, each items sticky to screen. Such as below image:
I write below codes, but after run application show me such as this :
And I want set 120dp for layout_width and layout_height!
My XML code :
<?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"
android:id="#+id/rowMasterQuestion_root"
android:layout_width="#dimen/_120mdp"
android:layout_height="#dimen/_120mdp"
android:background="#drawable/gray_border">
<TextView
android:id="#+id/rowMasterQuestion_title"
android:layout_width="#dimen/_120mdp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/_15mdp"
android:gravity="center_horizontal"
android:textColor="#color/black"
android:textSize="#dimen/_9font_mdp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/rowMasterQuestion_answer"
android:layout_width="wrap_content"
android:layout_height="#dimen/_15mdp"
android:layout_marginLeft="#dimen/_10mdp"
android:layout_marginRight="#dimen/_10mdp"
android:layout_marginBottom="#dimen/_20mdp"
android:gravity="center"
android:paddingLeft="#dimen/_5mdp"
android:paddingRight="#dimen/_5mdp"
android:textColor="#color/white"
android:textSize="#dimen/_8font_mdp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
My Activity codes :
requestMain_childList.apply {
layoutManager = GridLayoutManager(this#RequestMainPage, 2, GridLayoutManager.VERTICAL, false)
hasFixedSize()
adapter = masterChildAdapter
}
How can I fix this issue?
This is tricky because you are wanting a different layout gravity for each item, depending on the column. I think that will have to be set in the RecyclerView.Adapter.onBindViewHolder function.
First, wrap the ConstraintLayout of your item view in a FrameLayout. Make the width of the outer FrameLayout match_parent so it will fill its column in the RecyclerView. You can also give the FrameLayout the amount of start/end padding you want to hold the items away from the sides of the screen.
<FrameLayout 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="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/wrappedView"
android:layout_width="#dimen/_120mdp"
android:layout_height="#dimen/_120mdp"
...>
...
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
Then in onBindViewHolder, you can set the gravity of the inner view to the left or right depending on which column it is in (even or odd data position).
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
// ...
val innerView = holder.wrappedView // or however you have it stored in view holder
(innerView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER_VERTICAL or
(if (position % 2 == 0) Gravity.START else Gravity.END)
}
Note, if you want the items to be centered in their columns instead, this is much easier. You can just set the gravity of the inner view to center in the XML and you don't have to do anything in the Adapter.
I have a fragment with an expandable list view which should be scrollable if the content is larger than the remaining screen. However, I cannot find a way to get the scrolling working properly. Here's the fragment xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="#+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="#string/titleView"
android:textColor="#color/textColor"
android:paddingHorizontal="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="8pt"
tools:layout_editor_absoluteY="9dp" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:id="#+id/nestedScrollView"
android:fillViewport="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/titleView"
app:layout_constraintBottom_toBottomOf="parent"
>
<ExpandableListView
android:id="#+id/expListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:padding="8dp"
/>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
I tried to wrap a LinearLayout around the NestedScrollView (because some other post said that), but it did not work. When I open an element at the bottom of the list it scrolls down to show the complete child in the view, but the view does not respond to any scrolling attempts, neither by sliding up/down on the items nor clicking on the scroll bar.
Adapter code taken 1:1 from here: http://developine.com/expandable-listview-android-kotlin/ only xml is changed as shown above.
Any ideas?
I found an answer to my question which solves the problem but as pointed out by #Mwasz this should not be the way you do it. So I document it here just for reference and maybe if somebody has really a reason to use the NestedScrollView
In the ExpandableListView add
android:nestedScrollingEnabled="true"
to enable the scrolling inside the NestedScrollView.
I don't understand why is ExpandableListView wrapped in the NestedScrollView. If you don't have an exact reason for this - remove NestedScrollView
If you need to keep the NestedScrollView - I think Your list isn't scrolling, because the ScrollView consumes touch events. You could try to make a custom ExpendableListView that disallows to intercept the touch event.
Kotlin (if you're using java then change it to java code):
class MyOwnExpendableListView #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ExpandableListView(context, attrs) {
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(ev)
}
}
Then use it in your xml:
<MyOwnExpandableListView
android:id="#+id/expListView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:padding="8dp"
/>
I am trying to make a chat application. Where I have different layouts for receive messages and send messaged. I am not able to get smooth scroll maybe because of variable image size? I think this issue related to layout I have made for the chat message box.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools"
android:layout_marginTop="11dp"
android:layout_marginRight="16dp"
android:gravity="right"
>
<android.support.v7.widget.AppCompatTextView
android:id="#+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="11dp"
android:paddingVertical="9dp"
android:visibility="gone"
tools:text="fdsfdsfsdfs"
tools:visibility="gone"
android:textColor="#2A2617"
android:textSize="15dp"
android:fontFamily="#font/nunito_regular"
/>
<android.support.v7.widget.AppCompatImageView
android:id="#+id/iv_message"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="#+id/tv_message"
android:layout_width="200dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:padding="11dp"
android:visibility="gone" />
</RelativeLayout>
It flickers when i scroll up. Code of View Holder is as follow
when(data.messageType){
Constant.MessageType.TEXT ->{
messageIv.visibility = View.GONE
messageTv.apply {
visibility = View.VISIBLE
text = data.content
}
}
Constant.MessageType.IMAGE -> {
Glide.with(messageIv.context)
.load(data.content)
.apply(
RequestOptions()
.apply {
diskCacheStrategy(DiskCacheStrategy.ALL)
skipMemoryCache(true)
})
.into(messageIv)
messageIv.visibility = View.VISIBLE
messageTv.apply {
visibility = View.GONE
}
}
}
I guess it call bind every time i scroll and it makes the visibility gone and visible. Which cause the view to have 0dp size for some amount of time and suddenly when image loads, it creates a sudden scroll in the view making it very unstable.
Any work-around for this?
I am creating app in which there is screen with searchBar by this lib.
My activity layout looks like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.constraint.ConstraintLayout
android:id="#+id/wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true">
<TextView
android:id="#+id/title"
android:textColor="#color/colorPrimary"
android:text="#string/mainScreenTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textAlignment="center"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:paddingTop="100dp"/>
<com.arlib.floatingsearchview.FloatingSearchView
android:id="#+id/searchView"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginTop="100dp"
android:animateLayoutChanges="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title"/>
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
There i have to move FloatingSearchingView to top of screen and change height to for example 500dp - like this:
app:layout_constraintTop_toTopOf="parent"
android:layout_height="500dp"
And later i will have to move it back and change height again to default value
app:layout_constraintTop_toBottomOf="#+id/title"
android:layout_height="50dp"
The problem is, i have to animate it. I have spent a lot of time searching for solution but unfortunately i didnt find anything.
I had to animate the TextView height inside a ConstraintLayout.
<androidx.constraintlayout.widget.ConstraintLayout
...
android:animateLayoutChanges="true"
>
<TextView
...
I first informed the ConstraintLayout that the layout was about to change:
constraintlayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
and then updated the textView height (or whatever other param)
textView.getLayoutParams().height = 200;
textView.requestLayout();
for something simple as that, no need to set animators or involve other libraries.
#beeb's version didn't work for me either, unfortunately. Although the question is old, here is a solution that worked for me, using the TransitionManager. It's a bit "redundant", as you have to duplicate some code, but therefor very easy and includes only two basic steps:
Create two layout.xml files. One for each state (before/after animation).
If your constraint layout is one of multiple children of a parent layout, you can create two files containing only this constraint layout and include the version you wan't at app-start via <include #layout=""/>, where previously you constraint layout has been. That way, you don't have to duplicate the entire layout file. This however caused the findViewById() method to return null, when my include element had an id. So be careful with this.
In your case this would be e.g.: floating_searching_view_at_bottom.xml:
<com.arlib.floatingsearchview.FloatingSearchView
android:id="#+id/searchView"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginTop="100dp"
android:animateLayoutChanges="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title"
android:layout_height="50dp" />
and floating_searching_view_at_top.xml:
<com.arlib.floatingsearchview.FloatingSearchView
android:id="#+id/searchView"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginTop="100dp"
android:animateLayoutChanges="true"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#+id/title"
android:layout_height="50dp"
app:layout_constraintTop_toTopOf="parent"
android:layout_height="500dp" />
Animate the transition between these layout files using TransitionManager:
boolean toggle = false // you should give this guy a more descriptive name!
Context context = this; // in scope of the activity.
public void animateTransition() {
// Load ConstraintSets from the two layouts
ConstraintSet atBottom = new ConstraintSet();
ConstraintSet atTop = new ConstraintSet();
atBottom.clone(context, R.layout.floating_searching_view_at_bottom);
atTop.clone(context, R.layout.floating_searching_view_at_top);
// apply ConstraintSet depending on "toggle"
ConstraintSet newLayout = (toggle) ? expanded : collapsed;
TransitionManager.beginDelayedTransition(constraintLayout);
newLayout.applyTo(constraintLayout);
// invert the toggle
toggle = !toggle;
}
Note:
1. Make sure context is of type Activity and you're using an
AppCompat theme. I accidentally used getApplicationContext(), what
obviously resulted in a crash.
2. If you are receiving an NPE on your constraintk
Try to use Animation class for this:
Animation animation = new Animation() {
#Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
LayoutParams params = yourView.getLayoutParams();
// modify your layout params here
yourView.setLayoutParams(params);
}
};
animation.setDuration(500); // in ms
yourView.startAnimation(animation);