在 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了。