The following is the code for the adapter I'm using to display movie items that contain Movie Images and Movie Titles.
public class MovieAdapter extends RecyclerView.Adapter<MovieAdapter.MovieViewHolder> {
private List<Movie> movieList;
public MovieAdapter(List<Movie> movieList) {
this.movieList = movieList;
}
#Override
public MovieViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Context context = parent.getContext();
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.movie_item, parent, false);
return new MovieViewHolder(view);
}
#Override
public void onBindViewHolder(final MovieViewHolder holder, int position) {
Movie movie = movieList.get(position);
holder.mv_name.setText(movie.getName());
class ImageDownloadTask extends AsyncTask<String, Void, Bitmap> {
#Override
protected Bitmap doInBackground(String... strings) {
Bitmap bitmap = null;
URL url = createUrl(strings[0]);
Log.v("stringurl", strings[0]);
try {
bitmap = makeHttpRequest(url);
} catch (IOException e) {
}
return bitmap;
}
#Override
protected void onPostExecute(Bitmap bitmap) {
holder.mv_img.setImageBitmap(bitmap);
}
private URL createUrl(String StringUrl) {
URL url = null;
try {
url = new URL(StringUrl);
} catch (MalformedURLException e) {
return null;
}
return url;
}
private Bitmap makeHttpRequest(URL url) throws IOException {
HttpURLConnection urlConnection = null;
InputStream stream = null;
Bitmap bitmap = null;
try {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setConnectTimeout(10000);
urlConnection.setReadTimeout(15000);
urlConnection.connect();
stream = urlConnection.getInputStream();
bitmap = BitmapFactory.decodeStream(stream);
//Log.v("image:",bitmap.toString());
return bitmap;
} catch (IOException e) {
} finally {
if (urlConnection != null) urlConnection.disconnect();
if (stream != null) stream.close();
}
return bitmap;
}
}
new ImageDownloadTask().execute(movie.getImg());
}
#Override
public int getItemCount() {
return movieList == null ? 0 : movieList.size();
}
class MovieViewHolder extends RecyclerView.ViewHolder {
ImageView mv_img;
TextView mv_name;
public MovieViewHolder(View itemView) {
super(itemView);
mv_img = (ImageView) itemView.findViewById(R.id.mv_img);
mv_name = (TextView) itemView.findViewById(R.id.mv_name);
}
}
}
While scrolling down images are loading images of other movies are displaying. For example, for a movie, another movie image is getting displayed and after a second also another movie image is getting displayed in that way after a series of different images correct image is getting displayed.
I'm downloading images from the Url I get from the response inBindViewHolder method.
How to resolve this issue?
Problem
The thing here is to realize that the ViewHolders are pool of reused objects and as you scroll a reused ViewHolder will appear and the previously loaded in it image will be shown.
By using Picasso or Glide or similar you are saving incredible amount of code also using additional features like cache and effects.
Solution
First you can use a library like Picasso
compile 'com.squareup.picasso:picasso:2.5.2'
then in your recyclerview on bind viewholder
Picasso.with(holder.imageView.getContext()).cancelRequest(holder.imageView);
Picasso.with(holder.imageView.getContext()).load("http://image.com/image.png").into(holder.imageView);
The first line of code will stop any previous images from loading.
The second line will load the current image.
Replace "http://image.com/image.png" with your image url. It takes String, URL, etc...
Read more about Picasso here
Problem
The root cause for your problem is that the bitmap is retrieved after the view has been recycled and thus you get a mismatch when you use it.
How to resolve
The short answer is to use an image caching library like Picasso or Glide. These libraries where designed to do exactly what you are trying to accomplish.
Another option is to try to do this on your own. What you can do is save the URL inside the holder and call setImageBitmap() only in case the current URL matches the one you just fetched. To save the URL, just add a URL field in MovieViewHolder and store the URL whenever you retrieve the image and check that it matches when you finish downloading.
Use Picasso or Glide Library to display images in RecyclerView
i had also searched for this solution and from Doron Yakovlev-Golani answer i made a solution for my own, as my reputation is low so i'm unable to add a comment. Below is my solution, here when i added an image in ImageView I also added a ContentDescription to the ImageView as a reference for future use to determine which image need to show when ViewHolder call. The ContentDescription is current ListView position as String.
Here is two condition, first one for checking image previously added or not, if image not added previously, I added the image. second one image already added in the ImageView, so I checking which image need to load by calling ImageView ContentDescription, if ContentDescription matched current position it means i need to add current positions image to ImageView from my list.
In your onViewHolder you can use like this:
//checking it is first time or not
if (holder.mv_img.getContentDescription() == null) {
//adding current position for loading current position image
imageView.setContentDescription(position + "");
}
if (holder.mv_img.getContentDescription().equals(position + "")) {
holder.mv_img.setImageBitmap(userLogoUri);
}
Related
I'm trying to preview (thumbnail) PDF documents that are remotely, using the Glide library from bumptech, version 4.8.0.
To achieve this, following the excellent tutorial Writing a custom ModelLoader, I've written a custom ModelLoader, a custom DataFetcher for the buildLoadData method; added the AppGlideModule, implemented ModelLoaderFactory and registered my ModelLoader.
Inside the DataFetcher I've added some logic to process the following two cases:
The content is an image. Works like a charm!
The content is a PDF document. W/Glide: Load failed for https://www.testserver.net/folder/sample.pdf with size [522x600]
class com.bumptech.glide.load.engine.GlideException: Failed to load resource
One approach has been to download the PDF file locally, and then render it (this DOES work), but it adds a considerable delay when having to download a file from a url and copy it locally; on the other hand, it doesn't take advantage of Glide's use of the cache.
Should I add another extra ModelLoader to use OkHttp3 instead of Volley (default)?
Any ideas? Thanks in advance!
public final class MyModelLoader implements ModelLoader<File, InputStream> {
private final Context context;
public MyModelLoader(Context context) {
this.context = context;
}
#NonNull
#Override
public ModelLoader.LoadData<InputStream> buildLoadData(#NonNull File model, int width, int height, #NonNull Options options) {
return new ModelLoader.LoadData<>(new ObjectKey(model), new MyDataFetcher(context, model));
}
#Override
public boolean handles(#NonNull File file) {
return true;
}
}
public class MyDataFetcher implements DataFetcher<InputStream> {
#SuppressWarnings("FieldCanBeLocal")
private final Context context;
private final File file;
private InputStream inputStream;
public MyDataFetcher(Context context, File file) {
this.context = context;
this.file = file;
}
#Override
public void loadData(#NonNull Priority priority, #NonNull DataCallback<? super InputStream> callback) {
try {
if (isPdf(file)) {
//We have a PDF document in "file" -- fail (if document is remote)
try {
//render first page of document PDF to bitmap, and pass to method 'onDataReady' as a InputStream
PdfRenderer pdfRenderer = new PdfRenderer(ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
PdfRenderer.Page page = pdfRenderer.openPage(0);
int width = 2048;
int height = (page.getHeight() * (width / page.getWidth()));
Bitmap pageBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
page.render(pageBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
pageBitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream);
ByteArrayInputStream stream = new ByteArrayInputStream(outputStream.toByteArray());
callback.onDataReady(stream);
} catch (IOException ignored) {}
} else {
//We have an image in "file" -- OK
FileInputStream fileInputStream = new FileInputStream(file);
callback.onDataReady(fileInputStream);
}
} catch (IOException ignored) {}
}
// checks for file content
public boolean isPdf(File f) throws IOException {
URLConnection connection = f.toURL().openConnection();
String mimeType = connection.getContentType();
return mimeType.equals("application/pdf");
}
#Override
public void cleanup() {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException ignored) {}
}
}
#Override
public void cancel() {
//empty
}
#NonNull
#Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
#NonNull
#Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
public class MyModelLoaderFactory implements ModelLoaderFactory<File, InputStream> {
private final Context context;
public MyModelLoaderFactory(Context context) {
this.context = context;
}
#NonNull
#Override
public ModelLoader<File, InputStream> build(#NonNull MultiModelLoaderFactory multiFactory) {
return new MyModelLoader(context);
}
#Override
public void teardown() {
//empty
}
}
#GlideModule public class MyAppGlideModule extends AppGlideModule {
#Override
public void registerComponents(#NonNull Context context, #NonNull Glide glide, Registry registry) {
registry.prepend(File.class, InputStream.class, new MyModelLoaderFactory(context));
}
}
Finally, after all of the above, the call is of the form:
GlideApp.with(image.getContext()).load("resource_url").into(image);
Where "resouce_url" could be: https://www.testserver.net/folder/sample.pdf, eg.
You can show image and video thumbnails using Glide library if you want to show a pdf thumbnail you need to use the library for it e.g. Android PdfViewer. Then instead of using ImageView use PdfView and load only the first page instead of all e.g. .pages(0). That's it.
In your xml/layout
<com.github.barteksc.pdfviewer.PDFView
android:id="#+id/pdfView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
In your class
pdfView.fromBytes(bytes)
.pages(0) //show only first page
.spacing(0)
.swipeHorizontal(false)
.enableSwipe(false)
.load();
Well, I've finally found a method to solve the problem, from another perspective.
Instead of preprocessing the pdf on client-side, using a Glide ModelLoader, I've come up with an outlier but effective subterfuge: do it on server-side.
By means of the php Imagick extension, I've modified the server api so that it automatically generates a thumbnail on the server (in the "upload.php" module), same path where the pdf is saved. Thus, assuming that we have the pdf already loaded, we do the following:
// Create thumbnail, if pdf
if ($ext == 'pdf') {
$im = new imagick($base_path.$next_id["next"].".".$ext.'[0]');
$im->setImageFormat('jpg');
header('Content-Type: image/jpeg');
file_put_contents($base_path.$next_id["next"]."_thumbnail.jpg", $im);
}
(with the help of this link on using Imagick to convert pdf to jpg: How to convert pdf to a preview image in PHP).
On the other hand, when a record is deleted, the attachments that it may have associated must also be deleted, if any. This makes it necessary to also delete the thumbnail, in the same act, as shown below:
// Remove uploaded file from server
unlink($base_path.$id.".".$ext);
// If pdf, we also have to remove the thumbnail
if ($ext == 'pdf') {
unlink($base_path.$id."_thumbnail.jpg");
}
Now we have a set of files, some jpg/png and another pdf; but this is indifferent for Glide, which will only show jpg/png images, without any problem, even if they are remotely; and of course very quickly. The code on client-side is:
/* Have a pdf file, eg. "sample.pdf", Glide will load a file
with this name: "sample_thumbnail.jpg",
that contains first page of pdf file (preview)
(A single tap on one element will download the file
and launch an intent to display it)
*/
if (item.getType().equals("jpg") || item.getType().equals("jpeg") || item.getType().equals("png")) {
Glide.with(image.getContext()).load(item.getPath()).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(image);
} else if (item.getType().equals("pdf")) {
Glide.with(image.getContext()).load(getName(item.getPath()) + "_thumbnail.jpg").diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(image);
} else {
throw new Exception("File type not supported");
}
Although maybe not everyone can have Imagick on the server, in my case this solution has worked wonderfully.
I have an issue with my RV.
I am loading the cover art of an mp3 into a byte array and then use glide to load the byte array into an image (vh.coverArt). However, when I use glide instead of just setting the image with .SetImageBackground(image) glide returns the same picture over and over again.
I know I am giving it different data each time it is called, however only the very first picture is returned into my image each time. This is the whole function where I Bind the ViewHolder in my RV:
private async Task SetContentAsync(PhotoViewHolder vh, int position)
{
string SongName = "";
string ArtistName = "";
Bitmap bitmap = null;
byte[] data = null;
try
{
reader.SetDataSource(mp3Obj[position].Mp3Uri);
}
catch { }
await Task.Run(() => // cause problems with the reload
{
SongName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyTitle);
ArtistName = reader.ExtractMetadata(MediaMetadataRetriever.MetadataKeyArtist);
data = reader.GetEmbeddedPicture();
if (data != null)
{
try
{
bitmap = BitmapFactory.DecodeByteArray(data, 0, data.Length);
}
catch { }
}
});
((Activity)ctx).RunOnUiThread(() =>
{
vh.SongName.SetTypeface(tf, TypefaceStyle.Normal);
vh.AristName.SetTypeface(tf, TypefaceStyle.Normal);
vh.SongName.Text = SongName;
vh.AristName.Text = ArtistName;
try
{
if (bitmap != null && data != null)
{
Glide
.With(ctx)
.Load(data)
.Apply(RequestOptions.CircleCropTransform()).Into(vh.CoverArt);
ConvertBitmapToBackground(bitmap, vh, false); // Set As Backgorund, blurry and black ( just sets the variable)
}
else // because recycler items inherit their shit and if it is altered it just shows views were there shouldnt be any ...
{
vh.CoverArt.SetImageResource(Resource.Drawable.btn_musicalnote);
ConvertBitmapToBackground(bitmap, vh, true); // Set As Backgorund, blurry and black ( just sets the variable)
}
}
catch { }
});
}
Am I using Glide incorrectly?
you should set DiskCacheStrategy to NONE and skipMemoryCache to true like this:
Glide.with(DemoActivity.this)
.load(Uri.parse("file://" + imagePath))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(mImage);
So I'm currently investigating the possibility to replace a lot of custom image loading AsyncTasks in a project, with the Picasso library and it seems promising. However there is one issue that I'm not completely sure how to solve using Picasso.
In this case we are downloading Album Art for music tracks, however when our ListView is to be shown we only have the track ids. So first we have to lookup an Album Art URL based on the track id and then load it into an ImageView. Currently we have an AsyncTask where we in doInBackground() first lookup the image URL and then load a Bitmap from it, which is then pasted to onPostExecute.
Is there any way to make this pre-lookup using Picasso or will we have to wrap the Picasso call in a AsyncTask that first perform the lookup (and feels like it kinda defeats the purpose).
Update: How it works now:
private class AlbumArtTask extends AsyncTask<String, Bitmap, Bitmap> {
#Override
protected Bitmap doInBackground(String... strings) {
final String id = strings[0];
// This part is what I'm asking for, basically to be able to make an Async
// lookup of the url that will later be used by Picasso
final Uri uri = getAlbumArtUriFromTrackId(id);
// Creating the Bitmap and return it to be used in an ImageView
// This part is handled by Picasso
return bitmap = createBitmapFromUri(uri);
}
#Override
protected void onPostExecute(Bitmap bitmap) {
// load image into ImageView
}
}
Update 2: Made up example of what I'm after
Picasso.with(mContext)
.load(new UriProvider() {
// Get the actual image URI here
public Uri getImageUri() {
return getAlbumArtUriFromTrackId(id);
}
})
// And load it here
.into(mImageView);
I had to solve the same problem for my app. I used OkHTTP interceptor to redirect the request.
This is my picassa declaration:
OkHttpClient client = new OkHttpClient();
client.interceptors().add(new PhotoInterceptor());
Picasso picasso = new Picasso.Builder(context)
.downloader(new OkHttpDownloader(client))
.build();
This is a basic implementation of the interceptor:
public class PhotoInterceptor implements Interceptor {
Gson gson = new Gson();
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.isSuccessful()) {
Photo photo = gson.fromJson(response.body().charStream(), Photo.class);
if (photo!=null) {
request = request.newBuilder()
.url(photo.getPhoto_file_url())
.build();
response = chain.proceed(request);
}
}
return response;
}
}
You have to load images from your Album Art URL using Piccaso while your listView is created. Then on Detail screen of Track you have to call your actual image url to set it in imageview.
Edited
private class AlbumArtTask extends AsyncTask<String, Bitmap, Uri> {
ImageView mImageView;
Context mContext;
public AlbumArtTask(Context context,ImageView imageView){
this.mImageView=imageView;
this.mContext=context;
}
#Override
protected Uri doInBackground(String... strings) {
final String id = strings[0];
// This part is what I'm asking for, basically to be able to make an Async
// lookup of the url that will later be used by Picasso
final Uri uri = getAlbumArtUriFromTrackId(id);
// Creating the Bitmap and return it to be used in an ImageView
// This part is handled by Picasso
return uri;
}
#Override
protected void onPostExecute(Uri uri) {
// You have to pass imageview in constructor.
// load image into ImageView
Picasso.with(mContext).load(uri)/* And load it here*/.into(mImageView);
}
}
This question was asked here: Universal-Image-Loader: wrong Bitmaps are attached to ImageView
I am using the the latest, 1.9.3. I implemented the solution by having this in my application class:
#Override
public void onCreate() {
super.onCreate();
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder().resetViewBeforeLoading(true).build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext())
.defaultDisplayImageOptions(defaultOptions)
.build();
ImageLoader.getInstance().init(config);
}
Per Android-Universal-Image-Loader doesn't keep loaded images on scroll in gridview, I load the image like so in my adapter:
ImageAware imageAware = new ImageViewAware(viewHolder.profileIV, false);
ImageLoader.getInstance().displayImage(imgUrl, imageAware);
It still doesn't work; I'm wondering if it's because I have to make a call to get the image url since the api that supplied the model data did not include an image url.
So in my adapter's getView(), before I use imageloader with the image url, I do another asynch call to get the image url, like so:
APIclient.getImageJson(getContext(), googleUrl, new JsonHttpResponseHandler() {
#Override
public void onSuccess(JSONObject imgJson) {
try {
JSONObject responseDataValue = imgJson.getJSONObject("responseData");
JSONArray resultsValue = responseDataValue.getJSONArray("results");
JSONObject result = resultsValue.getJSONObject(0);
String imgUrl = result.getString("url");
ImageAware imageAware = new ImageViewAware(viewHolder.profileIV, false);
ImageLoader.getInstance().displayImage(imgUrl, imageAware);
//ImageLoader.getInstance().displayImage(imgUrl, viewHolder.profileIV);
}
catch(Exception e) {
e.printStackTrace();
}
}
google url here looks like: https://ajax.googleapis.com/ajax/services/search/images?rsz=1&start=1&v=1.0&q=%22barack%20obama%22
each row would have a different url since the names are different. I don't know if the problem is still a listview recycer problem that wasn't fixed in the universal image loader library or if the culprit lies in the additional network call. How can I make the thumbnails consistent with the data next to it?
I think it's because of async call APIclient.getImageJson(...) in getView(). It's unknown time when onSuccess() callback is fired so you can call ImageLoader.getInstance().displayImage(...) for already recycled ImageView.
I can propose you join these 2 async operations (get JSON by APIclient, load image by ImageLoader) following way. Implement own ImageDoanloader which will process google JSON URLs (https://ajax.googleapis.com/ajax/services/search/images?rsz=1&start=1&v=1.0&q=%22barack%20obama%22), extract image URL and load image.
Let's introduce our own URI scheme - "json". So we know that incoming URIs like "json://..." are correspond to JSON link.
Prepare own downloader:
public class JsonImageDownloader extends BaseImageDownloader {
public static final String SCHEME_JSON = "json";
public static final String SCHEME_JSON_PREFIX = SCHEME_JSON + "://";
public JsonImageDownloader(Context context) {
super(context);
}
public JsonImageDownloader(Context context, int connectTimeout, int readTimeout) {
super(context, connectTimeout, readTimeout);
}
#Override
public InputStream getStream(String uri, Object extra) throws IOException {
if (uri.startsWith(SCHEME_JSON_PREFIX)) {
String jsonUri = uri.substring(SCHEME_JSON_PREFIX.length());
JSONObject imgJson = APIclient.getImageJson(context, jsonUri); // sync JSON loading
try {
JSONObject responseDataValue = imgJson.getJSONObject("responseData");
JSONArray resultsValue = responseDataValue.getJSONArray("results");
JSONObject result = resultsValue.getJSONObject(0);
String imgUrl = result.getString("url");
return super.getStream(imgUrl, extra);
} catch (JSONException e) {
throw new RuntimeException(e);
}
} else {
return super.getStream(uri, extra);
}
}
}
Set it into configuration
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.imageDownloader(new JsonImageDownloader(context))
....
.build();
Use ImageLoader in getView(...) without using APIclient:
ImageAware imageAware = new ImageViewAware(viewHolder.profileIV, false);
ImageLoader.getInstance().displayImage("json://" + googleUrl, imageAware);
I first make an async task to retrieve JSON data. One of the JSON objects retrieved is an URL.
// This goes on my asynctask to get JSON data from the server
#Override
public void onPostExecute(JSONObject json) {
try {
String title = json.getString("title");
String description = json.getString("description");
String url = json.getString("url");
// Here comes the tricky part
Bitmap bitmap = Bitmap.createBitmap(100,100,Bitmap.Config.ARGB_8888);
new DownloadBitmap(bitmap).execute(url);
myArrayList.add(new DataContainer(title,description,bitmap));
// End of the tricky part
}catch(Exception e) {}
adapter.NotifyDataSetChange();
}
OK as you can see I need a reference of bitmap to send to the downloader class and to the arraylist for the listview's adapter.
The problem is that the downloader class goes like this:
class DownloadBitmap extends AsyncTask<the 3 paramateres goes here>{
Bitmap bitmap;
public DownloadBitmap(Bitmap b) {
//Here I have a reference to the SAME bitmap object I added to the arraylist
bitmap = b;
}
#Override
protected void doInBackground(String... Params) {
// some code.....
// THIS IS THE PROBLEM, it re-initialize.
bitmap = BitmapFactory.decodeStream(inputstream);
}
I wasn't able to find any other way around, and If possible I'd like a solution library independant, my company has strict policy over software dependance.
You can use Universal Image Loader Library.
link