I have done quite some research on this and answers did not help 100%, I did find a way to make it work but it's awful and I don't understand the behavior.
This is what I finally have:
private Bitmap generateBitmapFromTxt() {
mTxtEmojicon.setDrawingCacheEnabled(true);
Bitmap bmap = Bitmap.createBitmap(mTxtEmojicon.getDrawingCache());
mTxtEmojicon.setDrawingCacheEnabled(false);
}
But the bitmap is not refreshing. I also did try:
invalidate()
buildDrawingCache() destroyDrawingCache()
...
I have a custom class that extends from TextView and the bitmap generated did NOT wrap words unless dynamically forcing to calculate it's size. Like this:
private void prepareTxtViewSize() {
int specWidth = View.MeasureSpec.makeMeasureSpec(mEdt.getMeasuredWidth(), View.MeasureSpec.AT_MOST);
int specHeight = View.MeasureSpec.makeMeasureSpec(mEdt.getMeasuredHeight(), View.MeasureSpec.AT_MOST);
mTxt.measure(specWidth, specHeight);
mTxt.layout(0, 0, mTxt.getMeasuredWidth(), mTxt.getMeasuredHeight());
}
My custom TextView class does NOT override onDraw().
Now, my solution, awful solution, was to call prepareTxtViewSize() twice before calling generateBitmapFromTxt() and it works, the bitmap is refreshed and I always get the correct one.
But I am sure there is something I am missing and I am just lucky it works. So does somebody know what am I missing and/or why calling prepareTxtViewSize() twice actually makes the drawingCache refresh?
Related
Please help me understand why getDrawingCache() is returning null when called on my image button view (appcompatimagebutton) from within my ontouchlistner attached to my image button view.
Mainly, in trying to solve this, I have been referring to this question:
Android View.getDrawingCache returns null, only null
which has not solved my problem. I am sure I am overlooking something or not fully understanding something. Please help me figure out what it is.
Notes:
1) it seems to make no difference weather i use ImageButton or AppCompatImageButton (but i am using appcompat because in the actual project I need features it offers that ImageButton does not).
2) I do not see why i should need to call the view's measure and layout methods since the layout is already painted to the screen by the time a user would fire ontouch() but thought i'd include it anyway as many answers i found about seemed to indicate this was required.. it doesn't work if i take them out either.
3) it does not appear to matter if I place the setEnableDrawingCache and buildDrawingCache calls in the onTouch or in the onCreate with the AppCompatImageButton view initialization. In either case, my ontouchlistener call to the image button view's getDrawingCache returns null.
here is the code sample,
Thanks!
public class MainActivity extends AppCompatActivity {
AppCompatImageButton mImageButton;
Bitmap mBitmapDrawingCache;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//create image button
mImageButton = new AppCompatImageButton(this);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(270, 270);
mImageButton.setLayoutParams(layoutParams);
//getDrawingCache() in ontouchlistener returns null regardless if this is set
int mseWidth = View.MeasureSpec.makeMeasureSpec(270, View.MeasureSpec.EXACTLY);
int mseHeight = View.MeasureSpec.makeMeasureSpec(270, View.MeasureSpec.EXACTLY);
mImageButton.measure(mseWidth, mseHeight);
mImageButton.layout(0, 0, 270, 270);
/* this does not work either
int mseWidth = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int mseHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
mImageButton.measure(mseWidth, mseHeight);
mImageButton.layout(0, 0, mImageButton.getMeasuredWidth(), mImageButton.getMeasuredHeight());
*/
//getDrawingCache() in ontouchlistner returns null regardless if either, both or none of these are set
//mImageButton.buildDrawingCache();
mImageButton.setDrawingCacheEnabled(true);
//attach image button view to content view
((FrameLayout) findViewById(android.R.id.content)).addView(mImageButton);
//set touch listener
mImageButton.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
//why is this line returning null instead of a Bitmap ?
mBitmapDrawingCache = Bitmap.createBitmap(v.getDrawingCache());
Log.d("TAG_A", mBitmapDrawingCache.toString());
return false;
}
});
}
}
In order to get bitmap out from your View, the order in which you generate cache is something like this:
mImageButton.setDrawingCacheEnabled(true);
mImageButton.buildDrawingCache();
mBitmapDrawingCache = Bitmap.createBitmap(mImageButton.getDrawingCache());
You need to call this all together inside OnTouchListener or more preferably OnClickListener (if you want to generate bitmap only when clicked).
However, I'm not sure why you are calling all the MeasureSpec stuff... If you are loading the image through setContentView(R.layout.activity_main); then don't be bothered with all manual measuring.
I have searched for hours on the subject of memory leaks and I cant seem to solve my problem. After exactly 9 rotations my app crashes with an OutOfMemory error. I have both Bitmaps and Drawables with Bitmaps in my app. I have tried putting in code that removes callbacks to drawables, I have tried setting all Bitmaps to null as well as manually calling the garbage collector. I had all of this code in the onSaveInstanceState() method since I assume that is called whenever the screen changes and before destruction of the view. None of these solutions worked.
I got some results with the Bitmaps turned to null, however that only added about another 9 screen rotations before another memory leak. Where is my leak? What am I doing wrong? I was under the impression that when a screen is rotated, everything is destroyed and recreated, that must obviously be false.
I dont want to post my code because A. there is a lot of it and B. its close to finishing and so becomes a company secret per se. If there is something you must absolutely see to solve this then I will post it. I think I just need a really good explanation of what is actually going on when the screen is rotated and how to correctly handle bitmaps and drawables. Note: This error does not pop up if I leave the app and then go back in, it only happens when the view is resized upon screen rotation.
I have about 7 bitmaps and 7 drawables, all created at launch and resized when the screen rotates. DOing that several times stops the app.
Some simplified code:
How I set up a Bitmap to a drawable. This one sets to a ClipDrawable as well:
//Full Bar
colorbarMap = BitmapFactory.decodeResource(res, R.drawable.colorbar);
colorbarDraw = new BitmapDrawable(res, colorbarMap);
colorbarDraw.setBounds(barX, barY, barX2, barY2);
colorbarClip = new ClipDrawable(colorbarDraw, Gravity.BOTTOM, ClipDrawable.VERTICAL);
colorbarClip.setBounds(barX, barY, barX2, barY2);
colorbarClip.setLevel(currentLevel);
//Empty Bar
colorbaremptyMap = BitmapFactory.decodeResource(res, R.drawable.colorempty);
colorbaremptyDraw = new BitmapDrawable(res, colorbaremptyMap);
colorbaremptyDraw.setBounds(barX, barY, barX2, barY2);
The above code runs once at the start of the view initializing based on this code:
private void init(){
//System
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
res = getResources();
options = new BitmapFactory.Options();
viewTreeObserver = getViewTreeObserver();
if (viewTreeObserver.isAlive()) {
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
view_width = getWidth();
view_height = getHeight();
afterLayout();
}
});
}
}
The first code snippet runs under the method: afterLayout()
Nothing is done with the bitmaps after this. Each bitmap is initialized with an x and y location based on the view width and height. That location is edited using a rectangle to set its bounds for example a moving object.
If the Bitmap that to be displayed is small enough than use this or you don't need a high res image
bitmapOption = new BitmapFactory.Options();
bitmapOption.inScaled = true;
bitmapOption.inSampleSize = 2;
imageBitmap = BitmapFactory.decodeFile(imgFile.getAbsolutePath(), bitmapOption);
this will reduce the amount of size of bitmap see the bald guys explanation
optimizing bitmap
put your bitmap loading code in try finally block and use this at the end
try {
...
}finally {
if (imageBitmap != null) {
//recycle the bitmap to improve performance and avoid OO Exception
imageBitmap.recycle();
imageBitmap = null;
}
}
This will recycler the bitmap space taken by the image whenever GC is called
Recently, I started using Google ImageWorker class for loading bitmaps on a background thread. This class handles everything, including putting the bitmaps in the ImageView. Google talks about this class (and it's helper classes for Image manipulation and caching) here: http://developer.android.com/training/displaying-bitmaps/process-bitmap.html
The ImageView(s) in my case are parts of list items in a ListView. Here's the interesting part of getView on my ArrayAdapter:
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
View view = convertView == null ? inflater.inflate(R.layout.nodelist_item, null) : convertView;
NodeHolder holder = getHolder(view);
Node node = mList.get(position);
holder.txtTitle.setText(node.Title);
//Setting the rest of the different fields ...
//And finally loading the image
if(node.hasArt())
worker.loadImage(node.ImageID, holder.imgIcon);
}
The entire getView method can be seen here: http://pastebin.com/CJbtVfij
The worker object is a very simple implementation of the ImageWorker:
public class NodeArtWorker extends ImageWorker {
protected int width;
protected int height;
public NodeArtWorker(Context context) {
super(context);
addImageCache(context);
//Code for getting the approximate width and height of the ImageView, in order to scale to save memory.
}
#Override
protected Bitmap processBitmap(Object data) {
Bitmap b = getBitmap(data);
if(b == null) return null;
return Utils.resizeBitmap(b, width, height);
}
protected Bitmap getBitmap(Object data) {
//Downloads the bitmap from a server, and returns it.
}
}
This works very well, and the performance is much better now than before. However, if I change the ImageID on some of the items in the list, and call adapter.notifyDataSetChanged() to rebuild the view (and thereby start loading new bitmaps), I get a RuntimeException:
0 java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap#41adf448
1 at android.graphics.Canvas.throwIfRecycled(Canvas.java:1058)
2 at android.graphics.Canvas.drawBitmap(Canvas.java:1159)
3 at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:440)
4 at android.widget.ImageView.onDraw(ImageView.java:1025)
5 at android.view.View.draw(View.java:14126)
The full stacktrace can be seen in the following pastebin entry, but is probably uninteresting, as it is the typical Android view hierarchy redraw stacktrace: http://pastebin.com/DsWcidqw
I get that the Bitmaps are being recycled, but I don't understand exactly where and what can be done about it. As my code comes directly from Google, and is, as far as I understand, the recommended way of doing this, I am very confused.
On Android, Bitmaps can be recycled to be later re-used (much faster than re-creating a Bitmap).
The Bitmap#recycle()method will flag the Bitmap as recycled.
So if you try to set such a recycled bitmap to an ImageView or draw it on a Canvas, you will end up with this exception:
java.lang.RuntimeException: Canvas: trying to use a recycled bitmap
The official demo that you linked on your question is dealing with recycled Bitmaps.
It uses a dedicated method hasValidBitmap() and checks the Bitmap#isRecycled() value:
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
So you need to do the same. When you're searching on your cache for a Bitmap, before applying it to an ImageView, check if it's recycled or not. If it's not you can directly set it, otherwise, you need to update the Bitmap.
To create a new Bitmap from a recycled Bitmap, you can use the Bitmap::createBitmap(Bitmap) method.
You can find more details on this page.
i think this is not the ImageWorker class bug, this is
com.mobeta.android.dslv.DragSortListView.dispatchDraw(DragSortListView.java:797)
issue. so in order to see my answer is correct or not just remove that library and see if it works or not. after that i think the best solutions are:
1- not use that library.
2- report issue on github.
3- solve it by your own and debugge it.
4- or you can set null for image holder bitmap
holder.imgIcon.setBitmapDrawble(null);
and then call worker.loadImage.
I have a custom view which I quickly learned to not do object allocations in and moved all my Paint allocations into a different method, fine.
I need to use a StaticLayout however for some text that had some spannable 'stuff' applied.
layoutBpm = new StaticLayout(bpmValue,
textPaintBPM, arcRadius, Layout.Alignment.ALIGN_NORMAL, 0, 1,
false);
layoutBpm.draw(canvas);
This seemed to make sense to me to have in onDraw but I am of course getting warned to avoid this. It is important to me that I do everything I can to avoid any performance issues so I am trying to do this properly.
I can't seem to find any documentation I can understand that explains what some of the parameters to StaticLayout actually do (float spacingmult? float spacingadd?), but the third one there I think is maximum width for the StaticLayout text which I can only get from onMeasure as it relates to my canvas size. This leaves me wanting to put the assignment in onMeasure or onDraw, neither of which it seems I am meant to do. Is it okay to put StaticLayout assignment in onDraw or is there a better way to do this?
Hope this makes sense, I am very new to this. Thank you for any help.
(edit: I assume putting this assignment in a method and calling from onDraw/onMeasure is just being silly and will stop Eclipse warning me but wont actually help?)
The reason for the warning is because, very often, you do not need to recreate an object multiple times in a draw operation. onDraw() could be called hundreds of times compared to other methods in a View. Most of the time, the objects being recreated are being recreated with the exact parameters. Other times, it's simply less overhead to change an object's state than it is to create a new object.
In the case of a StaticLayout, you only need to create a new one when the text changes or when you adjust the padding, spacing, or maxwidth. If the text changes often, then you may want to consider DynamicLayout which will remeasure itself every time. Remeasuring costs more overhead than creating a new object, but there's no way it's happening more often than onDraw() calls.
If the text doesn't change often and you absolutely MUST use a StaticLayout, then you can get away with something like this structure.
StaticLayout myLayout;
String textSource = defaultSource;
Paint textPaint = defaultPaint;
int textWidth = defaultWidth;
public CustomView(Context ctx) {
super(ctx);
createLayout();
}
public void setText(String text) {
textSource = text;
createLayout();
}
public void setWidth(int width) {
textWidth = width;
createLayout();
}
#Override
public void onDraw(Canvas canvas) {
myLayout.draw(canvas);
}
private void createLayout() {
myLayout = new StaticLayout(textSource, textPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 0, 1, false);
}
Basically, you only create a new layout when something changes. Else you just reuse the last object you created.
EDIT:
One way to skip a measure pass is to call View#measure on the newly resized view itself. So your createLayout() method would be something like this.
private void createLayout() {
myLayout = new StaticLayout(textSource, textPaint, textWidth, Layout.Alignment.ALIGN_NORMAL, 0, 1, false);
int textWidthSpec = MeasureSpec.makeMeasureSpec(maxLayoutWidth, MeasureSpec.AT_MOST);
int textHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.AT_MOST);
myLayout.measure(textWidthSpec, textHeightSpec);
forceLayout();
}
Basically what this does is tells the layout the maximum the View can be. It will measure itself and it's children. The forceLayout() will be invoked on the parent (your custom view) which will re-layout the other contents based on the new measurements.
I should note that I have never done this with a StaticLayout so I have no idea what will happen. It seems like this type of measurement might already be handled when the View is created, but maybe not.
I hope this helps.
I'm calling getDrawingCache in the onDraw function. The problem is that it contains the changes to the canvas only in the first time, and after that, it's not updated at all with the new changes. Here's my code:
paintAction.draw(canvas);
if (paintAction.isPermanentChange())
{
Bitmap partialBitmap=getDrawingCache();
int numColored=0;
for (int index1=0;index1<partialBitmap.getWidth();index1++)
{
for (int index2=0;index2<partialBitmap.getHeight();index2++)
{
if (partialBitmap.getPixel(index1,index2)==0xFF000000)
numColored++;
}
}
Log.i("PaintDroid","Bitmap pixels: " + numColored);
int areaWidth=partialBitmap.getWidth()-SCROLLBAR_SIZE;
int areaHeight=partialBitmap.getHeight()-SCROLLBAR_SIZE;
int[] pixels=new int[areaWidth*areaHeight];
partialBitmap.getPixels(pixels,0,areaWidth,0,0,areaWidth,
areaHeight);
numColored=0;
for (int index=0;index<pixels.length;index++)
if (pixels[index]==0xFF000000) numColored++;
Log.i("PaintDroid","Pixels: " + numColored);
(setDrawingCache(true) is called when the view is created, because if I call it from onDraw, getDrawingCache will return null.)
As can be seen, I'm counting the number of black pixels, both by traversing the bitmap and getting the values in an array, and as I said, I get the number I expected for in the first time, but after that, it was supposed to increase, yet doesn't change at all!
Does anybody have an idea what's wrong?
Thanks.
I solved it. The problem was that I called setDrawingCacheEnabled(true) before the last draw operation on the canvas in onDraw. It must be called after you finished drawing, otherwise you won't get correct results.
Just to clarify, the problem here is that the View's drawing cache was not invalidated before the second time you call getDrawingCache().
In order for the drawing cache to be refreshed it must be invalidated and the order of call to the methods should be as follows:
public Bitmap renderView(View view) {
view.setDrawingCacheEnabled(true)
Bitmap bitmap = view.getDrawingCache();
view.setDrawingCacheEnabled(false)
return bitmap;
}