android 滑动基础篇

·  阅读 2109

1 前言

滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:

  1. 滑动基础
  2. ScrollView滑动源码解读
  3. NestedScrollView嵌套滑动源码解读
  4. CoordinatorLayout-AppBarLayout-CollapsingToolbarLayout复杂滑动逻辑源码解读

在本章内,主要介绍实现的一些相关基础框架逻辑

  1. 平滑处理、滑翔处理
  2. View中对滑动的处理效果以及逻辑
  3. androidx中的滑动接口、嵌套滑动接口的理解

看到这里,你不再觉得仅仅是在OnTouchEvent中处理滑动事件吧,其实这样想也可以,不过效果什么的全自定义了

2 滑动常量

介绍滑动前,我们需要了解一些滑动常量,这些常量有利于我们实现更流畅的滑动效果

这些常量都是通过ViewConfiguration来获取的,其实例通过下面来获取

ViewConfiguration configuration = ViewConfiguration.get(mContext)
复制代码
  • 最小滑动距离:getScaledTouchSlop()
  • 最小滑翔速度:getScaledMinimumFlingVelocity(),像素每秒
  • 最大滑翔速度:getScaledMaximumFlingVelocity(),像素每秒
  • 手指滑动越界最大距离:getScaledOverscrollDistance()
  • 滑翔越界最大距离:getScaledOverflingDistance()

这里滑翔的速度,是为了处理惯性的快慢,这个做过的深有体会,总是感觉,快慢不是很舒服;所以我们一一般在滑翔时,获取滑翔距离时,要在最大和最小之间;

3 平滑滑动、滑翔

平滑滑动根据时间进行平缓的滑动,而滑翔需要对移动事件进行跟踪分析之后,再根据时间计算状态进而进行分析;而根据时间进行状态处理,使用Scroller或者OverScroller来处理,OverScroller可以处理回弹效果;对事件跟踪分析,使用VelocityTracker类处理

3.1 VelocityTracker类

这个类有下面用法

实例获取

mVelocityTracker = VelocityTracker.obtain()
复制代码

跟踪事件

mVelocityTracker.addMovement(ev)
复制代码

获取滑翔初始速度

mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int xVelocity = (int)mVelocityTracker.getXVelocity(mActivePointerId);
int yVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
复制代码

两个方法要跟着使用,减少误差;另外计算时;computeCurrentVelocity参数意义

  1. 多少毫秒,假设n
  2. 速度,单位由参数1的数值来确定,也即是像素每n毫秒

数据临时清理

mVelocityTracker.clear();
复制代码

当切换手指时,之前的数据就没有意义了,所以需要清理重新计算

对象回收

mVelocityTracker.recycle();
复制代码

3.2 OverScroller类

Scroller也可以处理,只是不能处理回弹而已;这里就只是解释OverScroller类,它仅仅只是一个状态计算的类,对view并没有进行操作;下面就是一些使用

初始化

mScroller = new OverScroller(getContext());
复制代码

滑动

public void startScroll(int x, int y, int dx, int dy, int duration)
复制代码

单位时间内,x增加dx,y增加dy;默认时间250ms

计算

public boolean computeScrollOffset()
复制代码

计算当前时间对应状态,返回true表示,仍在进行,可通过下面获取当前状态

  • getCurrX():当前x位置
  • getCurrY(): 当前y位置
  • getCurrVelocity():当前速度

回弹

public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
复制代码
  • 当前x值
  • 当前y值
  • x最小值
  • y最小值
  • x最大值
  • y最大值

如果运用在滑动中,则表示已滑动距离,滑动的最小距离,滑动的最大距离;

滑翔

fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY, int overX, int overY)
复制代码
  • 当前x位置
  • 当前y位置
  • 当前x速度,像素每秒
  • 当前y速度,像素每秒
  • x最小取值
  • y最小取值
  • x最大取值
  • y最大取值
  • x最大越界距离
  • y最大越界距离

有越界范围才有回弹效果

丢弃

mScroller.abortAnimation();
复制代码

完成判断

mScroller.isFinished()
复制代码

3.3 平滑移动

这个只需要调用OverScroller的startScroll方法进行触发,在View的computeScroll方法获取滑动状态调用scrollTo方法即可;

3.4 滑翔

滑翔就分为两种情况了

  1. 在手指离开时,未越界,则进行滑翔,如果可以回弹,也会进行回弹,调用OverScroller的fling方法
  2. 在手指离开时,已经越界,则进行回弹,调用OverScroller的springBack方法

同样需要在computeScroll根据计算状态,进行具体滑动

4 View类

View类中对于滑动,提供了滑动执行机制、滑动时指示条、滑动时fade蒙层、长按事件处理还有滑动的一些数据判断,这些和androidx中滑动接口ScrollingView

4.1 滑动具体执行

具体执行是通过View的变量mScrollX、mScrollY来完成的,这两个变量在绘制的时候,会对画布进行平移(详见View类中draw方法被调用的地方),进而导致其内绘制内容发生了变化;这个平移对当前view的背景并没有影响,由于在处理背景时再次进行了反方向平移(详见View类中drawBackground方法);而对这两个变量的操作方法有

  • scrollTo(int x, int y):移动到x、y
  • scrollBy(int x, int y):移动范围增加x、y
  • overScrollBy方法,此方法会自动处理越界时的处理,并调用onOverScrolled进行实际的移动处理

我称这两个方法为执行者;但是很多滑动控件中都有平滑移动,平滑移动基本都是利用OverScroller或Scroller的滑动方法来完成的;需要回弹用OverScroller,否则使用Scroller即可

protected boolean overScrollBy(int deltaX, int deltaY,
            int scrollX, int scrollY,
            int scrollRangeX, int scrollRangeY,
            int maxOverScrollX, int maxOverScrollY,
            boolean isTouchEvent)
复制代码

overScrollBy方法,返回结果,true标识越界了需要回弹,参数意思如下:

  1. x增量值
  2. y增量值
  3. x当前移动值
  4. y当前移动值
  5. x当前最大值
  6. y当前最大值
  7. x最大回弹值
  8. y最大回弹值
  9. 是手指移动还是滑翔
protected void onOverScrolled(int scrollX, int scrollY,
            boolean clampedX, boolean clampedY)
复制代码

onOverScrolled方法,参数意义如下:

  1. 当前x滑动
  2. 当前y滑动
  3. x是否越界,true表示越界了
  4. y是否越界,true表示越界

4.2 长按事件

源码见View类中onTouchEvent方法、isInScrollingContainer方法

长按事件有一定的规则:

  • 是在down事件中进行触发发送延时回调长按处理任务,回调执行并不一定需要手指抬起
  • 在cancel、move、up事件中取消的

而对于普通非滑动容器内的view,长按事件的延迟时间为ViewConfiguration.getLongPressTimeout();而如果是滑动容器中,此时会再次触发发送一个触发长按的延期任务,这个延时为ViewConfiguration.getTapTimeout();我觉得这是考虑到滑动的特殊性增加一点时间,可以更精准的判断是否为长按事件;

是否滑动容器的判断方法,是由ViewGroup的shouldDelayChildPressedState方法来处理的;也就是滑动容器中此方法需要返回true

4.3 fade蒙层

源码详见View.draw方法,绘制分为两种情况,是根据mViewFlags标志来判断的;也即是否需要绘制水平的fade蒙层或者竖直的蒙层;

这个标志可以进行设置,两种方法改变,默认是none

  • xml中参数配置
android:requiresFadingEdge="horizontal|vertical|none"
复制代码
  • 代码设置
setHorizontalFadingEdgeEnabled(true);
setVerticalFadingEdgeEnabled(true);
复制代码

并不是这个设置了,水平或者竖直,这些地方就以一定出现蒙层,还有其它限制,蒙层分为4个,这四个方法,逻辑是一致的,方法略有区别;

蒙层有一个高度设置,同样有两种方法改变,默认是ViewConfiguration.getScaledFadingEdgeLength()

  • xml设置
android:fadingEdgeLength="16dp"
复制代码
  • 通过方法设置
setFadingEdgeLength(int length)
复制代码

具体绘制的高度基本是这个高度,除非高度超过了控件本身高度,其变控件高度的一半

蒙层还有一个每个边缘的参数比例,这个在0-1之间;返回的值不在区间会被忽略掉;方法默认实现如下:

    protected float getTopFadingEdgeStrength() {
        return computeVerticalScrollOffset() > 0 ? 1.0f : 0.0f;
    }
    
    protected float getBottomFadingEdgeStrength() {
        return computeVerticalScrollOffset() + computeVerticalScrollExtent() <
                computeVerticalScrollRange() ? 1.0f : 0.0f;
    }
    
    protected float getLeftFadingEdgeStrength() {
        return computeHorizontalScrollOffset() > 0 ? 1.0f : 0.0f;
    }
    
    protected float getRightFadingEdgeStrength() {
        return computeHorizontalScrollOffset() + computeHorizontalScrollExtent() <
                computeHorizontalScrollRange() ? 1.0f : 0.0f;
    }
复制代码

那第二个条件就是:蒙层的比例 * 蒙层高度 > 1.0f 则这个位置边缘会绘制处理

蒙层是一个矩形的线性渐变蒙层,通过线性shade来处理的;渐变是从颜色的完全不透明到完全透明

shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000, color & 0x00FFFFFF, Shader.TileMode.CLAMP)
复制代码

这个颜色可以通过重写下面方法进行改变,默认是黑色

    public int getSolidColor() {
        return 0;
    }
复制代码

其实这个蒙层在android所有标准控件中,只有时间的控件直接采用了,其它的保留了特性;而且从系统的默认实现来看,这个就是为滑动实现的

4.4 滚动条

由两部分组成,一个是Track(滑道),一个是Thumb(滑块);滑道可以认为是可以滑动整体,固定的,而滑块只是其中一部分,位置可变动;

有显示和隐藏控制,源码见awakenScrollBars()、onDrawScrollBars方法;

4.4.1 显示

显示受参数控制,即显示位置,且显示位置方向是可以滑动的才可以显示;有两种方式

  1. xml中设置
android:scrollbars="vertical|horizontal"
复制代码
  1. 代码设置
public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled)
public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled)
复制代码

4.4.2 隐藏

受参数控制,可以通过xml布局中配置,也可以设置;默认为true,如下

android:fadeScrollbars="true"

public void setScrollbarFadingEnabled(boolean fadeScrollbars) 
复制代码

淡出效果,是在显示操作后提交的延迟操作,延时时长,默认为ViewConfiguration.getScrollDefaultDelay(),可以通过两种方式改变

  • onDrawScrollBars方法调用不传递时间时,xml中配置可改变
android:scrollbarDefaultDelayBeforeFade="10"
复制代码
  • 代码中onDrawScrollBars传递时间控制

淡出效果为alpha变换,时长默认为ViewConfiguration.getScrollBarFadeDuration();同样可以通过两种方法改变

  1. xml配置
android:scrollbarFadeDuration="1000"
复制代码
  1. 方法设置
public void setScrollBarFadeDuration(int scrollBarFadeDuration)
复制代码

4.4.3 样式控制

样式也有两种形式的控制

  1. 圆形屏幕设备:主要针对是android手表等设备,这个我看不了效果,就不说它的显示控制了
  2. 其它设备:绘制的是ScrollBarDrawable图片类型

ScrollBarDrawable是个不对开发者公开的类,那么这里我们只介绍下其属性

  • android:scrollbarSize: 竖直时宽度,水平时高度
  • scrollbarThumbHorizontal/scrollbarThumbVertical:滑块颜色
  • scrollbarTrackVertical/scrollbarTrackHorizonta:滑道颜色
  • scrollbarStyle:滑块样式,默认值insideOverlay,还有三个值insideInset,outsideOverlay,outsideInset;insideXXX不考虑padding,也就是会覆盖在padding上,而outside不考虑margin,会覆盖在margin上

4.5 指示条

源码见onDrawScrollIndicators方法

其是否可见,由3个方面控制

  1. 指示条显示位置不为none;两种方法设置,xml和代码
android:scrollIndicators="none"

public void setScrollIndicators(@ScrollIndicators int indicators[, @ScrollIndicators int mask])
复制代码
  1. 指示条显示位置相应方向可滑动;top:向上滑动,bottom-向下滑动,left向左滑动,right-向右滑动

左右滑动判断;参数为负,表示左,正为右

  public boolean canScrollHorizontally(int direction) {
        final int offset = computeHorizontalScrollOffset();
        final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }
复制代码

上下滑动判断;参数为父表示上,为正表示下

  public boolean canScrollVertically(int direction) {
        final int offset = computeVerticalScrollOffset();
        final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }
复制代码

指示条图标,为R.drawable.scroll_indicator_material,不可改变;这是我查找到的图片情况:

<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:tint="?attr/colorForeground">
    <solid android:color="#1f000000" />
    <size
        android:height="1dp"
        android:width="1dp" />
</shape>
复制代码

指示条位置:以所在位置为其一边,垂直的两边,以及位置[-|+]为图片另外一边

唯一用途,指示你此时可以往哪个方向滑动;我觉得很不实用

4.6 回弹

默认是可以回弹,但是未进行回弹效果处理;对于回弹的开启关闭,可以通过两种方式

  1. xml中处理
android:overScrollMode="always"
复制代码
  1. 代码设置
public void setOverScrollMode(int overScrollMode)
复制代码

通过下面方法可以获取,为OVER_SCROLL_NEVER时不可回弹

public int getOverScrollMode()
复制代码

回弹长度,按照 2章节中 获取的相应常量设置即可

回弹效果,可以在手指移动、滑翔两个过程中出现;需要通过上述方法判断,进行进行处理;系统提供了默认的回弹效果类EdgeEffect;下面介绍下此类运用

EdgeEffect类

EdgeEffect mEdgeGlowTop = new EdgeEffect(getContext());  // 实例化
mEdgeGlowTop.setColor(color); // 改变回弹颜色
mEdgeGlowTop.onPull(deltaDistance, displacement) // 回弹参数,均为0-1,变化距离以及位置比例
mEdgeGlowTop.isFinished() // 状态判断
mEdgeGlowTop.onRelease() // 释放
mEdgeGlowTop.onAbsorb(velocity) // 放弃

mEdgeGlowTop.setSize(width, height) // 设置绘制的矩形范围,上面onPull传的参数比例,就是依据这个来绘制回弹的图形的
mEdgeGlowTop.draw(canvas) // 绘制,结果表示是否还需要继续处理
复制代码

需要特殊说明的是,这个类绘制的时候,默认绘制方向,以当前视图左上角为起点进行绘制的;所以要onPull的参数传递以及绘制时,要考虑坐标以及旋转的关系,进而达到正确的效果

4.7 嵌套滑动启动关闭配置

这个可以通过xml配置,或者代码设置

android:nestedScrollingEnabled="true"

public void setNestedScrollingEnabled(boolean enabled)
public boolean isNestedScrollingEnabled()
复制代码

4.8 测量

这里就是重写onMeasure方法,有两种情况

  1. 继承ViewGroup;需要完全自己重写逻辑
  2. 继承ViewGroup子类;可以依赖父类的测量逻辑,在其测量关键方法重写,也可以先进行父类测量

这两种情况都需要对子布局测量传递不限制模式MeasureSpec.UNSPECIFIED,以达到有滑动距离的可能

更具体的逻辑就需要自己来操作;不过在操作的时候,需要特殊注意一个对象,那就是ViewGroup.LayoutParams,也就是容器的布局参数,这个类是容器规定了一些功能,也是子view通过属性来通知父容器的一种重要途径

5 ScrollingView接口

如果你能理解上面的内容,那么这个接口方法就比较好理解了

  • computeHorizontalScrollRange()/computeVerticalScrollRange():滑动内容长度;
  • computeHorizontalScrollOffset()/computeVerticalScrollOffset():相应方向已滑动的距离
  • computeHorizontalScrollExtent()/computeVerticalScrollExtent():显示内容的长度

这些方法,都是进行滑动判断、fade蒙版、指示条、滑动条用到的核心方法;如果不实现,就无法拥有View已实现的效果,并且相应方法肯定是不可用了,比如:

  • 是否可滑动判断:canScrollHorizontally,canScrollVertically
  • 滚动条隐藏:awakenScrollBars

6 嵌套接口

接口也分为子视图方法和父容器方法;子视图方法用来通知父容器进行处理的,而父容器方法是高速子滑动视图其是否去处理以及处理的结果状态;

6.1 NestedScrollingParent3接口

其继承NestedScrollingParent2,NestedScrollingParent2又继承了NestedScrollingParent;方法如下

  1. onStartNestedScroll方法:父容器是否需要处理子view的滑动事件,true表示接受处理
  2. onNestedScrollAccepted方法:接受子视图的滑动事件询问
  3. onStopNestedScroll方法:得知子视图停止滑动时的通知
  4. onNestedScroll方法:子view已经处理滑动后,父容器进行滑动处理
  5. onNestedPreScroll方法:子view处理滑动前,父容器进行滑动处理
  6. onNestedFling方法:子view需要滑翔时,子view处理,父view进行处理
  7. onNestedPreFling方法:子view需要滑翔时,父view进行处理;返回结果表示是否处理
  8. getNestedScrollAxes方法:当前父容器在子view滑动时,处理滑动的维度

需要注意的是,嵌套时,手指滑动是可接力完成的,而滑翔一定是互斥完成的

其中涉及一下参数,说明如下:

  1. type:表示滑动或者滑翔,ViewCompat.TYPE_TOUCH滑动,ViewCompat.TYPE_NONE_TOUCH滑动
  2. consumed:包含x、y两个方向的数组;一般为输出变量,表明当前处理时,消费了多少
  3. dxConsumed/dyConsumed:表明传递到父容器时,子视图已经消耗了多少滑动距离
  4. dxUnconsumed/dyUnconsumed:表明传递到父容器时,还有多少滑动距离待消耗
  5. target:表明从那个子view传递而来
  6. dx/dy:此次事件滑动的距离
  7. child:包含target的,当前容器的直接子容器
  8. axes:滑动的方向,ViewCompat.SCROLL_AXIS_HORIZONTAL,ViewCompat.SCROLL_AXIS_VERTICAL两个值
  9. velocityX/velocityY: 滑翔时初始速度

6.2 NestedScrollingChild3

继承了NestedScrollingChild2, NestedScrollingChild2又继承了NestedScrollingChild;方法如下:

  1. setNestedScrollingEnabled/isNestedScrollingEnabled: 嵌套滑动是否支持
  2. startNestedScroll:通知嵌套滑动开始
  3. stopNestedScroll:通知嵌套滑动结束
  4. hasNestedScrollingParent:是否存在嵌套处理的直系长辈容器
  5. dispatchNestedScroll:自己处理后继续通知滑动事件
  6. dispatchNestedPreScroll:自己未处理滑动,通知滑动事件
  7. dispatchNestedFling:自己处理后,通知滑翔事件
  8. dispatchNestedPreFling:优先自己处理,通知滑翔事件

参数就不解释了,和6.1类似

6.3 辅助类

这两章中的方法在View和ViewGroup均有使用,androidx也提供了辅助类进行默认实现,这两个类就是NestedScrollingParentHelper、NestedScrollingChildHelper;这两个类主要是为了解决版本兼容问题

7 小结

在基础篇里面,主要介绍了滑动基础的方方面面,比较枯燥;部分内容也没有详细的介绍,在后续的源码解读中,会慢慢到来

如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持

技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!

下一篇:ScrollView滑动源码解读

分类:
Android
标签: