Android知识梳理之自定义ViewGroup

         在我们进行android开发的时候虽然官方提供了形形色色的控件,但是有的时候根据不同业务需求我们找不到官方的控件支持,那么这个时候就需要我们自己去定义控件来适应不同的需求了。众所周知Android自定义为一般分为三类:自定义ViewGroup,自绘View,组合View。本篇将和大家一起探讨自定义ViewGrop 的相关知识. 

转载请注明出处: http://blog.csdn.net/unreliable_narrator?viewmode=contents

                                                                                           

一.ViewGrop是什么。

首先我们先来看看官方文档是如何进行描述的:

        翻译过来的大体意思就是: 一个ViewGroup是一个可以包含其他View特殊的View。并且是View或者布局的父容器。而且ViewGroup定义了ViewGroup.LayoutParams类。简单说ViewGroup实际上就是存放一些view容器,比如官方自带的一些LinerlayoutRelativeLayout等等都是ViewGroup的具体实现。由于Viewgroup一般是用来作为装子View的容器,所以一般不会去自己绘制(ondraw)内容,viewgroup通过onMeasure方法来确定子View的大小,然后通过Onlayout方法来确定View摆放的位置。

二.ViewGrop的测量。

1.onMeasure()方法作用:

onMeasure()主要的作用就是测量ViewGroup的大小。而viewgroup的作用主要用于管理子view,而在测量的时候可以分两种情况:

1.viewgroup在xml中定义时,宽和高是match_parent或者是固定值,此时说明该viewgroup的宽高是明确的,所以不需要自己去计算。就可以用系统的onMeasure。

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

2.viewgroup在xml中定义时,宽和高是wrap_content,此时viewgroup的大小不确定,就需要重写onMeasure遍历所有的子View,然后对每个子View使用measureChild(childView)进行测量,测量完以后然后根据childView.getMeasuredWidth()获取到子控件的宽高,然后根据子View的排列规则,计算出最终ViewGroup的大小,最后用setMeasuredDimension(int measuredWidth, int measuredHeight)设置进去。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWith = 0;
        int measureHeight = 0;
        int childCount = getChildCount();//获取子控件的总数
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);//获取子控件
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子控件
            measureWith += childView.getMeasuredWidth();
            measureHeight += childView.getMeasuredHeight();
        }
        setMeasuredDimension(measureWith, measureHeight);//设置控件的大小
    }

2.onMeasure()方法参数解析:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

调用此方法会传进来的两个参数:int widthMeasureSpec, int heightMeasureSpec 。他们是父类传递过来给当前view的一个建议值,虽然表面上看起来他们是int类型的数字,其实他们是由mode+size两部分组成的。 widthMeasureSpec和heightMeasureSpec转化成二进制数字表示,他们都是30位的。前两位代表mode(测量模式),后面28位才是他们的实际数值(size)。 

MeasureSpec.getMode(widthMeasureSpec)//获取测量模式

MeasureSpec.getSize(widthMeasureSpec)//获取测量尺寸  

三种不同的测量模式:

        EXACTLY:表示设置了精确的值,一般View设置其宽或者高为精确值(也就是我们在布局文件中设定的值如50dp)或者match_parent时,其宽高所对应的值为EXACTLY;

        AT_MOST:表示子布局被限制在一个最大值内,一般当View设置其宽或者高为wrap_content时,其宽高所对应的值为AT_MOST;

        UNSPECIFIED:表示子布局想要多大就多大,一般出现在AadapterView的item的heightMode中、ScrollView的childView的heightMode中,布局可以滚动,此种模式比较少见。

3.测量子控件相关的方法:

1. measureChild(subView, int wSpec, int hSpec),调用viewGroup的测量子view的方法:

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();//获取所有的子View个数
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);//获取子View
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);//测量子View
        }
    }

2. measureChildren(int wSpec, int hSpec)测量所有子view 都是 多宽,多高, 内部调用了measureChild方法 ;

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec,heightMeasureSpec);
    }

3. measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed),某一个子view,多宽,多高, 内部加上了viewGroup的padding值、margin值和传入的宽高wUsed、hUsed ;

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();//获取所有的子View个数
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);//获取子View
            measureChildWithMargins(childView,  widthMeasureSpec, int widthUsed,
            heightMeasureSpec, int heightUsed);//某一个子view,多宽,多高, 内部加上了viewGroup的padding值、margin值和传入的宽高wUsed、hUsed
        }
    }

4.getMeasuredWidth()与getWidth()的区别.

getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。
区别主要体现在下面两点:
(1)首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
(2) getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。

二.ViewGrop的布局

1.onLayout()方法:

onLayout()是实现所有子控件布局的函数。它的作用就是给每一个子控件设置摆放的位置.    

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }

View的放置都是根据一个矩形空间放置的,onLayout传下来的l,t,r,b分别是放置父控件的矩形可用空间(除去margin和padding的空间)的左上角的left、top以及右下角right、bottom值参数changed表示view有新的尺寸或位置。需要注意的是如果子view超出了viewgroup所onMeasure(设置好大小)的部分,那部分不会显示出来。

 例如:如果我们要实现如下图所示的效果:我们可以看到我们自定义的viewgrop里面的三个子控件并排显示,并且自定义的ViewGrop的高度是和子控件里面最高的View是一样高的.ViewGrop的宽度是和所有子控件的宽度相累加的是一样的.

public class MyViewGroup extends ViewGroup {
    public MyView(Context context) {
        this(context, null);
    }
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //调用该方法预先对子控件进行测量
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //设置控件的宽高
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }
    /**
     * 返回控件的宽
     *
     * @param widthMeasureSpec
     * @return
     */
    private int measureWidth(int widthMeasureSpec) {
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int specSize = MeasureSpec.getSize(widthMeasureSpec);
        int result = 0;
        //判断是否是包裹内容的模式
        if (specMode == MeasureSpec.AT_MOST) {
            int size = 0;
            //将所有的子控件的宽度进行叠加
            for (int x = 0; x < getChildCount(); x++) {
                View child = getChildAt(x);
                int measuredWidth = child.getMeasuredWidth();
                size += measuredWidth;
            }
            result = size;
        } else {
            result = specSize;
        }
        return result;
    }
    /**
     * 返回控件的高
     *
     * @param heightMeasureSpec
     * @return
     */
    private int measureHeight(int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int result = 0;
        //判断是否是包裹内容
        if (heightMode == MeasureSpec.AT_MOST) {
            for (int x = 0; x < getChildCount(); x++) {
                View child = getChildAt(x);
                int measuredHeight = child.getMeasuredHeight();
                //取子控件最大的高度
                int min = Math.max(result, measuredHeight);
                result = min;
            }
        } else {
            result = heightSize;
        }
        return result;
    }
    @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);
            int width = child.getMeasuredWidth();
            child.layout(left, 0, left + width, child.getMeasuredHeight());
            left += width;
        }
    }
}

布局文件:

 <com.dapeng.viewgropdemo.MyViewGroup 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/black">
        <View
            android:layout_width="50dip"
            android:layout_height="50dip"
            android:background="@android:color/holo_green_dark"></View>
        <View
            android:layout_width="100dip"
            android:layout_height="100dip"
            android:background="@android:color/holo_red_light"></View>
        <View
            android:layout_width="200dip"
            android:layout_height="200dip"
            android:background="@android:color/holo_orange_light"></View>
    </com.dapeng.viewgropdemo.MyViewGroup>

三.LayoutParams

1.出现margin等属性无效的原因

比如我们有的时候需要给ViewGrop里面的子控件设置margin等等相关的属性值,直接在布局文件里面进行书写完成以后,仍然是没有效果的。因为测量和布局都是我们自己实现的,我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果。

需要特别注意的是:如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算容器的大小时,也要加上margin,不然会导致容器太小,而控件显示不全的问题。

2.LayoutParams

LayoutParams翻译过来就是布局参数,通俗点说就是能获取到布局一些特定的属性,比如说布局的边距什么的。相当于一个Layout的信息包,它封装了Layout的位置、高、宽等信息。假设在屏幕上一块区域是由一个Layout占据的,如果将一个View添加到一个viewgroup中,最好告诉viewgroup用户期望的布局方式,也就是将一个认可的layoutParams传递进去。

参数  1.  AttributeSet attrs                           xml解析inflate时生成和容器类型匹配的布局LayoutParams

          2.  ViewGroup.LayoutParams p      传入viewGroupLayoutParams 然后生成和容器类型匹配的布局LayoutParams

childView调用的时候就可以取得相关的参数:

MarginLayoutParams cParams = (MarginLayoutParams) childView.getLayoutParams();
			int bottomMargin = cParams.bottomMargin;
			int topMargin = cParams.topMargin;
			int leftMargin = cParams.leftMargin;
			int rightMargin = cParams.rightMargin;

View通过LayoutParams类告诉其父视图它想要地大小(即,长度和宽度)。 因此,每个View都包含一个ViewGroup.LayoutParams类或者其派生类,View类依赖于ViewGroup.LayoutParams ViewGroup子类可以实现自定义LayoutParams,自定义LayoutParams提供了更好地扩展性。
LayoutParams类也只是简单的描述了宽高,宽和高都可以设置成三种值:
1,一个确定的值;
2,FILL_PARENT,即填满(和父容器一样大小);
3,WRAP_CONTENT,即包裹住组件就好。

 通过继承并且扩展LayoutParams类可以增加更多的属性.

四.Android中视图的坐标系:

  1. 获取控件在父控件里面的相对坐标(位置).如下图所示. 

2.scrollTo(int x,int y)和scrollBy(int x,int y)方法.

在自定义View的时候我们知道我们要重写几个方法,其中有一个方法是onDraw(),在这个方法里面我们可以创建画笔在这个画布上面任意的画图.这里需要特别注意的一点就是画布并没有边界的,,也就是说View视图并没有完全显示.调用scrollto()和scroolBy()实际上View并没有移动,而是View的内容(或者说移动的是View的视图)在水平或者说是垂直方向上面的偏移.如果要使得View的位置移动就需要调用offsetLeftAndRight()左右移动;.offsetTopAndBottom()上下移动;scrollTo 和 scrollBy 移动的是 View 的内容。即:对 TextView 使用的话,则是移动它的文本;对 ViewGroup 使用的话,则移动的是所有的子 View。所以,一般不对 View 使用这两个方法,而是对 ViewGroup 使用。

           scrollTo(int x,int y):

调用该方法,这个View的左上角坐标原点会在View视图里进行竖直偏移y,水平偏移x.该方法是针对坐标原点进行偏移.

注意:x,y代表的不是坐标点,而是偏移量。

                scrollBy(int x,int y):

该方法的实现是调用的scrollto()方法,区别就是在现有的基础上继续偏移视图的内容.相对于停留的位置进行偏移.

注意:x,y代表的不是坐标点,而是偏移量。

例如:我们自定义控件继承TextView.然后在布局文件里面将这个自定义的视图设置稍大一点.两个按钮对应的点击事件是:

 mCustomView.scrollBy(-20, -20);

和:

 mCustomView.scrollTo(0, 0);

五.Android中scroller的使用.

1.首先应该明白两个基本的概念

  1. 什么是scroller呢? Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是UI,也不是辅助UI滑动,反而是单纯地为滑动提供计算。也就是说Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是UI,也不是辅助UI滑动,反而是单纯地为滑动提供计算。
    2.Scroller只是提供计算,那谁来调用View是怎么滑动的? 答:真正让View滑动的是scrollTo,scrollBy。

2.scroller常用的方法:

mScroller.getCurrX() //获取mScroller当前水平滚动的位置  
mScroller.getCurrY() //获取mScroller当前竖直滚动的位置  
mScroller.getFinalX() //获取mScroller最终停止的水平位置  
mScroller.getFinalY() //获取mScroller最终停止的竖直位置  
mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置  
mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置  
  
//滚动,startX, startY为开始滚动的位置,dx,dy为滚动的偏移量, duration为完成滚动的时间  
mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms  
mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)  
 
mScroller.computeScrollOffset() //返回值为boolean,true说明滚动尚未完成,false说明滚动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滚动是否结束。  

3.例子一枚:

先自定义控件:
public class CustomView extends TextView {
    private Scroller mScroller;
    public CustomView(Context context) {
        this(context, null);
    }
    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);
    }
    public void smoothScrollTo(int destX, int destY) {
        int scrollX = getScrollX();
        int scrollY = getScrollY();
        //在2000ms 内滑动到目标位置
        mScroller.startScroll(scrollX, scrollY, destX - scrollX, destY - scrollY, 2000);
        //这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果
        invalidate();
    }
    @Override
    public void computeScroll() {
        //先判断mScroller滚动是否完成
        if (mScroller.computeScrollOffset()) {
            //这里调用View的scrollTo()完成实际的滚动
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            //必须调用该方法,否则不一定能看到滚动效果
            invalidate();
        }
        super.computeScroll();
    }
}

在activity中调用:

 mCustomView.smoothScrollTo(-100,-100);

六.自定义Viewgroup实现viewpager效果

1.测量和布局.

观察Viewpager所知,Viewpager里面有子View,所以我们需要自定义Viewgroup来实现。onmeasure()测量大小(这里我们暂时固定大小)onLayout()这个方法是对子View的一个位置摆放。

代码:

public class CustomViewPager extends ViewGroup {

    public CustomViewPager(Context context) {
        this(context, null);
    }

    public CustomViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有子View的大小.
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int childCount = getChildCount();
        //摆放子View
        for (int i4 = 0; i4 < childCount; i4++) {
            View childAt = getChildAt(i4);
            childAt.layout(childAt.getMeasuredWidth() * i4, i1, (i4 + 1) * childAt.getMeasuredWidth(), i3);
        }
    }
}

布局文件:

 <com.zbht.myapplication.CustomViewPager
        android:layout_width="400dp"
        android:layout_height="400dp">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@mipmap/one"/>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@mipmap/two"/>

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@mipmap/thr"/>
    </com.zbht.myapplication.CustomViewPager>

效果:

2.添加滑动效果

由以上效果图,我们看到了第一个子View显示出来了,但是没有滑动的效果。因此我们需要在onTouchEvent对事件进行消费。

  @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStartX = event.getX();//记录x轴的初始位置
                break;
            case MotionEvent.ACTION_MOVE:
                float distanceX = event.getX() - mStartX;//记录x轴滑动的距离
                scrollBy(-(int) distanceX, 0);//滑动x轴滑动的距离
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;//将事件进行消费
    }

效果如下

3.优化滑动效果

使用手势识别器GestureDetector进行优化.

public class CustomViewPager extends ViewGroup {
    private GestureDetector mGestureDetector;
	...
    public CustomViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });
    }
	...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);//使用手势识别器接管事件
        return true;//消费掉事件
    }
}

4.滑动到边界情况的处理。

Viewpager的效果是:手指松开时,当滑动偏移的距离超出子View1/2时,自动切换到下个或者是下一个子View;小于1/2,回弹到初始位置。

public class CustomViewPager extends ViewGroup {
    private int currentIndex;
    private int startX;
    ...
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) event.getX();//获取x轴开始距离
                break;
            case MotionEvent.ACTION_UP:
                float realEndX = event.getX();//x轴滑动结束的坐标
                int tempIndex = currentIndex;
                //结束x坐标大于开始x坐标,因此是向左滑动,要显示下一个View
                if ((startX - realEndX) > getWidth() / 2) {
                    tempIndex++;
                } else if ((realEndX - startX) > getWidth() / 2) {
                    //结束x坐标小于开始x坐标,因此是向右滑,显示上一张
                    tempIndex--;
                }
                //根据下标位置移动到指定的页面
                scrollToPage(tempIndex);
                break;
        }
        return true;
    }
    public void scrollToPage(int tempIndex) {
        //屏蔽非法值
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }

        //当前的坐标位置
        currentIndex = tempIndex;
        //根据位置移动到指定页面
        scrollTo(currentIndex * getWidth(), 0);
    }
}

效果:

5.优化回弹效果。

使用Scroller 来优化滚动效果.

public class CustomViewPager extends ViewGroup {
   
    private Scroller mScroller;
	...
    public void scrollToPage(int tempIndex) {
        //屏蔽非法值
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        //当前的坐标位置
        currentIndex = tempIndex;
        //根据位置移动到指定页面
        // scrollTo(currentIndex * getWidth(), 0);
        int backDistanceX = currentIndex * getWidth() - getScrollX();
        mScroller.startScroll(getScrollX(), getScrollY(), backDistanceX, 0);
        invalidate();//使用invalidate这个方法会有执行一个回调方法computeScroll,需要重写该方法
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();//必须使用postInvalidate(),这样才会一直回调computeScroll()这个方法,直到滑动结束。
        }
    }
}

效果

6.解决滑动冲突.

我们自己定义的viewpager如果里面的嵌套的子View为ScrollView,那么ScrollView会将事件吃掉,所以我们自定义的Viewpager就拿不到事件,所以无法左右滑动.

解决方式:

  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                InterceptStartX = (int) ev.getX();//获取x轴开始距离
                InterceptStartY = (int) ev.getY();//获取x轴开始距离
                //这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                int rangeX = (int) Math.abs(ev.getX() - InterceptStartX);
                int rangeY = (int) Math.abs(ev.getY() - InterceptStartY);
                if (rangeX > rangeY) {
                    //横向滑动,拦截事件
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }


完整的自定义CustomViewPager代码:

public class CustomViewPager extends ViewGroup {
    private GestureDetector mGestureDetector;
    private int currentIndex;
    private Scroller mScroller;

    private int startX;
    private int InterceptStartX;
    private int InterceptStartY;

    public CustomViewPager(Context context) {
        this(context, null);
    }

    public CustomViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mGestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
                scrollBy((int) distanceX, 0);
                return super.onScroll(e1, e2, distanceX, distanceY);
            }
        });
        mScroller = new Scroller(context);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);//测量所有子View的大小.
    }

    @Override
    protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
        int childCount = getChildCount();
        //摆放子view
        for (int i4 = 0; i4 < childCount; i4++) {
            View childAt = getChildAt(i4);
            childAt.layout(childAt.getMeasuredWidth() * i4, i1, (i4 + 1) * childAt.getMeasuredWidth(), i3);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                InterceptStartX = (int) ev.getX();//获取x轴开始距离
                InterceptStartY = (int) ev.getY();//获取x轴开始距离
                //这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件还是要给手势识别器处理,否则会丢失事件,滑动的时候会存在bug。
                mGestureDetector.onTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                int rangeX = (int) Math.abs(ev.getX() - InterceptStartX);
                int rangeY = (int) Math.abs(ev.getY() - InterceptStartY);
                if (rangeX > rangeY) {
                    //横向滑动,拦截事件
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) event.getX();//获取x轴开始距离
                break;
            case MotionEvent.ACTION_UP:
                float realEndX = event.getX();//x轴滑动结束的坐标
                int tempIndex = currentIndex;
                //结束x坐标大于开始x坐标,因此是向左滑动,要显示下一个View
                if ((startX - realEndX) > getWidth() / 2) {
                    tempIndex++;
                } else if ((realEndX - startX) > getWidth() / 2) {
                    //结束x坐标小于开始x坐标,因此是向右滑,显示上一张
                    tempIndex--;
                }
                //根据下标位置移动到指定的页面
                scrollToPage(tempIndex);
                break;
        }
        return true;
    }

    public void scrollToPage(int tempIndex) {
        //屏蔽非法值
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        //当前的坐标位置
        currentIndex = tempIndex;
        //根据位置移动到指定页面
        // scrollTo(currentIndex * getWidth(), 0);
        int backDistanceX = currentIndex * getWidth() - getScrollX();
        mScroller.startScroll(getScrollX(), getScrollY(), backDistanceX, 0);
        invalidate();//使用invalidate这个方法会有执行一个回调方法computeScroll,需要重写该方法
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();//必须使用postInvalidate(),这样才会一直回调computeScroll()这个方法,直到滑动结束。
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值