美文网首页优秀案例Android开发Android知识
ViewPager 实现自动循环轮播 高度自适应 显示前后部分

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

作者: ayuhani | 来源:发表于2017-06-28 10:38 被阅读640次

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

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

    欢迎关注我的微信公众号

    相关文章

      网友评论

      • 努力搬砖:图片的宽高比怎么设置呢?
        ayuhani:现在父布局使用ConstraintLayout就可以设置宽高比了

      本文标题: ViewPager 实现自动循环轮播 高度自适应 显示前后部分

      本文链接:https://www.haomeiwen.com/subject/mvtucxtx.html