本文出自门心叼龙的头条号,属于原创内容,转载请注明出处。
我们知道,在功能机时代我们在手机上的任何操作都是在键盘上完成的,只有通过键盘才能完成输入操作,只能通过键盘才能和手机交互,进入智能机时代以后我们所有操作都可以通过触摸屏的方式来完成,而我们最常见的操作就是滑动,手机屏幕和PC端的显示屏最大的区别就是,PC显示器屏幕很大,一屏可以显示跟多内容,而手机屏幕就小了很多,一屏幕所能显示的内容就非常有限,我们可以通过上下滑动,左右滑动翻页来显示我们想要看到的内容。我们打开任意一款手机应用,无处不在的上滑,下滑,左滑,右滑操作,由此可见滑动操作在移动手机开发当中是多么的重要,因此今天我们来研究View的滑动。
在Android系统中View给我们提供了两个非常重要关于滑动操作的方法scrollTo和scrollBy,下面我们通过scrollTo和scrollBy来完成View的滑动。
View的滑动
布局文件如下:
布局文件中有两个控件,一个TextView和一个Button,我们点击按钮Button调用TextView的scrollTo方法和scrollBy方法,来观察View滚动的效果。
mBtnScroll.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { mTxtScroll.scrollTo(200,200); } });
![47a7669422e93ace929143e72ecfebe2.gif](https://img-blog.csdnimg.cn/img_convert/47a7669422e93ace929143e72ecfebe2.gif)
此时Hello world往上方进行了移动,再次点击按钮调用 mTxtScroll.scrollTo(200,200),发现HelloWorld的位置没有发生任何的变化。
接下来我们把调用参数修改为-200,即:
mTxtScroll.scrollTo(-200,-200);
![77f6ea3755082791947c7bfeb468752b.gif](https://img-blog.csdnimg.cn/img_convert/77f6ea3755082791947c7bfeb468752b.gif)
再看看效果,HelloWorld往右下方移动,scrollTo测试完毕,我们在看看scrollBy是什么效果
mBtnScroll.setOnClickListener(new View.OnClickListener(){ @Override public void onClick(View v) { mTxtScroll.scrollBy(200,200); } });
![fde1589c8bf6b6fd8bf5fb7f87dfa8eb.gif](https://img-blog.csdnimg.cn/img_convert/fde1589c8bf6b6fd8bf5fb7f87dfa8eb.gif)
我连续点击了三次,HelloWorld连续往左上方移动了三次,这一点和scrollTo还是有些不同的,我们看看View的scrollTo和scrollBy的源码:
public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
通过源码我们不难解释这个效果了,scrollBy内部调用了scrollTo,而且每次移动都是在目前mScrollX和mScrollY的基础上进行的移动的,因此scrollTo是绝对移动,scrollBy是相对移动。
需要注意的上很多人在理解上有些转不过弯认为x,y都为正应该往右下方移动,怎么会向左上方移动呢,其实x,y并不是要 移动的坐标位置,而是相对于Hello world的原始位置的偏移量,通常在View在默认的情况下,我们首先都会往上滑,或者往左滑,这都是一个习惯的操作,所以往左滑,往上滑为正值也就不难理解了。
另外我们需要注意的是scrollTo和scrollBy滑动的是View的内容,而View自身的位置并不会发生任何变化,不妨我们做个测试验证一下页面的初始打开的时候我们打印下当前View的位置信息 V/ScrollTestActivity: scrollX:0;scrollY:0|x:0.0;y:0.0 紧接着调用mTxtScroll.scrollTo(-200,-200);移动View的位置,然后我们再次打印View的位置信息:V/ScrollTestActivity: scrollX:-200;scrollY:-200|x:0.0;y:0.0 你会惊奇的发现,View的x,y坐标没有任何变化,只是View的mScrollX和mScrollY的值发生了变化,也就是说View滑动的是自己的内容,而View本身在布局中的位置并没有发生任何的改变。
通过以上测试我们不难得到以下几条结论:
- 1.scrollTo是绝对滑动,它是相对于Hello world原始位置的滑动
- 2.scrollBy是相对移动,是相对于Hello world当前位置的滑动
- 3.无论是scrollTo(x,y)还是调用scrollBy(x,y),x为正往左边滑动,x为负往右边滑动,y为正往上滑动,y为负往下滑动
- 4.无论是scrollTo还是scrollBy它滑动的是View的内容,View在整个布局中的位置不会发生任何改变
Scroller实现弹性滑动
另外我们有没有发现这种滑动效果是瞬间完成的,没有任何的平滑过渡效果,这种方式的用户体验是在是太差了,我们需要实现渐进式滑动,也就是今天我们所要讲的弹性滑动,这种弹性滑动效果的实现方式有很多,但是实现的思想都是相同的,将view的一个大的滑动分割成若干个小的滑动并且在一段时间内完成,这样就可以实现弹性滑动,可以借助Scroller来完成,也可以通过Handler.postDelay和Thread.sleep来完成。下面我们就来介绍如何借助Scroller和View的scrollTo方法来实现View的弹性滑动,其实也很简单,我们只需自定义一个TextView并复写他的computeScroll方法即可,主要的逻辑逻辑代码如下:
public class TestTextView extends android.support.v7.widget.AppCompatTextView{ private Scroller mScroller; public TestTextView(Context context) { super(context); initView(); } public void initView(){ mScroller = new Scroller(getContext()); } public void smoothScrollTo(int x,int y){ mScroller.startScroll(getScrollX(),getScrollY(),x,y,500); invalidate(); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }}
这就是弹性滑动的典型模板代码,我们只需要调用mTxtContent.smoothScrollTo(-300,-300);就可以实现TextView的弹性滑动我看一下所实现的效果:
![0661f6979102202adf16424850706ece.gif](https://img-blog.csdnimg.cn/img_convert/0661f6979102202adf16424850706ece.gif)
就是这么的简单,上面是Scroller的典型的使用方法,当我们构造一个Scroller对象并且调用它的startScroll方法时,Scroller内部其实什么也没做,它只是保存了我们传递的几个参数,这几个参数从startScroll的方法上就可以看出来,如下所示:
public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }
这个方法参数还是比较明确的startX和startY表示滑动的起点位置,dx和dy表示滑动的距离,duration表示滑动需要花费的时间,然后你有没有发现这这个方法里面都是一堆的赋值操作,并没有调用View的scrollTo方法来进行滑动,也就是说仅仅调用Scroller的startScroll方法并不能让View滑动起来,很奇怪,为什么View就是开始滑动了?原因就在于mScroller.startScroll下面的这个invalidate方法,是不是很神奇,其实原因很简单invalidate会导致View的重绘,也就是会调用他的onDraw方法,onDraw方法又会调用computeScroll方法,computeScroll方法是个空方法,里面代码就是我们实现View滑动的核心代码,mScroller.computeScrollOffset来计算每次移动的距离,然后调用scrollTo方法进行平滑移动,移动完成再次调用postInvalidate方法,该方法又会调用onDraw方法的调用,onDraw继续会调用computeScroll方法,如此反复调用直到整个滑动结束,完成View的平滑移动。
通过上面的分析我们已经知道的Scroller的工作原理,Scroller本身并不会引起View的平滑移动,必须借助View的computeScroll方法才能完成弹性滑动,它不断让View进行重绘,不断的调用computeScroll方法来计算滑动距离再调用scrollTo方法进行滑动,每次都会滑动一小段距离,而多次滑动连接在一起就构成一次完美的弹性滑动,这就是Scroller的工作原理。
自定义一个ViewPager
通过上面的学习我们已经知道了如何实现一个View的弹性滑动,只是简单的介绍了它的使用方法,接下来我们要看看它在实战开发过程中都有哪些应用。ViewPager大家都用过,通过他可以实现多个View的横向的左滑右滑的横向切换效果,现在我们就利用刚才所掌握的Scroller弹性滑动技术自定义实现一个自己的ViewPager,先来看下实现的效果:
![10ec961057a7670b40c1e9f9b565d54d.gif](https://img-blog.csdnimg.cn/img_convert/10ec961057a7670b40c1e9f9b565d54d.gif)
现在我们来分析一下他的实现思路:
- 1.实现ViewPager里面子View的位置问题
- 2.手指在屏幕上左右拖动的时候子View进行左右移动
- 3.当手指松开的时候如果滑动速度很快,如果是向左滑则切换到下一页,如果是向右滑则切换到到上一页,如果速度不是很快是左滑但是手指拖动当前的页面已经划出了屏幕一半那么应该切换到下一页,如果没有没有划出当前页面的一半那么就回弹到初始的位置,当然左滑也是一样的道理
子view的添加
首先我给ViewPager添加了三个Textview
mViewPager = findViewById(R.id.view_my_pager); for(int i =0; i < 3; i++){ TextView txtContent = (TextView) LayoutInflater.from(this).inflate(R.layout.item_test_view_pager, mViewPager,false); txtContent.setText(String.valueOf(i)); txtContent.setBackgroundColor(colors[i]); mViewPager.addView(txtContent); }
单个页面view的布局文件
itemtestview_pager.xml这个布局文件也很简单,也就只有一个TextView
<?xml version="1.0" encoding="utf-8"?>
确定子view的位置
首先我们解决的是ViewPager的子View的位置问题,我们给ViewPager添加了三个子View,那他的位置是横向一字排开,我们知道确定View的位置就是给view设置它的left,top,right,bottom的这四个参数;那么第一个子View的位置就是left:0,top:0,right:子View的宽,bottom:子View的高,第二个子View的位置就是在一个第一个子View的基础上计算得到的,left:第一个view的right,top:0,right:第一个view的right+第二个子View的宽,bottom:第二个子View的高,第三个子View的位置也是基于第二个子view的位置计算得到,具体的代码实现如下:
protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int childLeft = 0; for (int i = 0; i < childCount; i++) { View child = getChildAt(i); int measuredWidth = child.getMeasuredWidth(); int measuredHeight = child.getMeasuredHeight(); child.layout(childLeft, 0, childLeft + measuredWidth, measuredHeight); childLeft += measuredWidth; } Log.v(TAG, "view pager width:" + getMeasuredWidth() + ";height:" + getMeasuredHeight()); }
注意了,现在计算的话,child.getMeasuredWidth()和child.getMeasuredHeight()获取的宽和高都为0,我们必须在onMeasure方法里要测量子View的宽和高,这样在onLayout方法才能获取子view的宽和高,否则获取的子view的宽和高的值始终是0.,具体的代码实现如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); }
核心代码就是measureChildren(widthMeasureSpec, heightMeasureSpec);这一行
手指拖动左滑右滑的实现
我们知道手指的拖动,他是由多个触摸事件组件的,手指按下应该是ACTIONDOWN,手指拖动是由多个ACTIONMOVE所组成的,手指抬起那就是ACITONUP了,此时我们需要处理的ACTIONMOVE类型的事件,我们只需要计算前后两个相邻的ACTION_MOVE事件的之间的滑动距离,然后在调用view的scrollBy方法就搞定了,注意了我们需要把上滑和下滑的事件过滤掉,只处理左滑和左滑的事件,具体的代码实现如下:
@Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); Log.v(TAG, "onTouchEvent x:" + x + ";y:" + y); switch (event.getAction()) { case MotionEvent.ACTION_MOVE: int dx = x - mLastX; int dy = y - mLastY; if (Math.abs(dx) > Math.abs(dy)) { scrollBy(-dx, 0); } break; } mLastX = x; mLastY = y; return consume; }
手指滑动翻页实现
当手指松开的时候如果滑动速度很快如果是向左滑则切换到下一页,如果是向右滑则切换到到上一页,这里我们需要借助一个非常重要的工具,速度检测器:VelocityTracker,通过他来计算滑动的速度大小,如果速度为正则为右滑,当前位置减1,如果为负值为左滑当前位置加1
- 速度检测计算要滑动到的页面下标
if(Math.abs(xVelocity) > 50){ // 如果滑动的速度快也跳到下一个位置 mChildIndex = xVelocity > 0 ? mChildIndex - 1:mChildIndex + 1; }
- 根据拖动的距离来计算要滑动的页面的下边
mChildIndex = (scrollX + childWidth / 2) / childWidth;
*根据页面下标mChildIndex计算将要滑动的距离
//越界处理 mChildIndex = Math.max(0, Math.min(mChildIndex, getChildCount() - 1));//计算索要滑动的距离int delx = mChildIndex * childWidth - scrollX;//弹性滑动开始smoothScrollTo(delx,0);
完整的上下翻页的代码如下:
public boolean onTouchEvent(MotionEvent event) { mVelocityTracker.addMovement(event); boolean consume = false; int x = (int) event.getX(); int y = (int) event.getY(); Log.v(TAG, "onTouchEvent x:" + x + ";y:" + y); switch (event.getAction()) { case MotionEvent.ACTION_UP: //手指抬起的时候,首先要计算的是要滚动到哪个位置上,然后在计算滚动的距离是多少 //3. mVelocityTracker.computeCurrentVelocity(1000); float xVelocity = mVelocityTracker.getXVelocity(); int scrollX = getScrollX(); View child = getChildAt(mChildIndex); int childWidth = child.getMeasuredWidth(); if(Math.abs(xVelocity) > 50){ // 如果滑动的速度快也跳到下一个位置 mChildIndex = xVelocity > 0 ? mChildIndex - 1:mChildIndex + 1; }else{ //1.如果滑动速度慢且滑动没有过半儿,应该还在当前位置,.如果已经过半则滑动到下一个位置 mChildIndex = (scrollX + childWidth / 2) / childWidth; }//越界处理 mChildIndex = Math.max(0, Math.min(mChildIndex, getChildCount() - 1)); //计算索要滑动的距离 int delx = mChildIndex * childWidth - scrollX; //弹性滑动开始 smoothScrollTo(delx,0); mVelocityTracker.clear(); break; } mLastX = x; mLastY = y; return consume; }
弹性滑动
这是具体的弹性滑动的核心模板代码,在前面我们已经分析过了,在这里我就不在重复了
private void smoothScrollTo(int x,int y) { mScroller.startScroll(getScrollX(), getScrollY(), x, y, 500); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } }
截止目前整个自定义ViewPager的弹性滑动的效果就彻底实现了,想必通过这个自定义View的实现,我们对弹性滑动的理解已经非常深刻了。最后我把整个测试代码的demo已经上传到了github上,感兴趣的可以下载源码查看 https://github.com/mxdldev/android-custom-view/tree/master/app/src/main/java/com/mxdl/customview/test/view/MyViewPager.java