本文出自门心叼龙的博客,属于原创类容,转载请注明出处。
好久没有更新博客了,特意的看了博客最后的更新时间为2019年7月21日,今天是10月24日掐指一算已经有三个月时间了,自从上篇《开发杂谈:说说数据结构和算法那点事儿》以后就一直没有更新了,确实有些尴尬,而今天又是一年一度的1024程序员节,我想在这个特殊的日子里,很有必要写一篇文章来写纪念这个属于程序员的节日。
我们知道,在功能机时代我们在手机上的任何操作都是在键盘上完成的,只有通过键盘才能完成输入操作,只能通过键盘才能和手机交互,进入智能机时代以后我们所有操作都可以通过触摸屏的方式来完成,而我们最常见的操作就是滑动,手机屏幕和PC端的显示屏最大的区别就是,PC显示器屏幕很大,一屏可以显示跟多内容,而手机屏幕就小了很多,一屏幕所能显示的内容就非常有限,我们可以通过上下滑动,左右滑动翻页来显示我们想要看到的内容。我们打开任意一款手机应用,无处不在的上滑,下滑,左滑,右滑操作,由此可见滑动操作在移动手机开发当中是多么的重要,因此今天我们来研究View的滑动。
在Android系统中View给我们提供了两个非常重要关于滑动操作的方法scrollTo和scrollBy,下面我们通过scrollTo和scrollBy来完成View的滑动。
布局文件如下:
布局文件中有两个控件,一个TextView和一个Button,我们点击按钮Button调用TextView的scrollTo方法和scrollBy方法,来观察View滚动的效果。
mBtnScroll.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
mTxtScroll.scrollTo(200,200);
}
});
此时Hello world往上方进行了移动,再次点击按钮调用 mTxtScroll.scrollTo(200,200),发现HelloWorld的位置没有发生任何的变化。
接下来我们把调用参数修改为-200,即:
mTxtScroll.scrollTo(-200,-200);
再看看效果,HelloWorld往右下方移动,scrollTo测试完毕,我们在看看scrollBy是什么效果
mBtnScroll.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
mTxtScroll.scrollBy(200,200);
}
});
我连续点击了三次,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本身在布局中的位置并没有发生任何的改变。
通过以上测试我们不难得到以下几条结论:
另外我们有没有发现这种滑动效果是瞬间完成的,没有任何的平滑过渡效果,这种方式的用户体验是在是太差了,我们需要实现渐进式滑动,也就是今天我们所要讲的弹性滑动,这种弹性滑动效果的实现方式有很多,但是实现的思想都是相同的,将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的弹性滑动我看一下所实现的效果:
就是这么的简单,上面是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的工作原理。
通过上面的学习我们已经知道了如何实现一个View的弹性滑动,只是简单的介绍了它的使用方法,接下来我们要看看它在实战开发过程中都有哪些应用。ViewPager大家都用过,通过他可以实现多个View的横向的左滑右滑的横向切换效果,现在我们就利用刚才所掌握的Scroller弹性滑动技术自定义实现一个自己的ViewPager,先来看下实现的效果:
现在我们来分析一下他的实现思路:
首先我给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);
}
item_test_view_pager.xml这个布局文件也很简单,也就只有一个TextView
首先我们解决的是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);这一行
我们知道手指的拖动,他是由多个触摸事件组件的,手指按下应该是ACTION_DOWN,手指拖动是由多个ACTION_MOVE所组成的,手指抬起那就是ACITON_UP了,此时我们需要处理的ACTION_MOVE类型的事件,我们只需要计算前后两个相邻的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
在使用中有任何问题,请留言,或加入Android、Java开发技术交流群