I am trying to create a board game
in Android which includes a board with many tiles on top of it, which can be dragged around the board, as well as from and to a player's rack. This is quite similar to the Wordfeud
game.
The board has a fixed size. I want the user to be able to pinch to zoom, and pan around the board, and dragging the tiles around the board. The tiles must be scaled along with the board when zooming in/out.
I am struggling to find the right way to set it up. I have thought of and tried two ways:
HorizontalScrollView
combined with a ScrollView
with a RelativeLayout
as a child. This RelativeLayout
then contains all tiles. This works OK, but how would I then implement the pinch to zoom?Both options don't seem to be the right solution. I'm interested to learn how other Android developers would set this up and hope they provide me a right direction.
OK, first, I would recommend to forget the first solution, which is not very straightforward. The second one is a good start.
Here is my solution :
Activity class
import android.os.Bundle;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class MainActivity extends Activity {
private RelativeLayout mMainLayout;
private InteractiveView mInteractiveView;
private int mScreenWidth;
private int mScreenHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set fullscreen mode
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_main);
// Retrieve the device dimensions to adapt interface
mScreenWidth = getApplicationContext().getResources()
.getDisplayMetrics().widthPixels;
mScreenHeight = getApplicationContext().getResources()
.getDisplayMetrics().heightPixels;
mMainLayout = (RelativeLayout)findViewById(R.id.mainlayout);
// Create the interactive view holding the elements
mInteractiveView = new InteractiveView(this);
mInteractiveView.setLayoutParams(new RelativeLayout.LayoutParams(-2,-2 ));
mInteractiveView.setPosition(-mScreenWidth/2, -mScreenHeight/2);
mMainLayout.addView(mInteractiveView);
// Adding a background to this view
ImageView lImageView = new ImageView(this);
lImageView.setLayoutParams(new RelativeLayout.LayoutParams(-1,-1));
lImageView.setImageResource(R.drawable.board);
mInteractiveView.addView(lImageView);
// Adding a tile we can move on the top of the board
addElement(50, 50);
}
// Creation of a smaller element
private void addElement(int pPosX, int pPosY) {
BoardTile lBoardTile = new BoardTile(this);
Bitmap lSourceImage = BitmapFactory.decodeResource(getResources(), R.drawable.tile);
Bitmap lImage = Bitmap.createScaledBitmap(lSourceImage, 100, 100, true);
lBoardTile.setImage(lImage);
Point lPoint = new Point();
lPoint.x = pPosX;
lPoint.y = pPosY;
lBoardTile.setPosition(lPoint);
mInteractiveView.addView(lBoardTile);
}
The InteractiveView class is just a simple RelativeLayout that reacts to pinch and drag and will hold further elements:
InteractiveView class
import android.content.Context;
import android.graphics.Canvas;
import android.util.FloatMath;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
public class InteractiveView extends RelativeLayout{
private float mPositionX = 0;
private float mPositionY = 0;
private float mScale = 1.0f;
public InteractiveView(Context context) {
super(context);
this.setWillNotDraw(false);
this.setOnTouchListener(mTouchListener);
}
public void setPosition(float lPositionX, float lPositionY){
mPositionX = lPositionX;
mPositionY = lPositionY;
}
public void setMovingPosition(float lPositionX, float lPositionY){
mPositionX += lPositionX;
mPositionY += lPositionY;
}
public void setScale(float lScale){
mScale = lScale;
}
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.translate(getWidth() / 2, getHeight() / 2);
canvas.translate(mPositionX*mScale, mPositionY*mScale);
canvas.scale(mScale, mScale);
super.dispatchDraw(canvas);
canvas.restore();
}
// touch events
private final int NONE = 0;
private final int DRAG = 1;
private final int ZOOM = 2;
private final int CLICK = 3;
// pinch to zoom
private float mOldDist;
private float mNewDist;
private float mScaleFactor = 0.01f;
// position
private float mPreviousX;
private float mPreviousY;
int mode = NONE;
@SuppressWarnings("deprecation")
public OnTouchListener mTouchListener = new OnTouchListener(){
public boolean onTouch(View v, MotionEvent e) {
float x = e.getX();
float y = e.getY();
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN: // one touch: drag
mode = CLICK;
break;
case MotionEvent.ACTION_POINTER_2_DOWN: // two touches: zoom
mOldDist = spacing(e);
mode = ZOOM; // zoom
break;
case MotionEvent.ACTION_UP: // no mode
mode = NONE;
break;
case MotionEvent.ACTION_POINTER_2_UP: // no mode
mode = NONE;
break;
case MotionEvent.ACTION_MOVE: // rotation
if (e.getPointerCount() > 1 && mode == ZOOM) {
mNewDist = spacing(e) - mOldDist;
mScale += mNewDist*mScaleFactor;
invalidate();
mOldDist = spacing(e);
} else if (mode == CLICK || mode == DRAG) {
float dx = (x - mPreviousX)/mScale;
float dy = (y - mPreviousY)/mScale;
setMovingPosition(dx, dy);
invalidate();
mode = DRAG;
}
break;
}
mPreviousX = x;
mPreviousY = y;
return true;
}
};
// finds spacing
private float spacing(MotionEvent event) {
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return FloatMath.sqrt(x * x + y * y);
}
}
Then, we have an "element" class (called BoardTile) that will create tiles going on this InteractiveView. This class is more complex, because the view doesn't take the whole screen and we will have to test if the touch event is inside the bounds of the object.
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.view.MotionEvent;
import android.view.View;
public class BoardTile extends View
{
private Bitmap mCardImage;
private final Paint mPaint = new Paint();
private final Point mSize = new Point();
private final Point mStartPosition = new Point();
private Region mRegion;
public BoardTile(Context context)
{
super(context);
mRegion = new Region();
this.setOnTouchListener(mTouchListener);
}
public final Bitmap getImage() { return mCardImage; }
public final void setImage(Bitmap image)
{
mCardImage = image;
setSize(mCardImage.getWidth(), mCardImage.getHeight());
}
@Override
protected void onDraw(Canvas canvas)
{
Point position = getPosition();
canvas.drawBitmap(mCardImage, position.x, position.y, mPaint);
}
public final void setPosition(final Point position)
{
mRegion.set(position.x, position.y, position.x + mSize.x, position.y + mSize.y);
}
public final Point getPosition()
{
Rect bounds = mRegion.getBounds();
return new Point(bounds.left, bounds.top);
}
public final void setSize(int width, int height)
{
mSize.x = width;
mSize.y = height;
Rect bounds = mRegion.getBounds();
mRegion.set(bounds.left, bounds.top, bounds.left + width, bounds.top + height);
}
public final Point getSize() { return mSize; }
public OnTouchListener mTouchListener = new OnTouchListener(){
@Override
public boolean onTouch(View v, MotionEvent event) {
// Is the event inside of this view?
if(!mRegion.contains((int)event.getX(), (int)event.getY()))
{
return false;
}
if(event.getAction() == MotionEvent.ACTION_DOWN)
{
mStartPosition.x = (int)event.getX();
mStartPosition.y = (int)event.getY();
bringToFront();
return true;
}
else if(event.getAction() == MotionEvent.ACTION_MOVE)
{
int x = 0, y = 0;
x = (int)event.getX() - mStartPosition.x;
y = (int)event.getY() - mStartPosition.y;
mRegion.translate(x, y);
mStartPosition.x = (int)event.getX();
mStartPosition.y = (int)event.getY();
invalidate();
return true;
}
else
{
return false;
}
}
};
}
This is not a complete solution, you would also have to dispatch the touch events on the tiles so that they will take into account the scale of the InteractiveView.
Hope it will help you start !