How do I create a scalable and customizable view with many drag-and-drop views on top of it?
I am trying to create board game
on Android that includes a board with many fragments on top of it that can be dragged around the board and from and to a turntable stand. It's a lot like a game Wordfeud
.
The board has a fixed size. I want the user to be able to pinch to zoom in and move around the board and drag tiles around the board. Plates need to scale with the board when scaling / outputting.
I am struggling to find the right way to set it up. I thought and tried two ways:
- Used
HorizontalScrollView
in conjunction withScrollView
withRelativeLayout
as a child. This oneRelativeLayout
then contains all the tiles. This works fine, but how can I implement scaling to zoom in? - Using this example to zoom and pan a view: http://android-developers.blogspot.nl/2010/06/making-sense-of-multitouch.html . But how could I add tiles on top of this view that zoom in and out with this view?
Both options don't seem to be the right solution. I am curious to see how other Android developers will install this and hope they provide me with the right direction.
source to share
Thanks to @PopGorn who pointed me in the right direction. I looked at several examples that involved dispatching touch events. I got this good answer: fooobar.com/questions/440441 / ...
source to share
OK, firstly, I would recommend forgetting about the first solution, which is not very easy. The second is a good start.
Here is my solution:
Action 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 RelativeLayout that reacts to pinch and drag and will contain additional 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 the fragments that go into this InteractiveView. This class is more complex because the view does not render the entire screen and we will need to check if the touch event is within 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 will also have to send touch events to the tiles to take into account the InteractiveView scale.
Hope this helps you get started!
source to share