I\'d like to do a simple control: a container with a view inside. If I touch the container and I move the finger, I want to move the view to follow my finger.
What
Changed a bit a solution provided by @Vyacheslav Shylkin to remove dependencies of manually entered numbers.
import android.app.Activity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class MainActivity extends Activity implements View.OnTouchListener
{
private int _xDelta;
private int _yDelta;
private int _rightMargin;
private int _bottomMargin;
private ImageView _floatingView;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this._floatingView = (ImageView) findViewById(R.id.textView);
this._floatingView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
{
@Override
public boolean onPreDraw()
{
if (_floatingView.getViewTreeObserver().isAlive())
_floatingView.getViewTreeObserver().removeOnPreDrawListener(this);
updateLayoutParams(_floatingView);
return false;
}
});
this._floatingView.setOnTouchListener(this);
}
private void updateLayoutParams(View view)
{
this._rightMargin = -view.getMeasuredWidth();
this._bottomMargin = -view.getMeasuredHeight();
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight());
layoutParams.bottomMargin = this._bottomMargin;
layoutParams.rightMargin = this._rightMargin;
view.setLayoutParams(layoutParams);
}
@Override
public boolean onTouch(View view, MotionEvent event)
{
if (view == this._floatingView)
{
final int X = (int) event.getRawX();
final int Y = (int) event.getRawY();
switch (event.getAction() & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_DOWN:
RelativeLayout.LayoutParams lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
this._xDelta = X - lParams.leftMargin;
this._yDelta = Y - lParams.topMargin;
break;
case MotionEvent.ACTION_MOVE:
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
layoutParams.leftMargin = X - this._xDelta;
layoutParams.topMargin = Y - this._yDelta;
layoutParams.rightMargin = this._rightMargin;
layoutParams.bottomMargin = this._bottomMargin;
view.setLayoutParams(layoutParams);
break;
}
return true;
}
else
{
return false;
}
}
}
Touch the container and the view will follow your finger.
xml code
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/floating_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<ImageView
android:id="@+id/btn_chat"
android:layout_width="42dp"
android:layout_height="42dp"
/>
<LinearLayout>
Java code
public class DashBoardActivity extends Activity implements View.OnClickListener, View.OnTouchListener {
float dX;
float dY;
int lastAction;
LinearLayout floatingLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dashboard);
floatingLayout = findViewById(R.id.floating_layout);
floatingLayout.setOnTouchListener(this);
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
dX = view.getX() - event.getRawX();
dY = view.getY() - event.getRawY();
lastAction = MotionEvent.ACTION_DOWN;
break;
case MotionEvent.ACTION_MOVE:
view.setY(event.getRawY() + dY);
view.setX(event.getRawX() + dX);
lastAction = MotionEvent.ACTION_MOVE;
break;
case MotionEvent.ACTION_UP:
if (lastAction == MotionEvent.ACTION_DOWN)
Toast.makeText(DashBoardActivity.this, "Clicked!", Toast.LENGTH_SHORT).show();
break;
default:
return false;
}
return true;
}
}
I've found an easy approach to do that with the ViewPropertyAnimator:
float dX, dY;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
dX = view.getX() - event.getRawX();
dY = view.getY() - event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
view.animate()
.x(event.getRawX() + dX)
.y(event.getRawY() + dY)
.setDuration(0)
.start();
break;
default:
return false;
}
return true;
}
The same as @Alex Karshin's answer, I change a bit.
public class MovingObject implements OnTouchListener {
private RelativeLayout.LayoutParams lParams;
private PointF viewPoint, prePoint, currPoint;
public MovingObject() {
lParams = null;
viewPoint = new PointF();
prePoint = new PointF();
currPoint = new PointF();
}
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
viewPoint.set(view.getX(), view.getY());
prePoint.set(event.getRawX(), event.getRawY());
lParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_POINTER_DOWN:
break;
case MotionEvent.ACTION_POINTER_UP:
break;
case MotionEvent.ACTION_MOVE:
currPoint.set(event.getRawX(), event.getRawY());
moveToCurrentPoint(view);
break;
}
view.invalidate();
return true;
}
private void moveToCurrentPoint(View view) {
float dx = currPoint.x - prePoint.x - prePoint.x + viewPoint.x;
float dy = currPoint.y - prePoint.y - prePoint.y + viewPoint.y;
lParams.leftMargin = (int) (prePoint.x + dx);
lParams.topMargin = (int) (prePoint.y + dy);
view.setLayoutParams(lParams);
}
}
Create a custom touch listener class (in Kotlin):
(This code restrict your view from dragging out of its parent view)
class CustomTouchListener(
val screenWidth: Int,
val screenHeight: Int
) : View.OnTouchListener {
private var dX: Float = 0f
private var dY: Float = 0f
override fun onTouch(view: View, event: MotionEvent): Boolean {
val newX: Float
val newY: Float
when (event.action) {
MotionEvent.ACTION_DOWN -> {
dX = view.x - event.rawX
dY = view.y - event.rawY
}
MotionEvent.ACTION_MOVE -> {
newX = event.rawX + dX
newY = event.rawY + dY
if ((newX <= 0 || newX >= screenWidth - view.width) || (newY <= 0 || newY >= screenHeight - view.height)) {
return true
}
view.animate()
.x(newX)
.y(newY)
.setDuration(0)
.start()
}
}
return true
}
}
How to use it?
parentView.viewTreeObserver.addOnGlobalLayoutListener { view.setOnTouchListener(CustomTouchListener(parentView.width, parentView.height)) }
parentView
is the parent of your view.
In this example you can move the view within it's parent bounds no matter it's size, flawless animation, and catch clicks.
The reason that this solution is superior to other comments is that this approach uses a Directional Pad which calculate itself and won't relay on the View positions which is a the source for a-lot of bugs.
// we could use this gameobject as a wrapper that controls the touch event of the component(the table)
// and like so, we can have a click event and touch events
public abstract class GameObjectStackOverflow {
private static final int CLICK_DURATION = 175;
protected View view;
protected ViewGroup container;
protected Context mContext;
private boolean onMove = false;
private boolean firstAnimation = true;
private Animator.AnimatorListener listener;
protected float parentWidth;
protected float parentHeight;
protected float xmlHeight;
protected float xmlWidth;
// Those are the max bounds
// whiting the xmlContainer
protected float xBoundMax;
protected float yBoundMax;
// This variables hold the target
// ordinates for the next
// animation in case an animation
// is already in progress.
protected float targetX;
protected float targetY;
private float downRawX;
private float downRawY;
public GameObjectStackOverflow(@NonNull Context context, @NonNull ViewGroup container)
{
mContext = context;
this.container = container;
}
// This method is the reason the constructor
// does not get view to work with in the first
// place. This method helps us to work with
// android main thread in such way that we
// separate the UI stuff from the technical
// stuff
protected View initGraphicView(@NonNull LayoutInflater inflater, int resource, boolean add)
{
view = inflater.inflate(resource, container, add);
view.post(getOnViewAttach());
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return onTouchEvent(event);
}
});
return view;
}
// This method attach an existing
// view that is already inflated
protected void attachGraphicView(@NonNull final View view)
{
this.view = view;
view.post(getOnViewAttach());
}
// This method is anti-boiler code.
// attaching runnable to the view
// task queue to finish the
// initialization of the game object.
private Runnable getOnViewAttach()
{
return new Runnable() {
@Override
public void run() {
parentHeight = container.getHeight();
parentWidth = container.getWidth();
view.setX(currentX);
view.setY(currentY);
}
};
}
private void click() {
// recover the view to the previous location [not needed]
// not needed
//view.animate()
// .x(prevPosX)
// .y(prevPosY)
// .setDuration(0)
// .start();
}
// maybe restore the View view, Motion event
public boolean onTouchEvent(MotionEvent event)
{
view.getParent().requestDisallowInterceptTouchEvent(true);
//if(!selected) return false;
switch (event.getAction())
{
case MotionEvent.ACTION_UP:
if (event.getEventTime() - event.getDownTime() < CLICK_DURATION) click(); // are you missing break here?
onMove = false;
// if needed to update network entity do it here
break;
case MotionEvent.ACTION_DOWN:
firstAnimation = true;
xBoundMax = parentWidth - xmlWidth;
yBoundMax = parentHeight - xmlHeight;
downRawX = event.getRawX();
downRawY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
if (!onMove) {
if (event.getEventTime() - event.getDownTime() < CLICK_DURATION) break;
else onMove = true;
}
// Calculating the position the
// view should be posed at.
float offsetX = event.getRawX() - downRawX;
float offsetY = event.getRawY() - downRawY;
downRawX = event.getRawX();
downRawY = event.getRawY();
targetX = currentX + offsetX;
targetY = currentY + offsetY;
// Checking if view
// is within parent bounds
if (targetX > parentWidth - xmlWidth) targetX = xBoundMax;
else if (targetX < 0) targetX = 0;
if (targetY > parentHeight - xmlHeight) targetY = yBoundMax;
else if (targetY < 0) targetY = 0;
// This check is becuase the user may just click on the view
// So if it's a not a click, animate slowly but fastly
// to the desired position
if (firstAnimation) {
firstAnimation = false;
animate(70, getNewAnimationListener());
break;
}
if (listener != null) break;
animate(0, null);
break;
case MotionEvent.ACTION_BUTTON_PRESS:
default:
return false;
}
return true;
}
// this method gets used only in
// one place. it's wrapped in a method
// block because i love my code like
// i love women - slim, sexy and smart.
public Animator.AnimatorListener getNewAnimationListener() {
listener = new Animator.AnimatorListener() {
@Override public void onAnimationStart(Animator animation) { }
@Override public void onAnimationCancel(Animator animation) { }
@Override public void onAnimationRepeat(Animator animation) { }
@Override public void onAnimationEnd(Animator animation) {
animation.removeListener(listener);
listener = null;
view.setAnimation(null);
animate(0, null);
}
};
return listener;
}
float currentX = 0, currentY = 0;
private void animate(int duration, @Nullable Animator.AnimatorListener listener) {
view.animate()
.x(targetX)
.y(targetY)
.setDuration(duration)
.setListener(listener)
.start();
currentX = targetX;
currentY = targetY;
}
protected void setSize(float width, float height)
{
xmlWidth = width;
xmlHeight = height;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
layoutParams.width = (int) width;
layoutParams.height = (int) height;
view.setLayoutParams(layoutParams);
}
public View getView() {
return view;
}
//This interface catches the onclick even
// that happened and need to decide what to do.
public interface GameObjectOnClickListener {
void onGameObjectClick(GameObjectStackOverflow object);
}
public float getXmlWidth() {
return xmlWidth;
}
public float getXmlHeight() {
return xmlHeight;
}
}
This version got stripped from the big stuff which used to have network entity that gets updated live and such, it should work.
you should use it this way
public class Tree extends GameObject
{
public Tree(Context context, ViewGroup container, View view, int width, int height) {
super(context, manager, container);
attachGraphicView(view);
super.setSize(_width, _height);
}
}
and than
mTree= new Tree(mContext, mContainer, xmlTreeView);
mTree.getView().setOnTouchListener(getOnTouchListener(mTree));
you should have this too but this can be easily removed
//Construct new OnTouchListener that reffers to the gameobject ontouchevent
private View.OnTouchListener getOnTouchListener(final GameObject object) {
return new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
return object.onTouchEvent(event);
}
};
}
If you have the container inside a ScrollView or double dimension ScrollView you should add this line to the onTouch
view.getParent().requestDisallowInterceptTouchEvent(true);