乐趣区

关于android:高级-UI-成长之路-一-View的基础知识你必须知道

前言

View 能够说是在日常开发中,天天应用的角色,尽管说 View 不属于四大组件,然而它的作用跟重要水平你真不可小视。该类型的文章打算写一个系列,对于本人温习或老手学习提供一个形式。

View 基础知识

该篇次要介绍 View 的一些基础知识,从而能够为后续文章内容做好铺垫,次要波及到的内容有 View 的地位参数、MotionEvent 和 TouchSlop 对象、VelocityTracker、GestureDetector 和 Scroller 对象,通过对这些基础知识的介绍,置信你对更加简单的操作也是手到擒来。

View 介绍

在介绍 View 的基础知识之前,咱们须要晓得它到底是什么? View 在 Android 中是所有控件的基类(构造参考上图),不论是简略的 TextView , 还是简单的 ViewGroup、CustomView 亦或者 RecyclerView 它们的独特顶级父类都是 View, 所以说, View 是一种界面层管制的一种形象,它代表的是一个控件。从上图可知 ViewGroup 是 View 的子类,ViewGroup 在视图层它能够有任意子 View。

明确 View 的层级关系有助于了解 View 的工作机制。从上图咱们也能够晓得实现自定义 View 控件能够继承自 View 也能够继承自 ViewGroup。

View 地位参数

View 的地位次要由它的四个顶点来决定,别离对应于 View 的四个属性: top、left、right、bottom , 其中 top 是左上角纵坐标,left 是左上角横坐标,right 是右下角横坐标,bottom 是右下角纵坐标。须要留神的是,这些坐标都是绝对于 View 的父容器,因为它是一种绝对坐标,View 的坐标和父容器的关系能够参考下图,在 Android 中,x 轴 y 轴 的正方向别离为右和下,这点不难理解,不仅仅是 Android , 其实大部分显示零碎都是依照这个规范来定义坐标系的。

依据上图,咱们很容易得出 View 的宽高和坐标的关系:

val width = right - left
val height = bottom - top

那么如何失去 View 的这四个参数呢?也很简略,在 View 的源码中它们对应于 mLeft、mRight、mTop、和 mBottom 这四个成员变量,通过代码获取形式如下:

val left = left;
val right = right
val top = top
val bottom = bottom

从 Android 3.0 开始,View 减少了 额定的几个参数,x、y、translationX、translationY , 其中 x 和 y 是 View 左上角的坐标,而 translationX 和 translationY 是 View 左上角绝对于父容器的偏移量。这几个参数也是绝对于父容器的坐标,并且 translationX 和 translationY 的默认值是 0,和 View 的四个根本的地位参数一样,View 也为他们提供了 set/get 办法,这几个参数的换算关系如下所示:

val x = left + translationX
val y = top + translationY

须要留神的是,View 在平移过程中,top 和 left 示意的是原始左上角的地位信息,其值并不会产生扭转,此时产生扭转的是 x、y、translationX、translationY 这四个参数。

MotionEvent 和 TouchSlop

MotionEvent

    override fun onTouchEvent(event: MotionEvent): Boolean {when (event?.action) {
            MotionEvent.ACTION_DOWN -> {println("手指按下")
            }
            MotionEvent.ACTION_UP -> {println("手指抬起")

            }
            MotionEvent.ACTION_MOVE -> {println("手指挪动")
            }
        }
        return true

    }

在手指接触屏幕后所产生的一系列事件中,罕用的并且十分典型的事件类型有如下几种:

  • MotionEvent.ACTION_DOWN: 手指刚接触屏幕
  • MotionEvent.ACTION_MOVE: 手指在屏幕上滑动
  • MotionEvent.ACTION_UP: 手指在屏幕上抬起的一瞬间触发该事件

失常状况下,一次手指触摸屏幕的行为会触发一些列点击事件,思考有如下几种状况:

  • DOWN —> UP: 点击屏幕后立即抬起手指松开屏幕触发的事件
  • DOWN —> MOVE —> MOVE —> MOVE —> UP: 点击屏幕而后随着在屏幕上滑动之后在松开产生的事件

上述三种状况是典型的事件序列,同时通过 MotionEvent 对象咱们能够失去点击事件产生的 x 和 y 坐标。因而,零碎提供了两组办法 getX / getY 和 getRawX / getRawY 它们的区别其实很简略, 如下:

  • getX / getY : 返回绝对于以后 View 左上角的 x 和 y 的坐标
  • getRawX / getRawY : 返回的是绝对于手机屏幕左上角的 x 和 y 坐标。

TouchSlop

TouchSlop 官网解释就是零碎所能辨认的被认为是滑动的最小间隔,艰深点说就是当手指在屏幕上滑动时,如果两次滑动之间的间隔小于这个常量,那么零碎就认为你没有在滑动,能够通过上面的 API 获取该常量值,

/**
  * 零碎所能辨认进去的被认为滑动的最小间隔
  */
val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop;

这个常量能够帮忙咱们在解决滑动时,利用该数值来做一些过滤,比方当两次滑动事件的滑动间隔小于这个值,咱们就能够未达到滑动间隔的临界点,因而就能够认为他们不是滑动,这样做能够有更好的用户体验。

VelocityTracker、GestureDetector 和 Scroller

VelocityTracker

VelocityTracker 的作用是用于追踪滑动过程中的速度,包含程度和竖直方向的速度。它的应用过程很简略,首先,在 View 的 onTouchEvent 办法中追踪以后单击事件的速度;

/**
  * 速度追踪
  */
 val velocityTracker = VelocityTracker.obtain()
   velocityTracker.addMovement(event)

接着,当咱们先晓得以后的滑动速度时,这个时候能够采纳如下形式来取得以后的速度:

velocityTracker.computeCurrentVelocity(1000)
val xVelocity = velocityTracker.getXVelocity()
val yVelocity = velocityTracker.getYVelocity()

这一步有 2 点须要留神,其一 获取速度之前必须先计算速度,既 getXVelocity 和 getYVelocity 这两个办法的后面必须要调用 computeCurrentVelocity 办法,第二点,这里的速度是指一段时间内手指所滑过的像素值,比方将工夫距离设为 1000 ms 时,那么就是在 1s 内手指在程度方向从左向右滑动 500 px 那么程度速度就是 500,留神速度能够为正数,当手指从右往左滑动时,程度方向速度即为负值,这个须要了解一下。速度的计算能够用如下公式:

速度 = (起点地位 – 终点地位) / 时间段

依据下面的公式再加上 Android 零碎的坐标体系,能够晓得,手指逆着坐标系的正方向滑动,所产生的速度就为负值,另外,computeCurrentVelocity 这个办法的参数示意的是一个工夫单元或者说工夫距离,它的单位是毫秒(ms), 计算速度时失去的速度就是在这个工夫距离内手指在程度或竖直方向上所滑动的像素值。

针对下面的例子,如果咱们通过 obtain.computeCurrentVelocity(1000) 来获取速度,那么失去的速度就是手指在 1000 ms 毫秒内所滑过的 px 值,因而能够间接套下面公式:

程度速度 = 500 px / 每 1000 ms

既程度速度为 500 , 这里须要好好了解一下。

最初,当不须要它的时候,须要调用 clear 办法来重置并回收内存:

velocityTracker.clear()
velocityTracker.recycle()

VelocityTracker 的 API 简单明了,咱们能够记住一个套路。

  1. 在触摸事件为 ACTION_DOWN或是进入 onTouchEvent办法时,通过 obtain获取一个 VelocityTracker
  2. 在触摸事件为 ACTION_UP时,调用 recycle进行开释 VelocityTracker
  3. 在进入 onTouchEvent办法或将 ACTION_DOWNACTION_MOVEACTION_UP的事件通过 addMovement办法增加进 VelocityTracker
  4. 在须要获取速度的中央,先调用 computeCurrentVelocity办法,而后通过 getXVelocitygetYVelocity获取对应方向的速度

解锁更多姿态能够参考该文章

GestureDetector

GestureDetector 的作用用于辅助检测用户的单机、滑动、长按、双击等行为。要应用 GestureDetector 也不简单,参考如下过程:

  1. 首先创立一个 GestureDetector 对象并实现 OnGestureListener 接口,依据须要咱们还能够实现 OnDoubleTapListener 从而可能监听双击行为;

    val mGetDetector = GestureDetector(context,this)
    // 解决长按屏幕后无奈拖动的景象
    mGetDetector.setIsLongpressEnabled(false)
  2. 接管目前 View 的 onTouchEvent 办法,在 View 的 onTouchEvent 办法中增加如下代码:

    override fun onTouchEvent(event: MotionEvent) = mGetDetector.onTouchEvent(event)

做完了下面这 2 步,咱们就能够有抉择的实现 OnGestureListener 和 OnDoubleTapListener 中的办法了,这 2 个接口中的办法介绍如下所示:

OnGestureListener / 办法名 形容
onDown 手指微微触摸屏幕的一瞬间,由 1 个 ACTION_DOWN 触发
onShowPress 手指微微触摸屏幕, 尚未松开或拖动,由一个 ACTION_DOWN 触发, 它强调的是没有松开或者拖动的状态
onSingleTapUp 手指 (微微触摸屏幕后) 松开,随同着 1 个 MotinEvent.ACTION_UP 而触发,这是单击行为
onScroll 手指按下屏幕并拖动,由 1 个 ACTION_DOWN,多个 ACTION_MOVE 触发,这是拖动行为
onLongPress 用户短暂的按着屏幕不放,既长按
onFling 用户按下触摸屏、疾速滑动后松开,由 1 个 ACTION_DOWN、多个 ACTION_MOVE 和 1 个 ACTION_UP 触发,这是疾速滑动行为
OnDoubleTapListener / 办法名 形容
onDoubleTap 双击,由 2 次连续的单击组成,它不可能和 onSingleTapConfirmed 共存
onSingleTapConfirmed 严格的单机行为(留神它和 onSingleTapUp 的区别,如果触发了 onSingleTapConfirmed, 那么前面不可能再紧跟着另一个单击行为,既这只可能是单击,而不可能是双击中的一次单击)
onDoubleTapEvent 示意产生了双击行为,在双击的期间,ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 都会触发此回调

下面图表外面的办法很多,然而并不是所有的办法都会被时罕用到,在日常开发中,比拟罕用的有 onSingleTapUp 单击、onFling 疾速滑动、onScroll 拖动、onLongPress 长按、onDoubleTap 双击。另外在说一下,在理论开发中,能够不应用 GestureDector, 齐全能够本人在 View 的 OnTouchEvent 办法中实现所须要的监听,这个看理论场景跟集体爱好了。

Scroller

Scroller 用于实现 View 的弹性滑动。咱们晓得,当应用 View 的 scrollTo / scrollBy 办法进行滑动时,其过程是霎时实现的,没有一个过渡的成果体验是不敌对的,那么这个时候就能够借助 Scroller 来实现过渡成果的滑动。Scroller 自身无奈让 View 弹性滑动,它须要和 View 的 computeScroll 办法配合应用能力共同完成这个性能。那么如何应用 Scroller 呢?它的典型代码能够说是固定的,如下所示:

class ScrollerSample_1 : LinearLayout {constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context) : super(context) 

    /**
     * 定义滑动 Scroller
     */
    private val mScroller = Scroller(context)

    public fun smoothScrollTo(destX: Int = -100, destY: Int = -100) {
        // 滑动了的地位
        val scrollX = scrollY;
        val delta = destY - scrollY;
        //2000 ms 内滑动到 destX 地位,成果就是迟缓滑动
        mScroller.startScroll(scrollX, 0, 0, delta, 2000)
        invalidate()}

    override fun computeScroll() {if (mScroller.computeScrollOffset()) {scrollTo(mScroller.currX, mScroller.currY)
            postInvalidate()}
    }
}

次要实现有 3 步:

  • 第一步实例化 Scroller
  • 第一步调用 Scroller 的 startScroll 办法,让其外部保留新的变量值
  • 第二步重写 View 的 computeScroll 办法,调度本身的 scrollTo 办法,让其迟缓弹性滑动

View 的滑动

下面咱们介绍了 View 的一些基本知识和一些地位参数概念,该大节未来介绍一个重要的内容 View 的滑动

当初市面上所有软件应该简直都具备滑动的性能吧?能够说滑动性能是一个 APP 的标配,滑动在 Android 开发中具备很重要的作用,因而,把握滑动的办法是实现优化用户体验的根底。滑动能够通过以下三种形式来实现,当然并不是只有三种,其它还得靠本人去开掘。其形式如下:

  1. 通过 View 自身提供的 scrollTo / scrollBy 办法来实现滑动(上一大节咱们曾经用到 scrollTo 办法了)
  2. 通过动画给 View 施加平移成果来实现滑动
  3. 通过扭转 View 的地位参数

scrollTo、scrollBy

为了实现 View 的滑动看,本身专门提供了 scrollTo 和 scrollBy 办法来实现,如下所示:

//View.java
    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) {
        /**
         * 其外部也是调用了 View 的 scrollTo 办法,把以后滑动的 mScrollX,mScrollY 别离加上指定的位                     * 置,而后滑动,屡次调用相当于接着上一次地位滑动
         */
        scrollTo(mScrollX + x, mScrollY + y);
    }

通过下面的源码咱们晓得 scrollBy 办法外部是调用了 scrollTo 办法,那么他们之前有什么区别呢?请看上面剖析:

scrollTo: 基于所传递的 x , y 坐标来进行相对滑动,反复点击如果不扭转滑动参数,那么外部就会做判断,相等就不会再滑动了。

scrollBy: 通过源码咱们晓得外部调用了 scrollTo 办法传递了 mScrollX + x, mScrollY + y 那么这是什么意思呢?其实就是基于以后的地位来做的绝对滑动。反复点击滑动会持续在以后所在的地位上持续滑动。

还有一个知识点咱们要晓得,就是这里呈现了 2 个默认的变量 mScrollX , mScrollY 通过 scrollTo 外部实现咱们晓得,其传递进去的 x,y 别离赋值给了 mScrollX 和 mScrollY 那么它们在这里这么做的具体含意是什么呢?它们能够通过 getScrollX 和 getScrollY 来获取具体的值。上面咱们就来具体分析下:

mScrollX: 在滑动过程中,mScrollX 的值总是等于 View 左边缘和 View 内容左边缘在程度方向的间隔。并且当 View 左边缘在 View 内容左边缘的左边时,mScrollX 值为正,反之为负,艰深的来讲就是如果从左向右滑动,那么 mScrollX 为 负值,反之为正值。

mScrollY: 在滑动过程中,mScrollY 的值总是等于 View 上边缘和 View 内容上边缘在程度方向的间隔。并且当 View 上边缘在 View 内容上边缘下边时,mScrollY 为正,反之为负,艰深的来讲就是如果从上往下滑动,那么 mScrollY 为负值,反之为正值。

下面解释了这么多,为了更好的了解我这里就画一张程度跟竖值方向都滑动了 100 px, 而后来看对应的 mScrollX 和 mScrollY 值是多少,请看下图:

留神: 在应用 scrollBy / scrollTo 对 View 滑动时,只能将 View 的内容进行挪动,并不能将 View 自身进行挪动。

应用动画

上一大节咱们晓得能够采纳 View 本身的 scrollTo / scrollBy 办法来实现滑动性能,本大节介绍另外一个实现滑动的形式,即应用动画,通过动画咱们可能让一个 View 进行平移,而平移就是一种滑动。应用动画来挪动 View,次要是操作 View 的 translationX 和 translationY 属性,能够采纳传统的 View 动画,也能够应用属性动画,如果采纳属性动画留神要兼容 3.0 一下版本,当然当初都 androidX 版本了,能够看理论我的项目状况来具体解决,实现滑动的平移代码如下:

  1. 采纳 View 动画,将 View 在 100ms 内从原始地位向右下角挪动 100 px

    <?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:toXDelta="100"
                android:toYDelta="100"
                android:repeatCount="-1"            
        />
    </set>

    留神: View 动画并不能真正扭转 View 的地位。

  2. 采纳属性动画,将 View 在 100ms 内从原始地位向右平移 100 px

    // 动画属性有 translationX、translationY、alpha、rotation、rotationX、rotationY、scaleX、scaleY  
    val objAnimator = ObjectAnimator.ofFloat(View targetView,"translationX",0f,100f).setDuration(100).start()

扭转 View LayoutParams

本大节将介绍第三种实现 View 滑动的办法,那就是间接扭转布局参数, 即 LayoutParams。比方咱们想把一个 LinearLayout 向右平移 100px 只须要将它的 LayoutParams 外部的 marginLeft 参数的值减少 100 px 就行,代码如下:

val layoutParams = scroller.layoutParams as LinearLayout.LayoutParams
layoutParams.let {
                it.leftMargin += 100
                it.weight += 100
            }
            scroller.requestLayout()

通过扭转 LinearLayout 的 LayoutParams 参数同样也实现了 View 的滑动。

滑动形式比照

下面别离介绍了 3 种不同的滑动形式,它们都能实现 View 的滑动,那么它们之间的差别是什么呢?请看下表:

实现形式 长处 毛病
scrollTo/scrollBy 专门用于 View 的滑动,比拟不便地实现滑动成果且不影响本身的单机事件 只能滑动 View 的内容,不能滑动 View 自身
动画 简单动画应用属性动画来实现比较简单 View 动画不能扭转本身属性
扭转布局参数 应用不简洁

针对下面状况这里做一个小总结:

  • scrollTo/scrollBy 操作简略,适宜对 View 内容的滑动
  • 动画操作简略,次要适宜用于没有交互的 View 和实现简单的动画成果
  • 扭转布局参数操作略微简单,实用于有交互的 View

弹性滑动

晓得了 View 如何滑动,咱们还要晓得如何实现 View 的弹性滑动,比拟僵硬的滑动体验的确很差,上面咱们介绍 View 如何实现弹性滑动

应用 Scroller

请参考该篇 View 基础知识 #Scroller 介绍

通过动画

利用动画的个性来实现一些动画不能实现的成果,模拟 Scroller 来实现 View 的弹性滑动,代码如下:

val valueAnimator = ValueAnimator.ofInt(0, 1).setDuration(2000);
valueAnimator.addUpdateListener {
   val animatedFraction = it.animatedFraction
   scroller.scrollTo(- (100 * animatedFraction).toInt(), 0)
}
valueAnimator.start()

在上述代码中,咱们的动画实质上没有作用于任何对象上,它只是在 2s 内实现了整个动画过程,利用这个个性咱们就能够在动画的每一帧到来时获取动画实现的比例,而后依据这个比例计算滑动的间隔。

通过延时策略

该大节咱们持续介绍另一种实现弹性滑动的成果,即延时策略,它的核心思想是通过发送一系列延时音讯从而达到一种渐近式的成果,代码如下:

    val MESSAGE_SCROLLER_TO = 1;
    val FRAME_COUNT = 30;
    val DELAYED_TIME = 33L;
    var mCount = 0;
    private val mHandler = object : Handler() {override fun handleMessage(msg: Message) {super.handleMessage(msg)
            when (msg.what) {
                MESSAGE_SCROLLER_TO -> {
                    mCount++
                    if (mCount <= FRAME_COUNT) {val fraction = mCount / FRAME_COUNT.toFloat()
                        val scrollX = (fraction * 100).toInt()
                        scroller.scrollTo(scrollX, 0)
                        sendEmptyMessageDelayed(MESSAGE_SCROLLER_TO, DELAYED_TIME)
                    }
                }

            }
        }
    }

其成果都是一样的,这里就不再贴成果了,在理论中能够依据我的项目需要或灵活性来抉择到底应用哪一种来实现弹性滑动。

基础知识就讲到这里了,上面基于咱们所学的基础知识练习几道对于滑动的自定义 View

使用所学常识进行实战

这里由浅到深的案例练习。

1. View 随着手指挪动

public class SlideView1(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    /**
     * 记录上次滑动的坐标
     */
    private var mLastX = 0;
    private var mLastY = 0;

    /**
     * 初始化画笔
     */
    val paint = Paint().apply {
        color = Color.BLACK
        isAntiAlias = true
        strokeWidth = 3f
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拿到绝对于屏幕按下的坐标点
                mLastX = event.getX().toInt();
                mLastY = event.getY().toInt();
                println("拿到绝对于屏幕按下的坐标点: x:$mLastX y:$mLastY")

            }
            MotionEvent.ACTION_MOVE -> {var offsetX = event.getX().toInt() - mLastX;// 计算 View 新的摆放地位
                var offsetY = event.getY().toInt() - mLastY;
                // 从新搁置新的地位
                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
            }

            MotionEvent.ACTION_UP -> {}}
        return true// 耗费触摸事件
    }

    override fun onDraw(canvas: Canvas) {super.onDraw(canvas)
        canvas.drawCircle(300f, 300f, 150f, paint)
    }

}

第二种 setX/setY 形式

public class SlideView2(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    /**
     * 记录上次滑动的坐标
     */
    private var mLastX = 0;
    private var mLastY = 0;

    private val mScroller = Scroller(context)

    /**
     * 初始化画笔
     */
    val paint = Paint().apply {
        color = Color.BLACK
        isAntiAlias = true
        strokeWidth = 3f
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拿到绝对于屏幕按下的坐标点
                mLastX = event.getRawX().toInt();
                mLastY = event.getRawY().toInt();
                println("拿到绝对于屏幕按下的坐标点: x:$mLastX y:$mLastY")

            }
            MotionEvent.ACTION_MOVE -> {
              //1
//                x = event.getRawX() - mLastX
//                y =  event.getRawY() - mLastY

                            //2
                translationX = event.getRawX() - mLastX
                translationY = event.getRawY() - mLastY}

            MotionEvent.ACTION_UP -> {}}
        return true// 耗费触摸事件
    }

    override fun onDraw(canvas: Canvas) {super.onDraw(canvas)
        canvas.drawCircle(300f, 300f, 150f, paint)
    }
}

第二种办法是调用 View 的 setX、setY 其实外部就是调用的是 setTranslationX、setTranslationY 这 2 中形式其实都一样 setX 外部也会调用 setTranslationX, 能够看一下源码, 如下:

//View.java
    public void setX(float x) {setTranslationX(x - mLeft);
    }

这里为了演示成果,代码没有做边界判断,下来感兴趣的能够本人去钻研,还有其它随着手指滑动的实现就靠本人去挖掘了。

2. 高仿 ViewPager

上面就以 Scroller 来实现一个简版的 ViewPager 成果,要实现 Scroller 成果其固定步骤如下:

  1. 创立 Scroller 的实例
  2. 调用 startScroll() 办法来初始化滚动数据并刷新界面
  3. 重写 computeScroll() 办法,并在其外部实现平滑滚动的逻辑
/**
 * <pre>
 *     author  : devyk on 2019-11-16 19:23
 *     blog    : https://juejin.cn/user/3368559355637566
 *     github  : https://github.com/yangkun19921001
 *     mailbox : yang1001yk@gmail.com
 *     desc    : This is ScrollerViewPager
 * </pre>
 */
class ScrollerViewPager(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    /**
     * 第一步 定义 Scroller 实例
     */
    private var mScroller = Scroller(context)

    /**
     * 判断拖动的最小挪动像素点
     */
    private var mTouchSlop = 0

    /**
     * 手指按下屏幕的 x 坐标
     */
    private var mDownX = 0f

    /**
     * 手指以后所在的坐标
     */
    private var mMoveX = 0f

    /**
     * 记录上一次触发 按下是的坐标
     */
    private var mLastMoveX = 0f

    /**
     * 界面能够滚动的左边界
     */
    private var mLeftBorder = 0

    /**
     * 界面能够滚动的右边界
     */
    private var mRightBorder = 0

    init {init()
    }

    constructor(context: Context?) : this(context, null) { }

    private fun init() {
        /**
         * 通过 ViewConfiguration 拿到认为手指滑动的最短的挪动 px 值
         */
        mTouchSlop = ViewConfiguration.get(context).scaledPagingTouchSlop
    }

    /**
     * 测量 child 宽高
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 拿到子 View 个数
        val childCount = childCount
        for (index in 0..childCount - 1) {val childView = getChildAt(index)
            // 为 ScrollerViewPager 中的每一个子控件测量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec)

        }
    }

    /**
     * 测量完之后,拿到 child 的大小而后开始对号入座
     */
    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {if (changed) {
            val childCount = childCount
            for (child in 0..childCount - 1) {
                // 拿到子 View
                val childView = getChildAt(child)
                // 开始对号入座
                childView.layout(
                    child * childView.measuredWidth, 0,
                    (child + 1) * childView.measuredWidth, childView.measuredHeight
                )
            }
            // 初始化左右边界
            mLeftBorder = getChildAt(0).left
            mRightBorder = getChildAt(childCount - 1).right

        }

    }

    /**
     * 重写拦挡事件
     */
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                // 拿到手指按下相当于屏幕的坐标
                mDownX = ev.getRawX()
                mLastMoveX = mDownX
            }
            MotionEvent.ACTION_MOVE -> {
                // 拿到以后挪动的 x 坐标
                mMoveX = ev.getRawX()
                // 拿到差值
                val absDiff = Math.abs(mMoveX - mDownX)
                mLastMoveX = mMoveX
                // 当手指拖动值大于 TouchSlop 值时,就认为是在滑动,拦挡子控件的触摸事件
                if (absDiff > mTouchSlop)
                    return true
            }
        }

        return super.onInterceptTouchEvent(ev)
    }

    /**
     * 父容器没有拦挡事件,这里就会接管到用户的触摸事件
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {
            MotionEvent.ACTION_MOVE -> {
                // 拿到以后滑动的绝对于屏幕左上角的坐标
                mMoveX = event.getRawX()
                var scrolledX = (mLastMoveX - mMoveX).toInt()
                if (scrollX + scrolledX < mLeftBorder) {scrollTo(mLeftBorder, 0)
                    return true
                }else if (scrollX + width + scrolledX > mRightBorder){scrollTo(mRightBorder-width,0)
                    return true

                }
                scrollBy(scrolledX,0)
                mLastMoveX = mMoveX
            }
            MotionEvent.ACTION_UP -> {
                // 当手指抬起是,依据以后滚动值来断定应该回滚到哪个子控件的界面上
                var targetIndex = (scrollX + width/2) / width
                var dx = targetIndex * width - scrollX
                /** 第二步 调用 startScroll 办法弹性回滚并刷新页面 */
                mScroller.startScroll(scrollX,0,dx,0)
                invalidate()}
        }
    return super.onTouchEvent(event)
    }

    override fun computeScroll() {super.computeScroll()
        /**
         * 第三步 重写 computeScroll 办法,并在其外部实现平滑滚动的逻辑
         */
        if (mScroller.computeScrollOffset()){scrollTo(mScroller.currX,mScroller.currY)
            postInvalidate()}
    }
}
<?xml version="1.0" encoding="utf-8"?>
<com.devyk.customview.sample_1.ScrollerViewPager
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <Button
            android:id="@+id/btn_1"
            android:text="1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_2"
            android:text="2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_3"
            android:text="3"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_4"
            android:text="4"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_5"
            android:text="5"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    <Button
            android:id="@+id/btn_6"
            android:text="6"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</com.devyk.customview.sample_1.ScrollerViewPager>

通过下面自定义 ViewGroup 实现了跟 ViewPager 一样的成果基本上用到了该篇文章所学常识,比方 Scroller、TouchSlop 还有下一节行将要剖析的事件拦挡解决及散发机制。

总结

该篇文章对于老手来说肯定要把握, 特地是文章中基础知识和 View 的滑动实现,只有打好了根底,看开源自定义框架或本人写自定义才棘手。

(ps: 能够看到下面代码示例都是基于 Kotlin 来编写了,如果有对 Kotlin 感兴趣的或者是从 0 开始学的,看完 Kotlin 根底语法 之后,我置信 新手入门级 Kotlin GitHub APP 正是你须要的)

感激你的浏览,谢谢。

作者:DevYK
链接:https://juejin.im/post/5dcff9d3f265da0bd20af0da

更多 Android 系列教程上传在 bilibili:** https://space.bilibili.com/686960634

退出移动版