基本思想:当触摸View时,系统记下当前触摸点的坐标;当手指移动时,系统记下移动后的触摸点坐标,从而获取到相对于前一次坐标点的偏移量,并通过偏移量来修改View的坐标,这样不断重复,从而实现滑动过程。
先看一下简单的app:
方法一:layout方法
原理:
我们知道,View进行绘制时,会调用onLayout方法来设置显示的位置。同样,可以通过修改View的left、top、right、bottom四个属性来控制View的坐标。
代码:
public class MainActivity extends AppCompatActivity { private Button btn; private int startX; private int startY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); //获取点击事件发生的x,y坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: startX = x; startY = y; break; case MotionEvent.ACTION_MOVE: //计算偏移量 int offsetX = x - startX; int offsetY = y - startY; //在当前left、top、right、bottom的基础上加上偏移量 //这样每次移动后,View都会调用Layout方法来对自己重新布局, // 从而达到移动View的效果 btn.layout(btn.getLeft() + offsetX, btn.getTop() + offsetY, btn.getRight() + offsetX, btn.getBottom() + offsetY); break; case MotionEvent.ACTION_UP: break; } return true; } }); }方法二:offsetLeftAndRight()与offsetTopAndBottom()
原理:
这个方法相当于系统提供了一个对左右、上下移动的API的封装。原理和方法一一样。
代码:
public class MainActivity extends AppCompatActivity { private Button btn; private int startX; private int startY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); //获取点击事件发生的x,y坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: startX = x; startY = y; break; case MotionEvent.ACTION_MOVE: //计算偏移量 int offsetX = x - startX; int offsetY = y - startY; //同时对left和right进行偏移 btn.offsetLeftAndRight(offsetX); //同时对left和right进行偏移 btn.offsetTopAndBottom(offsetY); break; case MotionEvent.ACTION_UP: break; } return true; } }); }方法三:LayoutParams
原理:
LayoutParams保存了一个View的布局参数。因此可以在程序中,通过改变LayoutParams来动态地修改一个布局的位置参数(通常改变的是View的Margin属性),从而达到改变View位置的效果。
代码:
public class MainActivity extends AppCompatActivity { private Button btn; private int startX; private int startY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); //获取点击事件发生的x,y坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: startX = x; startY = y; break; case MotionEvent.ACTION_MOVE: //计算偏移量 int offsetX = x - startX; int offsetY = y - startY; /** * 注意:通过getLayoutParams()获取LayoutParams时, * 需要根据View所在父布局的类型来设置不同的类型, * (为什么呢?我们知道LayoutParams作用就是子视图告诉父视图 * 它们想要变成什么样的布局,因此必须要使用父控件.LayoutParams,况且只有ViewGroup * 才有LayoutParams属性) * 我们的button是放在RelativeLayout中, * 那么就可以使用RelativeLayout.LayoutParams */ RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) btn.getLayoutParams(); layoutParams.leftMargin = btn.getLeft() + offsetX; layoutParams.topMargin = btn.getTop() + offsetY; btn.setLayoutParams(layoutParams); break; case MotionEvent.ACTION_UP: break; } return true; } }); }当然,除了使用父布局的LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这样的功能。(这样更加方便,不需要考虑父布局的类型,它们本质是一样的,这也是我一般使用的方法)
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) btn.getLayoutParams(); layoutParams.leftMargin = btn.getLeft() + offsetX; layoutParams.topMargin = btn.getTop() + offsetY; btn.setLayoutParams(layoutParams);方法四:scrollTo与scrollBy
原理:
在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View的位置。这两个方法的区别:
scrollTo(x,y)表示移动到一个具体的坐标点(x,y);
scrollBy(dx,dy)表示移动的增量为dx、dy;
注意:scrollTo、scrollBy方法移动的是View的content,即让View的内容移动,如果在ViewGroup中使用scrollTo、scrollBy方法,那么移动的将是所有子View,但如果在View中使用,那么移动的将是View的内容,例如:TextView,content就是它的文本,ImageView,content就是它的drawable对象。所以不能在View中使用这两个方法来拖动这个View。
因此:我们使用(View)getParent()获取到当前View所在的ViewGroup
之后调用scrollBy(-offsetX,-offsetY)即可。
纳尼?偏移量怎么写个负数?
解释:
我们之前说了,scrollTo、scrollBy方法移动的是View的content,必须得写成负数才能随着手指的移动而移动。画个小图可以看的更明白:
代码:
public class MainActivity extends AppCompatActivity { private Button btn; private int startX; private int startY; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn = (Button) findViewById(R.id.btn); btn.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { int action = event.getAction(); //获取点击事件发生的x,y坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: startX = x; startY = y; break; case MotionEvent.ACTION_MOVE: //计算偏移量 int offsetX = x - startX; int offsetY = y - startY; ((View) btn.getParent()).scrollBy(-offsetX,-offsetY); break; case MotionEvent.ACTION_UP: break; } return true; } }); }我们可以看源码就知道scrollBy底层也调用了scrollTo
public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }说到源码,这里由不得提一嘴:
mScrollX = getScrollX();
mScrollY = getScrollY();
在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。
方法五:Scroller
原理:
其实原理和scrollTo、scrollBy是类似。但是如果现在有这样一个需求:通过点击按钮,让一个ViewGroup的子View向右移动200个像素。我们当然可以使用scrollBy方法设置偏移量,但是不管使用scrollTo还是scrollBy方法,子View的平移都是瞬间发生的,在事件执行的时候平移就已经完成了,这样用户体验太差了,同时也不符合Google建议的使用自然的过度动画来实现移动效果的原则。因此Scroller类就应用而生了,通过Scroller类可以实现平滑移动的效果。
使用三部曲:
第一步:初始化Scroller
通过它的构造方法来创建一个Scroller对象
Scroller scroller = new Scroller(context);第二步:重写computeScroll()方法,实现模拟滑动
@Override public void computeScroll() { super.computeScroll(); //判断Scroller是否执行完毕 if (mScroller.computeScrollOffset()) { ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //通过重绘来不断调用computeScroll方法 invalidate(); } }第三步:startScroll开启模拟过程
mScroller.startScroll(int startX,int startY,int dx,int dy,int duration);
方法六:属性动画
ObjectAnimator .ofFloat(targetView,"translationX",0,100) .setDuration(100) .start();方法七:ViewDragHelper以及一些开源的动画库如Nineoldandroids
没有写实例代码的会在接下来的代码中都有涉及。