优秀案例Android开发Android知识

ViewPager 实现自动循环轮播 高度自适应 显示前后部分

2017-06-28  本文已影响640人  ayuhani

游民星空 3.0 界面大改之后,发现首页的轮播图很有特色,一直想着实现一下。先看一下原 app 的效果:

其实是可以自动轮播的,不过等的时间太长,我就动手帮了一把。

要实现这种效果无非需要考虑到以下几个问题:

  1. ViewPager 可以显示前后的一部分界面;
  2. 要在不同分辨率的手机上保持图片的长宽比例;
  3. 实现自动循环轮播;
  4. 注意 Activity 的生命周期和手指对 ViewPager 操作时对自动播放的影响;
  5. 页面的点击事件的处理。

那么我们就来一步一步解决这些问题:

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 的代码量。如果大家有更好的实现方法或者思路,欢迎指教。

->->->点击下载源码<-<-<-

欢迎关注我的微信公众号
上一篇下一篇

猜你喜欢

热点阅读