I am trying to implement a touch-draggable ImageViewin an Android app. There are loads of sample codes for this on SO and elsewhere, and all seem to be along the lines of this one.
I tried something similar to this, but it was very laggy and I kept getting the warning
Choreographer﹕ Skipped XX frames! The application may be doing too much work on its main thread.
I followed the Android documentation advice on this and tried to do the work in a separate thread, but this hasn't solved the issue.
Basically I register an OnTouchListener to my view, then in the onTouch() method I start a new thread to try and do the work (the final update of the layout params is sent back to the UI thread for processing using View.post()).
I can't see how to achieve this by doing any less work on the main thread. Is there some way to fix my code? Or is there perhaps a better approach altogether?
The code:
private void setTouchListener() {
final ImageView inSituArt = (ImageView)findViewById(R.id.inSitu2Art);
inSituArt.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
final MotionEvent event2 = event;
new Thread(new Runnable() {
#Override
public void run() {
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) inSituArt.getLayoutParams();
switch (event2.getAction()) {
case MotionEvent.ACTION_DOWN: {
int x_cord = (int) event2.getRawX();
int y_cord = (int) event2.getRawY();
// Remember where we started
mLastTouchY = y_cord;
mLastTouchX = x_cord;
}
break;
case MotionEvent.ACTION_MOVE: {
int x_cord = (int) event2.getRawX();
int y_cord = (int) event2.getRawY();
float dx = x_cord - mLastTouchX;
float dy = y_cord - mLastTouchY;
layoutParams.leftMargin += dx;
layoutParams.topMargin += dy;
layoutParams.rightMargin -= dx;
final FrameLayout.LayoutParams finalLayoutParams = layoutParams;
inSituArt.post(new Runnable() {
#Override
public void run() {
inSituArt.setLayoutParams(finalLayoutParams);
}
});
mLastTouchX = x_cord;
mLastTouchY = y_cord;
}
break;
default:
break;
}
}
}).start();
return true;
}
});
}
with the new Thread you just added more choppiness to the drag movement.
As mentioned by #JohnWhite onTouch is called lots of times in a row, and creating and firing new threads like this is REALLY, REALLY bad.
The general reason why you were encountering those issues to begin with is because the example code that u were following, event though it works, it's a bad unoptimised code.
every time you're calling setLayoutParams the system have to execute a new complete layout pass, and that is a lot of stuff to be doing continuously.
I won't completely write the code for you, but I'll point you to possible alternatives.
First remove all the thread logic you used. You're not really executing any complex calculation, everything can run on the UI-thread.
use offsetTopAndBottom and offsetLeftAndRight to move your ImageView on the screen.
use ViewTransformations with setTranslationX (and Y)
change your ImageView to match_parent and create a custom ImageView. In this custom view you'll override the onDraw method to move the image right before you draw it. Like this:
#Override
public void onDraw(Canvas canvas) {
int saveCount = canvas.save();
canvas.translate(dx, dy); // those are the values calculated by your OnTouchListener
super.onDraw(canvas); // let the image view do the normal draw
canvas.restoreToCount(saveCount); // restore the canvas position
}
this last option is very interesting as it's not changing the layouts in absolutely no way. It's only making the ImageView take care of the whole area and moving the drawing to the correct position. Very efficient way of doing.
on some of those options you'll need to call "invalidate" on the view, so the draw can be called again.
onTouch is an event that fires repeatedly, and you are asking the view to redraw itself in a new location every time onTouch is fired. Draw is a very expensive operation for your application - it should never be asked to do this repeatedly in the regular view hierarchy. Your goal should be to reduce the number of draw commands that occur, so I would set a simple modulus operation that only allows it to execute the move every, say, 10 onTouch events. You will have a tradeoff eventually where it looks choppy because it isn't occurring every frame if you put your modulus too high, but there should be a balance in the 5-30 range.
Above in your class, you'll want something like:
static final int REFRESH_RATE = 10; //or 20, or 5, or 30, whatever works best
int counter = 0;
And then in your touch event:
case MotionEvent.ACTION_MOVE: {
counter += 1;
if((REFRESH_RATE % counter) == 0){ //this is the iffstatement you are looking for
int x_cord = (int) event2.getRawX();
int y_cord = (int) event2.getRawY();
float dx = x_cord - mLastTouchX;
float dy = y_cord - mLastTouchY;
layoutParams.leftMargin += dx;
layoutParams.topMargin += dy;
layoutParams.rightMargin -= dx;
final FrameLayout.LayoutParams finalLayoutParams = layoutParams;
inSituArt.post(new Runnable() {
#Override
public void run() {
inSituArt.setLayoutParams(finalLayoutParams);
}
});
mLastTouchX = x_cord;
mLastTouchY = y_cord;
}
counter = 0;
}
I found a real solution after a long time.
You should use a Drawable instead of a Bitmap.
override fun onDraw(canvas: Canvas?) {
canvas?.save()
canvas?.concat(myCustomMatrix)
mDrawable?.draw(canvas!!)
canvas?.restore()
}
move image (dx, dy)
myCustomMatrix.postTranslate(dx, dy)
invalidate()
Related
I'm trying to create something like the unlock activity of the android, where you drag the lock image outside of the circle to unlock the phone. I want to drag an image and to start an activity when it reaches a certain position. I tried doing it with a canvas but it turned out to paint other parts of my screen. So I tried using the following snippet where I found in another post here:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_touch_ball);
windowwidth = getWindowManager().getDefaultDisplay().getWidth();
windowheight = getWindowManager().getDefaultDisplay().getHeight();
final ImageView balls = (ImageView)findViewById(R.id.ball);
balls.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
LayoutParams layoutParams = (LayoutParams) balls.getLayoutParams();
switch(event.getAction())
{
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int x_cord = (int)event.getRawX();
int y_cord = (int)event.getRawY();
if(x_cord>windowwidth){x_cord=windowwidth;}
if(y_cord>windowheight){y_cord=windowheight;}
layoutParams.leftMargin = x_cord;
layoutParams.topMargin = y_cord;
balls.setLayoutParams(layoutParams);
break;
default:
break;
}
return true;
}
});
}
But this also doesn't work so well, since when I touch the image it changes its size and also doesn't move smoothly on the screen.
Do you know how can I start an activity, lets say when I drag an image from the bottom of the screen to the top?
This is because of following line
layoutParams.leftMargin = x_cord;
layoutParams.topMargin = y_cord;
balls.setLayoutParams(layoutParams);
If you are setting margins, according to that balls will move.
I am trying to implement a drag and drop function in Android.I have choosed to implement it using motion event because I think it is easy to understand.I want to make that image I move to remain in the same position and move a duplication. I don't really know how to explain, I want an image on the corner and when I move it the image still remain there but I move another one that is the same, and If I want to move another one from the same position again I move another position.It's like I am trying to get 10 balls from a basket but in this case the basket and the balls are the same image.
My code so far is this:
public class MainActivity extends ActionBarActivity {
private View selected_item = null;
private int offset_x = 0;
private int offset_y = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewGroup vg = (ViewGroup)findViewById(R.id.container);
vg.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getActionMasked())
{
case MotionEvent.ACTION_MOVE:
int x = (int)event.getX() - offset_x;
int y = (int)event.getY() - offset_y;
int w = getWindowManager().getDefaultDisplay().getWidth() - 100;
int h = getWindowManager().getDefaultDisplay().getHeight() - 100;
if(x > w)
x = w;
if(y > h)
y = h;
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
new ViewGroup.MarginLayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));
lp.setMargins(x, y, 0, 0);
selected_item.setLayoutParams(lp);
break;
default:
break;
}
return true;
}
});
ImageView img = (ImageView)findViewById(R.id.newwall);
img.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
switch(event.getActionMasked())
{
case MotionEvent.ACTION_DOWN:
offset_x = (int)event.getX();
offset_y = (int)event.getY();
selected_item = v;
break;
default:
break;
}
return false;
}
});
Also, I am using a ViewGroup and I have a touchListener attached on it.When I am pressing the Layout I am getting the image where I press, I would like when I drop the image to remain there if I touch somewhere else on the screen.I have tried to set selected_item back to null when I end the drag but I didn't make it good I guess because I was getting error and app crashes.
Please help me solve these 2 problems.
Here is what I have used for similar purpose
ImageView snapshot = (ImageView)findViewById(R.id.nonFullScreenSnapshot);
vg.buildDrawingCache();
cache = Bitmap.createBitmap(vg.getDrawingCache());
Drawable drawableShot = new BitmapDrawable(getResources(), cache);
snapshot.setImageDrawable(drawableShot);
snapshot.setVisibility(View.VISIBLE);
In this case I have an ImageView already available from my xml layout, which should be at least as big as the image you want to duplicate.
So when the user touches your ViewGroup you should make a duplicate of it and then position it on the new location.
But be careful with the number of duplicates, because you can easily run out of memory and you will get OutOfMemory exception.
I've created an OnTouchListener that will allow me to drag views around the screen. This works well, except for one problem:
My views resize instead of leaving the parent. They are in a RelativeLayout, which I suspect is contributing to the problem. They only resize when touching the right or bottom boundaries of my RelativeLayout.
I'm hoping there is a simple solution to this problem, (ideally not involving resizing the RelativeLayout), but I haven't found a solution yet.
Ideally, this solution would be applicable for all views, as I use this OnTouchListener for TextViews as well as ImageViews. I was hoping to find an XML solution. I'm assuming others have dealt with this issue??
PS: Before I post this..I've just remembered I believe there is a flag for hitting a RelativeLayout's border, so maybe I can apply negative margin to the TextView when that happens. Thoughts?
EDIT
Here is my OnTouchListener:
// Dragging functionality for all views and double tap to remove
// functionality for ImageViews
final OnTouchListener onTouchListener = new OnTouchListener()
{
#Override
public boolean onTouch(View v, MotionEvent event)
{
v.bringToFront();
//if(v instanceof android.widget.ImageView)
//mScaleDetector.onTouchEvent(event);
layoutParams = (LayoutParams) v.getLayoutParams();
switch (event.getActionMasked())
{
case MotionEvent.ACTION_DOWN:
// Where the user started the drag
pressed_x = (int) event.getRawX();
pressed_y = (int) event.getRawY();
if (v instanceof android.widget.ImageView)
{
curTime = System.currentTimeMillis();
if(curTime - prevTime <= DOUBLE_TAP_INTERVAL)
{
v.setVisibility(View.GONE);
}
prevTime = curTime;
}
case MotionEvent.ACTION_MOVE:
// Where the user's finger is during the drag
final int x = (int) event.getRawX();
final int y = (int) event.getRawY();
// Calculate change in x and change in y
dx = x - pressed_x;
dy = y - pressed_y;
// Update the margins
layoutParams.leftMargin += dx;
layoutParams.topMargin += dy;
v.setLayoutParams(layoutParams);
// Save where the user's finger was for the next ACTION_MOVE
pressed_x = x;
pressed_y = y;
break;
default:
break;
}
return true;
}
}
I've make a method which moved my ImageView via my touch points , But I wanna see the refreshes like Motion tween in Adobe flash if it possible , here is my code :
int x=0;
int y=0;
#Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
int ActionEvent = event.getAction();
switch (ActionEvent){
case MotionEvent.ACTION_MOVE:{
x = (int) event.getX();
y = (int) event.getY();
}
break;
case MotionEvent.ACTION_UP:{
AbsoluteLayout.LayoutParams ActionMove = new
AbsoluteLayout.LayoutParams(FstBall.getLayoutParams());
ActionMove.x = x;
ActionMove.y = y;
FstBall.setLayoutParams(ActionMove);
}
break;
}
return true;
}
Any ideas?
First of all, you do not want to use AbsoluteLayout as it is deprecated. There are plenty of tutorials on Android Drag-and-Drop. Here's one I recall being useful. I believe it uses FrameLayout and adjusts the left and top margins. You'll also want to check the API Demos. I think I remember an example project there. What do you mean by Adobe motion tween?
I've tried adding some Drag&Drop mechanism to my project.
the way I'm doing it is basically making two operations each time a touch event occurs:
first, updating the parameters I'm using inside onLayout() according to touch event
second, calling requestLayout() for refreshing.
(I've tried it both with OnTouchListener methods and with View's onTouchEvent() as in the code to be followed)
problem is, the result on screen wasn't so good.
it gave the feeling like there is some tearing problem with the dragged view(the new drawing starts before the earlier ends)
the code looks something like this (a simplified version):
public class DragAndDrop extends ViewGroup
{
View touchView;
float touchX;
float touchY;
static int W = 180;
static int H = 120;
public DragAndDrop(Context context)
{
super(context);
touchView = new View(context);
touchView.setBackgroundColor(0xaaff9900);
addView(touchView);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
float width = r - l;
float height = b - t;
int centerX = (int) (touchX - W / 2);
int centerY = (int) (touchY - H / 2);
touchView.layout(centerX, centerY, centerX + W, centerY + H);
}
#Override
public boolean onTouchEvent(MotionEvent event)
{
touchX = event.getX();
touchY = event.getY();
requestLayout();
return true;
}
}
after investigating I found the problem is with calling the requestLayout() method inside the touchListener methods.
I moved that call to a timer which fired it periodically and the result was much better.
does anyone else experienced that and knows of a better way doing it, without using a timer?
I prefer to avoid refreshing all the time more then actually needed.
Thanks for your help!
I think you're saying that your full version involved subclassing View and overriding onTouchEvent() in there, in which case see this answer. With event.getX() and event.getY() in my own similar drag implementation I saw move events that seemed to show my touch oscillating around my finger as it moved; with event.getRawX() and event.getRawY() the move was smooth like my true finger movement.
As an aside, with raw coordinates it's also probably best to save the coordinates of the ACTION_DOWN event and then recalculate the relative distance and requestLayout() each time you get an ACTION_MOVE event.