Android 中 ViewPager 的使用

在 Android 开发中,经常会遇到 ViewPager 的使用,例如界面顶部的滚动广告条、音乐播放器歌词和写真界面的切换、App 的启动页面......

此处我们以实现一个广告条为例,介绍一下 ViewPager 的使用方法,以及涉及到的监听类。话不多说,先上图:

                                                 

主要有以下功能:广告条可手动的左右自由滑动;广告条可自动、循环播放广告页面;监听手势操作,触摸广告时,取消循环播放的监听器,取消触摸时,再自动恢复。

接下来,我会以循序渐进的方式,一步步实现,并说明其中会遇到哪些坑,如果是新手,可以跟着我的思路来学习。当然,如果你已经有一定了解了,那么完全可以跳过这一部分,直接下载文末给出的的完整源代码,我都加了详细注释。

首先,定义布局文件:

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

    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="200dp">
    </android.support.v4.view.ViewPager>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/view_pager"
        android:background="#44000000"
        android:orientation="vertical"
        android:padding="5dp">

        <TextView
        android:id="@+id/pager_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_horizontal"
        android:padding="3dp"
        android:text="@string/pager_title_one"
        android:textColor="#ffffff"/>

        <LinearLayout
            android:id="@+id/pager_pointer"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:orientation="horizontal" />

    </LinearLayout>

</RelativeLayout>

其中,ViewPager 我们自己设置了一个高度。如果想要做成 App 的启动页,那么就可以将高度设置成 match_parent;ViewPager 下面的 LinearLaout 布局包含两个子 View,分别用来表示图片对应的标题,以及指示当前滑动到了第几个页面。

接下来,我们先来完成一个最基本的展示,代码如下:

package com.example.lichaoqiang.viewpagertest;

import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.ArrayList;

public class MainActivity extends AppCompatActivity {

    //布局文件中将要对应的控件
    private ViewPager viewPager;
    private TextView pagerTitle;
    private LinearLayout pagerIndicator;

    //将会在在ViewPager中会使用到的图片和标题资源
    //图片集合
    private final int[] imageIds = {
            R.drawable.a,
            R.drawable.b,
            R.drawable.c,
            R.drawable.d,
            R.drawable.e
    };
    // 标题集合
    private final int[] titleIds = {
            R.string.pager_title_one,
            R.string.pager_title_two,
            R.string.pager_title_three,
            R.string.pager_title_four,
            R.string.pager_title_five,
    };

    //在 PagerAdapter 适配器中使用到的图片集合
    private ArrayList<ImageView> imageViews;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewPager = (ViewPager)findViewById(R.id.view_pager);
        pagerTitle = (TextView)findViewById(R.id.pager_title);
        pagerIndicator = (LinearLayout)findViewById(R.id.pager_indicator);
        init();  //初始化数据
        viewPager.setAdapter(new MyAdapter());
    }

    //ViewPager 的使用和 ListView 的使用很相似,都需配置一个适配器,ViewPager 使用的是 PagerAdapter,
    //因此需要为其准备一些数据
    private void init() {
        //设置图片
        imageViews = new ArrayList<ImageView>();
        int len = imageIds.length;
        for (int i = 0; i < len; i++) {
            ImageView imageView = new ImageView(this);
            imageView.setBackgroundResource(imageIds[i]);   //设置具体图片
            imageViews.add(imageView);  //添加到集合中

            //添加指示页面的圆点
            ImageView point = new ImageView(this);  //指示圆点我们使用图片展示
            point.setBackgroundResource(R.drawable.point_selector); //使用自定义的 StateListDrawable
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(18, 18);  //设置圆点的大小
            if (i == 0) {
                point.setEnabled(true); //将第一个圆点设置为可用,则会显示红色
            } else {
                point.setEnabled(false);
                params.leftMargin = 18; //其它圆点设置为不可用,则会显示为灰色
            }
            point.setLayoutParams(params);  //设置布局参数
            pagerIndicator.addView(point);  //添加指示的圆点
        }
    }

    private class MyAdapter extends PagerAdapter {
        //有几个方法需要覆写

        //获取图片的总数,即ViewPager页面的总数
        @Override
        public int getCount(){
            return imageViews.size();
        }
        
        /**
         * 这个方法相当于 ListView 的 getView() 方法
         * @param container 就是ViewPager自身
         * @param position 当前实例化页面的位置
         * @return
         */
        @Override
        public Object instantiateItem(ViewGroup container, final int position){
            final ImageView imageView = imageViews.get(position);   //  获取对于位置的图片
            container.addView(imageView);   //添加到ViewPager中
            return imageView;
        }

        /**
         * 比较 view 和 object 是否是同一个实例
         * @param view 页面
         * @param object 上述方法instantiateItem()返回的结果
         * @return
         */
        @Override
        public boolean isViewFromObject(View view,Object object){
            return view == object;
        }

        /**
         * 释放资源
         * @param container 就是ViewPager自身
         * @param position 准备要释放页面的位置
         * @param object 要释放的页面
         */
        @Override
        public void destroyItem(ViewGroup container,int position,Object object){
            //super.destroyItem(container, position, object);
            container.removeView((View)object); //需要进行一下强转
        }
    }
}

这时,我们的美景图片就可以滑动了,但是下面的标题和指示圆点还不会随着 ViewPager 的滑动而滑动,接着我们就来让它们跟着动起来。

这时,我们需要用到 ViewPager 的监听器:ViewPager.OnPageChangeListener,这个监听器可以监听 ViewPager 的滑动时的一些状态,这里我们自定义一个类实现这个监听器,代码如下:

 private class MyOnPageChangeListener implements ViewPager.OnPageChangeListener {
        
        /**
         * function: 当前页面滚动的时候回调这个方法
         * @param position 当前页面的位置
         * @param positionOffset 滑动页面的百分比
         * @param positionOffsetPixels 滑动的像素数
         * @return 
         */
        @Override
        public void onPageScrolled(int position,float positionOffset, int positionOffsetPixels){
            //此方法暂时用不到
        }
        
        /**
         * function: 当页面被选中时,会回调这个方法
         * @param position 被选中的页面的位置
         * @return 
         */
        @Override
        public void onPageSelected(int position){
            int realPosition = position;
            //设置对应页面的标题
            pagerTitle.setText(titleIds[position]);
            //把之前高亮显示的指示圆点设为灰色
            pagerIndicator.getChildAt(prePosition).setEnabled(false);
            //将当前页面对应的指示圆点设置为高亮
            pagerIndicator.getChildAt(realPosition).setEnabled(true);
            //更新上次位置的变量值
            prePosition = realPosition;
        }
        
        /**
         * function: 当页面滚动状态变化时,会回调这个方法
         * 有三种状态:静止、滑动、拖拽(这里区别滑动和拖拽,以手指是否接触页面为准)
         * @param state 当前状态
         * @return 
         */
        @Override
        public void onPageScrollStateChanged(int state){
            //后面我们再添加逻辑代码
        }
    }

这里我们引入了一个变量 prePosition。定义好监听器类后,我们在 ViewPager 中添加它(在设置适配器的代码之后):

viewPager.addOnPageChangeListener(new MyOnPageChangeListener());    //添加监听器

运行一下代码,看一下效果。哈哈,这时已经基本成型了,那么我们现在站在用户的角度,继续对这个 ViewPager 进行优化吧。

改进一:

首先,我们发现再第一个美景界面,只能向右滑,在最后一个美景界面,则只能向左滑,我们能不能在这两个场景下,也使其能左右滑动呢?当然可以,这里需要用到 ViewPager 提供的一个函数 setCurrentItem(),它能够设置显示哪个界面。那么我们在现有的几个界面基础上,再循环添加很多很多个界面,并在初始化时,使用 setCurrentItem() 将显示的页面,设置到处在中间位置的一副美景图上(且让这副图就是美景一的图),那不就可以了嘛。这里我们对 MyAdapter 中的 getCount() 函数进行修改,因为它表示有多少个页面,代码如下:

public int getCount(){
            //return imageViews.size();
            return Integer.MAX_VALUE;   //当然,此处也可以设置为其它比较大的数值
        }

拥有了无数页面,那么其它涉及到 position 的地方,也就需要做相应的修改了,在 onCreate() 方法中添加修改位置的代码:

        //设置中间位置。保证是imageViews的整数倍,这样在起始时,就会显示美景一的页面
        int item = Integer.MAX_VALUE/2 - Integer.MAX_VALUE/2%imageViews.size(); 
        viewPager.setCurrentItem(item);
        pagerTitle.setText(titleIds[prePosition]);

在监听器类的 onPageSelected() 方法中修改 realPosition 的值,防止下文用到时,指针越界。

            int realPosition = position%imageViews.size();
            //设置对应页面的标题
            pagerTitle.setText(titleIds[realPosition]);

修改 MyAdapter 的 instantiateItem() 方法为:

public Object instantiateItem(ViewGroup container, final int position){
            int realPosition = position%imageViews.size();  //设置位置
            final ImageView imageView = imageViews.get(realPosition);   //  获取对于位置的图片
            container.addView(imageView);   //添加到ViewPager中
            return imageView;
        }

好了,运行一下,OK。那么我们接着进行第二个优化。

改进二:

我们平时所见 ViewPager 如果是广告条的话,为了利益最大化,它是会定时滚动的,我们就来实现这个功能。

对于定时任务,这里我们使用 Handler,定义如下:

//这里由于非静态内部类会持有外部类的引用,会造成内存泄漏,但为了演示方便,就不做处理了,实际使用时要注意修改
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg){
            super.handleMessage(msg);
            int item = viewPager.getCurrentItem()+1;    //获取下一个待显示页面的位置
            viewPager.setCurrentItem(item); //显示下一个页面
            
            //自己驱动自己,延时、循环发送消息。消息的what字段无所谓,任意传一个就行,因为我们用不到分类处理机制
            handler.sendEmptyMessageDelayed(0,4000);
        }
        
    };

接着在 onCreate() 方法中触发并发送这个消息:

        handler.sendEmptyMessageDelayed(0,4000);    //发送消息

好了,运行一下,可以循环、定时切换页面了,但是还有一个问题,当我们用手指按住一个页面时,它仍然会按时跳转,这对用户体验是不好的,那么我们就优化。

肯定要从页面滑动、拖拽入手。

修改 MyAdapter 适配器中的 instantiateItem() 方法如下:

public Object instantiateItem(ViewGroup container, final int position){
            int realPosition = position%imageViews.size();  //设置位置
            final ImageView imageView = imageViews.get(realPosition);   //  获取对于位置的图片
            container.addView(imageView);   //添加到ViewPager中
            //设置图片的触摸事件
            imageView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                            handler.removeCallbacksAndMessages(null);   //取消消息队列
                            break;
                        case MotionEvent.ACTION_MOVE:
                            break;
                        case MotionEvent.ACTION_CANCEL:
                            break;
                        case MotionEvent.ACTION_UP:
                            handler.removeCallbacksAndMessages(null);   //开启消息队列前,最好先清除之前的消息
                            handler.sendEmptyMessageDelayed(0,4000);    //发送消息,开启消息队列
                            break;
                    }
                    return true;    //返回true,表示要消费这次触摸事件,其它事件将不能再处理这次触摸操作
                }
            });
            return imageView;
        }

运行程序,这是按住图片,那页面就不会自动跳转了,松手后,恢复跳转。但有一个 bug,当手指在页面上滑动,松手后,页面也不会恢复自动跳转,这是由于此时触发了 MotionEvent.ACTION_CANCEL 事件,而不会再触发 MotionEvent.ACTION_UP 事件导致的。那么我们修改 MotionEvent.ACTION_CANCEL 事件如下:

                    case MotionEvent.ACTION_CANCEL:
                            handler.removeCallbacksAndMessages(null);   //开启消息队列前,最好先清除之前的消息
                            handler.sendEmptyMessageDelayed(0,4000);    //发送消息,开启消息队列
                            break;

运行一下,前面的问题解决了,但又有一个新问题冒出来了:当我们手指拖拽一个页面时,在按住不动的情况下,它也会按时跳转,显然我们在  MotionEvent.ACTION_CANCEL 中处理事件是不恰当的,更好的解决办法就是在之前定义的监听器中去处理,删掉在  MotionEvent.ACTION_CANCEL 中的代码,我们修改监听器的 onPageScrollStateChanged() 方法,并定义一个变量 isDragging,来标记页面状态,代码如下:

public void onPageScrollStateChanged(int state){
            if (state == ViewPager.SCROLL_STATE_DRAGGING){  //拖拽状态
                isDragging = true;
                handler.removeCallbacksAndMessages(null);
            }else if (state == ViewPager.SCROLL_STATE_SETTLING){    //滑动状态,要区别拖拽
                //此处暂不做处理
            }else if (state == ViewPager.SCROLL_STATE_IDLE && isDragging){  //静止状态,且刚经历过拖拽状态
                isDragging = false;
                handler.removeCallbacksAndMessages(null);   //开启消息队列前,最好先清除之前的消息
                handler.sendEmptyMessageDelayed(0,4000);    //发送消息,开启消息队列
            }
        }

好了,运行一下,OK了。(注:此时,也可以删除  imageView.setOnTouchListener() 中的代码了)

那么我么接着增加需求,既然是广告条,那么当用户点击时,就应该能够做出反馈,我们在 MyAdapter 的 instantiateItem() 方法中添加点击事件:

imageView.setTag(position);
            imageView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int position = (int) v.getTag()%imageViews.size();
                    Toast.makeText(MainActivity.this, "你点击了图片", Toast.LENGTH_SHORT).show();

                }
            });

运行程序,如果没有删除 imageView.setOnTouchListener() 中的代码,则会发现点击无效,有两个解决办法:一是可以修改  imageView.setOnTouchListener() 的返回值为 false;而是删除  imageView.setOnTouchListener() 方法。

修改过后,再运行一下,OK了。 

代码下载地址一:https://download.csdn.net/download/chaoqiangscu/10768511 (有积分的麻烦就用这个吧)

代码下载地址二:https://github.com/chaoqiangscu/ViewPagerTest

猜你喜欢

转载自blog.csdn.net/chaoqiangscu/article/details/83752202