Recyclerview: update view elements outside the screen - android

I have a recycler view that looks like this:
At the start, these circle icons are empty. I need to update every icon of my recycler to be from empty to full within an interval of 5 seconds (see the image above).
I actually can update these icons, but my problem is:
If I have 20 items, I'll need to scroll the recycler in order to see every item. Whenever I scroll the recycler, the last 4-5 items don't get updated from empty to full.
I just need to update the UI, I don't need to remove or add anything to the recyclerview. I've already tried to use notifyDataSetChanged(), notifyItemChanged(), but nothing worked so far.
What's your suggestion? Thank you in advance

Here's one strategy. Have one function in your adapter to start/reset the animation. You can call it when you set the data list. In onBindViewHolder you calculate when relative to now the icon should change to filled (could be in the past). The ViewHolder class either immediately shows the filled icon if the time is negative, or else it posts a delayed runnable to change it in the future. You'll need to cancel any previous delayed runnable so when views get recycled, they always get updated to the correct state.
//Inside your ViewHolder class:
private val setIconRunnable = Runnable { setFilledIcon() }
fun fillIconAt(timeFromNowMillis: Long) {
itemView.removeCallbacks(setIconRunnable)
if (timeFromNowMillis <= 0L) {
setFilledIcon()
} else {
setEmptyIcon()
itemView.postDelayed(setIconRunnable, timeFromNowMillis)
}
}
// In your adapter class:
companion object {
private const val ANIMATION_DURATION = 5000L
}
private var animationStartTime = 0L
fun initiateIconAnimation() {
animationStartTime = System.currentTimeMillis()
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: YourViewHolderType, position: Int) {
//...
val iconChangeTime = (
ANIMATION_DURATION * (position + 1).toFloat() / yourDataList.size
).roundToLong() + animationStartTime
holder.fillIconAt(iconChangeTime - System.currentTimeMillis())
}

Related

Play animation to each view sequentially with a method call

The fragment has several CardViews. I've made an utility class that contains the next method:
public void applyAnimationToEachView(#NonNull Collection<View> views,
#NonNull AnimationSet animationSet,
long offset,
boolean offsetSequentially) {
int i = 0;
for (View view: views) {
view.setAnimation(animationSet);
if (offsetSequentially)
view.getAnimation().setStartOffset(offset * i);
else view.getAnimation().setStartOffset(offset);
view.getAnimation().start();
i++;
}
}
I call the method this way:
override fun onHiddenChanged(hidden: Boolean) {
if (!hidden) AnimationManager().applyAnimationToEachView(
visibleCards as Collection<View>,
getAnimationSet(),
100,
true)
}
However, when I call the method, it sums up the offsets and shows the animation inconsequentially. When I do the same thing inside of the fragment, it works as intended:
var i : Long = 0
if (!hidden)
for (cardView in visibleCards) {
cardView.animation = getAnimationSet()
cardView.animation.startOffset = 100 * i
i = i.inc()
}
What's the reason behind this behavior? And can I somehow hide this functionality inside of my utility class?
That's because you're passing the same instance of AnimationSet to each View and then retrieve it with view.getAnimation(). Thus its start offset increases on each loop iteration. In order to make it work, you'll need to create a new Animation for each View.

doOnPreDraw method not getting called for some items in a recyclerView

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.

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)
}
}
})
}

RecyclerView shuffles dynamically added ImageViews

So, I have simply code
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
DownloadImageTask(holder.avatar).execute(mDataSet[position].avatar);
holder.header.setText(mDataSet[position].header)
holder.body.setText(mDataSet[position].body)
for (i in 0 until mDataSet[position].images.size){
val imgUrl= mDataSet[position].images.get(i)
var image= ImageView(holder.itemView.context)
image.layoutParams = ViewGroup.LayoutParams(200, 200)
image.maxHeight = 200
image.maxWidth = 200
val layout= holder.itemView.findViewById<View>(R.id.linear_layout)
DownloadImageTask(image).execute(imgUrl)
layout.linear_layout.addView(image)
}
}
But after scrolling down and up in view images are shuffling between any items in recyclerView. So, how fix it?
Also don't forgot to remove previous image views added to your linear_layout.
try add linear_layout.removeAllViews() after canceling download process & right before start new images download.
EDIT: IF you update to use Glide..your code must be smaller to this:
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
Glide.with(holder.avatar.context)
.load(mDataSet[position].avatar)
.into(holder.avatar);
holder.header.setText(mDataSet[position].header)
holder.body.setText(mDataSet[position].body)
val layout= holder.itemView.findViewById<View>(R.id.linear_layout)
//cancel previous image download
layout.linear_layout.children.toList().filter { it is ImageView }
.forEach { Glide.with(holder.itemView.context).clear(it) }
// remove image views
layout.linear_layout.removeAllViews()
//add row images
for (i in 0 until mDataSet[position].images.size){
val imgUrl= mDataSet[position].images.get(i)
var image= ImageView(holder.itemView.context)
image.layoutParams = ViewGroup.LayoutParams(200, 200)
image.maxHeight = 200
image.maxWidth = 200
Glide.with(holder.itemView.context).load(imgUrl).into(image)
layout.linear_layout.addView(image)
}
}
note: I try keep code sample, but is better to reuse exist dynamic image views & remove the rest.
This is happening because same view is getting reused and you have multiple async task on a single view when view is scrolled fast. As mentioned in the previous answers library such as Fresco, Glide, Volley etc handles it automatically.
Simplest way to do solve it in current scheme of things is set the async task as a tag in the view and when reusing the same view cancel previous async task which has been set on it. (Pardon java syntax, I'm not friendly with Kotlin yet)
Something like this :
AsyncTask prevTask = (AsyncTask) holder.avatar.getTag();
if(prevTask != null) {
prevTask.cancel();
}
AsyncTask task = DownloadImageTask(holder.avatar);
task.execute(mDataSet[position].avatar)
holder.avatar.setTag(task);
You will have to write similar code for images in the for loop.
It looks like you are using AsyncTask which seems to be the culprit. RecyclerView is rebinding previously created ViewHolders as you scroll them on/off the screen and, since you don't seem to be canceling uncompleted async tasks, you have a race case.
It is probably the case that the async task completes after the ViewHolder is rebound and so it updates the ViewHolder with images for the old item in your mDataSet. To fix this, you need to track and cancel the async tasks as the ViewHolders are bound/unbound.
Better yet, I would advise using an image loading library like Glide. When a ViewHolder is bound, you can cancel any previous loading requests for the ImageViews with Glide.clear() and use Glide.load(...).into(...) to load the new images.

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
}
}
}

Categories

Resources