我想做一个简单的控件:一个里面有视图的容器。如果我触摸容器并移动手指,我想让视图跟着我的手指移动。
我应该使用什么样的容器(布局)?如何做到这一点?
我不需要使用一个表面,但一个简单的布局。
我想做一个简单的控件:一个里面有视图的容器。如果我触摸容器并移动手指,我想让视图跟着我的手指移动。
我应该使用什么样的容器(布局)?如何做到这一点?
我不需要使用一个表面,但一个简单的布局。
当前回答
在下面的代码中,我创建了一个名为RegionView (git)的东西,它是一个可重用的容器,负责管理每个嵌套子元素的拖动和缩放操作。
在这里,我们操纵子视图的LayoutParams的顶部和左侧系数来模拟图的移动。通过解耦处理被理解为拖动操作和被确定为缩放操作的解释,我们可以提供对子视图的可靠操作。
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<Integer, View> 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<Integer, View> 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);
}
}
其他回答
我推荐使用view。translationX和view。翻译来移动你的观点。
Kotlin snippet:
yourView.translationX = xTouchCoordinate
yourView.translationY = yTouchCoordinate
在这个例子中,你可以在它的父边界内移动视图,无论它的大小、完美的动画和捕捉点击。
这种解决方案优于其他注释的原因是,这种方法使用了一个方向板,它可以计算自己,不会依赖于视图位置,这是许多错误的来源。
// 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);
触摸容器,视图将跟随您的手指。
xml代码
<?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代码
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;
}
}
就像这样:
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;
}