实现滑动的最朴素直接的方式就是使用View类自带的scrollTo/scrollBy方法了。
能够直接linearLayout.getScaleY(); 和 查看 mScrollX 变量,源码里面看注释就能够发现:
mScrollX:表示离视图起始位置的x水平方向的偏移量
mScrollY:表示离视图起始位置的y垂直方向的偏移量
分别经过getScrollX() 和getScrollY()方法得到。
注意:mScrollX和mScrollY指的并非坐标,而是偏移量。java
public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); } } } public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
从以上的代码能够看出,scrollTo 和 scrollBy区别,其实2者的效果是同样的。android
scrollTo(int x,int y):
若是偏移位置发生了改变,就会给mScrollX和mScrollY赋新值,改变当前位置。
注意:x,y表明的不是坐标点,而是偏移量。
例如:
我要移动view到坐标点(100,100),那么个人偏移量就是(0,,0) -(100,100) = (-100,-100),我就要执行view.scrollTo(-100,-100),达到这个效果。ide
scrollBy(int x,int y):
从源码中看出,它其实是调用了scrollTo(mScrollX + x, mScrollY + y);mScrollX + x和mScrollY + y,即表示在原先偏移的基础上在发生偏移,通俗的说就是相对咱们当前位置偏移。布局
scrollTo方法是滑动到指定位置,而scrollBy方法是滑动指定的位移量,在原先的基础上发生偏移。
根据父类VIEW里面移动,若是移动到了超出的地方,就不会显示。post
代码以下 :动画
咱们了解到使用scrollTo/scrollBy方法实现View的滑动是很简单直接的,那么简单的背后有什么代价呢?代价就是滑动不是“弹性的”,能够用个定时器不停的调用,让它开起来是顺滑的。this
使用动画来实现View的滑动主要经过改变View的translationX和translationY参数来实现,使用动画的好处在于滑动效果是平滑的。
上面咱们提到过,View的x、y参数决定View的当前位置,经过改变translationX和translationY,咱们就能够改变View的当前位置。
咱们可使用属性动画或者补间动画来实现View的平移。
使用动画的实现方式比较简单,下面就使用间补动画和属性动画作个案例,以下 : spa
首先,咱们先来看一下如何使用补间动画来实现View的平移。补间动画资源定义以下(anim.xml):code
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true"> <translate android:duration="100" android:fromXDelta="0" android:fromYDelta="0" android:interpolator="@android:anim/linear_interpolator" android:toXDelta="100" android:toYDelta="100"/> </set>
而后在onCreat方法中调用startAnimation方法便可。使用补间动画实现View的滑动有一个缺陷,那就是移动的知识View的“影像”,这意味着其实View并未真正的移动,只是咱们看起来它移动了而已。拿Button来举例,倘若咱们经过补间动画移动了一个Button,咱们会发现,在Button的原来位置点击屏幕会出发点击事件,而在移动后的Button上点击不会触发点击事件。xml
接下来,咱们看看如何用属性动画来实现View的平移。使用属性动画实现View的平移更加简单,只须要如下一条语句:
button.setOnClickListener((v) -> { float translationX = (int) v.getTranslationX(); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "translationX", translationX, -100, translationX); objectAnimator.setDuration(3000); objectAnimator.start(); });
以上代码即实现了使用属性动画把targetView在3秒内向右平移100px。使用属性动画的限制在于真正的属性动画只能够在Android 3.0+使用(一些第三方库实现的兼容低版本的属性动画不是真正的属性动画),优势就是它能够真正的移动View而不是仅仅移动View的影像。
通过以上的描述,使用属性动画实现View的滑动看起来是个不错的选择,并且一些View的复杂的滑动效果只有经过动画才能比较方便的实现。
属性动画实现效果以下 : (其实很顺滑的,只是录制效果有点差。)
经过改变布局参数来实现View的滑动的思想很简单:
好比向右移动一个View,只须要把它的marginLeft参数增大,向其它方向移动同理,只需改变相应的margin参数。
还有一种比较拐弯抹角的方法是在要移动的View的旁边预先放一个View(初始宽高设为0)。
而后好比咱们要向右移动View,只需把预先放置的那个View的宽度增大,这样就把View“挤”到右边了。代码示例以下:
LinearLayout.LayoutParams lps = (LinearLayout.LayoutParams) v.getLayoutParams(); lps.leftMargin += 100; v.requestLayout();
以上代码即实现了把mButton向右滑动100px。经过改变布局参数来实现的滑动效果也不是平滑的。
Android里Scroller类是为了实现View平滑滚动的一个Helper类。一般在自定义的View时使用,在View中定义一个私有成员mScroller = new Scroller(context)。设置mScroller滚动的位置时,并不会致使View的滚动,一般是用mScroller记录/计算View滚动的位置,再重写View的computeScroll(),完成实际的滚动。 相关API介绍以下
mScroller.getCurrX() //获取mScroller当前水平滚动的位置 mScroller.getCurrY() //获取mScroller当前竖直滚动的位置 mScroller.getFinalX() //获取mScroller最终中止的水平位置 mScroller.getFinalY() //获取mScroller最终中止的竖直位置 mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置 mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置 //滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间 mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms mScroller.startScroll(int startX, int startY, int dx, int dy, int duration) mScroller.computeScrollOffset() //返回值为boolean,true说明滚动还没有完成,false说明滚动已经完成。这是一个很重要的方法,一般放在View.computeScroll()中,用来判断是否滚动是否结束。
举例说明,自定义一个CustomView,使用Scroller实现滚动:
import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.LinearLayout; import android.widget.Scroller; public class CustomView extends LinearLayout { private static final String TAG = "Scroller"; private Scroller mScroller; public CustomView(Context context, AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); } //调用此方法滚动到目标位置 public void smoothScrollTo(int fx, int fy) { int dx = fx - mScroller.getFinalX(); int dy = fy - mScroller.getFinalY(); smoothScrollBy(dx, dy); } //调用此方法设置滚动的相对偏移 public void smoothScrollBy(int dx, int dy) { //设置mScroller的滚动偏移量 mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy); invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,不然不必定会刷新界面,看不到滚动效果 } @Override public void computeScroll() { //先判断mScroller滚动是否完成 if (mScroller.computeScrollOffset()) { //这里调用View的scrollTo()完成实际的滚动 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); //必须调用该方法,不然不必定能看到滚动效果 postInvalidate(); } super.computeScroll(); } }
效果图以下 :
ViewDragHelper从名称上就能够看出, 这是一个用来简化view拖拽操做的帮助类。并且使用起来也很简单, 很方便,只须要几个方法和1个Callback就能够实现一个能够拖动到view。
须要注意个是:ViewDragHelper是做用在一个ViewGroup上,也就是说他不能直接做用到被拖拽的view, 其实这也很好理解,由于view在布局中的位置是父ViewGroup决定的。
如何使用ViewGroup实现一个能够拖动的view?
一、获取ViewDragHelper的实例,注意,这里不能直接new,而是使用ViewDragHelper的一个静态方法:
ViewDragHelper.create(ViewGroup forParent, float sensitivity, ViewDragHelper.Callback cb);
参数1: 一个ViewGroup, 也就是ViewDragHelper将要用来拖拽谁下面的子view
参数2:灵敏度,通常设置为1.0f就行
参数3:一个回调,用来处理拖动到位置
二、继承ViewDragHelper.Callback类,该类有个抽象方法:tryCaptureView(View view, int pointerId) 表示尝试捕获子view,这里必定要返回true, 返回true表示容许。
三、重写两个方法int clampViewPositionHorizontal(View child, int left, int dx)和int clampViewPositionHorizontal(View child, int left, int dx) 这两个方法分别用来处理x方向和y方向的拖动的,返回值该child如今的位置。
四、重写ViewGroup的onInterceptTouchEvent(MotionEvent ev)用来拦截事件
五、重写ViewGroup的onTouchEvent(MotionEvent event) 在这里面只要作两件事:mDragHelper.processTouchEvent(event);处理拦截到的事件,这个方法会在返回前分发事件;return true 表示消费了事件。
简单的5步就能够实现一个能够任意拖动到view了,固然咱们须要在clampViewPositionHorizontal和clampViewPositionHorizontal中作一点工做,以防止view超出了边界。
下面咱们本身定义一个CustomViews来实现能够随意拖动的View
public class CustomViews extends LinearLayout { private ViewDragHelper mDragHelper; public CustomViews(Context context) { super(context); init(); } public CustomViews(Context context, AttributeSet attrs) { super(context, attrs); init(); } public CustomViews(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { /** * @params ViewGroup forParent 必须是一个ViewGroup * @params float sensitivity 灵敏度 * @params Callback cb 回调 */ mDragHelper = ViewDragHelper.create(this, 1.0f, new ViewDragCallback()); } private class ViewDragCallback extends ViewDragHelper.Callback { /** * 尝试捕获子view,必定要返回true * * @param view 尝试捕获的view * @param pointerId 指示器id? * 这里能够决定哪一个子view能够拖动 */ @Override public boolean tryCaptureView(View view, int pointerId) { return true; } /** * 处理水平方向上的拖动 * * @param child 被拖动到view * @param left 移动到达的x轴的距离 * @param dx 建议的移动的x距离 */ @Override public int clampViewPositionHorizontal(View child, int left, int dx) { System.out.println("left = " + left + ", dx = " + dx); // 两个if主要是为了让viewViewGroup里 if (getPaddingLeft() > left) { return getPaddingLeft(); } if (getWidth() - child.getWidth() < left) { return getWidth() - child.getWidth(); } return left; } /** * 处理竖直方向上的拖动 * * @param child 被拖动到view * @param top 移动到达的y轴的距离 * @param dy 建议的移动的y距离 */ @Override public int clampViewPositionVertical(View child, int top, int dy) { // 两个if主要是为了让viewViewGroup里 if (getPaddingTop() > top) { return getPaddingTop(); } if (getHeight() - child.getHeight() < top) { return getHeight() - child.getHeight(); } return top; } /** * 当拖拽到状态改变时回调 * * @params 新的状态 */ @Override public void onViewDragStateChanged(int state) { switch (state) { case ViewDragHelper.STATE_DRAGGING: // 正在被拖动 break; case ViewDragHelper.STATE_IDLE: // view没有被拖拽或者 正在进行fling/snap break; case ViewDragHelper.STATE_SETTLING: // fling完毕后被放置到一个位置 break; } super.onViewDragStateChanged(state); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_DOWN: mDragHelper.cancel(); // 至关于调用 processTouchEvent收到ACTION_CANCEL break; } /** * 检查是否能够拦截touch事件 * 若是onInterceptTouchEvent能够return true 则这里return true */ return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { /** * 处理拦截到的事件 * 这个方法会在返回前分发事件 */ mDragHelper.processTouchEvent(event); return true; } } XML 使用以下 : <com.and.mvp.base.ganhuoi.module.test.CustomViews android:id="@+id/customViews" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="移动我" /> </com.and.mvp.base.ganhuoi.module.test.CustomViews>
效果图 以下 :
其实这两个方法分别是对左右移动和上下移动的封装,传入的就是偏移量。
下面就写一个案例,Java代码以下 :
public class MyText extends TextView { int lastX; int lastY; public MyText(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { //获取到手指处的横坐标和纵坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: //计算移动的距离 int offX = x - lastX; int offY = y - lastY; offsetLeftAndRight(offX); offsetTopAndBottom(offY); break; } return true;//记得返回true,说明被咱们这里消化了改事件 } }
XML 使用方法以下 :
<com.and.mvp.base.ganhuoi.module.test.MyText android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="移动我" />
效果图以下 :
(7) layout()
若是你将滑动后的目标位置的坐标传递给layout(),这样子就会把view的位置给从新布置了一下,在视觉上就是view的一个滑动的效果。
这个方法和 第6个是同样的 只不过最后调用的方法不同罢了
public boolean onTouchEvent(MotionEvent event) { //获取到手指处的横坐标和纵坐标 int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: lastX = x; lastY = y; break; case MotionEvent.ACTION_MOVE: //计算移动的距离 int offX = x - lastX; int offY = y - lastY; //调用layout方法来从新放置它的位置 ,其余的和第6个方法是同样的,只是下面调用的这行代码不同的罢了。 layout(getLeft() + offX, getTop() + offY, getRight() + offX, getBottom() + offY); break; } return true; }