Hello! Thank you for reading.
I've been looking for ways to correctly create a multi-direction scrollable Android layout of a certain size (5x that of the user's screen), that can be flinged (& panned) and that can also be zoomed on.
To note that having the child (ConstraintLayout) be the wanted size (5x the user's screen) is not the problem here.
Obviously, I found many answers online for ways to do this with different ways to integrate it. Originally, I was about to post all the different integration I had unsuccessfully tried so far, until I encountered Oded's solution. In this one, everything that didn't work correctly worked.
This means:
The layout allowed scrolling the entire width and height of the child but no more
The bounds/borders of the scrollable area scaled correctly with the Zooming (this was the deal-breaker, almost no custom layout could do this)
The layout could be panned
The pivot point was correct (in the middle).
This was a huge relief, as modifying different already-made but lacking integrations (to fix bounds+zooming issues mostly) had already taken days.
I would recommend anyone having a similar issue in the future to use this integration.
In any case, there are a few issues, including: goes back to (0,0) upon screen orientation changes, no double-tap-to-zoom, and no flinging. I don't believe it to be hard to fix those two first issues, however this third one is quite problematic. I want the user to be able to freely view this large area. Not having the ability to fling will make moving in the app slower than preferable.
I've already tried different ways to do this, such as using the Scroller class with a GestureDetector, using a "Flinger" runnable class (containing a Scroller), using some form of the Choreographer, and others.
I've been unable to get it to work, and solutions often completely break the way this layout is doing scrolling (for instance, I've noticed scrollX never changes, and no scrollTo function is present (it is replaced by mPosX)). Usually correct solutions to the addition of Flinging seem impossible to use here, and in some cases, implementations have caused the scroller to go several thousand units from where it should be.
Here is my backup copy of Odid's solution, with the only changes being minor adjustments to avoid Android Studio warnings.
Snippet (mandatory):
#Override
public boolean onTouchEvent(MotionEvent ev) {
mOnTouchEventWorkingArray[0] = ev.getX();
mOnTouchEventWorkingArray[1] = ev.getY();
mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);
ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
mScaleDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
// Save the ID of this pointer
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
if (mIsScaling && ev.getPointerCount() == 1) {
// Don't move during a QuickScale.
mLastTouchX = x;
mLastTouchY = y;
break;
}
float dx = x - mLastTouchX;
float dy = y - mLastTouchY;
float[] topLeft = {0f, 0f};
float[] bottomRight = {getWidth(), getHeight()};
/*
* Corners of the view in screen coordinates, so dx/dy should not be allowed to
* push these beyond the canvas bounds.
*/
float[] scaledTopLeft = screenPointsToScaledPoints(topLeft);
float[] scaledBottomRight = screenPointsToScaledPoints(bottomRight);
dx = Math.min(Math.max(dx, scaledBottomRight[0] - mCanvasWidth), scaledTopLeft[0]);
dy = Math.min(Math.max(dy, scaledBottomRight[1] - mCanvasHeight), scaledTopLeft[1]);
mPosX += dx;
mPosY += dy;
mTranslateMatrix.preTranslate(dx, dy);
mTranslateMatrix.invert(mTranslateMatrixInverse);
mLastTouchX = x;
mLastTouchY = y;
invalidate();
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
// Extract the index of the pointer that left the touch sensor
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
Thank you very much for your help.
I've been trying to figure out how to do it, I have the size of the screen and the size of the emulator I use, how do I change the size to fit all devices (Including tablets and phones) and also position them correctly because it seems like the X and Y on a tablet is different than one that is on the phone.
EDIT:
I tried converting pixels to DPI that way:
public CreatorView(Context c) {
super(c);
this.c=c;
WindowManager wm = (WindowManager) c.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
this.screenw= display.getWidth();
this.screenh=display.getHeight();
this.PixelDetect = BitmapFactory.decodeResource( getResources(), R.drawable.custom_pixel);
this.smallpixel = Bitmap.createScaledBitmap(PixelDetect, (int)getPixelsFromDip(3,c), (int)getPixelsFromDip(3,c), false);
this.grass=BitmapFactory.decodeResource(getResources(), R.drawable.block_grass);
this.grassSide=BitmapFactory.decodeResource(getResources(), R.drawable.block_grassside);
this.grassTop=BitmapFactory.decodeResource(getResources(), R.drawable.block_grasstop);
this.orange=BitmapFactory.decodeResource(getResources(), R.drawable.block_cube1);
this.dirt=BitmapFactory.decodeResource(getResources(), R.drawable.block_dirt);
this.dirt2=BitmapFactory.decodeResource(getResources(), R.drawable.block_dirt2);
this.dirt3=BitmapFactory.decodeResource(getResources(), R.drawable.block_dirt3);
this.arrowno=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_noclick);
this.arrown=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_normal);
this.arrowl=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_left);
this.arrowr=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_right);
this.arrowu=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_up);
this.arrowd=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_down);
this.arrowul=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_upperleft);
this.arrowur=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_upperright);
this.arrowdl=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_downleft);
this.arrowdr=BitmapFactory.decodeResource(getResources(), R.drawable.arrow_downright);
this.arrowno=Bitmap.createScaledBitmap(arrowno, arrowno.getWidth()*3, arrowno.getHeight()*3, false);
this.save=BitmapFactory.decodeResource(getResources(), R.drawable.button_save);
this.bin_Empty=BitmapFactory.decodeResource(getResources(), R.drawable.bin_empty);
this.bin_Full=BitmapFactory.decodeResource(getResources(), R.drawable.bin_full);
this.bin_Empty=Bitmap.createScaledBitmap(bin_Empty, bin_Empty.getWidth()*3, bin_Empty.getHeight()*3, false);
this.bin_Full=Bitmap.createScaledBitmap(bin_Full, bin_Full.getWidth()*3, bin_Full.getHeight()*3, false);
this.arrown=Bitmap.createScaledBitmap(arrown, arrown.getWidth()*3, arrown.getHeight()*3, false);
this.arrowl=Bitmap.createScaledBitmap(arrowl, arrowl.getWidth()*3, arrowl.getHeight()*3, false);
this.arrowr=Bitmap.createScaledBitmap(arrowr, arrowr.getWidth()*3, arrowr.getHeight()*3, false);
this.arrowu=Bitmap.createScaledBitmap(arrowu, arrowu.getWidth()*3, arrowu.getHeight()*3, false);
this.arrowd=Bitmap.createScaledBitmap(arrowd, arrowd.getWidth()*3, arrowd.getHeight()*3, false);
this.arrowul=Bitmap.createScaledBitmap(arrowul, arrowul.getWidth()*3, arrowul.getHeight()*3, false);
this.arrowur=Bitmap.createScaledBitmap(arrowur, arrowur.getWidth()*3, arrowur.getHeight()*3, false);
this.arrowdl=Bitmap.createScaledBitmap(arrowdl, arrowdl.getWidth()*3, arrowdl.getHeight()*3, false);
this.arrowdr=Bitmap.createScaledBitmap(arrowdr, arrowdr.getWidth()*3, arrowdr.getHeight()*3, false);
Menu_Add(arrowno,0,true,"arrows");
Menu_Add(bin_Empty,1,false,"bin");
Menu_Add(save,2,false,"save");
Menu_Add(grassTop,1,true,"grasstop");
Menu_Add(grassSide,2,true,"grassside");
Menu_Add(grass,3,true,"grass");
Menu_Add(dirt,4,true,"dirt");
Menu_Add(orange,5,true,"orange");
arrowsp=new Point();
arrowsp.x=0;
arrowsp.y=0;
}
private void Menu_Add(Bitmap b,int order,boolean vertical,String name)
{
Point p=new Point();
if(order==0){
p.x=0;
p.y=0;
MenuButton m=new MenuButton(order,b , vertical, p,name);
menuButtonList.add(m);
}
else{
for (MenuButton m : menuButtonList) {
if((m.isVertical()==vertical||order==1)&&m.getOrder()+1==order ){
if(vertical){
p.x=0;
p.y=m.getP().y+m.getBit().getHeight()+(int)getPixelsFromDip(2,c);
}
else{
p.x=m.getP().x+m.getBit().getWidth()+(int)(getPixelsFromDip(2,c));
p.y=0;
}
MenuButton m2=new MenuButton(order,b , vertical, p,name);
menuButtonList.add(m2);
return;
}
}
}
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paintAlpha = new Paint();
paintAlpha.setAlpha(200);
canvas.drawARGB(255, 86, 194, 243);
for(MenuButton m : menuButtonList){
switch(m.getName()){
case "bin":
if(bin_isEmpty){
canvas.drawBitmap(bin_Empty, getPixelsFromDip(m.getP().x,c), getPixelsFromDip(m.getP().y,c),paintAlpha);
}
else{
canvas.drawBitmap(bin_Full, getPixelsFromDip(m.getP().x,c), getPixelsFromDip(m.getP().y,c),paintAlpha);
}
break;
case "arrows":
canvas.drawBitmap(m.getBit(),getPixelsFromDip(m.getP().x,c),getPixelsFromDip(m.getP().y,c),paintAlpha);
switch (arrowcheck) {
case "normal":
canvas.drawBitmap(arrown, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "left":
canvas.drawBitmap(arrowl, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "right":
canvas.drawBitmap(arrowr, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "down":
canvas.drawBitmap(arrowd, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "up":
canvas.drawBitmap(arrowu, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "upleft":
canvas.drawBitmap(arrowul, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "upright":
canvas.drawBitmap(arrowur, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "downleft":
canvas.drawBitmap(arrowdl, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
case "downright":
canvas.drawBitmap(arrowdr, getPixelsFromDip(arrowsp.x,c), getPixelsFromDip(arrowsp.y,c),paintAlpha);
break;
}
break;
default:
canvas.drawBitmap(m.getBit(),getPixelsFromDip(m.getP().x,c),getPixelsFromDip(m.getP().y,c),paintAlpha);
break;
}
}
}
public static float getPixelsFromDip(float dip,Context context)
{
//TODO move this to aplication class?
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, context.getResources().getDisplayMetrics());
}
this is how it shows in the emulator (correct):
http://puu.sh/ehhHp/05c1530218.png
this is how it shows in my phone:
http://puu.sh/ehhKX/b28ee357e3.png
please help :(
What you don't want to do is to use pixels. What you most probably also don't want to do is to speficy the width/height using dps unless you're prepared to set different values for your different resolution devices (by specifying the values in #dimen and overriding them for different sizes).
If you know approximately how much of the screen width (or height) you would like your ImageView to occupy, the easiest thing you can do to achieve it is to use weights within a LinearLayout.
An example would be this (handwritten and missing all the NS prefixes)
<LinearLayout
...
...
orientation=vertical>
<some empty filler here with weight=1 and width="match_parent"/>
<ImageView
...
...
width="match_parent"
scaleType=fitCenter (or whatever works for your images)
weight=1 />
<some empty filler here with weight=1 width="match_parent"/>
</LinearLayout>
What this would do is to make sure your ImageView takes 1/3rd of the screen width as determined by weights. You then control the image scaling by setting the correct scaleType. This will work universally across all phone sizes and tablets and will always take 1/3rd of the width of the screen regardless of the resolution or the orientation of the device (which may or may not be what you want).
You can also play around with the weights if you want more/less than 1/3rd of the screen width for your images.
Other solutions (using dp for instance) are already mentioned but they're a bit more involved in that you have to override them for different resolutions as described above.
Why not use percents?
As example - percent_x = device.widht / 100;
And begin draw image in this position * percent_x?
In every device if should be look similar, but with streches.
use dp to define width and height of image. Or we can calculate the percentage of image size of our screen.
ok if you use an xml layout just defing layout_width and layout_hight using density pixel value "10dp" for example. those scale with the screen size so you get the same result according to screen size.
if you use canvas then when you set the hight and width of an objecs use the following method:
/**
* get width or hight as density independent pixels and return the real
* amount of pixels requiered for the current screen
* used to programaticlly set width and hight using dip
* #param dip - the amount of dip
* #param context - the activity used in
* #return - return the real amount of pixels required for the current screen
*/
public static float getPixelsFromDip(float dip,Context context)
{
//TODO move this to aplication class?
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, context.getResources().getDisplayMetrics());
}
another option is to calculate max screen size, and the precent from it you need
here is the method for the max size:
/**
* get maximum width and hight as dp avaliable in the screen
* #param context
* #return
*/
public static float[] getMaxPixels(Context context)
{
//TODO move this to application class
float result[]=new float[2];
DisplayMetrics displayMetrics=context.getResources().getDisplayMetrics();
float screenWidthInDp=displayMetrics.widthPixels/displayMetrics.density;
float screenHeightInDp=displayMetrics.heightPixels/displayMetrics.density;
result[0]=screenWidthInDp;
result[1]=screenHeightInDp;
return result;
}
last but not least rule of thumb density is:mdpi=1, hdpi=1.5 and ldpi is 0.75
so once you get the pixels for one screen size you can multiply by those values to get a rough estimate for the value in the other
enjoy.
Positioning in terms of pixels will definitely differ as there are different type of devices with different DPI.
So declaring Imageview with widht, height in dp should serve your problem. Coming to position, it should be maintained Relatively or through gravity.
We need your code to understand what exactly the problem is and what more you need.
EDIT:
Convert Dip to pixel and the apply it to your Bitmap size.
It will maintain physical size and gets resolution according to device.
public static int convertDipToPixels(float dips)
{
return (int) (dips * appContext.getResources().getDisplayMetrics().density + 0.5f);
}
Edit 2:
Just give a trail changing your loop with this code snippet and check it it replicates same on all devices
for (MenuButton m : menuButtonList) {
if((m.isVertical()==vertical||order==1)&&m.getOrder()+1==order ){
if(vertical){
p.x=0;
p.y=(int)getPixelsFromDip(100,c);
}
else{
p.x=m(int)(getPixelsFromDip(100,c));
p.y=0;
}
MenuButton m2=new MenuButton(order,b , vertical, p,name);
menuButtonList.add(m2);
return;
}
}
Does anyone know if there is a simple way to make images snap to grid when dragging a bitmap?
At the moment I can touch a bitmap and move it smoothly around the screen. I want to be able to make it snap to an invisible grid whilst you are dragging.
It's what I do in an application I'm just finishing at the moment. When the user is dragging something on the screen, I display a visible snap grid, and the object is snapped to that grid when the dragging has finished. To show a grid, my approach is to use a separate custom View that I named GridOverLayView. It is overlaid over the entire screen area and it very simply draws a snap grid in its onDraw() method. It is made visible only when something is being dragged.
Now, concerning the actual Activity in which dragging and dropping is handled, one particular constant I've defined is:
static final int SNAP_GRID_INTERVAL = 20;
When the object is being dragged around, i.e. when processing event.getAction()==MotionEvent.ACTION_MOVE events within my OnTouchListener, I perform snapping of the object's location to grid using the following:
RelativeLayout.LayoutParams par = (RelativeLayout.LayoutParams) mThingBeingDragged.getLayoutParams();
par.topMargin = Math.round((event.getRawY() - draggedInitialY) / SNAP_GRID_INTERVAL ) * SNAP_GRID_INTERVAL;
par.leftMargin = Math.round((event.getRawX() - draggedInitialX) / SNAP_GRID_INTERVAL ) * SNAP_GRID_INTERVAL;
mThingBeingDragged.setLayoutParams(par);
...where draggedInitialY and draggedInitialX store the initial touch position recorded during the initial MotionEvent.ACTION_DOWN.
A further nice touch is to allow the object being dragged to be moved around without snapping, but have it snap to the grid only in the .ACTION_UP when the user lifts their finger. In practice, this feels much nicer to use.
private PointF touchDown;
private int gridCellSize = 10;
private OnTouchListener touchListener = new OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
{
touchDown = new PointF(event.getRawX(), event.getRawY());
break;
}
case MotionEvent.ACTION_MOVE:
{
RelativeLayout.LayoutParams par = (RelativeLayout.LayoutParams) v.getLayoutParams();
float yDeff = ((event.getRawY() - touchDown.y) / gridCellSize ) * gridCellSize;
float xDeff = ((event.getRawX() - touchDown.x) / gridCellSize ) * gridCellSize;
if(Math.abs(xDeff) >= gridCellSize)
{
par.leftMargin += (int)(xDeff / gridCellSize) * gridCellSize;
touchDown.x = event.getRawX() - (xDeff % gridCellSize);
}
if(Math.abs(yDeff) >= gridCellSize)
{
par.topMargin += (int)(yDeff / gridCellSize) * gridCellSize;
touchDown.y = event.getRawY() - (yDeff % gridCellSize);
}
v.setLayoutParams(par);
break;
}
default :
{
break;
}
}
return true;
}
};