ViewPager 实现自动循环轮播 高度自适应 显示前后部分
游民星空 3.0 界面大改之后,发现首页的轮播图很有特色,一直想着实现一下。先看一下原 app 的效果:
其实是可以自动轮播的,不过等的时间太长,我就动手帮了一把。
要实现这种效果无非需要考虑到以下几个问题:
- ViewPager 可以显示前后的一部分界面;
- 要在不同分辨率的手机上保持图片的长宽比例;
- 实现自动循环轮播;
- 注意 Activity 的生命周期和手指对 ViewPager 操作时对自动播放的影响;
- 页面的点击事件的处理。
那么我们就来一步一步解决这些问题:
1.实现 ViewPager 显示前后部分界面与高度自适应
直接上 xml 文件的代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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.ayuhani.viewpagerdemo.MainActivity">
<LinearLayout
android:id="@+id/ll_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"></android.support.v4.view.ViewPager>
</LinearLayout>
</LinearLayout>
在 ViewPager 和它的父布局我们都添加了一行android:clipChildren="false"
,这句代码的意思是:是否限制子 View 在其范围内显示,默认为 true,是限制的,我们这里改成 false。然后在代码中放上五张图片,这里怎么自适应高度呢?
图片占用屏幕的高度 / 占用屏幕的宽度 = 原始图片高度 / 图片宽度
根据这个公式,我们就可以计算出 ViewPager 所需要的高度了。
DisplayMetrics metrics = getResources().getDisplayMetrics();
ViewGroup.LayoutParams params = viewPager.getLayoutParams();
params.width = (int) (metrics.widthPixels * 0.86); // 宽度设置成屏幕宽度的86%,这里根据自己喜好设置
params.height = params.width * 240 / 386; // 利用已知图片的宽高比计算高度
viewPager.setLayoutParams(params);
由于我们还需要展示前后的部分界面,所以不能完全占据屏幕宽度,再根据公式计算出高度,设置给 ViewPager 就好了。我们看一下效果:
等一下,这和我们想要的结果完全不一样啊!
这里发现了两个比较严重的问题:
1.图片不居中显示,B 区域不显示,只显示 C 区域
关于这个问题我首先想到的是大概由于 gravity 的原因,于是我在 ViewPager 的属性里面加上android:layout_gravity="center_horizontal"
或者在它的父布局中加上android:gravity="center_horizontal"
都可以解决这个问题。
2.手指放在 C 区域滑动时,无法滑到下一页,只有在 A 处可以正常滑动
这个问题我们想一下也可以知道,由于限制了 ViewPager 的宽度,所以 C 区域已经不属于 ViewPager 的当前界面,所以滑动是没有效果的,我们只需要重写父布局的OnTouchListener
方法就好了:
// 这里处理触摸两端滑动无效果的问题
parentLayout = (LinearLayout) findViewById(R.id.ll_parent);
parentLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
return viewPager.dispatchTouchEvent(motionEvent);
}
});
注意:给父布局重写这个方法之后,在父布局的任意位置滑动都能带动 ViewPager 的滑动,所以要给 ViewPager 单独包一个父布局。
这两个问题解决后,已经可以正常显示 B 区域了,并且滑动 B 和 C 区域也有了效果,但从游民的效果可以看到 A B C 之间都是有一小块间隙的,这个很简单:
viewPager.setPageMargin(20);
这个方法用来设置 ViewPager 页面之间的间距,单位是 px。现在再来看一下效果,是不是已经基本成型了?
2.实现循环功能
从网上找了查了一些资料,发现大多数实现这个功能用的都是一种“障眼法”:
我们当前只有 5 幅图片,但是我们在集合的首尾再增加两个元素,第一个显示图片 5,最后一个显示图片 1。当我们向后滑动到页面 6 的时候,通过viewPager.setCurrentItem(1)
方法跳转到页面 1;而向前滑动到页面 0 的时候,通过viewPager.setCurrentItem(5)
方法跳转到页面 5。这样就给人一种无限循环滑动的感觉。当然初始化的时候,我们要先设置viewPager.setCurrentItem(1)
来显示第一张图片。
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (position == 0) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
viewPager.setCurrentItem(adapter.getCount() - 2, false);
}
}, 250);
} else if (position == adapter.getCount() - 1) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
viewPager.setCurrentItem(1, false);
}
}, 250);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
该方法的第二个参数是一个布尔类型,代表是否要平滑地滚动到指定的位置,如果 true 的话,会有一段动画的效果,具体可以自己实现看一下。这里写 false,代表立刻跳转。但是onPageSelected()
这个方法在下个页面还没有完全显示完成的时候就执行了,所以给人一种很突兀的感觉,我在这里写了 250 毫秒的延迟。
注意:这种方法在 ViewPager 的页面完全显示的时候是没有问题的,但是我们要显示前后的部分,所以给人的视觉效果不是很好。比如说当前滑动到了页面 6,这是时候是要跳转到页面 1 的,但由于我们写了 250 毫秒的延迟,导致图片 2 也被延迟加载,所以显示页面 6 的时候会有短暂的时间右边显示一小部分空白,然后才加载出图片 2。目前想到的办法就是在页面 6 之后再放一个页面 7 用来显示图片 2,这样能暂时解决问题,但是又进一步消耗了资源,不是太好的处理办法。
3.实现自动播放
利用viewPager.setCurrentItem(viewPager.getCurrentItem() + 1)
方法与定时功能实现。
private void autoPlay() {
handler.postDelayed(runnable, 2000);
}
class PlayRunnable implements Runnable {
@Override
public void run() {
viewPager.setCurrentItem(viewPager.getCurrentItem() + 1);
autoPlay();
}
}
只需要调用autoPlay()
方法,便可以自动轮播了。
4.生命周期的管理与触摸管理
为了减少内存的开销,一般都是在 Activity 暂停的时候停止自动播放,在恢复时开始自动播放,在销毁时移除 handler 的回调。
private void autoPlay() {
handler.postDelayed(runnable, 2000);
}
private void stopAutoPlay(){
handler.removeCallbacks(runnable);
}
@Override
protected void onResume() {
super.onResume();
autoPlay();
}
@Override
protected void onPause() {
super.onPause();
stopAutoPlay();
}
@Override
protected void onDestroy() {
super.onDestroy();
// 页面被销毁时,移除所有的callbacks和messages
handler.removeCallbacksAndMessages(null);
}
而当用户手动滑动 ViewPager 时也要暂停播放,松手时再继续播放,所以需要重写ViewPager的OnTouchListener()
方法:
viewPager.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
stopAutoPlay();
break;
case MotionEvent.ACTION_UP:
autoPlay();
break;
default:
break;
}
return false;
}
});
5.点击事件的处理
我直接给 ImageView 设置了setOnClickListener()
方法,结果发现它拦截了 ViewPager 的 onTouchEvent 事件。尝试了解决这个问题,但是一时又没有找到好的办法,于是想着能不能用别的什么方案替代 ViewPager 的触摸管理或者点击事件,在同事的帮助下,做了如下改变:
不再利用 ViewPager 的OnTouchEvent()
方法进行手势的处理,而是利用onPageScrollStateChanged(int state)
这个方法:
@Override
public void onPageScrollStateChanged(int state) {
//state: 0 空闲,1 是滑行中,2 加载完毕
if (state == 1 && isRunning) {
// 当处于滑动中并且正在自动播放,则停止滑动
// 也就是自动播放的时候用手指去滑动,会停止播放
stopAutoPlay();
} else if (state == 0 && !isRunning) {
// 当ViewPager处于空闲状态并且没有在自动播放的时候,才开始自动播放
// 也就是当手指离开屏幕时,再次启动自动播放
autoPlay();
}
}
private void autoPlay() {
Log.e("isRunning", "true");
isRunning = true;
handler.postDelayed(runnable, 3000);
}
private void stopAutoPlay() {
Log.e("isRunning", "false");
isRunning = false;
handler.removeCallbacks(runnable);
}
这样处理之后基本解决了问题,用手指滑动时会停止自动播放,松手时又会继续,同时也可以响应点击事件。最终的效果:
6.遗留的两个问题
1.上文提到过的,首尾相互跳转的时候,导致相邻图片加载延迟问题;
2.利用onPageScrollStateChanged(int state)
方法之后,仅仅是触摸而不滑动 ViewPager 的话,自动播放不会暂停,不知这是否符合交互体验?(游民这个 app 只是触摸也不会暂停自动播放)
代码写的比较乱,其实完全可以写个类继承自 ViewPager,把需要用到的方法写在这个类里,这样更便于管理和拓展,也能减少 Activity 的代码量。如果大家有更好的实现方法或者思路,欢迎指教。
欢迎关注我的微信公众号