RecyclerView shuffles dynamically added ImageViews - android

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.

Related

Recyclerview: update view elements outside the screen

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

RecyclerView - Continuous Columns Layout

I am trying to create a layout where items would follow one another in columns (see image below) but I am not getting there yet. I have tried GridLayoutManager and StaggeredGridLayoutManager - the problem with both neither provides the feature of item flowing into another column and following each other this way. With my current attempt I am trying FlexboxLayoutManager but the result I am getting is always columns with single items instead of the items flowing one after another.
The desired behavior is that the items are located one after another and when the high of the recycler doesn't allow for the full item view it should be broken down to the next column.
Here is what I am trying right now:
mBinding?.activeRecycler?.layoutManager = FlexboxLayoutManager(context).apply {
flexDirection = FlexDirection.COLUMN
flexWrap = FlexWrap.WRAP
alignItems = AlignItems.STRETCH
}
And this is getting me one item per column.
Trying to achieve this:
I highly doubt this is possible.
The RecyclerView, its adapters and its layout managers all are not designed to alter the fundamental form of a view.
Meaning that "splitting" one would not be possible.
The RecyclerView is designed to understand how many views are in sight at the same time, create that many views only and then bind the underlying objects to the views respectively.
Meaning the RecyclerView doesn't "Cut a View in half and displays its halves in different places".
The only way in which a constellation like yours would be possible, was if the layout manager is specifically designed to display one item in multiple views and thereby multiple positions. Which would then allow it to be displayed as you described. However, as I said, that would mean the view 3 in the middle and the view 3 in the last column would be two views being bound to the same object or a copy of it. (Or someone went completely crazy and actually split the view, which I doubt).
I don't believe that any of the standard layout managers are capable of it and I doubt that you can even achieve this without also altering the adapter accordingly, at the very least. Because the adapter basically does the binding so without its help the standard layout managers wouldn't be able to do the double binding as described above.
That being said, this is just a very good guess, going by the principles of the view and its components. I have not read the source code or full description of every layout manager.
The way I understand your problem is like this: You have your current list of data that contains the text fields and you want to show them on the normal way, one list item one view item in recycler view.
But based on your design requirements this is not possible.
My idea to achieve that is like this:
You have to create a new list which will separate one item of the previous list into 2,3 or more items to fit in your columns.
private fun demo() {
val originalList = listOf<String>()
val newScreenSpecificList = mutableListOf<String>()
val columnHeight = 3//example number of lines
val columnWidth = 10//example number of chars
var columnsIndex = 0//index of column
var currentColumnHeight = 0 // current column filled height
originalList.forEach {
if (currentColumnHeight + getTextHeight(it, columnWidth) <= columnHeight) {
newScreenSpecificList.add(it)
currentColumnHeight = currentColumnHeight + getTextHeight(it, columnWidth)
} else {
//here is the part where your text is bigger then your column height so you need to divide it
val textForSpaceLeft = getTextForSpaceLeft(it, columnHeight - currentColumnHeight)
newScreenSpecificList.add(textForSpaceLeft)
currentColumnHeight = currentColumnHeight + getTextHeight(textForSpaceLeft, columnWidth)
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
if (getTextForNewSpaceLeft(it, columnHeight - currentColumnHeight)){
//continue to repeat logic for new column
//...
}
}
if (currentColumnHeight >= columnHeight) {
columnsIndex++
}
}
}
private fun getTextForSpaceLeft(it: String, spaceLeft: Int): String {
return "it"// return text for the available space
}
private fun getTextForNewSpaceLeft(it: String, spaceLeft: Int): String {
return "new column also"// return text left for the new available space
}
private fun getTextHeight(text: String, columnWidth: Int): Int {
return 2//todo your logic to convert text length to number of lines needed for a specific width of the column
}
Now you need to continue this logic it is not complete, I hope it helps you.
I guess your problem is with the LayoutParams of items which are being created in your adapter. probably the height is set to match_parent in items. You can try to change the LayoutParams of itemViews in your adapter's onCreateViewHolder/onBindViewHolder. Or if the items' heights are kinda tricky to calculate, you can create a customView and try calculate the height in onMeasure and set the height to wrap_content
try to set items' height to wrap_content or if you want to do it in code, something like this:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FlexItemViewHolder {
val infatedView = ...
infatedView.layoutParams = FlexboxLayoutManager.LayoutParams(FlexboxLayoutManager.LayoutParams.WRAP_CONTENT, FlexboxLayoutManager.LayoutParams.WRAP_CONTENT)
infatedView.addView(textView)
return FlexItemViewHolder(f)
}

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

onGlobalLayout differentiate between various invocations

I have a logo view, which is a full screen fragment containing single ImageView.
I have to perform some operations after the logo image is completely visible.
Following code is used to invoke the special task
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
ImageView logoImageMaster = new ImageView(getContext());
//logoImageMaster.setImageResource(resID); //even after removing this, i am getting the callback twice
try {
// get input stream
InputStream ims = getActivity().getAssets().open("product_logo.png");
// load image as Drawable
Drawable d = Drawable.createFromStream(ims, null);
// set image to ImageView
logoImageMaster.setImageDrawable(d);
}
catch(IOException ex) {
}
logoImageMaster.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() { //FIXME get called twice. Check this out, no info to distinguish first from second
// Log.e("PANEL", "onGlobalLayout of Logo IV ---------------------------------");
activityInterface.doSpecialLogic();
}
});
return logoImageMaster;
}
My exact problem is, onGlobalLayout is called twice for this view hierarchy.
I know that onGlobalLayout is invoked in performTraversal of View.java hence this is expected.
For my use case of Single parent with Single child view, I want to distinguish the view attributes such that doSpecialLogic is called once[onGlobalLayout is called twice] , after the logo image is completely made visible.
Please suggest some ideas.
OnGlobalLayoutListener gets called every time the view layout or visibility changes. Maybe you reset the views in your doSpecialLogic call??
edit
as #Guille89 pointed out, the two set calls cause onGlobalLayout to be called two times
Anyhow, if you want to call OnGlobalLayoutListener just once and don't need it for anything else, how about removing it after doSpecialLogic() call??
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
//noinspection deprecation
logoImageMaster.getViewTreeObserver().removeGlobalOnLayoutListener(this);
} else {
logoImageMaster.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
activityInterface.doSpecialLogic();
It seems to be called one time for each set done over the imageView
logoImageMaster.setImageResource(resID);
logoImageMaster.setImageDrawable(d);
You should Try using kotlin plugin in android
This layout listener is usually used to do something after a view is measured, so you typically would need to wait until width and height are greater than 0. And we probably want to do something with the view that called it,in your case
Imageview
So generified the function so that it can be used by any object that extends View and also be able to access to all its specific functions and properties from the function
[kotlin]
inline fun <T: View> T.afterMeasured(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
[/kotlin]
Note:
make sure that ImageView is described properly in the layout. That is its layout_width and layout_height must not be wrap_content. Moreover, other views must not result in this ImageView has 0 size.

Parallax header effect with RecyclerView

I want to change my ListView I currently have over to use RecyclerView so I can make use of StaggeredGridLayoutManager but RecyclerView does not have the ability to add a header like ListView.
Usually with a ListView I set an empty view in the header and put the image below the listview and translate the bottom image with the scrolling of the list to create the Parallax effect.
So with out a header how can I create the same parallax effect with RecyclerView?
the easiest way to do it, is using below onScrollListener without relying on any library.
View view = recyclerView.getChildAt(0);
if(view != null && recyclerView.getChildAdapterPosition(view) == 0) {
view.setTranslationY(-view.getTop() / 2);// or use view.animate().translateY();
}
make sure your second viewHolder item has a background color to match the drawer/activity background. so the scrolling looks parallax.
So today I tried to archive that effect on a RecyclerView. I was able to do it but since the code is too much I will paste here my github project and I will explain some of the key points of the project.
https://github.com/kanytu/android-parallax-recyclerview
First we need to look at getItemViewType on the RecyclerView.Adapter class. This methods defines what type of view we're dealing with. That type will be passed on to onCreateViewHolder and there we can inflate different views. So what I did was: check if the position is the first one. If so then inflate the header, if not inflate a normal row.
I've added also a CustomRelativeLayout that clips the view so we don't have any trouble with the dividers and with the rows getting on top of the header.
From this point you seem to know the rest of the logic behind it.
The final result was:
EDIT:
If you need to insert something in adapter make sure you notify the correct position by adding 1 in the notifyItemChanged/Inserted method. For example:
public void addItem(String item, int position) {
mData.add(position, item);
notifyItemInserted(position + 1); //we have to add 1 to the notification position since we don't want to mess with the header
}
Another important edit I've done is the scroll logic. The mCurrentOffset system I was using didn't work with the item insertion since the offset will change if you add an item. So what I did was:
ViewHolder holder = findViewHolderForPosition(0);
if (holder != null)
((ParallaxRecyclerAdapter) getAdapter()).translateHeader(-holder.itemView.getTop() * 0.5f);
To test this I added a postDelayed Runnable, started the app, scrolled to the end, add the item in position 0, and scroll up again. The result was:
If anyone is looking for other parallax effects they can check my other repo:
https://github.com/kanytu/android-parallax-listview
For kotlin, you may config the recycler view as below
//setting parallax effects
recyclerView.addOnScrollListener(object :RecyclerView.OnScrollListener(){
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val view = recyclerView?.getChildAt(0)
if (view != null && recyclerView?.getChildAdapterPosition(view) === 0) {
val imageView = view.findViewById(R.id.parallaxImage)
imageView.translationY = -view.top / 2f
}
}
})
This answer is for those curious about adding a parallax header to a GridLayoutManager or a StaggeredGridLayoutManager
You'll want to add the following code to your adapter in either onBindViewHolder or onCreateViewHolder
StaggeredGridLayoutManager.LayoutParams layoutParams =
(StaggeredGridLayoutManager.LayoutParams) holder.itemView.getLayoutParams();
layoutParams.setFullSpan(true);

Categories

Resources