Android弹性滑动在自定义View中的高级应用

本文出自门心叼龙的博客,属于原创类容,转载请注明出处。

好久没有更新博客了,特意的看了博客最后的更新时间为2019年7月21日,今天是10月24日掐指一算已经有三个月时间了,自从上篇《开发杂谈:说说数据结构和算法那点事儿》以后就一直没有更新了,确实有些尴尬,而今天又是一年一度的1024程序员节,我想在这个特殊的日子里,很有必要写一篇文章来写纪念这个属于程序员的节日。

我们知道,在功能机时代我们在手机上的任何操作都是在键盘上完成的,只有通过键盘才能完成输入操作,只能通过键盘才能和手机交互,进入智能机时代以后我们所有操作都可以通过触摸屏的方式来完成,而我们最常见的操作就是滑动,手机屏幕和PC端的显示屏最大的区别就是,PC显示器屏幕很大,一屏可以显示跟多内容,而手机屏幕就小了很多,一屏幕所能显示的内容就非常有限,我们可以通过上下滑动,左右滑动翻页来显示我们想要看到的内容。我们打开任意一款手机应用,无处不在的上滑,下滑,左滑,右滑操作,由此可见滑动操作在移动手机开发当中是多么的重要,因此今天我们来研究View的滑动。

在Android系统中View给我们提供了两个非常重要关于滑动操作的方法scrollTo和scrollBy,下面我们通过scrollTo和scrollBy来完成View的滑动。

View的滑动

布局文件如下:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android" >
    <TextView
        android:id="@+id/txt_scroll_to"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello world"
        android:gravity="center"
        />
    <Button
        android:id="@+id/btn_scroll_smooth"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:text="ScrollTo"
        android:textAllCaps="false"
        />
</RelativeLayout>

布局文件中有两个控件,一个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本身在布局中的位置并没有发生任何的改变。

通过以上测试我们不难得到以下几条结论:

  • 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的弹性滑动我看一下所实现的效果:


在这里插入图片描述

就是这么的简单,上面是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,先来看下实现的效果:


在这里插入图片描述

现在我们来分析一下他的实现思路:

  • 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的布局文件

item_test_view_pager.xml这个布局文件也很简单,也就只有一个TextView

<?xml version="1.0" encoding="utf-8"?>
<TextView
    android:id="@+id/txt_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:android="http://schemas.android.com/apk/res/android" />

确定子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);这一行

手指拖动左滑右滑的实现

我们知道手指的拖动,他是由多个触摸事件组件的,手指按下应该是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开发技术交流群

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容