自定义ViewGroup—实现自定义ViewPager

ViewGroup和View

1、 ViewGroup是一个可以容纳View的容器,负责测量子视图或子控件的宽和高;并决定子视图或子控件的位置。常用的方法有:

  • onMesure():测量子视图或子控件的宽高,以及设置自己的宽和高。
  • onLayout():通过getChildCount()获取子view数量,getChildAt获取所有子View,分别调用layout(int l, int t, int r, int b)确定每个子View的摆放位置。
  • onSizeChanged():在onMeasure()后执行,只有大小发生了变化才会执行onSizeChange。
  • onDraw():默认不会触发,需要手动触发。

2、View根据测量模式和ViewGroup给出的建议宽和高,在ViewGroup为其指定的区域内绘制出自己的形态。常用的方法有:

  • onMesure():测试视图大小,主要是处理wrap_content这种情况;
  • onDraw():在父视图指定的区域绘制图形。

自定义ViewPager

我们来实现一个轮播图片的自定义ViewPager。
1、继承ViewGroup,并写个添加图片数据的方法,方便添加图片到ViewGroup容器里

package com.wong.support;

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ImageView;

import java.util.List;

public class WonViewPager extends ViewGroup {

    /*要轮翻播放的图片*/
    private List<Integer> images;

    public WonViewPager(Context context) {
        super(context);
    }

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

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

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

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

    }
    /*批量设置轮播图片*/
    public void setImages(List<Integer> images) {
        this.images = images;
        updateViews();
    }
    /*将子视图添加到ViewGroup容器中*/
    private void updateViews(){
        for(int i = 0; i < images.size(); i++){
            ImageView iv = new ImageView(getContext());
            iv.setBackgroundResource(images.get(i));
            this.addView(iv);
        }
    }
}

2、重写onLayout()方法,获取所有的子View,各自调用layout()方法,按下图排列方式,确定它们各自的摆放位置。
首先来认识一下图片的位置:
在这里插入图片描述

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for(int i = 0; i < childCount; i++){
            View childView = getChildAt(i);
            childView.layout(i*getWidth(),t,(i+1)*getWidth(),b);
        }
    }

3、创建手势识别器Gesturedetector,来完成滑动子视图的功能。

(1) 创建一个手势识别器Gesturedetector
手势识别器通过MotionEvent可以识别出多种手势和事件。当某个特定动作事件发生时,手势识别器Gesturedetector的onTouchEvent(MotionEvent)就会被调用,此方法里再通过调用OnGestureListener定义的回调方法会通知用户具体是什么动作事件。

 GestureDetector mGestureDetector = new GestureDetector(getContext(),new GestureDetector.OnGestureListener(){
        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {
        }
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //相对滑动:X方向滑动多少距离,view就跟着滑动多少距离
            scrollBy((int) distanceX, 0);
            return false;
        }
        @Override
        public void onLongPress(MotionEvent e) {
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    });

(2)在WonViewPager里重写onTouchEvent()方法,并将触摸事件传递给手势识别器处理,并返回true,让该控件消费该事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        return true;
    }

2、应用
activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.wong.support.WonViewPager
        android:id="@+id/wvp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WonViewPager wonViewPager = findViewById(R.id.wvp);
        List<Integer> list = new ArrayList<>();
        list.add(R.drawable.a);
        list.add(R.drawable.b);
        list.add(R.drawable.c);
        list.add(R.drawable.d);
        wonViewPager.setImages(list);
    }
}

效果:
在这里插入图片描述
上面实现了通过手指滑动图片的功能。

4、优化:边界情况的处理和平滑的移动到指定位置。

  • 边界情况的处理:当手指松开时,如果滑动偏移的距离超出图片1/2时,自动切换到下个图片,否则回弹到初始位置。这里我们需要在onTouchEvent()中处理触摸事件:
  /*记录当前视图的序号*/
    private int position;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()){
            /*按下*/
            case MotionEvent.ACTION_DOWN:

                break;
            /*移动,在ACTION_DOWN和ACTION_UP之间*/
            case MotionEvent.ACTION_MOVE:
                /*返回视图正在展示部分的左边滚动位置(即返回滚动的视图的左边位置)*/
                int scrollX = getScrollX();
                /*加上父视图的一半*/
                int totalWidth = scrollX + getWidth()/2;
                /*计算视图划过一半后的下一个视图的序号*/
                position = totalWidth / getWidth();
                 /*计算视图划过一半后的下一个视图的序号*/
                position = totalWidth / getWidth();
                /* scrollX >= getWidth() * (images.size() - 1)说明是最后一张,那么我们就不能让其出界,否则它是可以滑出界的*/
                if (scrollX >= getWidth() * (images.size() - 1)) {
                    position = images.size() - 1;
                }
                /*scrollX < 0说明左边滑入界了,即第一张视图的左边偏右,距离父视图左边之间的距离出现空白*/
                if (scrollX <= 0) {
                    position = 0;
                }

                break;
            /*抬起手指*/
            case MotionEvent.ACTION_UP:
            	/*滑动到指定的视图*/
                scrollTo(position*getWidth(),0);
                break;

        }
        return true;
    }

效果:
在这里插入图片描述

  • 平滑的移动到指定位置
    scrollTo(position*getWidth(),0)会直接移动到指定位置,给人一种“突然”的感觉,没有平滑的过渡。我们可以使用Scroller类的startScroll(int startX, int startY, int dx, int dy) 方法来实现View的平滑滚动。

第一步:定义Scroller对象

private Scroller scroller = new Scroller(getContext());

第二步:调用startScroll(int startX, int startY, int dx, int dy)方法,此方法并不会触发滚动,因为它最终调了以下这个方法(来自android源码),而这个方法只是在收集过程数据而已,调用invalidate()方法触发视图刷新:

 /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    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;
    }

第三步:重写computeScroll(),完成实际的滚动

 @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),0);
            postInvalidate();
        }
    }

修改的代码:

    /*记录当前视图的序号*/
    private int position;
    private Scroller scroller = new Scroller(getContext());
    private int scrollX;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //将触摸事件传递手势识别器
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()){
            /*按下*/
            case MotionEvent.ACTION_DOWN:

                break;
            /*移动,在ACTION_DOWN和ACTION_UP之间*/
            case MotionEvent.ACTION_MOVE:
                /*返回视图正在展示部分的左边滚动位置(即返回滚动的视图的左边位置)*/
                scrollX = getScrollX();
                /*加上父视图的一半*/
                int totalWidth = scrollX + getWidth()/2;
                /*计算视图划过一半后的下一个视图的序号*/
                position = totalWidth / getWidth();
                 /*计算视图划过一半后的下一个视图的序号*/
                position = totalWidth / getWidth();
                /* scrollX >= getWidth() * (images.size() - 1)说明是最后一张,那么我们就不能让其出界,否则它是可以滑出界的*/
                if (scrollX >= getWidth() * (images.size() - 1)) {
                    position = images.size() - 1;
                }
                /*scrollX < 0说明左边滑入界了,即第一张视图的左边偏右,距离父视图左边之间的距离出现空白*/
                if (scrollX <= 0) {
                    position = 0;
                }

                break;
            /*抬起手指*/
            case MotionEvent.ACTION_UP:
                /*滑动到指定位置*/
//                scrollTo(position*getWidth(),0);
                /*平滑移动到指定位置*/
                scroller.startScroll(scrollX,0,-(scrollX-position*getWidth()),0);
                /*从UI线程触发视图更新*/
                invalidate();
                break;

        }
        return true;
    }

    /**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){
            /**
             * 每次x轴有变化都会移动一点,那么要持续变化完,就要调用postInvalidate()持续刷新视图,
             * 而上面的invalidate()方法只负责第一次触发computeScroll()调用,剩下的都是postInvalidate()触发的
             */
            scrollTo(scroller.getCurrX(),0);
            /*从非UI线程触发视图更新,只有调用*/
            postInvalidate();
        }
    }

效果:
在这里插入图片描述
5、优化:滑至最后一屏禁止向右滑,滑至第一屏禁止向左滑
在我们前面的例子里,都会发现第一屏向右滑,就出现空白,最后一屏也出现类似的情况。因为我们一开始是在手势识别器做的移动,所以我们可以在手势识别器GestureDetector做文章。
思路:
1、通过手指划过的路径的终点和起点相减,根据正负判断方向;
2、如果是正,则说明向右划,接着判断是不是第一屏,是的话就不滚动;
3、如果是负,则说明向左划,接着判断是不是最一屏,是的话就不滚动;
修改后的GestureDetector代码如下:

GestureDetector mGestureDetector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {}
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            int startX = 0;
            switch (e1.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = (int) e1.getX();
                    break;
            }
            boolean noScroll = false;
            switch (e2.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    int endX = (int) e2.getX();
                    int dx = endX - startX;
                    if (dx < 0) {
                        if (scrollX >= getWidth() * (images.size() - 1)) {
                            noScroll = true;
                        }
                    }
                    if (dx > 0) {
                        if (scrollX <= 0) {
                            noScroll = true;
                        }
                    }
                    break;
                default:
                    break;
            }
            if(!noScroll) {
                scrollBy((int) distanceX, 0);
            }
            return false;
        }

效果:
在这里插入图片描述
关于自定义ViewPager就这么多啦,谢谢围观!

具体代码请参考:demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值