ViewPager and AsyncTask for loading the Items - java

I've a ViewPager with an PageAdapter loading ImageViews.
The images displayed come from the internet and I use an AsyncTask for loading the Bitmap and then (if the Page hasn't been destroyed) assign it to the ImageView.
#Override
public Object instantiateItem(ViewGroup container, int position) {
ImageView img = new ImageView(container.getContext());
new LoadImageTask(img,Data.getImageUrL(position)).execute();
container.addView(img );
return img;
}
So the ImageView has two states: loading or loaded. I need to track if the user waits for the image. So I implement this at the LoadImageTask onPostExecute:
if (loadListener != null) {
loadListener.onImageLoaded(position);
}
And in the listener:
public void onImageLoaded(int position) {
if (position == mViewPager.getCurrentItem()) {
// Save record ..
}
}
Since I only need to count the images displayed at the "Current Item of the ViewPager" this method is not enought because if the user moves to the item 5 for example, the ViewPager loads in cache the item 6. If the user waits enought time for this load (beeing the item 5 the current) the callback to onImageLoaded is lost (because the item 6 is not the currentItem at the moment, and then when the user moves to the item 6 I've no way of knowing if the image is loaded or not.
I was thinking in adding something like [[PSEUDOCODE]]:
mViewPager.setOnPageChangeListener(new OnPageChangeListener() {
#Override
public void onPageSelected(int position) {
if (mViewPager.isLoaded(posistion)){
// Save record ...
}
}
But I dont know how to retrieve if the corresponding view loaded the image or not at this moment. How can I do it?

Related

How to make the holder in if else statements of onClick method work without final in argument Viewholder in arguments of onBindViewHolder method

I am trying to make a text change when a button located along with the text (layoutPasswd) in recycler view and to change it back if the button is again pressed.Like a password hiding button. The values to the adapter is from a static class object as arraylist. The problem occurring now is that the value for all the items (only for layoutPasswd) in recycler view is same.
public void onBindViewHolder(#NonNull final viewHolder holder, int position) {
holder.layoutUName.setText(users.get(position).getUserName());
pos = position;
holder.layoutPasswd.setText("********");
holder.btnViewChanger.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (holder.view1) {
holder.layoutPasswd.setText(users.get(pos).getPasswd());
holder.btnViewChanger.setText("hide");
holder.view1 = false;
} else {
holder.layoutPasswd.setText("********");;
holder.btnViewChanger.setText("Show");
holder.view1 = true;
}
}
});
You cannot rely on the ViewHolders or Views in a RecyclerView to hold any state, because they are recycled. Every time a view scrolls onto the screen, first it calls your onBindViewHolder function to update the contents of that ViewHolder to match the data.
Any configuration you set on the views or the ViewHolder instance in onBindViewHolder cannot be relied on to stay the same if the view scrolls off the screen, because the original ViewHolder might be recycled to be used for some other data, and when it scrolls back on screen, you might be looking at some other view that has been recycled from other data that just scrolled off the screen.
So if your views have configuration that you want to "stick", you have to back it up when you change it, and restore it in onBindViewHolder. The way you accomplish this will depend on how you are managing the data that you pass to the adapter.
If you can modify your User class, you can add a Boolean to it that stores whether it should show the password. Then in your onBindViewHolder, you restore the state based on this Boolean. And you also update this Boolean when the state changes.
I also updated the way the click listener works to simplify it for toggling. I removed the pos = position line, because almost certainly that is not something you should be doing.
public void onBindViewHolder(#NonNull final viewHolder holder, int position) {
final User user = users.get(position)
holder.layoutUName.setText(user.getUserName());
holder.layoutPasswd.setText(user.isShowPassword() ? user.getPasswd() : "********");
holder.btnViewChanger.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
user.setShowPassword(!user.isShowPassword());
holder.layoutPasswd.setText(user.isShowPassword() ? user.getPasswd() : "********");
holder.btnViewChanger.setText(user.isShowPassword() ? "hide" : "show");
}
});
// ...
}
If you cannot modify the User class, this is more complicated. Then the adapter should have its own ArrayList<Boolean> to store the state by position index, but you need to keep this list at least as long as the data that is bound, and reset everything to false if the whole list of data is refreshed.

Recyclerview : how to mix some post with image and no image? [duplicate]

This question already has answers here:
How to create RecyclerView with multiple view types
(23 answers)
Closed 3 years ago.
I want to display in same recycle-view which is some post have image and some post does not have images.
I can retrieve all the post with image and non-image,
but i want to change the size of the post when the user only post text(no image).
I expect the output like twitter feed..some post with image and without image have their own size.
Simple way to achieve this scenario is, All you have to do is create a view with both image and text, in recycler adapter check if image data is available make visibility of image visible else Image visibility gone.
Second Approach for this to make multiple view for RecyclerView.
#Override
public void onBindViewHolder(ViewHolder holder, int position) {
Log.d(TAG, "onBindViewHolder called");
ContentItem item = mContentItems.get(position);
if(item.getName()!=null){
holder.textName.setVisibility(View.Visible);
holder.textName.setText(item.getName());
}else{
holder.textName.setVisibility(View.GONE);
}
if(item.getPreviewImageDefault()!=null){
holder.imageIcon.setVisibility(View.Visible)
Picasso.with(mContext).load("file://" + item.getPreviewImageDefault()).into(holder.imageIcon);
}else{
holder.imageIcon.setVisibility(View.GONE)
}
}
Another possible solution is create 2 xml layouts and use ViewType in your RecyclerView.
look this How to create RecyclerView with multiple view type?
If you want to hide the image when it is ic_launcher you could do that (suppposing that data.getImage() returns the id of the drawable as integer):
#Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {
if(mItems!=null){
AdapterData data = mItems.get(i);
viewHolder.text.setText(data.getText());
viewHolder.image.setImageResource(data.getImage());
if(TextUtils.isEmpty(data.getText())){
viewHolder.text.setVisibility(View.GONE);
}else{
viewHolder.text.setVisibility(View.VISIBLE);
}
if(data.getImage()==R.drawable.ic_launcher){
viewHolder.image.setVisibility(View.GONE);
}else{
viewHolder.image.setVisibility(View.VISIBLE);
}
}
}
One possible solution, like some people have already said, is to hide/show the ImageView.
You could do that in the ViewHolder that you use for your RecyclerView.
class OptionalImageViewHolder extends RecyclerView.ViewHolder {
private ImageView image;
private TextView text;
// any other views you have
public OptionalImageViewHolder(View itemView) {
super(itemView);
image = itemView.findViewById(R.id.yourImageViewIdHere);
text = itemView.findViewById(R.id.yourTextViewIdHere);
// same for any other views you have
}
public void bindView(Tweet tweet) {
// This is where the magic happens
// Note: I make the assumption that you have a class called "Tweet"
// that has a field for "text", a field for "image" (that can be
// null if there's no image), and any other necessary fields.
text.setText(tweet.getTweetText());
if (tweet.hasImage() /* function that returns whether or not there is an image */) {
image.setVisibility(View.VISIBLE);
image.setImageBitmap(tweet.getImage()); // or however you are setting the image
} else {
// else just make the image invisible
image.setVisibility(View.GONE);
}
}
}
Hopefully this gives you an idea.
RecyclerView supports different viewTypes (layouts) which is the proper way in such scenario. E.g.,
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
override fun getViewTypes(position:Int) =
if (mydata[position].hasImage) return R.layout.mylayout_with_image
else R.layout.mylayout_no_image;
override fun onCreateViewHolder(viewType:Int, parent:ViewGroup) : MyViewHolder =
// here viewType = layout id
MyViewHolder(layoutInflater.inflate(viewType, parent))
override fun onBindViewHolder(viewHolder:MyViewHolder, position:Int) {
// guaranteed viewHolder.itemView is the view you want for that position
}
}

Items of RecyclerView are not showing correctly

In my onBindViewHolder of my RecyclerView.Adapter<SearchAdapter.ViewHolder> when user clicks on cardview a button becomes visible. But when I'm scrolling recyclerview some other items buttons are shown as visible too. Why is this happening?
this is my code:
#Override
public void onBindViewHolder(final ViewHolder viewHolder, final int position) {
viewHolder.card.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (viewHolder.content_layout.getVisibility() == View.VISIBLE) {
viewHolder.content_layout.setVisibility(View.GONE);
viewHolder.address.setMaxLines(2);
viewHolder.attribute.setMaxLines(2);
} else {
viewHolder.content_layout.setVisibility(View.VISIBLE);
viewHolder.address.setMaxLines(8);
viewHolder.attribute.setMaxLines(8);
}
}
});
...
}
Once you start scrolling down the list your views get recycled. This means a previously inflated ViewHolder (some that gets created in onCreateViewHolder) is reused.
So what you have to do is to remember the clicked positions (e.g. via a SparseBooleanArray) and check in onBindViewHolder whether the view should be visible (previously clicked) or not.
You can find a basic usage example of the SparseBooleanArray in this StackOverflow post
The 'other' visible items buttons are the ones using the same viewholder that was modified in the callback. So because viewholders (and views) are recycled :
They should only store information that can be retrieved each time the viewholder is bound to a position.
Anything that may be changed in the views state should be refreshed in onBindViewHolder()
In your case you should store the 'is selected' somewhere else and reset the visibility and maxlines in onBindViewHolder() (not only in the callback)
Good idea is to make a class object with all data you need for one item in recycler view, also add there one boolean isItemWasClicked and inside onBindViewHolder() check this boolean and make buttons visible or not.
For example:
public class OneItemOfList{
int priceToDisplay;
String name;
String date;
boolean wasClicked;
}
public class YourAdapter extends RecyclerView.Adapter<OneItemOfList.ViewHolder> {
ArrayList<OneItemOfList> items;
...
#Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
viewHolder.view.setText(items.get(position).name);
if (items.get(position).wasClicked)
viewHolder.button.setVisible(View.VISIBLE);
else
viewHolder.button.setVisible(View.GONE);
viewHolder.view2.setOnClickListener(...
OnClick(...){
items.get(position).wasClicked = !items.get(position).wasClicked;
});
}
...
}
create an array for example Boolean array, and when each position clicked, set true in same position of array. and in onBindViewHolder check if that array[position] is true set that item visible if.

screenshots don't change when layout changes

I'm using ViewPager as my main navigation in app. I'm using ActionBar, as well. What I want to achieve is that when user clicks search button in ActionBar I want to have blurred background. So my approach is to take a screenshot, blur it, set as ImageView and display as an overlay over the whole view. And it works. But problem is that actually when I first open search it displays proper screenshot. Then I turn overlay off, change page in ViewPager, click search again and ... I can see the previous screenshot- not new one with new page on it. Here are my snippets of code:
show and hide overlay on search click (I'm passing R.id.container view which is parent id of my content view, menuOverlay is my ImageView referenced from layout)
Here's my method that takes screenshot and makes it blurry
// Ad. 1
item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
#Override
public boolean onMenuItemActionExpand(MenuItem menuItem) {
if(menuItem.getItemId() == R.id.action_search) {
menuOverlay.setImageBitmap(Utils.takeSnapshot(findViewById(R.id.container)));
menuOverlay.setVisibility(View.VISIBLE);
}
return true;
}
#Override
public boolean onMenuItemActionCollapse(MenuItem menuItem) {
if(menuItem.getItemId() == R.id.action_search) {
menuOverlay.setVisibility(View.GONE);
}
return true;
}
});
// Ad. 2
public static Bitmap takeSnapshot(View v) {
v.setDrawingCacheEnabled(true);
v.buildDrawingCache();
Bitmap bm = v.getDrawingCache();
Bitmap scaled = Bitmap.createScaledBitmap(bm, bm.getWidth()/5, bm.getHeight()/5, false);
return Utils.fastblur(scaled, 5);
}
I;m using fast blur algorithm found somewhere here on StackOverflow which is quite fast.
Where's the problem? Why it happens?
ok, I found the answer. if you want to always have "fresh" screenshot you need to destroy cache after all operations:
v.setDrawingCacheEnabled(false);

ViewPager + Adapter in Fragment => laggy swiping

I have a ViewPager with some fragments. Each fragment has a ListView in a SlidingDrawer (=invisible before swiping) with an ArrayAdapter.
Adapter is set on onCreateView(), that slows down swiping, because 30 list items have to load each time I swipe, because new fragments are being created.
My Question is, whether it is possible to set the adapter after swiping when it ViewPager is idle? Or is there a better way? The List needs to be already loaded when the SlidingDrawer is expanded.
I had a similar problem... I used listeners. Still, when you swipe two pages back to back it was laggy... I did something like this that improved the experience....
viewpager.setOnPageChangeListener(new OnPageChangeListener() {
int positionCurrent;
boolean dontLoadList;
#Override
public void onPageScrollStateChanged(int state) {
if(state == 0){ // the viewpager is idle as swipping ended
new Handler().postDelayed(new Runnable() {
public void run() {
if(!dontLoadList){
//async thread code to execute loading the list...
}
}
},200);
}
}
}
#Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
positionCurrent = position;
if( positionOffset == 0 && positionOffsetPixels == 0 ) // the offset is zero when the swiping ends{
dontLoadList = false;
}
else
dontLoadList = true; // To avoid loading content for list after swiping the pager.
}
}
If you take a few milli seconds to load the list that comes as supplement to the viewpager, its ok in terms of UX rather than giving a bad swiping experience... So, the idea is to wait for 400ms in the thread before loading the list and making sure that you actually dont load content when the user is trying to swipe fast to see the viewpager content...
My Question is, wether it is possible to set the Adapter after swiping
when it Pager is idle?
There is the OnPageChangeListener that you could set on the ViewPager to monitor the swipe gestures. You could then use the onPageSelected()(or the onPageScrollStateChanged() to monitor the current state) method to get notified when a new page has been selected and start from that method the loading of data.
Also, make sure the ListView are responsible for the lag and not some other part of your code.

Categories