I need some help understanding the fundamentals of scrolling over items that are drawn to a canvas in Android. Suppose I want to create a timeline where time at 0 is the top of a visualization and as time increased the timeline continues to be rendered below the previous point. If I wish to render this on Android I know I could simply create a bunch of items on a canvas by overriding onDraw(). However, let's suppose the visualization is bigger than the screen allows.
For example in the first picture below the large black box contains the entire canvas as I render it. I create a blue line that runs vertically up and down as well as several yellow, green and blue rectangles. The red box represents the screen of the Android that is rendering the visualization. As it initially opens all items are drawn but only the items contained within the red box show up on the screen.
Now if the user is to scroll down, items that initially appeared below the red box are in view while items that have gone out of the confines of the red box are no longer visable, as represented in the second picture.
I believe I need to use scrollables but I'm quite lost how to do so. I've read over this page http://developer.android.com/training/custom-views/custom-drawing.html explaining how to create your own customer images and this page http://developer.android.com/training/custom-views/making-interactive.html explaining how to make the UI interactive, but I think I'm missing something.
A sample code that illustrates this problem (this is basic, assume there is logic dictating WHERE the boxes/lines go, etc.) is as follows:
package com.example.scrolltest;
import com.example.scrolltest.Draw;
import android.os.Bundle;
import android.app.Activity;
import android.graphics.Color;
public class MainActivity extends Activity {
Draw draw;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
draw = new Draw(this);
draw.setBackgroundColor(Color.WHITE);
setContentView(draw);
}
}
and
package com.example.scrolltest;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
public class Draw extends View {
Paint paint = new Paint();
public Draw(Context context) {
super(context);
}
#Override
public void onDraw(Canvas canvas) {
paint.setColor(Color.GREEN);
canvas.drawRect(30, 30, 90, 200, paint);
paint.setColor(Color.BLUE);
canvas.drawLine(100, 20, 100, 1900, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(200, 2000, 400, 3000, paint);
}
}
What I cannot figure out though, is how I use a scrollable to scroll down to the rectangles that are off of the screen. I'm also unsure if I started this correctly or should use drawables instead...
Simple Method (If the height required is not very large).
Use a ScrollView and add your Draw view in it. Compute the required height for that view in onMeasure.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
draw = new Draw(this);
draw.setBackgroundColor(Color.WHITE);
ScrollView scrollView = new ScrollView(this);
scrollView.addView(draw);
setContentView(scrollView);
}
public class Draw extends View {
Paint paint = new Paint();
public Draw(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Compute the height required to render the view
// Assume Width will always be MATCH_PARENT.
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 3000 + 50; // Since 3000 is bottom of last Rect to be drawn added and 50 for padding.
setMeasuredDimension(width, height);
}
#Override
public void onDraw(Canvas canvas) {
paint.setColor(Color.GREEN);
canvas.drawRect(30, 30, 90, 200, paint);
paint.setColor(Color.BLUE);
canvas.drawLine(100, 20, 100, 1900, paint);
paint.setColor(Color.GREEN);
canvas.drawRect(200, 2000, 400, 3000, paint);
}
}
An alternative can be to use offset X/Y values.
How you handle the offset values is up to you, all though I prefer using a class I call Camera. The access should be static.
public void render(Canvas c){
c.drawRect(100 - offsetX, 100 - offsetY, 120 - offsetX, 120 - offsetY, Paints.BLUE);
}
And to pan the canvas:
float x, y;
#Override
public boolean onTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN){
x = ev.getX();
y = ev.getY();
}else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
float currX = ev.getX();
float currY = ev.getY();
float newOffsetX = (x - currX),
newOffsetY = (y - currY);
//The next two if-statements are to add sensitivity to the scrolling.
//You can drop these if you don't plan on having touch events
//with pressing. Pressing works without this as well, but the canvas may move slightly
//I use DP to handle different screen sizes but it can be replaced with pixels. c is a context-instance
if (newOffsetY < Maths.convertDpToPixel(2, c) && newOffsetY > -Maths.convertDpToPixel(2, c))
newOffsetY = 0;
if (newOffsetX < Maths.convertDpToPixel(2, c) && newOffsetX > -Maths.convertDpToPixel(2, c))
newOffsetX = 0;
offsetX += newOffsetX;
offsetY += newOffsetY;
x = ev.getX();
y = ev.getY();
}
return true;
}
And a sample Camera-class:
public class Camera{
public static float offsetX, offsetY;
//The constructor. ix and iy is the initial offset. Useful if you are creating a game and need to change the initial offset to center around a starting position.
//Most of the time it will be enough to set the values to 0
public Camera(float ix, float iy){
this.offsetX = ix;
this.offsetY = iy;
}
}
Related
I'm building a mobile Box2D body/joint editor in libGDX, but I'm getting inconsistent results between my desktop and android builds, the android build producing incorrect position Y-values. In summary, the editor consists of a HUD with its own FitViewport for consistent screen presence, and the actual build area grid with its own FitViewport that can be navigated and zoomed, much like any run-of-the-mill image editor. The user touches the screen, and a target appears above that position that can be dragged around to select points on the grid, connecting points that serve as the vertices of the shapes that will become Box2D bodies. This target-selector exists on the HUD viewport so that it can remain a static size if the user were to zoom in on the grid area. So, in order to select a point on this grid, I have to convert the target's position in the HUD viewport to screen coordinates, then convert it to that point on the build area grid. Next to the target, I have a BitmapFont that renders text of the targets current position.
So here's an example of my problem. Let's say the target is at the arbitrary point (0,0) on the grid. The text would say "0,0". In my desktop build it does this accurately. In my Android build, the Y-value is offset by varying values, based on camera zoom, even though that point is without a doubt (0, 0) in that space(The bottom-left of the grid is coded at 0,0). I don't understand how this could happen, since both builds share the core module where this code resides. I knew this would be hard to explain, so I took some screen shots.
These images show the desktop build, no issues, correct values:
desktop app image, default zoom, y-value is correct
desktop app image, camera zoomed in, y-value is still correct
These images show the android build, y-value is incorrect but becomes more correct as the camera zooms in:
android app image, default zoom, y-value is way off
android app image, zoomed in, y-value is closer to correct but still off
I really want to show some code, but I have no idea which parts are relevant to this issue. Essentially what is happening is that the user touches the screen, the target pops up 200px (HUD stage local) above that point, the targets position is converted to screen coordinates, then unprojected to build area grid coordinates. What gets me is that the Y-values are correct in the desktop build, so why would they be different in the android build. Here is my TargetSelector class where everything goes down:
public class TargetSelector extends Group {
private ShapeRenderer shapeRenderer;
private BitmapFont font;
private Vector2 position;
private float yOffset;
private boolean targetActive;
private Vector2 unprojectedPosition;
private OrthographicCamera linkedCamera;
private boolean hasLink = false;
public TargetSelector(){
initShapeRenderer();
initTargetFields();
initFont();
initInputListener();
}
private void initShapeRenderer(){
shapeRenderer = new ShapeRenderer();
shapeRenderer.setAutoShapeType(true);
shapeRenderer.setColor(Color.RED);
}
private void initTargetFields(){
unprojectedPosition = new Vector2();
position = new Vector2();
yOffset = 200;
targetActive = false;
}
private void initFont(){
font = new BitmapFont();
font.getData().scale(1.0f);
font.setColor(Color.WHITE);
}
private void initInputListener(){
setBounds(0, 0, Info.SCREEN_WIDTH, Info.SCREEN_HEIGHT);
addListener(new InputListener(){
#Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, int button) {
targetActive = true;
position.set(x, y).add(0, yOffset);
return true;
}
#Override
public void touchDragged(InputEvent event, float x, float y, int pointer) {
position.set(x, y).add(0, yOffset);
}
#Override
public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
targetActive = false;
}
#Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
targetActive = false;
}
});
}
#Override
public void act(float delta) {
super.act(delta);
if(targetActive && hasLink){
updateUnprojectedPosition();
}
}
#Override
public void draw(Batch batch, float parentAlpha) {
if(targetActive){
batch.end();
Gdx.gl.glEnable(GL20.GL_BLEND);
Gdx.gl.glBlendFunc(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
shapeRenderer.setProjectionMatrix(getStage().getCamera().combined);
shapeRenderer.begin();
// Render target
Gdx.gl20.glLineWidth(3);
shapeRenderer.set(ShapeRenderer.ShapeType.Line);
shapeRenderer.setColor(Color.RED);
shapeRenderer.circle(position.x, position.y, 32);
shapeRenderer.circle(position.x, position.y, 1);
// Render position text background
Vector2 fontPosition = position.cpy().add(64, 64);
shapeRenderer.set(ShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(0, 0, 0, 0.5f);
shapeRenderer.rect(fontPosition.x - 8, fontPosition.y - 32, 172, 40);
shapeRenderer.end();
Gdx.gl.glDisable(GL20.GL_BLEND);
Gdx.gl20.glLineWidth(1);
batch.setProjectionMatrix(getStage().getCamera().combined);
batch.begin();
// Render position text
String text;
if(hasLink)
text = "(" + (int) unprojectedPosition.x + " , " + (int) unprojectedPosition.y + ")";
else
text = "No Link";
font.draw(batch, text, fontPosition.x, fontPosition.y, 100, 10, false);
}
super.draw(batch, parentAlpha);
}
public void updateUnprojectedPosition(){
Vector2 screenPosV2 = localToScreenCoordinates(position.cpy());
Vector3 posV3 = new Vector3(screenPosV2.x, screenPosV2.y, 0);
linkedCamera.unproject(posV3);
unprojectedPosition.set(MathUtils.round(posV3.x), MathUtils.round(posV3.y));
}
public void linkToCamera(OrthographicCamera camera){
if(camera != null){
linkedCamera = camera;
hasLink = true;
}
}
}
The linkedCamera is assigned in a manager class that handles the relationship between all HUD actions and the build area grid.
This is my first time ever asking a question here, I'm sorry if I'm being too verbose but the issue is complicated to explain, at least it was for me.
I have problem with animation in android.
My animations working good only on 1080x1920 480dpi display. But i don't know how made app for any display.
If I use method from http://www.i-programmer.info/programming/android/8797-android-adventures-beginning-bitmap-graphics.html?start=4 Animations run, ok but made shadows, so no rewrite display if I write on display something I not delete thise from display.
If I use method. Main activity have one threat
public class UpdateThread implements Runnable {
#Override
public void run() {
while(bez){
try {
myThread.sleep(50);
} catch (Exception ex) {}
MainActivity.this.updateHandler.sendEmptyMessage(0);
}
}
and Class Game who every 50ms do
public void update() {
x += 1;
y += 2;
}
And on draw method in this class
protected void onDraw(Canvas canvas) {
Plocha pozadie = new Plocha(paint, canvas, X0, Y0, vzdialenost);
if (nehra==true) {
paint.setStrokeWidth(13F);
paint.setStyle(Paint.Style.STROKE);
LevelUp(level);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setStyle(Paint.Style.FILL);
pozadie.vypis(paint, canvas, "Score", 100, 100, 6);
pozadie.vypis(paint, canvas, Integer.toString(zasah), 90, 200, 9);
pozadie.vypis(paint, canvas, "Level", 850, 100, 6);
pozadie.vypis(paint, canvas, Integer.toString(level), 850, 200, 8);
pozadie.vypis(paint, canvas, "Clicks", 1080 / 2 - 100, 100, 7);
pozadie.vypis(paint, canvas, Integer.toString(click), 1080 / 2, 200, 9);
}
everything be okay (y) but only one type display.
If use
Bitmap b = Bitmap.createBitmap(1080,
1920,Bitmap.Config.ARGB_8888);
this.canvas = new Canvas(b);
this.canvas.drawColor(Color.WHITE);
this.b=b;
and rewrite some parts of code
application result is these canvas
protected void onDraw( Canvas canvas ) {
I don't know why...
ImageView im=(ImageView)findViewById(R.id.imageView222);
game = new Game(this,im);
setContentView(game);
I think so If I save setContentView(game); to imageview. will be everything okay. But I try and every ideas finish more errors.
I'm sad from...
Thanks from some ideas.
I am just trying to create a horizontal line. I have painted the view red and then added a blue line which I thought should take half the height.
Since I say that my custom view is 40 dp in height I would have thought that the blue bar with 20 dp in height would fill it half ways. But it doesn't. It takes 1/4 instead of 1/2. How can I fix that?
public class MyProgressView extends View {
public MyProgressView(Context context) {
super(context);
}
public MyProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
paint.setColor(Color.BLUE);
paint.setStrokeWidth(dipToPx(20));
canvas.drawLine(0, 0, 300, 0, paint);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int parentWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(parentWidth, dipToPx(40));
}
private int dipToPx(int dp) {
return (int) getResources().getDisplayMetrics().density * dp;
}
}
and XML:
<view
class="com.company.MyProgressView"
android:background="#ff0000"
android:layout_height="wrap_content"
android:layout_width="match_parent"
/>
In your case drawLine method draws a line on Y=0. This means your line's center position on Y is 0. If you set stroke width to 20, it is going to fill -10 and +10.
There are 2 different solutions:
You can set stroke with to 40(which will fill -20 and +20)
You can set your Y to 10 on your drawLine method(which will fill 0 and +20).
How do I find the coordinates of the screen? I know e.g. a phone would have say a 960 x 540 resolution, but in the emulators some of the edges are not filled if I draw a shape to that resolution. Is there a way around this?
For the colour of the rectangle, it is seen there are two rectangles, and two of them have the same colour despite giving two separate colours for drawPaint. Just setting a new variable e.g. drawPaint2 returns errors. How to change the colour of both?
How to use the path function in the canvas. E.g. to draw a triangle? I have included my attempt in the code but it doesn't display a triangle.
public class DrawView extends View implements OnTouchListener
{
private Paint backgroundPaint = new Paint();
private Paint drawPaint = new Paint();
private Paint circlePaint = new Paint();
private Paint textPaint = new Paint();
private Paint path = new Paint();
private float sx, sy;
public DrawView(Context context)
{
super(context);
setFocusable(true);
setFocusableInTouchMode(true);
backgroundPaint.setColor(Color.CYAN);
backgroundPaint.setAntiAlias(true);
backgroundPaint.setStyle(Style.FILL);
drawPaint.setColor(Color.WHITE);
drawPaint.setStyle(Style.FILL);
circlePaint.setColor(Color.DKGRAY);
circlePaint.setStyle(Style.FILL);
textPaint.setColor(Color.WHITE);
textPaint.setStyle(Style.FILL);
drawPaint.setColor(Color.GREEN);
drawPaint.setStyle(Style.FILL);
circlePaint.setColor(Color.RED);
circlePaint.setStyle(Style.FILL);
path.setColor(android.graphics.Color.RED);
path.setStyle(Paint.Style.FILL);
Path path = new Path();
path.moveTo(1, 1);
path.lineTo(20, 50);
path.moveTo(20, 50);
path.lineTo(100, 100);
path.moveTo(100, 100);
path.lineTo(1, 1);
path.close();
this.setOnTouchListener(this);
}
#Override
public void onDraw(Canvas canvas)
{
//canvas.drawPath(path, paint); <-- error
// to draw background
canvas.drawRect(this.getLeft(), this.getTop(), this.getRight(), this.getBottom(), backgroundPaint);
//to draw two rectangle with blue and green paint
canvas.drawRect(100,100, 340,540, drawPaint);
canvas.drawRect(00,00, 120,80, drawPaint);
//draw text with paint
canvas.drawText("Hello Dear Leader!", 110, 160, textPaint);
//draw a circle with green paint with the touch coordinates
canvas.drawCircle(sx-30,sy-30, 30, circlePaint);
//draw a circle with red paint with the touch coordinates
canvas.drawCircle(sx-80, sy-80, 10, circlePaint);
}
public boolean onTouch(View v, MotionEvent event)
{
//update the coordinates for the OnDraw method above, with wherever we touch
sx = event.getX();
sy = event.getY();
invalidate();
return true;
}
}
for the size you can easily call canvas.getWidth() and canvas.getHeight() and do all your values a percentage of those. Do never assume a constant!
You need to use two separate Paints to use two different colors. And remember that each paint is an object that needs initialisation.
// objects
private Paint drawPaint_WH = new Paint();
private Paint drawPaint_GR = new Paint();
// during construction
drawPaint_WH.setColor(Color.WHITE);
drawPaint_WH.setStyle(Style.FILL);
drawPaint_GR.setColor(Color. GREEN);
drawPaint_GR.setStyle(Style.FILL);
// and then during draw()
canvas.drawRect(100,100, 340,540, drawPaint_WH);
canvas.drawRect(0,0, 120,80, drawPaint_GR);
and then to make a triangle:
// object
private Path trianglePath;
// during construction
trianglePath = new Path();
trianglePath.moveTo(10, 10); // starting point
trianglePath.lineTo(10, 50); // 1st vertix
trianglePath.lineTo(50, 10); // 2nd vertix
trianglePath.lineTo(10, 10); // 3rd vertix and close
// then during draw()
canvas.drawPath(trianglePath, drawPaint_GR) // or whatever paint you want
ps.: do color the background it's easier to call canvas.drawColor(int colorVal);
so im building brick breaker on Java and so far I have most of the UI done but Im having an issue with showing my bricks on my UI. I have written the code for it in my paint method which builds my panel and then my panel is added to a JFrame in another class. Everything shows except for my bricks and I cant seem to figure out why..
// AnimationPanel.java
//
// Informatics 45 Spring 2010
// Code Example: Our Ball Animation Becomes a Game
//
// This is relatively similar to our AnimationPanel in the previous version
// of our ball animation, with two changes:
//
// * The paddle is now drawn, in addition to just the ball. For fun, I've
// drawn the paddle in a different color than the ball.
//
// * This panel has a MouseMotionListener attached to it. The job of a
// MouseMotionListener is to listen for mouse movement within a component.
// In this case, any mouse movement within our AnimationPanel will turn
// into events, which we'll handle by adjusting the center x-coordinate
// of the paddle accordingly.
//
// * Because we need to calculate a new position for the paddle as the mouse
// moves, we'll need to be able to convert coordinates in both directions
// (i.e., fractional coordinates to pixel coordinates and pixel coordinates
// to fractional coordinates).
package inf45.spring2010.examples.animation2.gui;
import inf45.spring2009.examples.animation2.model.Animation;
import inf45.spring2009.examples.animation2.model.AnimationState;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class AnimationPanel extends JPanel
{
private Animation animation;
private inf45.spring2009.examples.animation2.model.AnimationState currentState;
boolean brickVisible[][];
int bricksInRow = 4;
int bricksInColumn = 8;
int brickWidth;
int brickHeight;
int bricksLeft;
public AnimationPanel(Animation animation)
{
this.animation = animation;
currentState = null;
setBackground(Color.WHITE);
addMouseMotionListener(
new MouseMotionAdapter()
{
public void mouseMoved(MouseEvent e)
{
updatePaddlePosition(e.getX());
}
});
}
public void updateState(AnimationState newState)
{
currentState = newState;
}
private void updatePaddlePosition(int pixelX)
{
double paddleCenterX = convertPixelXToX(pixelX);
animation.updatePaddleCenterX(paddleCenterX);
}
public void paint(Graphics g)
{
super.paint(g);
if (currentState == null)
{
return;
}
int centerPixelX = convertXToPixelX(currentState.getBallCenterX());
int centerPixelY = convertYToPixelY(currentState.getBallCenterY());
int radiusX = convertXToPixelX(currentState.getBallRadius());
int radiusY = convertYToPixelY(currentState.getBallRadius());
int topLeftPixelX = centerPixelX - radiusX;
int topLeftPixelY = centerPixelY - radiusY;
int paddleCenterPixelX = convertXToPixelX(currentState.getPaddleCenterX());
int paddleCenterPixelY = convertYToPixelY(currentState.getPaddleCenterY());
int paddleWidthPixels = convertXToPixelX(currentState.getPaddleWidth());
int paddleHeightPixels = convertYToPixelY(currentState.getPaddleHeight());
int paddleTopLeftPixelX = paddleCenterPixelX - (paddleWidthPixels / 2);
int paddleTopLeftPixelY = paddleCenterPixelY - (paddleHeightPixels / 2);
Graphics2D g2d = (Graphics2D) g;
/* for (int x = 0;x<bricksInRow;x++){
for (int y = 0;y<bricksInColumn;y++){
boolean bricks[][] = null;
brickVisible[x][y] = bricks[x][y] ;
{
g2d.setColor(Color.black);
g2d.fillRect(x*brickWidth,y*brickHeight,brickWidth-1,brickHeight-1);
}
}
}
*/
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setColor(Color.BLUE);
g2d.fillOval(topLeftPixelX, topLeftPixelY, radiusX * 2, radiusY * 2);
g2d.setColor(Color.RED);
g2d.fillRect(
paddleTopLeftPixelX, paddleTopLeftPixelY,
paddleWidthPixels, paddleHeightPixels);
}
private int convertXToPixelX(double x)
{
return (int) (x * getWidth());
}
private int convertYToPixelY(double y)
{
return (int) (y * getHeight());
}
private double convertPixelXToX(int pixelX)
{
return (double) pixelX / getWidth();
}
}
It seems like you don't assign any value to brickHeight and brickWidth in your code. This might be the problem.
What MByD said, although as these fields are package-local you could possibly be setting these elsewhere. Also, there is also a NPE problem here:
boolean bricks[][] = null;
brickVisible[x][y] = bricks[x][y] ;
I'm not sure if you added this in before or after you found things weren't working, but this is a sure-fire NullPointerException which will cause the rest of your paint code to not execute when thrown.
EDIT: I'm assuming you commented out the code that wasn't working, but this is the bit you want to make work.