after one year of using SO to solve my questions, I came to a problem for which I cannot find solution.
I am using multiple threads to calculate bitmap. When new row of bitmap is calculated, I update custom view and call invalidate(). But when I rotate my phone, bitmap stops updating. After another screen rotation View is updated only once, and part of bitmap that was calculated in the meantime is also visible.
part of onCreate method:
view = (MyView) findViewById(R.id.view1);
if (savedInstanceState != null)
{
bitmap = savedInstanceState.getParcelable("bitmap");
view.bitmap = bitmap;
}
else
{
bitmap = Bitmap.createBitmap(400, 500, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(Color.GRAY);
view.bitmap = bitmap;
}
I use handler to update my view:
public void handleMessage(final Message msg) {
Log.i("Recieved", "redraw signal");
view.bitmap = bitmap;
view.invalidate();
}
In my view, i use:
Bitmap bitmap;
Rect rect;
//...constructors
#Override
protected void onSizeChanged (int w, int h, int oldw, int oldh)
{
rect = new Rect(0, 0, getWidth(), getHeight());
}
#Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
if (bitmap!=null)
{
canvas.drawBitmap(bitmap, null, rect, null);
}
Log.i("onDraw", "draw");
}
Solution
So far, I have found one solution. If I add invalidate() to the end of onDraw, bitmap is corectly updated(row by row) after screen orientation change. I don't think this is very "clean" solution, so I would like to understand why is invalidate() called from handler ignored after config change?
Thanks!
I found a solution.
Problem was in Handler, which was NOT static. I ignored "Leaks might occur" warning because I wasn't posting long delayed messages. Big mistake, because after configuration change Handler was somehow unable to reach View.
Here is solution:
static class MyHandler extends Handler {
private final WeakReference<MainActivity> mTarget;
public MyHandler(MainActivity target) {
mTarget = new WeakReference<MainActivity>(target);
}
#Override
public void handleMessage(Message msg) {
MainActivity target = mTarget.get();
...other stuff...
target.view.invalidate(); // now this works after configuration change
}
}
Bottom line: use static handler, as explained here
Related
I would like to manipulate canvas after loading a drawable from the resources on a background thread. Here is the code (the relevant part):
public class Test extends ReplacementSpan {
#Override
public void draw(
final Canvas canvas,
final CharSequence text,
final int start,
final int end,
final float x,
final int top,
final int y,
final int bottom,
final Paint paint) {
new Thread(new Runnable() {
#Override
public void run() {
drawable = context.getResources().getDrawable(resourceId);
drawable.setBounds(0, 0, size, size);
canvas.save();
int transY = bottom - size - paint.getFontMetricsInt().descent;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}).start();
}
}
This code leads to a native crash without a stack trace:
Fatal signal 11 (SIGSEGV), code 1, fault addr 0xf8 in tid 10735
I've noticed that the crash appears when trying to perform any kind of operation on canvas. Putting the code that operates on canvas into new Handler(Looper.getMainLooper()).post(new Runnable() {...}); doesn't resolve the issue.
The code works fine if done on the main thread, but it leads to noticeable lags when scrolling.
Is there a way to load the drawable on a background thread and then draw it on canvas?
Is there a way to load the drawable on a background thread and then draw it on canvas?
Yes. Load the drawable outside of draw(), on a background thread if you so choose.
Neither resourceId nor size are parameters to draw(). Instead, you are providing those by some other means (constructor, setters, etc.). Obtain and configure your drawable at that point, not in draw(). This limits your draw() work to only the statements that involve the Canvas.
BTW, I'm not 100% certain, but you may need to call mutate() on that Drawable before calling setBounds(), if the same resource is used elsewhere in your app.
try using runOnMainThread :
public class Test extends ReplacementSpan {
#Override
public void draw(
final Canvas canvas,
final CharSequence text,
final int start,
final int end,
final float x,
final int top,
final int y,
final int bottom,
final Paint paint) {
new Thread(new Runnable() {
#Override
public void run() {
drawable = context.getResources().getDrawable(resourceId);
drawable.setBounds(0, 0, size, size);
runOnUiThread(new Runnable() {
#Override
public void run() {
canvas.save();
int transY = bottom - size - paint.getFontMetricsInt().descent;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
});
}
}).start();
}
}
seems I have a problem to understand how offset works. When I use the code below it create me 5 arrowheads, but why? I create one and the use twice offset. So i thought i get 3 arrow heads.
public class legende extends View {
private Paint paint;
private Path arrowPath;
public legende(Context context) {
super(context);
init();
}
public void init(){
paint = new Paint();
arrowPath = new Path();
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = 1500;
int height = 5000;
setMeasuredDimension(width, height);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
arrowPath.moveTo(420, 300);
arrowPath.lineTo(430, 300);
arrowPath.lineTo(420, 310);
arrowPath.lineTo(410, 300);
arrowPath.close();
canvas.drawPath(arrowPath, paint);
arrowPath.offset(0,200);
canvas.drawPath(arrowPath, paint);
arrowPath.offset(0,380);
canvas.drawPath(arrowPath, paint);
}
}
EDIT
Looks like, the problem is, that onDraw is called twice, but i don't know why.
This is how I use it in my fragment activity:
hScrollView.addView(new legende(getActivity()));
scrollView.addView(hScrollView);
relativeLayout.addView(scrollView);
Looks like you´ve got a non thread safe implementation of the onDraw method.
This guy worked it out by using a thread safe implementation: Custom View not drawing properly
Maybe you should copy the path to a local object inside the onDraw and then alter and paint.
I created two class - one is a class which extends SurfaceView and has a method onDraw(), inside which I'm drawing an image on my canvas:
public class Board extends SurfaceView{
public BitmapFactory myBitmapFactory = new BitmapFactory();
public Bitmap myBitmap = new Bitmap;
public Board(Context context) {
super(context);
setFocusable(true);
}
//here i implement all the abstract classes, which are not important now
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
myBitmap = Bitmap.createScaledBitmap(myBitmapFactory.decodeResource(getResources(), R.drawable.image), 90, 90, false);
Paint paint = new Paint();
canvas.drawBitmap(myBitmap, 20, 20, paint);
}
public void setBitmap(Bitmap b){
this.myBitmap = b;
}
}
And I have also my activity class, where after a button click which runs the method changeImage. I want two change this image which i put on my canvas
public class MyActivity extends Activity {
public Context context = this;
public Board board;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start_game);
}
public void changeImage(View view){
Bitmap tmpBitmap = new Bitmap();
BitmapFactory tmpBitmapFactory = new BitmapFactory();
tmpBitmap = Bitmap.createScaledBitmap(tmpBitmapFactory.decodeResource(getResources(), R.drawable.image2), 130, 130, false);
board.setBitmap(tmpBitmap);
board.invalidate();
}
}
But after calling method changeImage nothing is changing - the image is the same as at the beginning. I tried to use invalidate() also inside setBitmap() method, but it also doesn't work. Is this because every time when I call invalidate() I'm creating a new Bitmap or I'm doing something else wrong?
---EDIT---
So ofc, wrong was that:
myBitmap = Bitmap.createScaledBitmap(myBitmapFactory.decodeResource(getResources(), R.drawable.image), 90, 90, false);
was setting all the time the same image. I changed it, but still invalidate() thosent work. I mean the image is not changing. I was looking for the result in other topics, but I did't find any solution, now my classes looks like this:
public class Board extends SurfaceView{
public BitmapFactory myBitmapFactory = new BitmapFactory();
public Bitmap myBitmap = Bitmap.createScaledBitmap(myBitmapFactory.decodeResource(getResources(), R.drawable.image), size, size, false);
public int tmp = 0;
public Paint paint = new Paint();
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
canvas.drawBitmap(myBitmap, 50, 50, paint);
}
public void setBitmaps(Bitmap one){
this.paint = new Paint();
this.myBitmap = one;
this.tmp=4;
this.invalidate();
}
And the method changeImage() inside my activity:
public void changeImage(View view){
Bitmap tmpBitmap1 = Bitmap.createScaledBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.blue), 230, 230, false);
myGameBoard.setBitmaps(tmpBitmap1);
}
I tried to use: invalidate(), postInvalidate(), but it doesn't work and i hae now idea what am I doing wrong
You update myBitmap in onDraw():
myBitmap = Bitmap.createScaledBitmap(myBitmapFactory.decodeResource(getResources(), R.drawable.image), 90, 90, false);
So your updated image is still not used as you directly override it with another image...
(old answer, which is now obsolete due to fixed question)
There seems something else off in your example code. In setBitmap() you store the new bitmap in pawnBitmap. In the onDraw() method, you do not use pawnBitmap. So it is expected that nothing changes, as you do not use the changed values...
(old answer, which is now obsolete due to change in question)
You are trying to call a static class method with:
Board.setBitmap(tmpBitmap);
Board.invalidate();
(I am wondering why you did not get compile errors... Or did you forgot to mention them?)
As Board is the name of your class. Instead you need to call the instance method.
Board board = ...
board.setBitmap(tmpBitmap);
board.invalidate();
This obviously requires the board instance, which you need to available in your activity class.
Note: if you are calling invalidate() from a non-UI thread, you need to use postInvalidate().
I'm trying to apply a visual effect to a viewgroup. My idea is to grab a bitmap of the viewgroup, shrink it down, expand it back up, and draw it over the viewgroup to give it a blocky, low quality effect.
I've got most of the way there using this code:
public class Blocker {
private static final float RESAMPLE_QUALITY = 0.66f; // less than 1, lower = worse quality
public static void block(Canvas canvas, Bitmap bitmap_old) {
block(canvas, bitmap_old, RESAMPLE_QUALITY);
}
public static void block(Canvas canvas, Bitmap bitmap_old, float quality) {
Bitmap bitmap_new = Bitmap.createScaledBitmap(bitmap_old, Math.round(bitmap_old.getWidth() * RESAMPLE_QUALITY), Math.round(bitmap_old.getHeight() * RESAMPLE_QUALITY), true);
Rect from = new Rect(0, 0, bitmap_new.getWidth(), bitmap_new.getHeight());
RectF to = new RectF(0, 0, bitmap_old.getWidth(), bitmap_old.getHeight());
canvas.drawBitmap(bitmap_new, from, to, null);
}
}
I simply pass in the canvas to draw on and a bitmap of what needs to be scaled down+up and it works well.
public class BlockedLinearLayout extends LinearLayout {
private static final String TAG = BlockedLinearLayout.class.getSimpleName();
public BlockedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
applyCustomAttributes(context, attrs);
setup();
}
public BlockedLinearLayout(Context context) {
super(context);
setup();
}
private void setup() {
this.setDrawingCacheEnabled(true);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
// block(canvas); If I call this here, it works but no updates
}
#Override
public void onDraw(Canvas canvas) {
// block(canvas); If I call this here, draws behind children, still no updates
}
private void block(Canvas canvas) {
Blocker.block(canvas, this.getDrawingCache());
}
}
The problem I'm having is in my viewgroup. If I run the block method in the viewgroup's draw, it draws over everything but doesn't ever update when child views change. I've traced function calls with Log, and the draw method seems to be running, but nothing changes.
I've also tried implementing this in onDraw. This draws the bitmap behind all the children views, and again they aren't updating.
Can anyone explain how I would go about fixing this?
Try this:
#Override
protected void dispatchDraw(Canvas canvas) {
// call block() here if you want to draw behind children
super.dispatchDraw(canvas);
// call block() here if you want to draw over children
}
And call destroyDrawingCache() and then, buildDrawingCache() each time you change a child.
Draw() method will work well for you.
I'm now trying to make a count time view in a circle shape, when time is passing, the view will reducing his angle. It's used to cover profile photo(a circle shape photo).
Starting with Android API 23, you can use onDrawForeground(Canvas) to draw on top of child views: https://developer.android.com/reference/android/view/View#onDrawForeground(android.graphics.Canvas)
Unlike onDraw() though, be sure to call through to the super class:
#Override
public void onDrawForeground(final Canvas canvas) {
super.onDrawForeground(canvas);
// Your code here
}
I have a really simple task to do which is to draw a background image in a custom View. I create a bitmap and scale it to fit the width and height of the view. This makes the app way slower, like half as fast (I print out the value of time every 10 milliseconds to measure the speed of performance).
This is the code:
public class GView extends View {
int w, h;
Bitmap bg;
int time = 0;
boolean created = false;
public GView(Context context) {
super(context);
bg = BitmapFactory.decodeResource(getResources(), R.drawable.my_image);
new Timer();
}
#Override
public void onDraw(Canvas c) {
Paint p = new Paint();
if(!created) {
w=getWidth();
h=getHeight();
p.setColor(Color.RED);
p.setTextSize(40);
bg = Bitmap.createScaledBitmap(bg, w, h, false);
created = true;
}
if(created) {
time++;
c.drawBitmap(bg, 0,0,null);
c.drawText(time+"", (int)(w/4), (int)(h/4), p);
}
}
class Timer extends Handler {
private Timer() {
handleMessage(obtainMessage(0));
}
#Override
public void handleMessage(Message m) {
invalidate();
sendMessageDelayed(obtainMessage(0), 10);
}
}
}
FYI, the original image is 300x225. The screen res of my tablet that I scale the image to is 1280x800.
The thing is if I scale the background image to sth like (int)(.8*w), (int)(.8*h) or sth smaller or not scale the image at all, then it runs fast as expected.
I tried using ImageView and use setImageResource(R.drawable.my_image) but it was as slow though.
I thought drawing an image to fit the background should be very simple for any programming languages, but I've had this problem for a really long time even after a lot of searching. I hope somebody can give me a proper answer. I would really appreciate that.
createScaledBitmap() should not be done inside your onDraw() routine. It is quite slow because it uses a pixel averaging routine to "smooth" out the stretched bitmap. Create your stretched bitmap outside of the gui thread and then just draw it (canvas.drawBitmap) in your onDraw() method. The filter parameter can be used to create a "nearest neighbor" scaling if set to false. This will look "blocky", but the processing is much faster.
public static Bitmap createScaledBitmap
(Bitmap src, int dstWidth, int dstHeight, boolean filter)