Just starting out with Android, canvas, the works. I have existing Java code that can draw shapes on a graphics object. I am trying to use that code in an Android app with Canvas. Basically, I'm trying to avoid refactoring all of my Java code to use Canvas explicitly. So I'm in the process of making my own "Graphics" object. All it should do is call the appropriate Canvas methods to draw the specified shape.
I've read multiple posts here about not being able to use the canvas object outside of the onDraw() method. From what I understand, you can't pass a canvas object to a different class and expect it to work correctly. But I also do not have a complete understanding of how all this works.
The app crashes in the Graphics class in the drawOval method. I've read up a log about all of this, but I haven't found a good answer as to why this specifically doesn't work.
I haven't been able to find a way to get crash logs in a java friendly way (aka a stacktrace). It just throws Fatal signal 11 (SIGSEGV), code 1, fault addr 0x130 in tid 1520.
Thanks! Let me know if more details are needed. Here's my code:
MainActivity
public class MainActivity extends AppCompatActivity {
MyCanvas canvas;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
canvas = new MyCanvas(this);
setContentView(canvas);
}
}
MyCanvas View
public class MyCanvas extends View {
Graphics graphics;
List<Shape> shapes;
public MyCanvas(Context context) {
super(context);
graphics = new Graphics();
shapes = new ArrayList<>();
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
graphics.setCanvas(canvas); //Sets the canvas object in the graphics class
for (Shape shape : shapes) {
try { //this is in a try/catch block for custom exception handling
//This just calls the specific shapes render method,
//in this case, a circle from the makeShape Method
//The graphics object then calls the specific shape to
//render on the canvas
shape.render(graphics, 0, 0);
} catch (ShapeException e) {
e.printStackTrace();
}
}
invalidate();
}
#Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) event.getX();
int y = (int) event.getY();
try { //this is in a try/catch block for custom exception handling
makeShape(x, y);
} catch (ShapeException e) {
e.printStackTrace();
}
invalidate();
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
break;
}
}
invalidate();
return true;
}
public void makeShape(int x, int y) throws ShapeException{
Point p = new Point(x, y);
Shape shape = new Circle(p, 50, 50);
shapes.add(shape);
}
}
Graphics
public class Graphics {
private Canvas canvas = null;
public Graphics() {
}
public void setCanvas(Canvas canvas) {
if (this.canvas == null) {
this.canvas = canvas;
}
}
public void drawOval(int x, int y, int width, int height) {
Paint paint1 = new Paint();
paint.setColor(Color.BLACK);
canvas.drawCircle(500, 500, 50, paint1); //App crashes here
}
}
You have a slight misunderstanding about being able to pass a Canvas to different classes. There is absolutely nothing wrong with that; passing a canvas to a class method is effectively the same as passing it to a regular function. Even storing the Canvas reference in a member variable isn't going to hurt anything.
HOWEVER, the above is only true with the understanding that the Canvas cannot be used outside the bounds of the draw()/onDraw() method. That is, any method that uses the Canvas must be called from within onDraw(), or a function called by onDraw(), etc.
This is because the Canvas is initialized (by the framework) immediately before onDraw(), in order to prepare for the current drawing operation. You might wish to think of it as initializing the Canvas with a Bitmap that will serve as the drawing output surface for this particular screen frame (which is not far from the truth). Once onDraw() has returned, the framework assumes your code will no longer be using it, and it can submit the output surface/Bitmap/etc. to the screen compositor for rendering, without fear of further alterations.
And, your approach is a good one, as a technique for adapting existing code to use a novel graphics object.
So, to address the crash: SIGSEGVs should never happen in normal Android development when you're not dealing (directly) with native/JNI routines. However, it occurs to me that you're not updating the Canvas object in the event it changes. This could be causing the crash (in native code, which is why you don't get a Java stack trace). You used an old Canvas after a newer one was given to you. Remove the condition if (this.canvas == null) and you should be fine.
Related
I'm creating a game using Java Swing, and I'm finding the need for graphical displays of what's going on at this point. The current display uses a grid of JButtons to represent the tiles on a battlefield. Would it be possible to display floating/disappearing damage numbers over the JButtons, using perhaps custom made pixel art GIFs? If so, how would I go about implementing this?
Almost all Swing components can be extended to change its presentation.
Create an own button class extending JButton and override its paintComponent method to display the required damage. This method receives a Graphic (actually an instance of Graphics2D) on which you can draw the damage, if one is active.
Very simple example:
public class DamageButton extends JButton {
private String damage = null;
public DamageButton(String text) {
super(text);
}
public void setDamage(String damage) {
this.damage = damage;
System.out.println(damage);
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (damage != null) {
Graphics2D gg = (Graphics2D) g.create();
try {
gg.setColor(Color.RED);
gg.drawString(damage, 10, 10);
} finally {
gg.dispose();
}
}
}
}
Creating a new Graphics (gg) so the settings of g are not changed and need not to be restored.
Call setDamage() with some text to have it displayed over the button or with null to cancel the effect.
Animation or other effects can (more or less) easily be added. Just be aware that the given Graphics2D has a clipping area set to the dimension of the button.
Sorry if my question doesn't adjust to the Stackoverflow requirements due to it is theorical but i don't know where else to ask.
For the past few weeks i've been trying to understand better how the Swing API works and it's components in order to create my own custom components. I've read tons of tutorials, searched in here, i am neck-deep in Java's swing source code and frankly... my mind is a mess.
As far as i understand, the swing components are composed of 3 parts:
the model: where the component state and data are stored
the UI delegate: which paints the component and
the JComponent: it ties everything together.
In this tutorial https://docs.oracle.com/javase/tutorial/uiswing/painting/step2.html there is a paragraph that says:
The paintComponent method is where all of your custom painting takes place. >This method is defined by javax.swing.JComponent and then overridden by your >subclasses to provide their custom behavior
Why is there a paintComponent method on a JComponent? Shouldn't it be an exclusive method of an UI delegate?
Swing is based on AWT. So the initial restriction is based on how AWT paints it components. Because Swing is light weight, when a components root component is painted, the child, Swing, components need to be notified that they need to update as well. This is done by calling the paint methods of all the child components affected by the update (which in turn call the paintComponent methods).
Much of the decisions about the Swing API's paint chain are based around the concept of customisation. It reduces the complexity involved (by discouraging the overriding of paint and focus the functionality down t the paintComponent method).
The Look and Feel API is based on a "delegate" model. This means that the functionality used to perform a said action is "delegated" to some other object. This means that the UI delegate UI doesn't actually know when a component "needs" to be painted, but instead is told, by the component, that it needs to be painted. This makes it much more flexible and in many cases, easier to customise.
Based on your previous question a custom ButtonUI is probably one the better choices, this way you gain much more control over how the button is painted.
Trying to get the buttons to follow the current look and feels color schemes would be very difficult, but you could try having a look at the src.jar which is installed with the JDK, which includes many implementations of look and feels (and if your on Windows you should get the Windows look and feel as well, if your on Mac, then you don't get either Mac or Windows 🙄)
I started by having a look at BasicButtonUI and ButtonUI to get a better understanding of there properties. I pulled some of more interesting methods into a custom ShapeButtonUI...
public class ShapeButtonUI extends BasicButtonUI {
private Shape shape;
public ShapeButtonUI(Shape shape) {
this.shape = shape;
}
protected Color getSelectColor() {
return UIManager.getColor(getPropertyPrefix() + "select");
}
protected Color getDisabledTextColor() {
return UIManager.getColor(getPropertyPrefix()
+ "disabledText");
}
protected Color getFocusColor() {
return UIManager.getColor(getPropertyPrefix() + "focus");
}
#Override
protected void installDefaults(AbstractButton b) {
super.installDefaults(b);
}
#Override
protected void uninstallDefaults(AbstractButton b) {
super.uninstallDefaults(b);
}
#Override
public void paint(Graphics g, JComponent c) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setClip(shape);
Rectangle bounds = shape.getBounds();
LinearGradientPaint lgp = new LinearGradientPaint(
new Point(bounds.x, bounds.y),
new Point(bounds.x, bounds.y + bounds.height),
new float[]{0, 1},
new Color[]{c.getBackground().brighter(), c.getBackground().darker()});
g2d.setPaint(lgp);
g2d.fill(shape);
g2d.dispose();
g2d = (Graphics2D) g.create();
g2d.setColor(c.getForeground());
g2d.draw(shape);
g2d.dispose();
super.paint(g, c);
}
#Override
protected void paintButtonPressed(Graphics g, AbstractButton b) {
super.paintButtonPressed(g, b);
}
#Override
protected void paintFocus(Graphics g, AbstractButton b,
Rectangle viewRect, Rectangle textRect, Rectangle iconRect) {
super.paintFocus(g, b, viewRect, textRect, iconRect);
}
#Override
protected void paintText(Graphics g, AbstractButton b, Rectangle textRect, String text) {
super.paintText(g, b, textRect, text);
// ButtonModel model = b.getModel();
// FontMetrics fm = SwingUtilities2.getFontMetrics(c, g);
// int mnemIndex = b.getDisplayedMnemonicIndex();
//
// /* Draw the Text */
// if (model.isEnabled()) {
// /**
// * * paint the text normally
// */
// g.setColor(b.getForeground());
// } else {
// /**
// * * paint the text disabled **
// */
// g.setColor(getDisabledTextColor());
// }
// SwingUtilities2.drawStringUnderlineCharAt(c, g, text, mnemIndex,
// textRect.x, textRect.y + fm.getAscent());
}
#Override
public Dimension getMinimumSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
#Override
public Dimension getPreferredSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
#Override
public Dimension getMaximumSize(JComponent c) {
Rectangle bounds = shape.getBounds();
return new Dimension(bounds.x + bounds.width + 1, bounds.y + bounds.height + 1);
}
}
Most of these you probably don't need to worry about, but you should at least know they exist as you may want to customise some of the other properties/functionality later.
This delegate is intended to be installed on a button as needed, as apposed to installing it as the default UI delegate for buttons. The reason for this is the need for the shape object. This allows each button to have it's own shape if you want.
You could seed a single shape into the UIManager's properties as well and use that instead, but I've not bothered for this example.
I then created my own shape/path...
public class PointerPath extends Path2D.Double {
public PointerPath() {
moveTo(1, 1);
lineTo(150, 1);
lineTo(198, 100);
lineTo(150, 198);
lineTo(1, 198);
lineTo(50, 100);
closePath();
}
}
And the applied it to a button...
ShapeButtonUI shapeUI = new ShapeButtonUI(new PointerPath());
JButton btn = new JButton("That way");
btn.setUI(shapeUI);
Which finally generated something like...
Now, this is a really basic example, as realistically, the button should be sizing itself around the text (with some additional padding/margin information), but this would require a multi part shape, so we knew which sections could be resized and in what directions, so, complicated.
I was surprised to find that View.onDraw() wipes the canvas before drawing. This isn't what I want. What's a good way to retain the previous drawings so that I only need to draw the changes on top of the previous drawings during each call?
There are couple of APIs to define dirty rect for view to invalidate:
public void invalidate(Rect dirty)
public void invalidate(int l, int t, int r, int b)
However more likely the View is redrawn all the way, when for example you swipe it, or another View above it in Z order get's invalidated.
You can try to use setDrawingCacheEnabled and if you are doing the drawing yourself, be sure to cache Bitmaps you are drawing.
I implemented a SurfaceView. Then in I created my own bitmap and canvas that I draw into, to draw to the screen I draw the bitmap to the screen.
For example:
private Canvas myCanvas = null;
private Bitmap myCanvasBitmap = null;
private Matrix identityMatrix;
public mySurfaceCreated(canvasWidth, canvasHeight){
myCanvasBitmap = Bitmap.createBitmap(canvasWidth, canvasHeight, Bitmap.Config.ARGB_8888);
myCanvas = new Canvas();
myCanvas.setBitmap(myCanvasBitmap);
identityMatrix = new Matrix();
}
public void myDraw(Canvas canvas) {
// Draw stuff onto myCanvas, NOT the canvas given by the android OS.
drawStuff(myCanvas);
// Draw onto the canvas given by the android OS.
canvas.drawBitmap(myCanvasBitmap, identityMatrix, null);
}
This way I do not need to draw the entire canvas each time, but instead only the needed changes. Note that the canvas created by the OS is still completely redrawn each time.
I'm just trying to draw a circle on the spot where I touch the screen. When I touch the screen, a circle is drawn there, but then when I touch the screen somewhere else a new circle is drawn (the old one is supposed to appear there, not a new one). Does anyone understand why this is happening? Code:
Thread class:
public class GameThread extends Thread{
private SurfaceHolder sHolder;
private DrawingSurface dSurface;
private boolean okToRun;
Paint redPaint = new Paint();
public int x, y;
boolean myTouchEvent(MotionEvent event){
int touch = event.getAction();
switch(touch){
case MotionEvent.ACTION_DOWN:
x = (int) event.getX();
y = (int) event.getY();
}
return true;
}
public GameThread(SurfaceHolder holder, DrawingSurface surface){
sHolder = holder;
dSurface = surface;
redPaint.setARGB(255, 255, 0, 0);
}//GameThread()
public void setOkToRun(boolean status){
okToRun = status;
}//setOkToRun()
public void run(){
while(okToRun){//gameloop
Canvas canvas = null;
try{
canvas = sHolder.lockCanvas(null);
synchronized(sHolder){
try{
canvas.drawCircle(x, y, 60, redPaint);
} catch (Exception e){
}
}
} finally {
if (canvas != null) {
sHolder.unlockCanvasAndPost(canvas);
}
}
}
}//run()
}
Here are the relevant methods of my surfaceview class:
#Override
public boolean onTouchEvent(MotionEvent event) {
return drawingThread.onTouchEvent(event);
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
drawingThread = new GameThread(getHolder(), this);
drawingThread.start();
drawingThread.setOkToRun(true);
}
#Override
public void surfaceDestroyed(SurfaceHolder arg0) {
radius += 10;
drawingThread.setOkToRun(false);
try {
drawingThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
TO BE CLEAR: I want to draw a single circle, and have it appear wherever I tap the screen. It does not do that. Can you tell why that is the case in this code?
I hope I am right that you need to draw a circle at the place where the screen was touched and the problem is that circles from previous touches still appear on the screen.
In this case the problem is, that you are drawing circles on canvas without clearing it. Canvas is represented in a memory as a array. When you call Canvas.drawCircle() part of memory is rewritten by an image of circle. When you do it repeatedly, the canvas contains more circles which are being drawn on the screen. You need to repaint the whole canvas before you draw a new circle. It can be done by calling the method Canvas.drawColor() before calling the method Canvas.drawCircle(). It clears the whole Canvas with selected color.
I am trying to create an object that can be called whenever I want to draw something on the onDraw() function of my class. Here is my code which is not working:
//object class
public class DrawObject extends Canvas {
Paint paint = new Paint();
public void setColor(int color){
paint.setColor(color);
}
// I want to draw an arrow to instead of a line
public void drawArrow(float startPointX, float startPointY, float endPointX, float endPointY){
drawLine(startPointX, startPointY, endPointX, endPointY, paint);
// draw the rest of the arrow here
}
}
for the main class:
public class Screen extends ImageView{
Paint paint = new Paint();
public Screen(Context context){
super(context);
paint.setColor(Color.BLACK);
paint.setStyle(Style.STROKE);
}
public void onDraw(DrawObject drawObject){
//called DrawObject instead on Canvas
drawObject.drawArrow(10,10,100,100);
// I want to draw the arrow here but it is not working.
}
can someone please tell me what is the proper way to do it? Thanks.
I would actually do something along these lines:
Define an interface for drawable objects (to avoid confusion with the Android Drawable class, I've called it Paintable)
public interface Paintable {
public void paint(Canvas canvas) {}
}
Make any classes that you need to draw in a custom manner, such as the arrow you suggested, implement the Paintable interface, and override paint(Canvas canvas) and handle your drawing operations there.
public class Arrow implements Paintable {
//all sorts of cool shit here to define your arrow
#Override
public void paint(Canvas canvas) {
//draw your arrow on the canvas here
}
}
Maintain a list of Paintable objects in your custom ImageView, and simply call the paint() method for each object in the list. Yay for polymorphism!
public class Screen extends ImageView {
List<Paintable> paintableObjects;
public Screen(Context context) {
super(context);
paint.setColor(Color.BLACK);
paint.setStyle(Style.STROKE);
paintableObjects = new ArrayList<Paintable>();
}
public void addPaintable(Paintable p) {
paintableObjects.add(p);
}
public void onDraw(Canvas canvas) {
for(Paintable p : paintableObjects) {
p.paint(canvas);
}
}
}
Extending Canvas is the wrong approach since you get a canvas supplied by the system which you have to use. You can't force Android to use your subclass of Canvas. Instead you can simply pass the Canvas to your class whenever it needs it.
public class DrawObject {
Paint paint = new Paint();
public void setColor(int color) {
paint.setColor(color);
}
// I want to draw an arrow to instead of a line
public void drawArrow(Canvas canvas, float startPointX, float startPointY, float endPointX, float endPointY) {
canvas.drawLine(startPointX, startPointY, endPointX, endPointY, paint);
// draw the rest of the arrow here
}
}
And use like
public void onDraw(Canvas canvas){
drawObject.drawArrow(canvas, 10,10,100,100);
// I want to draw the arrow here but it is not working.
}
I really don't see the reason of extending the Canvas and defining your own object, unless you need to have some special control over it.
Normally you extend View and do all the drawing stuff in the onDraw() method through the Canvas object, ex:
public void onDraw(Canvas canvas){
canvas.drawLine(startPointX, startPointY, endPointX, endPointY, paint);
}
this bit is wrong
public void onDraw(DrawObject drawObject){ ... }
onDarw is a frame work method so sice you change the method signature this method will not be called.
There is no need to subclass canvas - its not the right approach.
so in you Screen class - override the standard onDraw method:
public void onDraw(Canvas c){
drawLine(startPointX, startPointY, endPointX, endPointY, paint);
}
to trigger the onDraw mthod programatically - call invalidate() on the Screen object.