android: перемещение вида при перемещении касания (ACTION_MOVE)

Я бы хотел сделать простой элемент управления: контейнер с внутренним видом. Если я касаюсь контейнера, и я перемещаю палец, я хочу переместить вид, чтобы следовать за моим пальцем.

Какой контейнер (макет) я должен использовать? Как это сделать?

Мне не нужно использовать поверхность, но простой макет.

Что-то вроде этого:

public class MyActivity extends Activity implements View.OnTouchListener { TextView _view; ViewGroup _root; private int _xDelta; private int _yDelta; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); _root = (ViewGroup)findViewById(R.id.root); _view = new TextView(this); _view.setText("TextView!!!!!!!!"); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(150, 50); layoutParams.leftMargin = 50; layoutParams.topMargin = 50; layoutParams.bottomMargin = -250; layoutParams.rightMargin = -250; _view.setLayoutParams(layoutParams); _view.setOnTouchListener(this); _root.addView(_view); } public boolean onTouch(View view, MotionEvent event) { 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(); _xDelta = X - lParams.leftMargin; _yDelta = Y - lParams.topMargin; break; case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_POINTER_DOWN: break; case MotionEvent.ACTION_POINTER_UP: break; case MotionEvent.ACTION_MOVE: RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams(); layoutParams.leftMargin = X - _xDelta; layoutParams.topMargin = Y - _yDelta; layoutParams.rightMargin = -250; layoutParams.bottomMargin = -250; view.setLayoutParams(layoutParams); break; } _root.invalidate(); return true; }} 

В main.xml только RelativeLayout с @+id/root

Я нашел легкий подход, чтобы сделать это с помощью 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; } 

Следуя подходу @Andrey, если вы хотите переместить представление из своего центра, вам нужно только выставить половину высоты и ширины представления для движения.

 float dX, dY; @Override public boolean onTouchEvent(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 - (view.getWidth() / 2)) .y(event.getRawY() + dY - (view.getHeight() / 2)) .setDuration(0) .start(); break; default: return false; } return true; } 

В приведенном ниже коде я создал нечто, называемое RegionView ( git ), которое является контейнером многократного использования, ответственным за управление операциями перетаскивания и масштабирования для каждого из его вложенных детей.

Здесь мы манипулируем top и left коэффициентами детского LayoutParams для имитации движения по диаграмме. Развязывая интерпретацию обработки того, что понимается как операция перетаскивания, и что определяется как операция масштабирования, мы можем обеспечить надежную манипуляцию дочерним View .

 package com.zonal.regionview; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.os.Vibrator; import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.View; import android.widget.RelativeLayout; import java.util.HashMap; import java.util.Map; /** * Created by Alexander Thomas (@Cawfree) on 20/07/2017. */ /** Enables users to customize Regions Of Interest on a Canvas. */ public class RegionView extends RelativeLayout implements View.OnTouchListener, GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener { /* Member Variables. */ private final GestureDetector mGestureDetector; private final ScaleGestureDetector mScaleGestureDetector; private final Map mViewMap; private boolean mScaling; private float mScale; private boolean mWrapContent; private boolean mDropOnScale; public RegionView(Context context) { // Implement the Parent. super(context); // Initialize Member Variables. this.mGestureDetector = new GestureDetector(context, this); this.mViewMap = new HashMap<>(); this.mScaleGestureDetector = new ScaleGestureDetector(context, this); this.mScaling = false; this.mScale = Float.NaN; this.mWrapContent = false; this.mDropOnScale = false; // Register ourself as the OnTouchListener. this.setOnTouchListener(this); } public RegionView(Context context, @Nullable AttributeSet attrs) { // Implement the Parent. super(context, attrs); // Initialize Member Variables. this.mGestureDetector = new GestureDetector(context, this); this.mViewMap = new HashMap<>(); this.mScaleGestureDetector = new ScaleGestureDetector(context, this); this.mScaling = false; this.mWrapContent = false; this.mDropOnScale = false; // Register ourself as the OnTouchListener. this.setOnTouchListener(this); } public RegionView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { // Implement the Parent. super(context, attrs, defStyleAttr); // Initialize Member Variables. this.mGestureDetector = new GestureDetector(context, this); this.mViewMap = new HashMap<>(); this.mScaleGestureDetector = new ScaleGestureDetector(context, this); this.mScaling = false; this.mWrapContent = false; this.mDropOnScale = false; // Register ourself as the OnTouchListener. this.setOnTouchListener(this); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public RegionView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { // Implement the Parent. super(context, attrs, defStyleAttr, defStyleRes); // Initialize Member Variables. this.mGestureDetector = new GestureDetector(context, this); this.mViewMap = new HashMap<>(); this.mScaleGestureDetector = new ScaleGestureDetector(context, this); this.mScaling = false; this.mWrapContent = false; this.mDropOnScale = false; // Register ourself as the OnTouchListener. this.setOnTouchListener(this); } @Override public boolean onTouch(final View v, final MotionEvent event) { // Calculate the PointerId. final int lPointerId = event.getPointerId(event.getActionIndex()); // Handle the TouchEvent. this.getGestureDetector().onTouchEvent(event); this.getScaleGestureDetector().onTouchEvent(event); // Did the user release a pointer? if(event.getAction() == MotionEvent.ACTION_UP) { // Was there a View associated with this Action? final View lView = this.getViewMap().get(lPointerId); // Does the View exist? if(lView != null) { // Remove the View from the Map. this.getViewMap().remove(lPointerId); /** TODO: Provide a Callback? */ } } // Consume all events for now. return true; } @Override public boolean onDown(MotionEvent e) { // Calculate the PointerId. final Integer lPointerId = Integer.valueOf(e.getPointerId(e.getActionIndex())); // Fetch the View. final View lView = this.getViewFor(Math.round(e.getRawX()), Math.round(e.getRawY())); // Is it valid? if(lView != null) { // Watch the View. this.getViewMap().put(lPointerId, lView); // Configure the Anchor. lView.setPivotX(0); lView.setPivotY(0); // Assert that we handled the event. return true; } // Assert that we ignored the event. return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // Are we not scaling? if(!this.isScaling()) { // Calculate the PointerId. final Integer lPointerId = Integer.valueOf(e1.getPointerId(e1.getActionIndex())); // Fetch the View. final View lView = this.getViewMap().get(lPointerId); // Is the scroll valid for a given View? if(lView != null) { // Calculate the Scaled Width and Height of the View. final float lWidth = lView.getWidth() * lView.getScaleX(); final float lHeight = lView.getHeight() * lView.getScaleY(); // Declare the initial position. final int[] lPosition = new int[] { (int)(e2.getX() - ((lWidth) / 2)), (int)(e2.getY() - ((lHeight) / 2)) }; // Are we wrapping content? if(this.isWrapContent()) { // Wrap the Position. this.onWrapContent(lPosition, lWidth, lHeight); } // Update the Drag. this.onUpdateDrag(lView, lPosition); } // Assert we handled the scroll. return true; } // Otherwise, don't permit scrolling. Don't consume the MotionEvent. return false; } /** Forces X/Y values to be coerced within the confines of the RegionView. */ private final void onWrapContent(final int[] pPosition, final float pWidth, final float pHeight) { // Limit the parameters. (Top-Left) pPosition[0] = Math.max(pPosition[0], 0); pPosition[1] = Math.max(pPosition[1], 0); // Limit the parameters. (Bottom-Right) pPosition[0] = Math.min(pPosition[0], (int)(this.getWidth() - pWidth)); pPosition[1] = Math.min(pPosition[1], (int)(this.getHeight() - pHeight)); } /** Updates the Drag Position of a child View within the Layout. Implicitly, we update the LayoutParams of the View. */ private final void onUpdateDrag(final View pView, final int pLeft, final int pTop) { // Allocate some new MarginLayoutParams. final MarginLayoutParams lMarginLayoutParams = new MarginLayoutParams(pView.getLayoutParams()); // Update the Margin. lMarginLayoutParams.setMargins(pLeft, pTop, 0, 0); // Refactor the MarginLayoutParams into equivalent LayoutParams for the RelativeLayout. pView.setLayoutParams(new RelativeLayout.LayoutParams(lMarginLayoutParams)); } @Override public boolean onScale(ScaleGestureDetector detector) { // Calculate the ScaleFactor. float lScaleFactor = detector.getScaleFactor() - 1; // Fetch the Scaled View. final View lView = this.getViewMap().entrySet().iterator().next().getValue(); // Update the ScaleFactor. final float lScale = this.getScale() + lScaleFactor; // Calculate the Proposed Width and Height. final int lWidth = Math.round(lView.getWidth() * lScale); final int lHeight = Math.round(lView.getHeight() * lScale); // Is the View already too large for wrap content? if(lWidth >= this.getWidth() || lHeight >= this.getHeight()) { // Don't update the scale. return false; } // Persist this Scale for the View. lView.setScaleX(lScale); lView.setScaleY(lScale); // Assign the Scale. this.setScale(lScale); // Compute the Position. final int[] lPosition = new int[] { Math.round(detector.getFocusX()) - (lWidth / 2), Math.round(detector.getFocusY()) - (lHeight / 2) }; // Are we wrapping the Position? if(this.isWrapContent()) { // Wrap the Position. this.onWrapContent(lPosition, lWidth, lHeight); } // Update the Drag. this.onUpdateDrag(lView, lPosition); // Assert that we handled the scale. return true; } /** Update the Drag. */ private final void onUpdateDrag(final View pView, final int[] pPosition) { // Call the sub-implementation. this.onUpdateDrag(pView, pPosition[0], pPosition[1]); } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { // Is the user not dragging at all? if(this.getViewMap().size() == 1) { // Fetch the View. final View lView = this.getViewMap().entrySet().iterator().next().getValue(); // Initialize the Scale. this.setScale(lView.getScaleX()); // Assert that we've started scaling. this.setScaling(true); // Inform the callback. return true; } // Otherwise, don't allow scaling. return false; } @Override public void onScaleEnd(ScaleGestureDetector detector) { // Were we scaling? if(this.isScaling()) { // Assert that we've stopped scaling. this.setScaling(false); // Reset the Scale. this.setScale(Float.NaN); // Should we stop dragging now that we've finished scaling? if(this.isDropOnScale()) { // Clear the ViewMap. this.getViewMap().clear(); } } } /** Returns the View colliding with the given co-ordinates. */ private final View getViewFor(final int pX, final int pY) { // Declare the LocationBuffer. final int[] lLocationBuffer = new int[2]; // Iterate the Views. for(int i = 0; i < this.getChildCount(); i++) { // Fetch the child View. final View lView = this.getChildAt(i); // Fetch its absolute position. lView.getLocationOnScreen(lLocationBuffer); // Determine if the MotionEvent collides with the View. if(pX > lLocationBuffer[0] && pY > lLocationBuffer[1] && (pX < lLocationBuffer[0] + (lView.getWidth() * lView.getScaleX())) && (pY < lLocationBuffer[1] + (lView.getHeight() * lView.getScaleY()))) { // Return the View. return lView; } } // We couldn't find a View. return null; } /* Unused Overrides. */ @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onLongPress(MotionEvent e) { } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } /* Getters and Setters. */ private final GestureDetector getGestureDetector() { return this.mGestureDetector; } private final ScaleGestureDetector getScaleGestureDetector() { return this.mScaleGestureDetector; } private final Map getViewMap() { return this.mViewMap; } private final void setScaling(final boolean pIsScaling) { this.mScaling = pIsScaling; } private final boolean isScaling() { return this.mScaling; } private final void setScale(final float pScale) { this.mScale = pScale; } private final float getScale() { return this.mScale; } /** Defines whether we coerce the drag and zoom of child Views within the confines of the Layout. */ public final void setWrapContent(final boolean pIsWrapContent) { this.mWrapContent = pIsWrapContent; } public final boolean isWrapContent() { return this.mWrapContent; } /** Defines whether a drag operation is considered 'finished' once the user finishes scaling a view. */ public final void setDropOnScale(final boolean pIsDropOnScale) { this.mDropOnScale = pIsDropOnScale; } public final boolean isDropOnScale() { return this.mDropOnScale; } } 

Здесь я показываю пример использования:

 package com.zonal.regionview; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.AnalogClock; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Allocate a RegionView. final RegionView lRegionView = new RegionView(this); // Add some example items to drag. lRegionView.addView(new AnalogClock(this)); lRegionView.addView(new AnalogClock(this)); lRegionView.addView(new AnalogClock(this)); // Assert that we only want to drag Views within the confines of the RegionView. lRegionView.setWrapContent(true); // Assert that after we've finished scaling a View, we want to stop being able to drag it until a new drag is started. lRegionView.setDropOnScale(true); // Look at the RegionView. this.setContentView(lRegionView); } } 

Немного изменил решение, предоставленное @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; } } } 
  • Google Glass GDK: как общаться с Android-устройством
  • Как включить / отключить bluetooth программно в android
  • Программно подключаться к сопряженному устройству Bluetooth
  • Давайте будем гением компьютера.