我想做一个简单的控件:一个里面有视图的容器。如果我触摸容器并移动手指,我想让视图跟着我的手指移动。

我应该使用什么样的容器(布局)?如何做到这一点?

我不需要使用一个表面,但一个简单的布局。


当前回答

创建一个自定义的触摸监听器类(在Kotlin中):

(这段代码限制了视图从父视图中拖出)

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
    }
}

如何使用它?

parentView.viewTreeObserver.addOnGlobalLayoutListener { view.setOnTouchListener(CustomTouchListener(parentView.width, parentView.height)) }

parentView是视图的父视图。

其他回答

改变了一点由@Vyacheslav Shylkin提供的解决方案,以删除手动输入数字的依赖性。

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;
        }
    }
}

我推荐使用view。translationX和view。翻译来移动你的观点。

Kotlin snippet:

yourView.translationX = xTouchCoordinate
yourView.translationY = yTouchCoordinate

创建一个自定义的触摸监听器类(在Kotlin中):

(这段代码限制了视图从父视图中拖出)

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
    }
}

如何使用它?

parentView.viewTreeObserver.addOnGlobalLayoutListener { view.setOnTouchListener(CustomTouchListener(parentView.width, parentView.height)) }

parentView是视图的父视图。

//如果你想移动你的相机或任何东西,然后按照下面的方法来做 //我在相机上实现的情况下,你可以应用它在任何你想要的

public class VideoCallActivity extends AppCompatActivity implements 
  View.OnTouchListener {
 FrameLayout myLayout1;

@SuppressLint("ClickableViewAccessibility")
@Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  //in the frame layout I am setting my camera
   myLayout1.setOnTouchListener(this);
  
  }

   float dX, dY;

@Override
public boolean onTouch(View view, MotionEvent event) {
    switch (event.getAction()) {
 //this is your code
        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;
}

在这个例子中,你可以在它的父边界内移动视图,无论它的大小、完美的动画和捕捉点击。

这种解决方案优于其他注释的原因是,这种方法使用了一个方向板,它可以计算自己,不会依赖于视图位置,这是许多错误的来源。

// 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;
}
}

这个版本被剥夺了大的东西,过去有网络实体,得到更新的live和这样,它应该工作。


你应该这样使用它

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);
    }
}

和比

mTree= new Tree(mContext, mContainer, xmlTreeView);     
mTree.getView().setOnTouchListener(getOnTouchListener(mTree));

你也应该有这个,但这个很容易去掉

//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);
        }
    };
}

如果你有容器在一个ScrollView或双层ScrollView,你应该添加这行到onTouch

view.getParent().requestDisallowInterceptTouchEvent(true);