doOnPreDraw method not getting called for some items in a recyclerView - android

I'm having troubles with some animation in a recycler view. I do the relevant measurements in onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: PairingViewHolder) {
super.onViewAttachedToWindow(holder)
// get originalHeight & expandedHeight if not gotten before
if (holder.expandedHeight < 0) {
// Execute pending bindings, otherwise the measurement will be wrong.
holder.itemViewDataBinding.executePendingBindings()
holder.cardContainer.layoutParams.width = expandedWidth
holder.expandedHeight = 0 // so that this block is only called once
holder.cardContainer.doOnLayout { view ->
holder.originalHeight = view.height
holder.expandView.isVisible = true
// show expandView and record expandedHeight in next layout pass
// (doOnPreDraw) and hide it immediately.
view.doOnPreDraw {
holder.expandedHeight = view.height
holder.expandView.isVisible = false
holder.cardContainer.layoutParams.width = originalWidth
}
}
}
}
The problem is that doOnPreDraw gets called just for some views. It is something related to the visibility of the views I guess, since the smaller the items (expanded) are, the highest the count of the ones on which onPreDraw gets called.
My guess is that since I'm expanding them in onLayout, the recyclerView consider visible only the ones that when expanded are actually visible on screen. In onPreDraw I collapse them, resulting in some views being able to animate correctly and some not.
How would you solve this?
Thanks in advance.

Related

Changing TextView width in RecyclerView CardView

I have a RecyclerView made of CardView with several TextViews
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
<TextView...
<TextView...
...
</LinearLayout>
</androidx.cardview.widget.CardView>
I'm trying to change the width of a TextView to be equal in each RecyclerView row and to fit the widest content, so it looks like a table with equal columns. To do that I made a function:
private fun optimizeLayout() {
var maxWidth = 100
val recyclerView = myRecyclerViewLayout
recyclerView.doOnLayout {
// Get max width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.measure(0, 0)
if (tv.measuredWidth > maxWidth) maxWidth = tv.measuredWidth
println(i.toString() + " " + tv.measuredWidth)
}
}
// Set width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.width = maxWidth
println("set $i")
}
}
// Header width
txtHeader.doOnLayout { txtHeader.width = maxWidth }
}
}
This function was created after reading many other posts on a similar topic on the Internet. I call it from onViewCreated of the Fragment that contains the RecyclerView and it works fine beside that I get warnings:
requestLayout() improperly called by com.google.android.material.textview.MaterialTextView{b1b06f0 V.ED..... ......ID 0,5-143,110 #7f0a0225 app:id/txtDest} during layout: running second layout pass
I also have a dialog for editing items. The problem starts when I try to change an item - for example I enter wider text and want all the rows in the RecyclerView to have a new width. When the dialog closes I call the function. It works, but not for all elements(?!). For example, I have 10 rows and the function stops in fourth like recyclerView.childCount only returned 4 out of 10. When I close and open Fragment all columns are again even for all elements. I tried to run the function in thread and from onLayoutCompleted:
recyclerView.layoutManager = object : LinearLayoutManager(this.context) {
override fun onLayoutCompleted(state: RecyclerView.State?) {
optimizeLayout()
super.onLayoutCompleted(state)
}
}
val runnable = Runnable {
while (true) {
optimizeLayout()
Thread.sleep(1000)
}
}
Every time with the same result. Why is this happening?
You should never reference recycled views outside of onBindViewHolder.
The reason all of the recycler views are not updating is due to the views not being drawn yet when you call optimizeLayout(). A RecyclerView recycles views and only draws them when they become visible.
I suggest following google design guidelines for list patterns
Which would make the view match parent or keep a consistent width across all views.
If that is not an option I would loop through the string list and find the string with the greatest length and calculate the width and pass it to the adapter before setting the adapter list.

Adding a Progress Bar to a loading image

I am trying to create a progress bar that will display while an image is downloading from a server. This image is loaded into a custom view. (I need it to be custom because I draw on the image.)
My solution was to add the custom view into the XML under the layout of the fragment, and mark its visibility as Visibility.GONE. This worked in the XML editor, as the progress bar took up the full space. Invisible did not work as it's position was still displayed.
The issue comes when the image path is given to my custom view. It would seem that setting Visibility.GONE on a view means that the view is not measured. But I need the dimensions of the view to measure how large the bitmap should be.
// Create the observer which updates the UI.
val photoObserver = Observer<String?> { photoPath ->
spinner.visibility = View.GONE
thumbnailFrame.visibility = View.VISIBLE
thumbnailFrame.invalidate()
thumbnailFrame.setImage(photoPath)
Looking at the Logs from the custom view, it is calling onMeasured() but it is doing it too late. I need onMeasure() to be called before setImage(). Is there a better way of handling this and if not is there a way to force the code to wait until I know the view has finished its measuring process?
Solved using a basic listener pattern with an anonymous class inline. I'm not sure if there is a better way but this way works just fine. Delay is not much of an issue since the view draws quite fast anyways.
* Set a listener to notify parent fragment/activity when view has been measured
*/
fun setViewReadyListener(thumbnailHolder: ViewReadyListener) {
holder = thumbnailHolder
}
interface ViewReadyListener {
fun onViewSet()
}
private fun notifyViewReadyListener() {
holder?.onViewSet()
}
spinner.visibility = View.INVISIBLE
thumbnailFrame.visibility = View.VISIBLE
//We have to make sure that the view is finished measuring before we attempt to put in a picture
thumbnailFrame.setViewReadyListener(object : ThumbnailFrame.ViewReadyListener {
override fun onViewSet() {
thumbnailFrame.setImage(photoPath)
//If we have a previous saved state, load it here
val radius = viewModel.thumbnailRadius
val xPosit = viewModel.thumbnailXPosit
val yPosit = viewModel.thumbnailYPosit
if (radius != null) {
thumbnailFrame.setRadius(radius)
}
if (xPosit != null) {
thumbnailFrame.setRadius(xPosit)
}
if (yPosit != null) {
thumbnailFrame.setRadius(yPosit)
}
}
})
}

Why does scrolling a ViewPager before changing it's padding and pagemargin completely jack it up?

I want to be able to toggle my viewpager between two different states. One state where the fragment takes up the entire screen, and the second state where it shows previews of the fragments on either side.
The following is the desired effect: https://im2.ezgif.com/tmp/ezgif-2-d5afc38aa7.gif
Notice that the gif is only for the first item in the viewpager. Watch what happens when I scroll to another item in the viewpager before attempting to switch states: https://im2.ezgif.com/tmp/ezgif-2-bf17757173.gif
As you can see I was on the item labelled "2" and it "zoomed out" to some incorrect point in the second state.
The following is my code (in Kotlin):
fun setupListeners() {
btnTest.setOnClickListener {
TransitionManager.beginDelayedTransition(binding.viewPager)
if(stateTwo) {
resetViewPagerStyling()
} else {
setViewPagerStateTwo()
}
stateTwo = !stateTwo
}
}
fun setViewPagerInStateTwo() {
// Setup view pager to show siblings
viewPager.clipToPadding = false
// Setup margins to show pages to the side
viewPager.setPadding(100, 100, 100, 100)
viewPager.pageMargin = 50
}
fun resetViewPagerStyling() {
// Setup view pager to show siblings
viewPager.clipToPadding = true
// Reset margin and padding to 0
viewPager.setPadding(0, 0, 0, 0)
viewPager.pageMargin = 0
}

how to keep RecyclerView always scroll bottom

I Use Recyclerview Replace with list view
I want to keep Recyclerview always scroll bottom.
ListView can use this method setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL)
RecyclerView I use method smoothScrollToPosition(myAdapter.getItemCount() - 1)
but when Soft keyboard Pop ,its replace RecyclerView content.
If you want to keep the scroll position anchored to the bottom of the RecyclerView, it's useful in chat apps. just call setStackFromEnd(true) to on the LinearLayoutManager to make the keyboard keep the list items anchored on the bottom (the keyboard) and not the top.
This is because RV thinks its reference point is TOP and when keyboard comes up, RV's size is updated by the parent and RV keeps its reference point stable. (thus keeps the top position at the same location)
You can set LayoutManager#ReverseLayout to true in which case RV will layout items from the end of the adapter.
e.g. adapter position 0 is at the bottom, 1 is above it etc...
This will of course require you to reverse the order of your adapter.
I'm not sure but setting stack from end may also give you the same result w/o reordering your adapter.
recyclerView.scrollToPosition(getAdapter().getItemCount()-1);
I have faced the same problem and I solved it using the approach mentioned here. It is used to detect whether soft keyboard is open or not and if it is open, just call the smoothScrollToPosition() method.
A much simpler solution is to give your activity's root view a known ID, say '#+id/activityRoot', hook a GlobalLayoutListener into the ViewTreeObserver, and from there calculate the size diff between your activity's view root and the window size:
final View activityRootView = findViewById(R.id.activityRoot);
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();
if (heightDiff > 100) {
recyclerView.smoothScrollToPosition(myAdapter.getItemCount() - 1);
}
}
});
Easy!
I have also faced same problem. But following code help me. I hope this is useful.
In this staus is arraylist.
recyclerView.scrollToPosition(staus.size()-1);
next one is:-
In This you can use adapter class
recyclerView.scrollToPosition(showAdapter.getItemCount()-1);
I ran into this problem myself and I ended up creating my own LayoutManager to solve it. It's a pretty straightforward solution that can be broken down into three steps:
Set stackFromEnd to true.
Determine whether forceTranscriptScroll should be set to true whenever onItemsChanged is called. Per the documentation, onItemsChanged gets called whenever the contents of the adapter changes. If transcriptMode is set to Disabled, forceTranscriptScroll will always be false, if it's set to AlwaysScroll, it will always be true, and if it's set to Normal, it will only be true if the last item in the adapter is completely visible.
In onLayoutCompleted, scroll to the last item in the list if forceTranscriptScroll is set to true and the last item in the list isn't already completely visible.
Below is the code that accomplishes these three steps:
import android.content.Context
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TranscriptEnabledLinearLayoutManager(context: Context, transcriptMode: TranscriptMode = TranscriptMode.Normal) :
LinearLayoutManager(context) {
enum class TranscriptMode {
Disabled, Normal, AlwaysScroll
}
private var transcriptMode: TranscriptMode = TranscriptMode.Disabled
set(value) {
field = value
// Step 1
stackFromEnd = field != TranscriptMode.Disabled
}
private var forceTranscriptScroll = false
init {
this.transcriptMode = transcriptMode
}
// Step 2
override fun onItemsChanged(recyclerView: RecyclerView) {
super.onItemsChanged(recyclerView)
forceTranscriptScroll = when (transcriptMode) {
TranscriptMode.Disabled -> false
TranscriptMode.Normal -> {
findLastCompletelyVisibleItemPosition() == itemCount - 1
}
TranscriptMode.AlwaysScroll -> true
}
}
// Step 3
override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
val recyclerViewState = state ?: return
if (!recyclerViewState.isPreLayout && forceTranscriptScroll) {
// gets the position of the last item in the list. returns if list is empty
val lastAdapterItemPosition = recyclerViewState.itemCount.takeIf { it > 0 }
?.minus(1) ?: return
val lastCompletelyVisibleItem = findLastCompletelyVisibleItemPosition()
if (lastCompletelyVisibleItem != lastAdapterItemPosition ||
recyclerViewState.targetScrollPosition != lastAdapterItemPosition) {
scrollToPositionWithOffset(lastAdapterItemPosition, 0)
}
forceTranscriptScroll = false
}
}
}

Check if ScrollView is higher than screen / scrollable

How can I check if a ScrollView is higher than the screen? When the content of a ScrollView fits on the screen, the ScrollView isn't scrollable, when it's contents exceed the screen height it's scrollable.
How can I check the condition of a ScrollView in that regard?
This is the code from ScrollView, which is private, but can be adapted to be used outside of the class itself
/**
* #return Returns true this ScrollView can be scrolled
*/
private boolean canScroll() {
View child = getChildAt(0);
if (child != null) {
int childHeight = child.getHeight();
return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
}
return false;
}
Too late, but I'm using the following code and it looks more safe for me:
if (view.canScrollVertically(1) || view.canScrollVertically(-1)) {
// you code here
}
A ScrollView always has 1 child. All you need to do is get the height of the child
int scrollViewHeight = scrollView.getChildAt(0).getHeight();
and Calculate Height of Your Screen
if both are equal(or scrollView Height is more) then it fits on your screen.
In my case, I was checking to see if my scrollView(which contained text) was scrollable vertically when the activity was created. On phones, it would scroll but on tablets it couldn't. canScrollVertically was returning me incorrect value because it couldn't be determined yet. I fixed this issue by calling it in the OnGlobalLayoutListener.
(Kotlin)
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Must use onGlobalLayout or else canScrollVertically will not return the correct value because the layout hasn't been made yet
scrollView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
// If the scrollView can scroll, disable the accept menu item button
if ( scrollView.canScrollVertically(1) || scrollView.canScrollVertically(-1) )
acceptMenuItem?.isEnabled = false
// Remove itself after onGlobalLayout is first called or else it would be called about a million times per second
scrollView.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
})
}
My use case was displaying terms of use. I didn't want the accept button to be enabled until the user scrolled to the bottom. I know this is late but i hope this resolves some confusion about canScrollVertically

Categories

Resources