Android 使用ViewGroup实现ViewPager的效果

本文介绍如何使用ViewGroup、scrollTo及scroller实现自定义ViewPager控件,并实现滑动动画及手势识别功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ViewPager控件可以让我们做出很多漂亮的界面,例如导航, 页面菜单等. 那么我们自己能否去实现ViewPager的效果呢? 本文将介绍如何使用ViewGroup + scrollTo + scroller实现ViewPager控件, 并且会简单地实现一个自己的scroller, 来了解学习系统提供的scroller类滑屏功能的实现思想.
首先看一下实现的效果:

  1. 定义自己的ViewPager类–MyViewPager继承ViewGroup
public class MyViewPager extends ViewGroup {
    private Context context;
    //手势识别工具类
    private GestureDetector detector;

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        initView();
    }

    private void initView() {

        detector = new GestureDetector(context, 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 , y)个单位,可视区域也跟着偏移(x,y)个单位
                //也就是说让视图跟着鼠标移动, distanceX为鼠标在屏幕上移动的距离
                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;
            }
        });
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //设置每个子view的位置
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        detector.onTouchEvent(event);

        return true;
    }

}

(1) 在MyViewPager中, 重写OnLayout方法, 在OnLayout方法中去确定子view在ViewPager中的位置:

通过getChildAt获得所有的子view, 调用子view的layout方法设置每个子view的位置, layout接收4个参数(就是左上角坐标与右下角坐标), 来确定view的大小, 如上图可以分析出每个view在MyViewPager中的位置, 例如第2个view的位置是:
[getWidth(), 0, 2 * getWidth(), getHeight()] , getWidth 与 getHeight为MyViewPager的宽高. 我们可以看到变化的只有横坐标.

(2) 重写onTouchEvent方法, 此方法处理屏幕触摸事件, 在这里使用用户手势识别工具类GestureDetector来处理action.move事件, 在OnGestureListener的onScroll中处理move事件, onScroll方法的参数distanceX, 就是工具类帮我们计算好的手指在屏幕上x轴方向移动的距离, 然后就可以很方便的使用此参数, 调用scrollBy(x, y)方法移动视图, 让视图偏移(x, y). 这样就实现了MyViewPager随手指移动而移动.

2.使用MyViewPager. 并给MyViewPager设置子view

public class MainActivity extends AppCompatActivity {

    private MyViewPager myViewPager;
    private int[] imgIds = new int[] {
            R.mipmap.a, R.mipmap.b, R.mipmap.c,
            R.mipmap.d, R.mipmap.e, R.mipmap.f
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        myViewPager = (MyViewPager) findViewById(R.id.my_viewpager);

        for (int i = 0; i < imgIds.length; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setImageResource(imgIds[i]);
            myViewPager.addView(imageView);
        }
    }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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="com.myviewpager.MainActivity">

    <com.myviewpager.MyViewPager
        android:id="@+id/my_viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

给MyViewPager设置6张图片. 现在效果图如下:

3.现在图片可以跟随手指滑动而滑动, 但是还不能自动地切换, 图片只能停在你移动到的地方, 如果想实现切换的效果, 还需要我们自己去处理touch事件, 在onTouchEvent中继续添加代码:

//标记当前显示在屏幕上的图片
private int currPos = 0;
//记录按下时的横坐标
private int firstX = 0;

@Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        detector.onTouchEvent(event);
        //添加下面的代码
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = getScrollX();    //获得按下去时的横坐标
                break;
            case MotionEvent.ACTION_UP:   //判断显示哪个子view, 如果滑动大于父控件的一半切换子view
                int tmpPos = currPos;
                if ((getScrollX() - firstX) > getWidth() / 2) {    //向左滑动
                    tmpPos++;
                } else if ((firstX - getScrollX()) > getWidth() / 2) {  //向右滑动
                    tmpPos--;
                }
                MoveToDest(tmpPos);  //切换到指定的图片
                break;
            default:
                break;
        }
        return true;
    }

    public void MoveToDest(int tmpPos) {
        //确定currPos的值, 保证currPos的范围在[0, getChildCount() - 1]
        currPos = tmpPos > 0 ? tmpPos : 0;
        currPos = currPos < getChildCount() - 1 ? currPos : (getChildCount() - 1);
        //将视图内容偏移至(x , y)坐标处,可视区域位于(x , y)坐标处
        scrollTo(currPos * getWidth(), 0);
    }

定义两个成员变量, firstX, currPos来记录点击屏幕时的点与当前显示在屏幕上的子view. 在touch_up事件中处理: 当在屏幕上滑动的距离大于屏幕的一半切换视图, 否则留在当前视图. 在MoveToDest方法中调用scrollTo来实现.
现在的效果为:

4.在ViewPager中, 我们去切换视图时, 并不是瞬间完成, 而是有个过程.
我们实现MyScroller类来实现滑动过程,新建MyScroller类:

public class MyScroller {

    private Context context;
    private int disX;
    private int startY;
    private int startX;
    private int disY;
    private long startTime; //开始动画时间
    private boolean isFinish; //标志是否结束动画
    //默认运行时间,500ms
    private int duration = 500;
    //当前绘制所在的X位置
    private long currX;
    //当前绘制所在的Y位置
    private long currY;

    public MyScroller(Context context) {
        this.context = context;
    }

    public long getCurrY() {
        return currY;
    }

    public void setCurrY(long currY) {
        this.currY = currY;
    }

    public long getCurrX() {
        return currX;
    }

    public void setCurrX(long currX) {
        this.currX = currX;
    }



    /**
     * 开始移动
     * @param startX  开始时的x坐标
     * @param startY  开始时的y坐标
     * @param disX    x方向要移动的距离
     * @param disY    y方向要移动的距离
     */
    public void startScroll(int startX, int startY, int disX, int disY) {
        this.startX = startX;
        this.startY = startY;
        this.disX = disX;
        this.disY = disY;
        this.startTime = SystemClock.uptimeMillis();
        this.isFinish = false;
    }

    /**
     * 计算当前的运行状况
     * @return
     * true 还在运行
     * false 运行结束
     */
    public boolean computeScrollOffset() {
        if (isFinish) {
            return false;
        }
        //获得绘制时的时间
        long passTime = SystemClock.uptimeMillis() - startTime;
        //如果时间还在允许的范围内
        if (passTime <= duration) {
            currX = startX + disX * passTime / duration;
            currY = startY + disY * passTime / duration;
        } else { //绘制运行结束
            currX = startX + disX;
            currY = startY + disY;
            isFinish = true;
        }
        return true;
    }

}

在startScroll方法中记录下当前滑动点坐标与目标点坐标, 并记录下当前时间. 然后在computeScrollOffset中开始去更新当前的滑动的坐标.

接下来使用MyScroller来实现滑动过程,在MyViewPager类中定义MyScroller类成员变量并去使用它来实现滑动过程:

private MyScroller myScroller;

//在initView中初始化
private void initView() {
   ...............
   myScroller = new MyScroller(context);
   ...............

然后修改moveToDest方法,不去使用ScrollTo来实现移动,使用MyScroller来实现:

    public void moveToDest(int nextId) {
    ..................
    //不使用scrollTo来实现移动
//      scrollTo(currId * getWidth(), 0);
        //获得移动的距离,移动距离等于最终位置-当前位置
        int distance = currId * getWidth() - getScrollX();
        myScroller.startScroll(getScrollX(), 0, distance, 0);
        //会导致computeScroll方法执行
        invalidate();
    }

    @Override
    public void computeScroll() {
        //计算当前绘制状况,进行绘制
        if (myScroller.computeScrollOffset()) {
            int nowX = (int) myScroller.getCurrX();
            scrollTo(nowX, 0);
            invalidate();
        }
    }

现在的效果你会看到视图切换时就会有个过程而不是很快就完成了:

现在视图的切换是匀速的,如果想达到ViewPager那种加速效果,将MyScroller改成系统提供的Scroller类即可,只需要将private MyScroller myScroller; 改为private Scroller myScroller; 其他都不需要动就可以实现加速效果, 因为我们的MyScroller类的接口和系统Scroller接口一样。

5.现在MyViewPager中的View全为ImageView, 那么向MyViewPager中添加一个布局(包括一个button和listView)来看一下是否同样的支持滑动效果.

(1)布局文件list_view.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/list_layout"
    android:orientation="vertical" >

    <Button 
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_margin="10dp"
        android:text="button"/>
    <ListView 
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="10dp"/>

</LinearLayout>

(2)我们将布局文件加载到MyViewPager的第四个view

    private LinearLayout listLayout;
    private ListView listview;
    private String[] datas = { "Apple", "Banana", "Orange", "Watermelon",
            "Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
 ............
 //添加下面代码
        listLayout = (LinearLayout) LayoutInflater.from(getApplicationContext())
        .inflate(R.layout.list_view, null);
        listview = (ListView) listLayout.findViewById(R.id.listview);

        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
                MainActivity.this, android.R.layout.simple_list_item_1, datas);
        listview.setAdapter(adapter);

        for (int i = 0; i < ids.length; i++) {
            if (i == 3) {  //将布局文件添加到第四个位置
                msv.addView(listLayout);
            } else {
                ImageView imageView = new ImageView(this);
                imageView.setBackgroundResource(imgIds[i]);
                msv.addView(imageView);
            }
        }

    }

这个时候你运行会发现这个布局不会显示出来, 因为这个时候我们没有在MyViewPager中去调用布局控件的OnMeasure方法, 让其去测量子View的大小, 导致布局view没有显示出来,所以我们需要在MyViewPager中去重载OnMeasure方法,去调用每个子view的measure,绘制子view的大小。

    //绘制每个子控件的尺寸
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

现在的效果为:

现在我们发现,当滑动到布局文件中,在listview上可以上下滑,但是此时左右滑动失效了,不能左右滑动。

原因因为Touch事件的传递导致, 我们知道touch事件是从父view到子view一层一层传递,当我们左右滑动时,此事件由MyViewPager一层层传递到listview,但是listview并不支持(消费)此事件,所以导致左右滑动不起作用了, 现在我们重写onInterceptTouchEvent,来去判断touch事件,如果是左右滑动事件那么就去中断此事件,不让其再往下传递,我们去处理消费它。

    /**
     * 返回true, 中断事件,执行自己的onTouchEvent方法
     * 返回false, 默认处理,不中断事件, 也不会执行自己的onTouchEvent方法
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result = false;
        switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //解决点击图片时跳动的bug
            detector.onTouchEvent(ev);

            firstX = (int) ev.getX();
            firstY = (int) ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            int disx = (int) Math.abs(ev.getX() - firstX); //不管是左右移,只判断是否左右移动
            int disy = (int) Math.abs(ev.getY() - firstY); //竖直方向移动距离
            //判断是否为水平方向移动,disx > 10 防止手指按住屏幕抖动
            if(disx > disy && disx > 10) {
                result = true;
            } else {
                result = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
        default:
            break;
        }
        return result;
    }

现在效果如下:

此时还缺少一点就是开放一个接口让外部来使用, 比如导航界面,点击某一个点,会直接跳到那个点指定的页面。
在MyViewPager中添加接口:

    //监听器对象
    private MyPagerChangedListener listener;

    public MyPagerChangedListener getListener() {
        return listener;
    }

    public void setListener(MyPagerChangedListener listener) {
        this.listener = listener;
    }
    //页面改变时的监听接口
    public interface MyPagerChangedListener{
        void moveToDest(int currId);
    }

然后在moveToDest方法中去判断此监听器是否为空,不为空就调用其接口。

//移动到指定的子控件上
    public void moveToDest(int nextId) {
        ..................

        //触发listener事件
        if (listener != null) {
            listener.moveToDest(currId);
        }
        ..............
    }

至此, 使用ViewGroup实现ViewPager效果完成, 如有问题可以留言。
源码下载地址:

http://download.csdn.net/detail/lbcab/9536961

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值