##1、Android控件跟随手指移动方法补充

在工作中遇到了这个问题,然后百度了下大致方法多为一种,即通过重写onTouchEvent()记录前后移动的相对坐标,然后根据相对坐标设置控件位置.我们先来看看这个方法,先贴代码
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yxf.removeablewidget.MainActivity">

    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/button_1"
        android:background="@android:color/holo_green_dark"
        />
</RelativeLayout>

MainActivity.java

package com.yxf.removeablewidget;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
    Button btn1;//第一个按钮
    float lastRawX, lastRawY;//用于记录按钮上一次状态坐标

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button) findViewById(R.id.button_1);
        btn1.setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.button_1:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int dx = (int) (event.getRawX() - lastRawX);//相对坐标
                        int dy = (int) (event.getRawY() - lastRawY);//相对坐标
                        btn1.layout(btn1.getLeft() + dx, btn1.getTop() + dy, btn1.getRight() + dx, btn1.getBottom() + dy);//设置按钮位置
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                }
                break;

            default:

                break;
        }


        return false;//一般这个保留false,若为true,此控件其他事件会被截断
    }
}

运行结果如图

android 拖动列表控件 android移动控件位置_动态控件


发现其对鼠标的跟随效果并不明显,有一定的位置偏差,那这是什么造成的呢?

原因是实际上相对坐标是float型的,但是

View.layout(int left,int top,int right,int bottom);

这个方法是int型,对于多次的去尾后,自然实际移动和真实相对坐标的累加会有一定偏差.
那么如何解决这个问题呢?
可以先看看下个方法的代码,为了方便对比,我在原代码中添加新加一个按钮作为对比
activity_main

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yxf.removeablewidget.MainActivity">

    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/button_1"
        android:background="@android:color/holo_green_dark"
        />
    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/button_2"
        android:background="@android:color/holo_red_dark"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        />
</RelativeLayout>

MainActivity

package com.yxf.removeablewidget;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
    Button btn1;//第一个按钮
    float lastRawX, lastRawY;//用于记录按钮上一次状态坐标

    Button btn2;//第二个按钮

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button) findViewById(R.id.button_1);
        btn1.setOnTouchListener(this);
        btn2 = (Button) findViewById(R.id.button_2);
        btn2.setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.button_1:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int dx = (int) (event.getRawX() - lastRawX);//相对坐标
                        int dy = (int) (event.getRawY() - lastRawY);//相对坐标
                        btn1.layout(btn1.getLeft() + dx, btn1.getTop() + dy, btn1.getRight() + dx, btn1.getBottom() + dy);//设置按钮位置
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                }
                break;
            case R.id.button_2:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int lastX= (int) (v.getLeft()+event.getX());//现在的相对Parent触摸坐标点
                        int lastY= (int) (v.getTop()+event.getY());//现在的相对Parent触摸坐标点
                        int width=v.getWidth();
                        int height=v.getHeight();
                        v.layout(lastX - width / 2, lastY - height / 2, lastX + width / 2, lastY + height / 2);
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                }
                break;
            default:

                break;
        }


        return false;//一般这个保留false,若为true,此控件其他事件会被截断
    }
}

运行结果如图

android 拖动列表控件 android移动控件位置_移动_02


有了对比则可以很明显的发现之前蓝色块的鼠标非常容易脱离原位置,但是红色块的鼠标则一直处在红色方块的中间.第二种方法相较于第一种,只有一点点小小的不同,第二种方法没有选择用相对位移,而是采用了直接获取手指点击的坐标,这样就不存在强制类型转化去尾多次造成的真实距离偏差.

##2、在RelativeLayout移动控件控件还原位置问题
然后在工作中还遇到一个位置还原的问题也在此说明下
百度了还算挺久才找到原因,资料很少便在此说明希望能帮到遇到同样问题的朋友们
在上面代码中的的onTouchEvent()方法的case MotionEvent.ACTION_DOWN:下增加一条

v.bringToFront();

会出现如图状况

android 拖动列表控件 android移动控件位置_relativelayout_03


bringToFront()这个函数只是将空间放到布局的最前面,但是怎么导致位置重置了呢?

原因在于我们使用了ViewGroup的setTop(),setLeft(),layout()等方法,而这些方法只被建议用在ViewGroup内部,外部调用时结果如何是不确定的.当我们使用bringToFront()方法时,会调用父控件的bringChildToFront()方法,方法如下

/**
     * Change the view's z order in the tree, so it's on top of other sibling
     * views. This ordering change may affect layout, if the parent container
     * uses an order-dependent layout scheme (e.g., LinearLayout). Prior
     * to {@link android.os.Build.VERSION_CODES#KITKAT} this
     * method should be followed by calls to {@link #requestLayout()} and
     * {@link View#invalidate()} on the view's parent to force the parent to redraw
     * with the new child ordering.
     *
     * @see ViewGroup#bringChildToFront(View)
     */
    public void bringToFront() {
        if (mParent != null) {
            mParent.bringChildToFront(this);
        }
    }

然后在ViewGroup源码中找到了bringChildToFront()方法

@Override
    public void bringChildToFront(View child) {
        final int index = indexOfChild(child);
        if (index >= 0) {
            removeFromArray(index);
            addInArray(child, mChildrenCount);
            child.mParent = this;
            requestLayout();
            invalidate();
        }
    }

发现这个方法调用了requestLayout()方法刷新了布局,ViewGroup刷新布局则会调用onLayout()方法,在RelativeLayout中找到onLayout()方法如下

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //  The layout has actually already been performed and the positions
        //  cached.  Apply the cached values to the children.
        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

然后问题所在出现了,发现这里重新调用了child的layout方法,而这里的layout方法是根据RelativeLayout自己的规则来确定child的位置的,有兴趣的可以去RelativeLayout.LayoutParams类中查看下,在此不多作介绍,那么我们如何解决这个问题呢?既然RelativeLayout是有自己的规则方法的,那我们就使用他承认的方法就好了,我们可以自己定义一个方法如下:

private void setRelativeViewLocation(View view, int left, int top, int right, int bottom) {
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(right - left, bottom - top);
        params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
        ViewParent parent = view.getParent();
        View p = (View) parent;
        int marginRight = p.getWidth() - right;
        int marginBottom = p.getHeight() - bottom;
        params.setMargins(left, top, marginRight, marginBottom);
        view.setLayoutParams(params);
    }

用这个方法便可以替代View.layout(int left,int top,int right,int bottom)方法
至此问题解决了.
贴上最终完整代码:
activity_main

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yxf.removeablewidget.MainActivity">

    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/button_1"
        android:background="@android:color/holo_green_dark"
        />
    <Button
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:id="@+id/button_2"
        android:background="@android:color/holo_red_dark"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        />
</RelativeLayout>

MainActivity

package com.yxf.removeablewidget;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener {
    Button btn1;//第一个按钮
    float lastRawX, lastRawY;//用于记录按钮上一次状态坐标

    Button btn2;//第二个按钮

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button) findViewById(R.id.button_1);
        btn1.setOnTouchListener(this);
        btn2 = (Button) findViewById(R.id.button_2);
        btn2.setOnTouchListener(this);
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (v.getId()) {
            case R.id.button_1:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        btn1.bringToFront();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int dx = (int) (event.getRawX() - lastRawX);//相对坐标
                        int dy = (int) (event.getRawY() - lastRawY);//相对坐标
                        setRelativeViewLocation(btn1, btn1.getLeft() + dx, btn1.getTop() + dy, btn1.getRight() + dx, btn1.getBottom() + dy);//设置按钮位置
                        lastRawX = event.getRawX();
                        lastRawY = event.getRawY();
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                }
                break;
            case R.id.button_2:
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        v.bringToFront();
                        break;
                    case MotionEvent.ACTION_MOVE:
                        int lastX = (int) (v.getLeft() + event.getX());//现在的相对Parent触摸坐标点
                        int lastY = (int) (v.getTop() + event.getY());//现在的相对Parent触摸坐标点
                        int width = v.getWidth();
                        int height = v.getHeight();
                        setRelativeViewLocation(v, lastX - width / 2, lastY - height / 2, lastX + width / 2, lastY + height / 2);

                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:

                        break;
                }
                break;
            default:

                break;
        }


        return false;//一般这个保留false,若为true,此控件其他事件会被截断
    }

    private void setRelativeViewLocation(View view, int left, int top, int right, int bottom) {
        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(right - left, bottom - top);
        params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
        params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
        ViewParent parent = view.getParent();
        View p = (View) parent;
        int marginRight = p.getWidth() - right;
        int marginBottom = p.getHeight() - bottom;
        params.setMargins(left, top, marginRight, marginBottom);
        view.setLayoutParams(params);
    }
}