I have a custom viewgroup (that contains Picasso-loaded images) that can be reused in two places:
Displayed to the user in the application (on the UI thread)
Drawn to a canvas and saved as a .jpeg (on the background thread)
My code for drawing to the canvas looks like this:
int measureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
view.measure(measureSpec, measureSpec);
Bitmap bitmap =
Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.draw(canvas);
The problem is that there is no time for the image to load before I draw the view to the canvas. I'm trying to avoid coupling as much as possible here so I'd prefer not to add a Picasso callback because the class that's doing the drawing doesn't know anything about the view that it's drawing.
I'm currently working around the issue by changing the image loading code to .get() rather than .load() and then using imageView.setImageBitmap(). Unfortunately, this adds a ton of complexity to the view and I really don't like it.
What I'd like to do is pass an option to Picasso's RequestCreator that the request should be executed synchronously on the current thread (and throw an exception if it's the main thread). I wonder if this is too much of an edge case for support to be built directly into Picasso? Or is it already in the API and I'm oblivious to it?
Here is my solution:
/**
* Loads the request into an imageview.
* If called from a background thread, the request will be performed synchronously.
* #param requestCreator A request creator
* #param imageView The target imageview
* #param callback a Picasso callback
*/
public static void into(RequestCreator requestCreator, ImageView imageView, Callback callback) {
boolean mainThread = Looper.myLooper() == Looper.getMainLooper();
if (mainThread) {
requestCreator.into(imageView, callback);
} else {
try {
Bitmap bitmap = requestCreator.get();
imageView.setImageBitmap(bitmap);
if (callback != null) {
callback.onSuccess();
}
} catch (IOException e) {
if (callback != null) {
callback.onError();
}
}
}
}
Perfect answer by Jacob Tabak
Here is small addition that handles the case if you load image into Target. I haven't found a way to get the origin of image to pass appropriate LoadedFrom argument.
public static void into(RequestCreator requestCreator, Drawable placeHolder, Drawable errorDrawable, Target target) {
boolean mainThread = Looper.myLooper() == Looper.getMainLooper();
if (mainThread) {
requestCreator.into(target);
} else {
try {
target.onBitmapFailed(placeHolder);
Bitmap bitmap = requestCreator.get();
target.onBitmapLoaded(bitmap, Picasso.LoadedFrom.MEMORY);
} catch (IOException e) {
target.onBitmapFailed(errorDrawable);
}
}
}
Related
I am developing an Android app using Kotlin.
I need to convert a layout (that is actually a report of the user's input data, some views converted to bitmap in imageviews and extra text) and send it by email as .pdf when the user clicks on a button inside the fragment (that is the previsualisation of the future PDF) without saving it.
I've read and watched hours of tutorials but none is helping (deprecated, old java or saving the file)
I guess I need a function that converts the desired view to a bitmap image, scales it to A4 size format, sets that scaled image to pdf file and returns it so I can then put it as an attachment to mail Intent,not forgetting I am inside a fragment.
I will be updating the post.
Thank you
EDIT : I already know how to convert a layout to bitmap and put it inside an image view (I can share if someone needs). So I only need a function that takes the bitmap as an argument and returns .pdf (or .jpg) and an other one that intents as attachment that returned pdf to send email
What I gather, you need to create a screenshot of sorts of your view.
This is something I did a while back so I have some java code, not yet converted to Kotlin, but AS can take care of that.
You can utilise a class PixelCopy, unfortunately it is Android O +.
This is a simplified version of what you can do:
int[] viewLocationInWindow = new int[2];
view.getLocationInWindow(viewLocationInWindow);
int x = viewLocationInWindow[0];
int y = viewLocationInWindow[1];
int width = view.getWidth();
int height = view.getHeight();
Rect rectScope = new Rect(x, y, x + width, y + height);
Bitmap screenshotBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Handler handler = new Handler();
PixelCopy.request(getWindow(), rectScope, screenshotBitmap, copyResult -> {
if (copyResult == PixelCopy.SUCCESS) {
listener.onScreenshotSuccess(screenshotBitmap);
} else {
listener.onScreenshotError();
}
handler.removeCallbacksAndMessages(null);
}, handler);
The view that is being referenced here should be your containing view. In my case, I had to take a screenshot of the entire view(the entire screen), so instead I did:
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
int height = Resources.getSystem().getDisplayMetrics().heightPixels;
Then just implement the listener
public interface OnScreenshotTakenListener {
void onScreenshotSuccess(Bitmap bitmap);
void onScreenshotError();
}
Inside onScreenshotSuccess you could modify the bitmap and do all of your other requirements.
fun getDetailImage(){
val image=getBitmapFromView(llParent)
ivShareImage.setImageBitmap(image)
}
private fun getBitmapFromView(view: View): Bitmap? {
//Define a bitmap with the same size as the view
val returnedBitmap=Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
//Bind a canvas to it
val canvas=Canvas(returnedBitmap)
//Get the view's background
val bgDrawable=view.background
if (bgDrawable != null) {
//has background drawable, then draw it on the canvas
bgDrawable.draw(canvas)
} else {
//does not have background drawable, then draw white background on the canvas
canvas.drawColor(Color.WHITE)
}
// draw the view on the canvas
view.draw(canvas)
//return the bitmap
return returnedBitmap
}
fun getShareImage(){
getDetailImage()
val mDrawable: Drawable=ivShareImage.getDrawable()
val mBitmap=(mDrawable as BitmapDrawable).bitmap
val path=MediaStore.Images.Media.insertImage(
appCompatActivity.getContentResolver(),
mBitmap,
"image-name",
null
)
val uri: Uri=Uri.parse(path)
val shareIntent=Intent()
shareIntent.action=Intent.ACTION_SEND
shareIntent.putExtra(Intent.EXTRA_STREAM, uri)
shareIntent.type="image/*"
appCompatActivity.startActivity(Intent.createChooser(shareIntent, "Share Image"))
}
The problem is as it is stated in question title. In fact I want to load images which I have their url in my records into RecyclerView and at the same time persist downloaded image to database. I am using realm.io and Glide and my RecyclerViewAdapter is as below:
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final ProductModel obj = getData().get(position);
holder.data = obj;
holder.title.setText(obj.getTitle());
if (obj.getImage() == null) {
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(new GlideDrawableImageViewTarget(holder.icon) {
#Override
protected void setResource(GlideDrawable resource) {
// this.getView().setImageDrawable(resource); is about to be called
super.setResource(resource);
// here you can be sure it's already set
((ProductsFragment) mFragment).saveImage(obj, resource);
}
});
} else {
byte[] data = obj.getImage();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length, options);
holder.icon.setImageBitmap(bmp);
}
}
class MyViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public TextView title;
public ImageView icon;
public ProductModel data;
public MyViewHolder(View view) {
super(view);
title = (TextView) view.findViewById(R.id.textView);
icon = (ImageView) view.findViewById(R.id.imageView);
view.setOnClickListener(this);
}
#Override
public void onClick(View view) {
if (data.getImage() != null)
activity.startActivity(new Intent(activity, ProductActivity.class).putExtra("id", data.getId()));
}
}
And here's how I save images:
public void saveImage(final ProductModel data, Drawable drw) {
new AsyncImagePersister(data).execute(drw);
}
private class AsyncImagePersister extends AsyncTask<Drawable, Void, byte[]> {
private final ProductModel data;
AsyncImagePersister(ProductModel data) {
this.data = data;
}
#Override
protected byte[] doInBackground(Drawable... drawables) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Bitmap bmp = drawableToBitmap(drawables[0]);
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
return stream.toByteArray();
}
#Override
protected void onPostExecute(final byte[] bytes) {
super.onPostExecute(bytes);
realm.executeTransaction(new Realm.Transaction() {
#Override
public void execute(Realm realm) {
data.setImage(bytes);
}
});
}
public Bitmap drawableToBitmap (Drawable drawable) {
Bitmap bitmap = null;
if (drawable instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
if(bitmapDrawable.getBitmap() != null) {
return bitmapDrawable.getBitmap();
}
}
if(drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}
However, when loading the images for the first time from internet (using Glide) it shows wrong pictures for different places and on the other hand after it fetches all images, the saved images to realm are in their correct place.
So what am I doing wrong? Please help. Thanks.
The misplaced images is due to views are being recycled, So the loaded bitmap does not necessarily belong to the current position, And another thing to consider is that using AsyncTask inside a RecyclerView won't play nice and will cause lags in your UI, And for the final point, saving the byte[] array in your model might end up to a OOM exception!
If you want do some long running task inside your adapter, think of using a Service, IntentService or ThreadHandler, so you will be sending tasks one by one and the'd be queued and executed one by one.
About having offline access to images:
One option could be using Glide.diskCacheStrategy method and use DiskCacheStrategy.ALL so the original image size will be cached and you can use later in offline mode
Second option is to use Picasso instead of Glide!
so that you can use a custom RequestHandler and download the image and save it somewhere so you can access it later, consider memory management is all on your side and you should handle it!
here's a hint for your second option:
create class which extends from RequestHandler:
CustomReqHandler : RequestHandler() {
Then you should override two methods: canHandleRequest(), load()
in canHandleRequest() you should determine whether you want to handle current request or not, so define a custom scheme for these requests and check if this is one of them like:
val scheme:String = data.uri.scheme
the 2nd method is load() which is executed on a background thread and returns a Result object, download the image, save it somewhere, and return Result object!
You don't actually have to save the loaded images in your database when you're using Glide for this purpose. Glide caches the images loaded once automatically and efficiently. The caching is a complex system and if you want to read more about the caching with Glide, you might have a look here.
Now, about the images loaded in wrong place - this should not happen. I found no serious bug in your onBindViewHolder but hence as I suggest you not to save the images locally you might consider loading the images simply with Glide like this.
Glide
.with(context)
.load(obj.getImageUrl())
.fitCenter()
.placeholder(R.drawable.bronteel_logo)
.into(holder.icon);
Just you need to make sure if the obj.getImageUrl() is returning proper url.
I am creating an app that has a file directory that contains pictures, videos, pdfs, etc. I am currently working on displaying thumbnails for pictures. I am using the RecyclerView and ViewHolder to display list items that each represent a photo item. I then use an AsyncTask to download the Bitmaps one at a time and store them in a Hashmap. Everything works fine except when I scroll down in a large list of photos very quickly. The placeholder image for random items at the bottom of the list are replaced with thumbnails that have already been loaded at the top of the list. When the background thread reaches the image at the bottom, then the correct image replaces the wrong image. After all the thumbnails are loaded then everything works as intended.
Here is the code for the AsyncTask. I think the problem has to do with the position integer I am passing into the constructor. The position variable represents the position in the Adapter. Maybe there is a way to make sure the image is loading the placeholder image I have in onPreExecute()?
/**
* AsyncTask to download the thumbnails in the RecyclerView list.
*/
private class CreateThumbnail extends AsyncTask<Void, Void, android.graphics.Bitmap> {
// ******
// FIELDS
// ******
private ImageView mPreviewInstance;
private File mFile;
private RelativeLayout.LayoutParams lp;
private FileHolder mFileHolder;
private int mPosition;
private UUID mId;
private FolderFile mFolderFile;
// ***********
// Constructor
// ***********
/**
* #param holder - ViewHolder passed for the list item.
* #param position - position in the Adapter.
* #param id - id for list item stored in database.
*/
private CreateThumbnail(FileHolder holder, int position, UUID id) {
mPosition = position;
mFileHolder = holder;
mPreviewInstance = mFileHolder.mImagePreview;
mId = id;
mFolderFile = FolderFileLab.get(getContext()).getFolderFile(mId);
}
// ****************
// OVERRIDE METHODS
// ****************
#Override
protected void onPreExecute() {
}
#Override
protected Bitmap doInBackground(Void... params) {
FolderFileLab lab = FolderFileLab.get(getContext());
if (!lab.getCurrentMap().containsKey(mId)) {
mFile = lab.getPhotoFile(mFolderFile);
// Create Bitmap (Biggest use of memory and reason this background thread exists)
Bitmap bitmap = PictureUtils.getScaledBitmap(
mFile.getPath(), getActivity());
// Scales Bitmap down for thumbnail.
Bitmap scaledBitmap;
if (bitmap.getWidth() >= bitmap.getHeight()) {
scaledBitmap = Bitmap.createBitmap(bitmap, bitmap.getWidth() / 2
- bitmap.getHeight() / 2,
0, bitmap.getHeight(), bitmap.getHeight());
} else {
scaledBitmap = Bitmap.createBitmap(bitmap, 0, bitmap.getHeight() / 2
- bitmap.getWidth() / 2,
bitmap.getWidth(), bitmap.getWidth());
}
// Cache bitmap
HashMap<UUID, Bitmap> map = lab.getCurrentMap();
map.put(mId, scaledBitmap);
lab.updateMap(map);
return scaledBitmap;
} else {
// If Hashmap already contains the id get the Bitmap.
return lab.getCurrentMap().get(mId);
}
}
#Override
protected void onPostExecute(Bitmap bitmap) {
// Checks to see if the bitmap is still displayed in the list. If not nothing happens.
// If it is then it displays the image.
if (mPreviewInstance.getVisibility() == View.VISIBLE && mFileHolder.getPosition()
== mPosition && bitmap != null) {
// Formatting for thumbnail
lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout
.LayoutParams.WRAP_CONTENT);
lp.setMargins(7, 7, 7, 0);
// Displaying thumbnail on UI thread.
mPreviewInstance.setLayoutParams(lp);
mPreviewInstance.setBackground(null);
mPreviewInstance.setImageBitmap(bitmap);
}
}
}
Here is some of the relevant Adapter code where the AsyncTask is started.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
createThumbnail = new CreateThumbnail(holder, position,file.getId());
createThumbnail.execute();
}
}
Figured it out!
I added code to change the photo to the placeholder image after every bind. This is what I changed in my adapter.
#Override
public void onBindViewHolder(FileHolder holder, int position) {
FolderFile file = mFiles.get(position);
holder.bindFile(file);
if (file.isPhoto()) {
Drawable placeholder = getResources().getDrawable(R.mipmap.picture_blu);
holder.mImagePreview.setBackground(placeholder);
holder.mImagePreview.setImageBitmap(null);
createThumbnail = new CreateThumbnail(holder, position, file.getId());
createThumbnail.execute();
}
}
Your views are recycled, so by the time the async task finishes, the imageView has been reused and has a new image assigned to it.
What you can do is assign to the imageView a tag that is the file name of the file you are trying to load into it. You keep track of that same file name in the async task. Then in your AsyncTask, in onPostExecute, you check if the tag the imageView has is the same file name that you just loaded. If it is, you go ahead and set the bitmap to the imageView. If it is not, then the view has been recycled and you simply drop the Bitmap you just created; another async task will be loading the right bitmap.
I use CrossWalk to display webpage, it works well. I have a feature to capture the whole content as bitmap to save into SDCard, but I cannot found a solution. Using TextureView can only capture a screen size bitmap, anyone has the same issue? BTW, I can capture the whole content using WebView.draw().
Here's some pseudo code that may help. You can get the content height and width from the webview and then you can leverage webview.draw()...so something like the following.
Bitmap bitmap = Bitmap.createBitmap( webView.getWidth(), webView.getContentHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
webView.draw(canvas);
Then you can output your bitmap file using bitmap.compress and output it to a file output stream
//fos is a FileOutputStream, you can use something else if needed.
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
The only issue I foresee with this is that your bitmap may clip the content horizontally since WebView doesn't have a method to get the content width. Otherwise I think this should work.
I never using crosswalk before, but I'm guessing the Crosswalk WebView is also descendant from android.view.View. If so, then you could use android view drawing cache. It look like this
yourWebView.setDrawingCacheEnabled(true);
Bitmap bmp = yourWebView.getDrawingCache();
Then as Paul said, save it through FileOutputStream
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
You can't capture XWalkView image by standard ways as described in answers above because Crosswalk XWalkView works with hardware layer (i.e. SurfaceView or TextureView). But you can use getBitmap() method of TextureView class to get it.
In short, you should enable using of TextureView in Crosswalk before XWalkView will be initialized (in Application subclass for example) with:
XWalkPreferences.setValue(XWalkPreferences.ANIMATABLE_XWALK_VIEW, true);
And find TextureView in views tree to call getBitmap() on it.
Something like that:
/**
* Returns TextureView which is used in XWalkView
*
* #param group
* #return
*/
private TextureView findXWalkTextureView(ViewGroup group) {
int childCount = group.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = group.getChildAt(i);
if (child instanceof TextureView) {
String parentClassName = child.getParent().getClass().toString();
boolean isRightKindOfParent = (parentClassName.contains("XWalk"));
if (isRightKindOfParent) {
return (TextureView) child;
}
} else if (child instanceof ViewGroup) {
TextureView textureView = findXWalkTextureView((ViewGroup) child);
if (textureView != null) {
return textureView;
}
}
}
return null;
}
/**
* Example of capturing image from XWalkView based on TextureView
*
* #return
*/
public Bitmap captureImage() {
if (mXWalkView != null) {
Bitmap bitmap = null;
boolean isCrosswalk = false;
try {
Class.forName("org.xwalk.core.XWalkView");
isCrosswalk = true;
} catch (Exception e) {
e.printStackTrace();
}
if (isCrosswalk) {
try {
TextureView textureView = findXWalkTextureView(mXWalkView);
bitmap = textureView.getBitmap();
} catch (Exception e) {
e.printStackTrace();
}
} else {
bitmap = Bitmap.createBitmap(mXWalkView.getWidth(), mXWalkView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bitmap);
mXWalkView.draw(c);
}
return bitmap;
} else {
return null;
}
}
Check this code: https://github.com/gitawego/cordova-screenshot/blob/master/src/android/Screenshot.java for more details.
I'm new to Android and let me first tell you what I'm trying to achieve. Using sockets, I'm sending pictures from my computer's webcam every 50ms. In Android app, I've created my display that extends View. I've added this view to FrameLayout. Parallel thread is receiving images from server app (desktop) and refreshing my display.
On this image I want to display some accelerometer data that refreshes every... Well it's set to SENSOR_DELAY_FASTEST. So I also created another display that extends View, and also I add it to another FrameLayout. Now I set my main.xml to overlap those FrameLayouts..
I'm getting my image from desktop application, I'm drawing accelerometer data, and It's overlapped, but the issue is.. It's flickering. Can anyone help? Or suggest something.. As I've pointed out, I'm new with Android.
Thanks..
This is a simple override that draws an image. - And and the method that calls for a redraw.
#Override
protected void onDraw(Canvas canvas){
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
paint.setARGB(10, 255, 255, 255);
if(Core.pic != null) {
canvas.drawBitmap(Core.pic, 0, 0, paint);
}
}
Here is a different class that calls for redraw when new image is available:
protected static volatile Bitmap pic;
public static void refreshDisplay(Bitmap img){
pic = img;
if(cameraDisplay != null) {
try{
cameraDisplay.invalidate();
}
catch(Exception e){
e.printStackTrace();
}
}
}
And here is a threaded class that ready port every 50ms:
while(running){
opt.inDither = true;
opt.inPreferredConfig = Bitmap.Config.ARGB_8888;
if(in != null){
byte[] recieve = new byte[7000];
try {
in.read(recieve, 0, 7000);
Core.pic = BitmapFactory.decodeByteArray(recieve, 0, 7000, opt);
} catch (IOException e) {}
}
try {
sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
This alone works fine. When I overlap this views then it flickers. In the similar way I'm reading accelerometer data and draw it:
public void onAccelerationChanged(float x, float y, float z) {
if(getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
velocityBars.DrawVelocity(x, -z);
}
else if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE){
velocityBars.DrawVelocity(y, -z);
}
}
velocityBars is my variable that is type of my custom View. This method DrawVelocity invokes the invalidate() method. This forces redraw.