Custom view's onDraw receives canvas with different sizes - android

I have a class derived from ImageView:
public class TouchView extends ImageView
{
#Override
protected void onDraw(Canvas canvas)
...
The touchview is created only once in the activity's onCreate and populated with a drawable from a SVG file.
ImageView imageView = new TouchView(this);
imageView.setScaleType(ImageView.ScaleType.MATRIX);
FrameLayout f = (FrameLayout)findViewById(R.id.frame2);
FrameLayout.LayoutParams l = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.FILL_PARENT, FrameLayout.LayoutParams.FILL_PARENT);
f.addView(imageView, l);
...
is = openFileInput(svgname);
svg = SVGParser.getSVGFromInputStream(is);
is.close();
Drawable d = svg.createPictureDrawable();
imageView.setImageDrawable(d);
All the environment remains the same all the time. Yet in the onDraw method I'm getting canvas with different sizes between the events. That is the code:
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
Log.v("DRAW", " w= " + canvas.getWidth() + " h=" + canvas.getHeight());
...
}
produces logs with lines, where width and height of the canvas change back and forth from normal 1024*728 (this is the correct view dimension on the tablet) to 200*160 (strange thing introducing bugs in my drawings). I'm embarrassed.
Should the canvas be always of the same size for the same view/drawable? The documentation says that getWidth and getHeight methods return dimensions of the current drawing layer, but it's not clear what is the layer, how many of them is created for the canvas "behind the scene" and how to control this process.
I'd appreciate any explanation on how to get consistent drawing behaviuor, specifically by getting actual size of the view being painted in onDraw.
Currently I'm using a workround with a call to the view's getDrawingRect, but I'm not sure it is a proper way, because it seems that the canvas parameter of onDraw should be all-sufficient for drawing sizing.

I had the same issue, here is how I got this fixed, hope it helps you
protected void onDraw(Canvas c) {
super.onDraw(c);
int w = getWidth(), h = getHeight();
// resize
Matrix resize = new Matrix();
resize.postScale((float)Math.min(w, h) / (float)mMarker.getWidth(), (float)Math.min(w, h) / (float)mMarker.getHeight());
imageScaled = Bitmap.createBitmap(mMarker, 0, 0, mMarker.getWidth(), mMarker.getHeight(), resize, false);
c.drawBitmap(imageScaled, 0,0, paint);
}
Where mMarker is defined in the custom ImageView constructor.
...
private Bitmap mMarker, imageScaled;
Paint paint = new Paint();
//Java constructor
public AvatarImageView(Context context) {
super(context);
init();
}
//XML constructor
public AvatarImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// load the image only once
mMarker = BitmapFactory.decodeResource(getResources(), R.drawable.silhouette_48);
mMarker.setHasAlpha(true);paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(4);
//invalidate(); // don't know if I need this
}

Related

Drawing line programmatically on a Layout

The red Margin represents an AbsoluteLayout, I have and arbitrary number of 'Board' objects placed on the screen. All I want is to draw a line on the screen using the coordinates of the Board object and the center of the screen. Each board object is responsible to draw this line.
Also I want the line to be behind the Board objects I'm guessing I have to change the z-index, or maybe draw the line on the AbsoluteLayout?
I have something like this:
public class Board {
ImageView line; //Imageview to draw line on
Point displayCenter; //Coordinates to the center of the screen
int x;
int y;
Activity activity;
Board(Point p, Point c, Activity activity) // Point c is the coordinates of the Board object
{
x = c.x
y = c.y
displayCenter.x = p.x;
displayCenter.y = p.y;
this.activity = activity;
updateLine();
}
public void updateLine(){
int w=activity.getWindowManager().getDefaultDisplay().getWidth();
int h=activity.getWindowManager().getDefaultDisplay().getHeight();
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
line.setImageBitmap(bitmap);
Paint paint = new Paint();
paint.setColor(0xFF979797);
paint.setStrokeWidth(10);
int startx = this.x;
int starty = this.y;
int endx = displayCenter.x;
int endy = displayCenter.y;
canvas.drawLine(startx, starty, endx, endy, paint);
}
}
first af all,
you should never ever use the absolute layout, it is deprecated for a good reason.
With that said you have two options. For both options you need to implement your own Layout.
For option no. 1 you can override the dispatchDraw(final Canvas canvas) see below.
public class CustomLayout extends AbsoluteLayout {
...
#Override
protected void dispatchDraw(final Canvas canvas) {
// put your code to draw behind children here.
super.dispatchDraw(canvas);
// put your code to draw on top of children here.
}
...
}
Option no. 2 If you like the drawing to occur in the onDraw me you need to set setWillNotDraw(false); since by default the onDraw method on ViewGroups won't be called.
public class CustomLayout extends AbsoluteLayout {
public CustomLayout(final Context context) {
super(context);
setWillNotDraw(false);
}
...
#Override
protected void onDraw(final Canvas canvas) {
super.onDraw(canvas);
// put your code to draw behind children here.
}
}

Android: Change size of canvas / drawable in custom drawable

I'm trying to implement a custom drawable which should have the shape of a speechbubble. Therefore I use two paths, one draws the rect and the other should draw the triangle for the bubble.
My class looks like the following:
public class SpeechBubbleView extends Drawable {
private Paint mBubblePaint;
private Paint mBubblePaint2;
private Path mRectPath;
private Path mBubblePath;
public SpeechBubbleView() { }
public void initPaint() {
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setStyle(Paint.Style.FILL);
mBubblePaint.setColor(Color.GREEN);
mBubblePaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint2.setStyle(Paint.Style.FILL);
mBubblePaint2.setColor(Color.RED);
int width = getBounds().width();
int height = getBounds().height();
mRectPath = new Path();
mRectPath.addRoundRect(new RectF(0, 0, width, height), 8, 8, Path.Direction.CW);
mRectPath.close();
mBubblePath = new Path();
mBubblePath.moveTo(50, height);
mBubblePath.lineTo(100, height + 50);
mBubblePath.lineTo(150, height);
mBubblePath.close();
}
#Override
public void draw(Canvas canvas) {
if(mRectPath == null && mPathValues == null) {
initPaint();
}
canvas.drawPath(mRectPath, mBubblePaint);
canvas.drawPath(mBubblePath, mBubblePaint2);
}
#Override
public void onBoundsChange(Rect bounds) {
Rect customBound = new Rect(0, 0, bounds.width(), bounds.height() + 50);
super.onBoundsChange(customBound);
}
The problem now is, that I take the width and height from the drawable to draw the rect of the speechbubble. The full space of the canvas is taken and there is no more room for the triangle to display below the rect.
My question now is: Is it possible to change the size of canvas or the drawable, so that I am able to display the small triangle below the rect?
I already tried the method onBoundsChange, but it takes no effect. In the draw-method the size is still the same.
If possible, it would be nice to change the size directly in the custom drawable class, shown above, because I do not have the size of the view, when I call it. Also I cannot make the size of the rect smaller, because in the drawable there is content and if the rect is smaller, some of the content will be outside of the drawable. I use a drawable, so that I can simple call setBackgroundDrawable of my layout or TextView and it matches always the content size.
If anyone of you got an idea on how to do the size change, this would be very great. Thank you :D

How to use custom views, e.g. setting drawable

I think I'm a bit confused about how to use custom views. I'm following along with slides from a talk given by Eric Burke from Square (from this year's anddevcon, slides here: http://www.andevcon.com/AndevCon_II/downloadpresentation.aspx?aid=Taming_Android__User_Experience_Lessons_from_Square_pdf.zip&sid=2).
His code, or at least the part he showed in the slides, went something like this:
public class EditablePhoto extends View {
private Bitmap framedPhoto;
private Bitmap image;
private Drawable placeholder;
public EditablePhoto(Context context) {
super(context);
}
#Override protected void onMeasure(int widthMeasureSpec,
int heightMeasureSpec) {
int measuredWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);
int measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
//ensure view always square
int min = Math.min(measuredHeight, measuredWidth);
setMeasuredDimension(min, min);
}
#Override
protected void onDraw(Canvas canvas) {
if(placeholder == null && image==null) return;
if(framedPhoto == null) {
createFramedPhoto(Math.min(getWidth(), getHeight()));
}
canvas.drawBitmap(framedPhoto, 0, 0, null);
}
private void createFramedPhoto(int size) {
Drawable imageDrawable = (image!=null)
? new BitmapDrawable(image) : placeholder;
Bitmap output = Bitmap.createBitmap(size, size,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
RectF outerRect = new RectF(0, 0, size, size);
float outerRadius = size / 18f;
//Red rectangle
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.RED);
canvas.drawRoundRect(outerRect, outerRadius, outerRadius, paint);
paint.setXfermode(new PorterDuffXfermode(
PorterDuff.Mode.SRC_IN));
imageDrawable.setBounds(0, 0, size, size);
canvas.saveLayer(outerRect, paint, Canvas.ALL_SAVE_FLAG);
imageDrawable.draw(canvas);
canvas.restore();
}
}
What I don't get is how to actually use this View now.... Where and when do you set the bitmaps, which are private fields in this class...?
Generally confused and would love some enlightenment.
More than one year passed, but I hope this will help anyone who looking for the right answer. In my case, I putted this line of code
framedPhoto = output;
as the last one in createFramedPhoto() method. It works.
In the example, the author created a rounded rectangle as background then he draw the bitmap on it with XOR mode, so all pixel outside the rounded rectangle will be trim off.
OnDraw() is the method where you will Draw your view on canvas. here too you can analyze onDraw() will fisrt call CreateFramePhoto then draw this Bitmap on canvas .
You can add this customView in layout Either from xml or in Java Class
1) Through Xml :
<EditablePhoto android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
..........................
/>
dont forgate to add constructor EditablePhoto(Context context, AttributeSet attributeSet) for this case
2) through Java class :
EditablePhoto editablePhoto = new EditablePhoto(this);
addView(editablePhoto) // or do anthing you want with this

Android, canvas: How do I clear (delete contents of) a canvas (= bitmaps), living in a surfaceView?

In order to make a simple game, I used a template that draws a canvas with bitmaps like this:
private void doDraw(Canvas canvas) {
for (int i=0;i<8;i++)
for (int j=0;j<9;j++)
for (int k=0;k<7;k++) {
canvas.drawBitmap(mBits[allBits[i][j][k]], i*50 -k*7, j*50 -k*7, null); } }
(The canvas is defined in "run()" / the SurfaceView lives in a GameThread.)
My first question is how do I clear (or redraw) the whole canvas for a new layout?
Second, how can I update just a part of the screen?
// This is the routine that calls "doDraw":
public void run() {
while (mRun) {
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
if (mMode == STATE_RUNNING)
updateGame();
doDraw(c); }
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c); } } } }
Draw transparent color with PorterDuff clear mode does the trick for what I wanted.
Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
How do I clear (or redraw) the WHOLE canvas for a new layout (= try at the game) ?
Just call Canvas.drawColor(Color.BLACK), or whatever color you want to clear your Canvas with.
And: how can I update just a part of the screen ?
There is no such method that just update a "part of the screen" since Android OS is redrawing every pixel when updating the screen. But, when you're not clearing old drawings on your Canvas, the old drawings are still on the surface and that is probably one way to "update just a part" of the screen.
So, if you want to "update a part of the screen", just avoid calling Canvas.drawColor() method.
Found this in google groups and this worked for me..
Paint clearPaint = new Paint();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRect(0, 0, width, height, clearPaint);
This removes drawings rectangles etc. while keeping set bitmap..
use the reset method of Path class
Path.reset();
I tried the answer of #mobistry:
canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
But it doesn't worked for me.
The solution, for me, was:
canvas.drawColor(Color.TRANSPARENT, Mode.MULTIPLY);
Maybe some one has the same problem.
mBitmap.eraseColor(Color.TRANSPARENT);
canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint);
canvas.drawColor(Color.TRANSPARENT, Mode.MULTIPLY);
please paste below code on surfaceview extend class constructor.............
constructor coding
SurfaceHolder holder = getHolder();
holder.addCallback(this);
SurfaceView sur = (SurfaceView)findViewById(R.id.surfaceview);
sur.setZOrderOnTop(true); // necessary
holder = sur.getHolder();
holder.setFormat(PixelFormat.TRANSPARENT);
xml coding
<com.welcome.panelview.PanelViewWelcomeScreen
android:id="#+id/one"
android:layout_width="600px"
android:layout_height="312px"
android:layout_gravity="center"
android:layout_marginTop="10px"
android:background="#drawable/welcome" />
try above code...
Here is the code of a minimal example showing that you always have to redraw every pixel of the Canvas at each frame.
This activity draw a new Bitmap every second on the SurfaceView, without clearing the screen before.
If you test it, you will see that the bitmap is not always written to the same buffer, and the screen will alternate between the two buffers.
I tested it on my phone (Nexus S, Android 2.3.3), and on the emulator (Android 2.2).
public class TestCanvas extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new TestView(this));
}
}
class TestView extends SurfaceView implements SurfaceHolder.Callback {
private TestThread mThread;
private int mWidth;
private int mHeight;
private Bitmap mBitmap;
private SurfaceHolder mSurfaceHolder;
public TestView(Context context) {
super(context);
mThread = new TestThread();
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.icon);
mSurfaceHolder = getHolder();
mSurfaceHolder.addCallback(this);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
mWidth = width;
mHeight = height;
mThread.start();
}
#Override
public void surfaceCreated(SurfaceHolder holder) {/* Do nothing */}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mThread != null && mThread.isAlive())
mThread.interrupt();
}
class TestThread extends Thread {
#Override
public void run() {
while (!isInterrupted()) {
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
c.drawBitmap(mBitmap, (int) (Math.random() * mWidth), (int) (Math.random() * mHeight), null);
}
} finally {
if (c != null)
mSurfaceHolder.unlockCanvasAndPost(c);
}
try {
sleep(1000);
} catch (InterruptedException e) {
interrupt();
}
}
}
}
}
For me calling Canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) or something similar would only work after I touch the screen. SO I would call the above line of code but the screen would only clear after I then touched the screen. So what worked for me was to call invalidate() followed by init() which is called at the time of creation to initialize the view.
private void init() {
setFocusable(true);
setFocusableInTouchMode(true);
setOnTouchListener(this);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeJoin(Paint.Join.ROUND);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(6);
mCanvas = new Canvas();
mPaths = new LinkedList<>();
addNewPath();
}
Erasing on Canvas in java android is similar erasing HTML Canvas via javascript with globalCompositeOperation. The logic was similar.
U will choose DST_OUT (Destination Out) logic.
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
Note: DST_OUT is more useful because it can erase 50% if the paint color have 50% alpha. So, to clear completely to transparent, the alpha of color must be 100%. Apply paint.setColor(Color.WHITE) is recommended. And make sure that the canvas image format was RGBA_8888.
After erased, go back to normal drawing with SRC_OVER (Source Over).
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
Update small area display literally will need to access graphic hardware, and it maybe not supported.
The most close for highest performance is using multi image layer.
With the following approach, you can clear the whole canvas or just a part of it.
Please do not forget to disable Hardware acceleration since PorterDuff.Mode.CLEAR doesn’t work with hardware acceleration and finally call setWillNotDraw(false) because we override the onDraw method.
//view's constructor
setWillNotDraw(false);
setLayerType(LAYER_TYPE_SOFTWARE, null);
//view's onDraw
Paint TransparentPaint = new Paint();
TransparentPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRect(0, 0, width, height, TransparentPaint);
Don't forget to call invalidate();
canvas.drawColor(backgroundColor);
invalidate();
path.reset();
I found my solution.
PaintView class:
public void clear() {
mPath.reset();
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
paths.clear();
}
And MainActivity:
clear_canvas_.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
paintView.clear();
}
});
Your first requirement, how to clear or redraw whole canvas - Answer - use canvas.drawColor(color.Black) method for clearing the screen with a color of black or whatever you specify .
Your second requirement, how to update part of the screen - Answer - for example if you want to keep all other things unchanged on the screen but in a small area of screen to show an integer(say counter) which increases after every five seconds. then use canvas.drawrect method to draw that small area by specifying left top right bottom and paint. then compute your counter value(using postdalayed for 5 seconds etc., llike Handler.postDelayed(Runnable_Object, 5000);) , convert it to text string, compute the x and y coordinate in this small rect and use text view to display the changing counter value.
Try to remove the view at onPause() of an activity and add onRestart()
LayoutYouAddedYourView.addView(YourCustomView);
LayoutYouAddedYourView.removeView(YourCustomView);
The moment you add your view, onDraw() method would get called.
YourCustomView, is a class which extends the View class.
In my case, I draw my canvas into linearlayout.
To clean and redraw again:
LinearLayout linearLayout = findViewById(R.id.myCanvas);
linearLayout.removeAllViews();
and then, I call the class with the new values:
Lienzo fondo = new Lienzo(this,items);
linearLayout.addView(fondo);
This is the class Lienzo:
class Lienzo extends View {
Paint paint;
RectF contenedor;
Path path;
ArrayList<Items>elementos;
public Lienzo(Context context,ArrayList<Items> elementos) {
super(context);
this.elementos=elementos;
init();
}
private void init() {
path=new Path();
paint = new Paint();
contenedor = new RectF();
paint.setStyle(Paint.Style.FILL);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
contenedor.left = oneValue;
contenedor.top = anotherValue;
contenedor.right = anotherValue;
contenedor.bottom = anotherValue;
float angulo = -90; //starts drawing at 12 o'clock
//total= sum of all element values
for (int i=0;i<elementos.size();i++){
if (elementos.get(i).angulo!=0 && elementos.get(i).visible){
paint.setColor(elementos.get(i).backColor);
canvas.drawArc(contenedor,angulo,(float)(elementos.get(i).value*360)/total,true,paint);
angulo+=(float)(elementos.get(i).value*360)/total;
}
} //for example
}
}
In my case, creating canvas every time worked for me, even though it's not memory-friendly
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.image);
imageBitmap = Bitmap.createBitmap(bm.getWidth(), bm.getHeight(), bm.getConfig());
canvas = new Canvas(imageBitmap);
canvas.drawBitmap(bm, 0, 0, null);
I had to use a separate drawing pass to clear the canvas (lock, draw and unlock):
Canvas canvas = null;
try {
canvas = holder.lockCanvas();
if (canvas == null) {
// exit drawing thread
break;
}
canvas.drawColor(colorToClearFromCanvas, PorterDuff.Mode.CLEAR);
} finally {
if (canvas != null) {
holder.unlockCanvasAndPost(canvas);
}
}
The following worked for me:
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.SCREEN);
Just call
canvas.drawColor(Color.TRANSPARENT)

How can I override onDraw so that I take what would have been drawn (as a bitmap) and transform it?

The method below works, but it unfortunately, this method involves creating a bitmap the size of the entire screen - not just the area that is drawn to. If I use this to draw UI elements, it is redrawn for each UI element. Can this be done more efficiently?
#Override
protected void onDraw(Canvas canvas) {
//TODO: Reduce the burden from multiple drawing
Bitmap bitmap=Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Config.ARGB_8888);
Canvas offscreen=new Canvas(bitmap);
super.onDraw(offscreen);
//Then draw onscreen
Paint p=new Paint();
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
canvas.drawBitmap(bitmap, 0, 0, p);
}
The following code will be much more efficient.
public class MyView extends TextView{
private Canvas offscreen;
public MyView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
//We want the superclass to draw directly to the offscreen canvas so that we don't get an infinitely deep recursive call
if(canvas==offscreen){
super.onDraw(offscreen);
}
else{
//Our offscreen image uses the dimensions of the view rather than the canvas
Bitmap bitmap=Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
offscreen=new Canvas(bitmap);
super.draw(offscreen);
//Create paint to draw effect
Paint p=new Paint();
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
//Draw on the canvas. Fortunately, this class uses relative coordinates so that we don't have to worry about where this View is actually positioned.
canvas.drawBitmap(bitmap, 0, 0, p);
}
}
}
Have you tried changing the size of the bitmap you create, to be only equal to the clip bounds rather than the entire size of the given canvas? I don't know if this is the case with all Views, but I found that the onDraw method for some Views is given a canvas as big as the screen, regardless of the actual size of the content. It then clips the canvas, restricting it to the top left corner only, draws there and then translates that part to somewhere else on the screen when it's actually ready to display the whole layout. In other words, despite it having a canvas the size of the whole screen, it only actually uses a small piece of it. So what you could try doing is instead of using
Bitmap bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), ...)
you could try
Rect rect = canvas.getClipBounds();
int width = rect.width();
int height = rect.height();
Bitmap bitmap = Bitmap.createBitmap(width, height, ...);
That way you reduce the overhead of working with a much larger bitmap than necessary. Note: I haven't actually tried doing this so it might not work, but it's worth trying.
I encountered something similar, where canvas.getWidth() and canvas.getHeight() returns the width and height of the whole screen instead of the view itself.
Try to use the below instead:
Bitmap bitmap=Bitmap.createBitmap(getWidth(), getHeight(), Config.ARGB_8888);
For my case, getWidth() and getHeight() returns me the dimensions of my view. Good luck!

Categories

Resources