一、自定义View

1.自定义View的类型

1.1.组合控件。

1.2.扩展系统View控件功能。例如TextView,继承它并扩展它的功能。

1.3.继承View。新建一个新的控件。

1.4.继承ViewGroup系统控件。继承LinearLayout等系统控件,并扩展它的功能。

1.5.继承ViewViewGroup。新建一个新的ViewGroup控件。

 

2、View绘制流程

View的绘制基本与measure()、layout()、draw()这三个函数完成,也就是说绘制分三步,measure->layout->draw

2.1、measure()。它的作用是测量View的宽高。相关方法有measure()、setMeasuredDimension()、onMeasure()

2.2、layout()。计算当前View以及子View的位置。相关方法有:layout()、onLayout()、setFrame()

2.3、draw()。视图的绘制工作。draw(),onDraw()

 

3、坐标系

Android坐标系:以手机屏幕为坐标系,把屏幕左上角作为原点,这个原点向右是X轴的正轴,向下是Y轴正轴,如下图所示:

自定义RecyclerView GridLayoutManager的ItemDecoration 自定义view的三个方法_android

 

 

 View坐标系:它是以View内部作为一个坐标系

由上图可算出View的 高度:

width = getRight() - getLeft();

height = getBottom() - getTop();

View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度,其内部方法和上文所示是相同的,我们可以直接调用获取View得宽高。

 

获取View自身的坐标

通过如下方法可以获取View到其父控件的距离:

2.1.getTop():获取View到其父布局顶边的距离。

2.2.getLeft():获取View到其父布局左边的距离。

2.3.getBottom():获取View到其父布局顶边的距离。

2.4.getRight():获取View到父布局左边的距离。

4、构造函数

无论是我们继承系统View还是直接继承View,都需要对构造函数进行重写,构造函数有多个,至少要重写其中一个才行。如我们新建TestView。

5、自定义属性

Android系统的控件以Android开头的都是系统自带的属性。为了方便配置自定义View的属性,我们也可以自定义属性值。

Android自定义属性可分为以下几步:

(1).自定义一个View

(2).编写values/attrs.xml,其中编写styleable和item等标签元素

(3).在布局文件中View使用自定义的属性(注意namespace)

 

新建一个resource文件,名字为values/attrs.xml,其内容如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    //自定义控件View
    <declare-styleable name="test">
        <attr name="text" format="string"/>
        <attr name="testAttr" format="integer"/>
    </declare-styleable>
</resources>

自定义View类
public class MyTextView extends View {
    public MyTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test);
        String text = ta.getString(R.styleable.test_text);
        int arr = ta.getInteger(R.styleable.test_testAttr,-1);
        System.out.println(text+","+arr);
        ta.recycle();
    }
}

在布局文件的使用
<com.jatpack.camerax01.ui.widget.MyTextView
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:text="测试"
    app:testAttr="520"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintBottom_toTopOf="@+id/confirm_btn"
    />

6.属性值的类型format
(1)、reference:参考某一资源ID
(2)、color颜色值
(3)、string
(4)、integer
(5)、boolean
(6)、dimension尺寸值
(7)、float浮点型
(8)、fraction百分数
(9)、enum枚举值。如下图所示
<attr name="orientation">
    <enum name="horizontal" value="0"/>
    <enum name="vertical" value="1"/>
</attr>
(10)、flag位或运算
<attr name="gravity">
    <flag name="top" value="0x01"/>
    <flag name="bottom" value="0x02"/>
    <flag name="left" value="0x04"/>
    <flag name="right" value="0x08"/>
    <flag name="center_vertical" value="0x16"/>
</attr>
(11)、混合类型:属性定义时可以指定多种类型值

二、View绘制流程
View的绘制基本有measure()、layout()、draw()这三个函数完成
2.1、Measure()
MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。

MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。
即MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY、AT_MOST
对于View来说,MeasureSpec的mode和Size有如下意义
(1)、EXACTLY:精确模式,View需要一个精确值,这个值即为MeasureSpe当中的Size。对应match_parent
(2)、AT_MOST:最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值。对应wrap_content
(3)、UNSPECIFIED:无限制,View对尺寸没有任何限制,View设置为多大就应当为多大
2.2、Layout()
layout()过程,对应View来说用来计算View的位置参数,对应ViewGroup来说,除了要测量自身位置,还需要测量子View的位置。
layout()方法是整个Layout()流程的入口,看一下这部分源码,进入View.layout有以下核心代码
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
    onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

//这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置
boolean changed = isLayoutModeOptical(mParent) ?
        setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

//调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
    onLayout(changed, l, t, r, b);

    if (shouldDrawRoundScrollbar()) {
        if(mRoundScrollbarRenderer == null) {
            mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
        }
    } else {
        mRoundScrollbarRenderer = null;
    }

    mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnLayoutChangeListeners != null) {
        ArrayList<OnLayoutChangeListener> listenersCopy =
                (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
        int numListeners = listenersCopy.size();
        for (int i = 0; i < numListeners; ++i) {
            listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
        }
    }
}
2.3、Draw()
draw流程也就是View的绘制,整个流程的入口在View的draw()方法中,整个过程分为7个步骤,源码的注释如下
/*
 * Draw traversal performs several drawing steps which must be executed
 * in the appropriate order:
 *
 *      1. Draw the background
 *      2. If necessary, save the canvas' layers to prepare for fading
 *      3. Draw view's content
 *      4. Draw children
 *      5. If necessary, draw the fading edges and restore layers
 *      6. Draw decorations (scrollbars for instance)
 *      7. If necessary, draw the default focus highlight
 */
(1)绘制背景
final Drawable background = mBackground;
if (background == null) {
    return;
}
setBackgroundBounds();
(2)如有必要,保存当前画布,以便后面的绘制

(3)绘制内容
(4)绘制子View
(5)如有必要,绘制边缘等其他图层
(6)绘制装饰(例如滚动条)
(7)如有必要,绘制默认重点
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
    // Step 3, draw the content
    onDraw(canvas);

    // Step 4, draw the children
    dispatchDraw(canvas);

    drawAutofilledHighlight(canvas);

    // Overlay is part of the content and draws beneath Foreground
    if (mOverlay != null && !mOverlay.isEmpty()) {
        mOverlay.getOverlayView().dispatchDraw(canvas);
    }

    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);

    // Step 7, draw the default focus highlight
    drawDefaultFocusHighlight(canvas);
。。。。。

3、自定义组合控件
自定义组合控件就是将多个控件组合成为一个新的控件,主要解决多次重复使用同一类型的布局。如我们顶部的HeaderView以及dailog等,我们都可以
把他们组合成一个新的控件。

我们通过一个自定义HeaderView实例来了解自定义组合控件的用法。
其全部代码如下
<declare-styleable name="HeaderBar">
    <attr name="title_text_color" format="color"/>
    <attr name="title_text" format="string"/>
    <attr name="show_views">
        <flag name="left_text" value="0x01"/>
        <flag name="left_img" value="0x02"/>
        <flag name="right_text" value="0x04"/>
        <flag name="right_img" value="0x08"/>
        <flag name="center_text" value="0x10"/>
        <flag name="center_ing" value="0x20"/>
    </attr>
</declare-styleable>
<com.jatpack.camerax01.ui.widget.YFHeaderView
    android:id="@+id/nav_top"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:title_text="拍照"
    app:show_views="center_text|left_img|right_img"/>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:id="@+id/header_root_layout"
    android:background="#827192">

    <ImageView
        android:id="@+id/header_left_img"
        android:layout_width="48dp"
        android:layout_height="48dp"
        android:layout_alignParentLeft="true"
        android:paddingLeft="12dp"
        android:paddingRight="12dp"
        android:scaleType="fitCenter"
        android:src="@drawable/back"/>

    <TextView
        android:id="@+id/header_center_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:lines="1"
        android:maxLines="11"
        android:ellipsize="end"
        android:text="title"
        android:textStyle="bold"
        android:textColor="#ffffff"/>

    <ImageView
        android:id="@+id/header_right_img"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_margin="12dp"
        android:layout_alignParentRight="true"
        android:src="@drawable/add"
        android:scaleType="fitCenter"/>

</RelativeLayout>
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import com.jatpack.camerax01.R;

public class YFHeaderView extends RelativeLayout {
    public YFHeaderView(Context context) {
        super(context);
        initView(context);
    }

    public YFHeaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
        initAttrs(context,attrs);
    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
        initAttrs(context,attrs);
    }

    public YFHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
        initAttrs(context,attrs);
    }

    RelativeLayout layoutRoot;
    ImageView imgLeft;
    ImageView imgRight;
    TextView textCenter;

    /**
     * 初始化UI,可根据业务需求设置默认值
     * @param context
     */
    private void initView(Context context){
        LayoutInflater.from(context).inflate(R.layout.headerview,this,true);
        imgLeft = findViewById(R.id.header_left_img);
        imgRight = findViewById(R.id.header_right_img);
        textCenter = findViewById(R.id.header_center_text);
        layoutRoot = findViewById(R.id.header_root_layout);
        //layoutRoot.setBackgroundColor(Color.BLACK);
        textCenter.setTextColor(Color.WHITE);
    }

    private void initAttrs(Context context,AttributeSet attrs){
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.HeaderBar);
        String title = mTypedArray.getString(R.styleable.HeaderBar_title_text);
        if(!TextUtils.isEmpty(title)){
            textCenter.setText(title);
        }
        int showView = mTypedArray.getInt(R.styleable.HeaderBar_show_views,0x26);
        int color = mTypedArray.getColor(R.styleable.HeaderBar_title_text_color, 100);
        if(color != 100) {
            textCenter.setTextColor(color);
        }
        mTypedArray.recycle();
        showView(showView);
    }

    private void showView(int showView) {
        //将showView转换为二进制数,根据不同位置上的值设置对应View的显示或者隐藏。
        Long data = Long.valueOf(Integer.toBinaryString(showView));
        String element = String.format("%06d", data);
        for (int i = 0; i < element.length(); i++) {
            if(i == 0) ;
            if(i == 1) textCenter.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 2) imgRight.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 3) ;
            if(i == 4) imgLeft.setVisibility(element.substring(i,i+1).equals("1")? View.VISIBLE:View.GONE);
            if(i == 5) ;
        }

    }

    public void setTitle(String title){
        if(title!=null&&title.trim().length()>0){
            textCenter.setText(title);
        }
    }

    public void setLeftListener(OnClickListener onClickListener){
        imgLeft.setOnClickListener(onClickListener);
    }

    public void setRightListener(OnClickListener onClickListener){
        imgRight.setOnClickListener(onClickListener);
    }

}


4.继承系统控件
继承系统的控件可以分为继承View子类和继承ViewGroup子类。下面介绍继承View的方式。

需求:为字体设置背景,并在布局中间添加一条横线。

实现这种方式不需要全部重写系统的逻辑,上面我面说过绘制控件分三步onMeaseur、onlayout、onDraw,而通常
我们只需复写onDraw方法就可以了,其他两个方法完全可以复用不需要重写。其代码如下所示:
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class LineTextView extends androidx.appcompat.widget.AppCompatTextView {
    public LineTextView(@NonNull Context context) {
        super(context);
        init();
    }

    public LineTextView(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LineTextView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    //定义画笔,用来绘制中心曲线
    private Paint paint;

    private void init(){
        paint = new Paint();
        paint.setColor(Color.BLACK);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int height = getHeight();

        //设置画笔颜色为蓝色
        paint.setColor(Color.BLUE);

        //绘制蓝色方形背景
        RectF rectF = new RectF(0,0,width,height);
        canvas.drawRect(rectF,paint);

        //绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)
        //绘制画笔为黑色
        paint.setColor(Color.BLACK);
        //绘制中心曲线,起点坐标(0,height/2),终点坐标(width,height/2)
        canvas.drawLine(0,height/2,width,height,paint);
    }
}

5.直接继承View
上一种方式是继承系统控件,也就是已经修好的继承View的控件。直接继承View(与系统控件地位一样,可以看成同一类型)。
所以复用的方法会不一样。在这里需要重写onMeasure和onDraw方法。

根据View的源码中AT_MOST(最大限制模式)和ExACTLY(精确模式)并没有做出区分,两种的处理逻辑是一样的。也就是说wrap_content
和match_parent处理逻辑是一样的,都会是match_parent,显然与我们想要用的不一样,所以需要重写onMeasure方法。
重写onMeasure方法

直接继承View需要注意以下几点:
1、在onDraw当中对padding属性进行处理。
2、在onMeasure过程中对wrap_content属性进行处理。
3、至少要有一个构造方法。
public class RectView extends View {
    public RectView(Context context) {
        super(context);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init();
    }

    //定义画笔
    private Paint paint = new Paint();

    private void init(){
        paint.setColor(Color.BLUE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取各个边距的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        //获取绘制的View的宽度
        int width = getWidth() - paddingLeft - paddingRight;
        //获取绘制的View的高度
        int height = getHeight() - paddingTop - paddingBottom;
        //绘制View,左上角坐标(0+paddingLeft,0+paddingTop),右下角坐标(width+paddingLeft,height+paddingTop)
        canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,paint);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(widthMeasureSpec);

        //处理wrap_contented情况
        if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(300,3000);
        } else if(widthMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(300,heightMeasureSpec);
        } else if(heightMode == MeasureSpec.AT_MOST){
            setMeasuredDimension(widthSize,300);
        }
    }
}

6.继承ViewGroup
自定义的ViewGroup比自定义的View复杂一些,因为他还需要对子View的参数进行处理。下面我们将做一个例子
例子:实现一个类似于Viewpager的可左右滑动的布局
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontaiView extends ViewGroup {

    private int lastX;
    private int lastY;

    private int currentIndex = 0;
    private int childWidth = 0;
    private Scroller scroller;
    private VelocityTracker tracker;

    public HorizontaiView(Context context) {
        super(context);
        init(context);
    }

    public HorizontaiView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public HorizontaiView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void init(Context context){
        scroller = new Scroller(context);
        tracker = VelocityTracker.obtain();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取宽高的测量模式以及测量值
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //测量所有子View
        measureChildren(widthMeasureSpec,heightMeasureSpec);
        //如果没有子View,则View0,0
        if(getChildCount() == 0){
            setMeasuredDimension(0,0);
        } else if(widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            int childHeight = childOne.getMeasuredHeight();
            //View的宽度=单个子View宽度*子View数,View的高度=子View的高度
            setMeasuredDimension(getChildCount()*childWidth,childHeight);
        } else if(widthMode == MeasureSpec.AT_MOST){
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            //View的宽度=单个子View宽度*子View个数,View的高度=自己设置的高度
            setMeasuredDimension(getChildCount()*childWidth,heightSize);
        } else if(heightMode == MeasureSpec.AT_MOST){
            View childOne = getChildAt(0);
            int childHeight = childOne.getMeasuredHeight();
            //View的宽度=xml当中设置的宽度,View的高度=子View高度
            setMeasuredDimension(widthSize,childHeight);
        }
    }

    /**
     * 接下来重写onLayout方法,对各个子View设置位置
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int left = 0;
        View child;
        for(int i = 0;i < childCount;i++){
            child = getChildAt(i);
            if(child.getVisibility() != View.GONE){
                childWidth = child.getMeasuredWidth();
                child.layout(left,0,left + childWidth,child.getMeasuredHeight());
                left += childWidth;
            }
        }
    }

    /**
     * 因为我们定义的是ViewGroup,从onInterceptTouchEvent开始
     * 重写onInterceptTouchEvent,对横向滑动事件进行拦截
     * @param event
     * @return
     */
    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        //return super.onInterceptHoverEvent(event);
        boolean intercrpt = false;
        //记录当前点击的坐标
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - lastX;
                int deltaY = y - lastY;
                //当X轴移动的绝对值大于Y轴移动的绝对值时,表示用户进行了横向滑动,对事件进行拦截
                if(Math.abs(deltaX) > Math.abs(deltaY)){
                    intercrpt = true;
                }
                break;
        }
        lastX = x;
        lastY = y;
        //intercrpt = true表示对事件进行拦截
        return intercrpt;
    }

    /**
     * 当ViewGroup拦截下用户的横向滑动事件后,后续的Touch事件将交付给
     * onTouchEvent进行处理,重写onTouchEvent方法
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //return super.onTouchEvent(event);
        tracker.addMovement(event);
        //获取事件坐标(x,y)
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - lastX;
                int delatY = y - lastY;
                //scrollBy方法将对我们当前View的位置进行偏移
                scrollBy(-deltaX, 0);
                break;
            //当产生ACTION_UP事件时,也就是我们抬起手指
            case MotionEvent.ACTION_UP:
                //getScrollX()为在X轴方向发生的便宜,childWidth * currentIndex表示当前View在滑动开始之前的X坐标
                //distance存储的就是此次滑动的距离
                int distance = getScrollX() - childWidth * currentIndex;
                //当本次滑动距离>View宽度的1/2时,切换View
                if (Math.abs(distance) > childWidth / 2) {
                    if (distance > 0) {
                        currentIndex++;
                    } else {
                        currentIndex--;
                    }
                } else {
                    //获取X轴加速度,units为单位,默认为像素,这里为每秒1000个像素点
                    tracker.computeCurrentVelocity(1000);
                    float xV = tracker.getXVelocity();
                    //当X轴加速度>50时,也就是产生了快速滑动,也会切换View
                    if (Math.abs(xV) > 50) {
                        if (xV < 0) {
                            currentIndex++;
                        } else {
                            currentIndex--;
                        }
                    }
                }
                //对currentIndex做出限制其范围为【0,getChildCount() - 1】
                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
                //滑动到下一个View
                smoothScrollTo(currentIndex * childWidth, 0);
                tracker.clear();
                break;
        }
        lastX = x;
        lastY = y;
        return true;
    }

    private void smoothScrollTo(int destX, int destY) {
        //startScroll方法将产生一系列偏移量,从(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()为移动的距离
        scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000);
        //invalidate方法会重绘View,也就是调用View的onDraw方法,而onDraw又会调用computeScroll()方法
        invalidate();
    }

    //重写computeScroll方法
    @Override
    public void computeScroll() {
        super.computeScroll();
        //当scroller.computeScrollOffset()=true时表示滑动没有结束
        if (scroller.computeScrollOffset()) {
            //调用scrollTo方法进行滑动,滑动到scroller当中计算到的滑动位置
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            //没有滑动结束,继续刷新View
            postInvalidate();
        }
    }
}