手动实现轮播图(二):循环滚动、定时切换与指示器

前言

文章发布于个人我的博客: 手动实现轮播图(二):循环滚动、定时切换与指示器

在上一篇文章手动实现轮播图(一):ViewPager 入门实践中,咱们认识了ViewPager这个布局,也简单上手了一下。java

接下来这篇文章,咱们会进一步朝着轮播图的方向前进。android

原来的文章末尾,我使用了 Glide 加载 Gif 图片做为轮播图的内容,因此如今也是基于那个代码继续下去的。git

若是对这部分比较陌生,建议回去看一下文章末尾仓库地址里的代码哦github

本文章中咱们将会实现:数组

  • 循环滚动bash

  • 切换指示器ide

  • 定时切换布局

接下来就让咱们开工吧。post

# 1. 循环滚动性能

ViewPager虽然好用,可是并不原生支持循环滚动,也就是你:

  • 第一个往左滑,会跳到最后一个

  • 从最后一个往右滑,会调回第一个

咱们以前实现的效果里,第一个就没法再往左滑了,最后一个就没法再往右滑了。这样轮播图就不是“轮”播了。

因此咱们须要本身来实现循环滚动这个效果。

该怎么实现呢?目前也有比较成熟的三个作法:

多页面假循环

  • 建立不少个页面,即使咱们真正须要展现的时候只有 5 个页面

    • 把起始点放在队列中间,若是到了要展现的第一个页面,继续往左的时候,咱们把接下来就把页面设置为最后一个的样式

    • 这样无论用户往左仍是往右滑,只要是正常状况下,用户都是滑不到头的,形成视觉上的循环

      • 正常 App 中,即使你使用一个这样的页面队列来显示,用户也没有耐心一直滑下去

假设咱们如今建立了 1000 个页面的ViewPager,而后咱们实际须要展现的只有 5 个页面,那么实现的效果以下:

咱们把第一个展现的页面设置为 500,那用户须要滑动 499 才会到头。

这样性能会不会不好?

  • 不会

由于虽说的是“建立1000”个页面,可是实际上咱们只是告诉ViewPagerAdapter咱们会使用这么多个,不表明他会建立这么多个。

咱们会在AdaptergetCount方法里返回 1000,这个方法只是帮助Adapter获取正确的position的,并非真正建立出来。(经过阅读PagerAdapter 的源码得出)

记得前面咱们说过的,FragmentPagerAdapter会默认帮咱们建立三个页面,因此这里也只会建立三个页面,超过前中后的其余页面都会被回收。

其他两种实现方法

咱们主要使用第一种,理解起来简单易懂,也没有明显的短板。

其他两种方法描述看下面这篇文章:Android实现真正的ViewPager【平滑过渡】...

介绍完实现思路,咱们就能够开始实现了。

打开MainActivity.java,修改代码以下:

public class MainActivity extends AppCompatActivity {
    private static final int MAX_NUMBER = 1000;
    private static final int START_POSITION = MAX_NUMBER/2;
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.setAdapter(new FragmentPagerAdapter(fm) {
            
            private int mIndex; // 存储经过 position 计算出正确的数组索引
            
            @Override
            public Fragment getItem(int position) {
                mIndex = Math.abs(position - START_POSITION) % mStringList.length;
                
                if (position < START_POSITION && mIndex != 0){
                    mIndex = mStringList.length - mIndex;
                }
                
                return PageFragment.newInstance(mIndex);
            }
​
            @Override
            public int getCount() {
                return MAX_NUMBER;
            }
​
​
        });
        
        mViewPager.setCurrentItem(START_POSITION);
​
        ...
    }
    
    
}复制代码

  • 定义两个常量,分别是

    • MAX_NUMBER:页面总数,一共 1000 个

    • START_POSITION:起始的页面,从中间第 500 个开始

mIndex = Math.abs(position - START_POSITION) % mStringList.length;
                
if (position < START_POSITION && mIndex != 0){
    mIndex = mStringList.length - mIndex;
}复制代码
  • 计算当前位置position和起始位置(START_POSITION)的距离,而后把结果和真正要展现的页面数量(此处暂时使用mStringList的长度代替)取余

    • 距离有正负,因此取了绝对值。可是若是只是绝对值而后去取余的话,左滑的时候,就不是 1->5 -> 4 这样子,而是 1 ->2 ->3 这样。这是取余运算的结果,不熟悉的同窗能够回忆一下取余的结果

    • 因此咱们加了判断

      • 页面position大于起始位置 ,那就直接用相对距离取余

      • 如若小于起始位置,那么用实际页面数量减去取余结果,就能够实现倒数的效果了

@Override
public int getCount() {
    return MAX_NUMBER;
}复制代码
  • 此处告诉Adapter一共有多少个页面

记得设置起始页面哦:

mViewPager.setCurrentItem(START_POSITION);复制代码

这样咱们的循环滚动就完成了~快试试看吧。

2. 页面指示器

许多轮播图都有一个小指示器,用来标志当前的页面。咱们如今就来作一个。

作了前面的循环滚动,这样的页面指示器原理应该不难理解。

思路是:

  • 建立控件样式

    • 选中的样式

    • 未选中的样式

  • 添加控件到视图里面

  • 当页面滑动的时候,修改指示器的样式

建立控件样式

res/drawable文件夹里,建立两个文件:

正常样式:

dot_normal.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@android:color/holo_red_dark"/>
</shape>复制代码

被选中样式:

dot_selected.xml

<?xml version="1.0" encoding="utf-8" ?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="5dp"
        android:height="5dp"/>
    <solid android:color="@color/colorPrimary"/>
</shape>复制代码

接着在activity_main.xml里加入一个LinearLayout布局,后面咱们使用代码的方式把小点加入进去:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
​
    <android.support.v4.view.ViewPager
        android:id="@+id/view_pager_inside"
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="@android:color/darker_gray"
        android:layout_centerInParent="true">
    </android.support.v4.view.ViewPager>
    
    <LinearLayout
        android:id="@+id/ll_inside"
        android:layout_below="@+id/view_pager_inside"
        android:layout_width="match_parent"
        android:layout_height="30dp"
        android:orientation="horizontal"
        android:gravity="center"/>
​
</RelativeLayout>复制代码

添加控件到视图中

此处的代码思路来自Android ViewPager 无限循环左右滑动(可自动) 实现

回到MainActivity.java中,

public class MainActivity extends AppCompatActivity {
    private List<TextView> mTextViews;
    private LinearLayout mLinearLayout;
​
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mViewPager = findViewById(R.id.view_pager_inside);
        mLinearLayout = findViewById(R.id.ll_inside);
        
        initCircle();
        ....
    }
    
    ...
    private void initCircle() {
        mTextViews = new ArrayList<>();
        int d = 20;
        int m = 7;
​
        for (int i = 0; i < mStringList.length; i++){
            TextView textView = new TextView(this);
            if (i == 0){
                textView.setBackgroundResource(R.drawable.dot_selected);
            }else {
                textView.setBackgroundResource(R.drawable.dot_normal);
            }
​
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(d, d);
​
            params.setMargins(m, m, m, m);
            textView.setLayoutParams(params);
            mTextViews.add(textView);
            mLinearLayout.addView(textView);
        }
    }
    ...
}复制代码

  • 定义两个变量

    • mTextViews:存放小点的列表

      • 咱们的小点实际上是由TetxView构成,而后背景颜色设置为圆形的

    • mLinearLayout:引用刚刚建立的LinearLayout布局

  • 建立一个initCIrcle()方法

    • 使用代码的方式建立TextView视图,为每一个视图设置宽高、外边距和背景等属性

      • 背景样式就是刚刚建立的两个.xml文件

    • 使用 addView方法把小点添加到布局当中

Oncreate()方法中调用以后,咱们就会看到小点已经出现了。

如今咱们须要根据页面来修改样式,以达到指示器的做用。

public class MainActivity extends AppCompatActivity {
    
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
​
            }
​
            @Override
            public void onPageSelected(int position) {
                changePoints(position % mStringList.length);
            }
​
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });  
    }
    
    private void changePoints(int pos){
        if (mTextViews != null){
            for (int i = 0; i < mTextViews.size(); i++){
                if (pos == i){
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_selected);
                }else {
                    mTextViews.get(i).setBackgroundResource(R.drawable.dot_normal);
                }
            }
        }
    }
}复制代码
  • mViewPager添加一个状态监听器ViewPager.OnPageChangeListener

    • 重写onPageSelected()方法:该方法会在页面被选中的时候调用

    • 在该方法内,咱们调用changePoint()方法来改变指示器的样式

咱们在调用changePoint()的时候,传入的是position % mStringList.length。这里是有问题的。

若是直接使用positionmString.length进行取模,在这个例子里是没问题,由于咱们起始位置(500)刚好是mString.length的倍数。因此此时会从 0 开始。但若是咱们之后修改了起始位置亦或者修改了展现图片的数量的话,这里就会出错了。

因此咱们仍是使用和以前同样的方式来得到索引值。修改一下onPageSelected()方法:

private int mIndex;
​
@Override
public void onPageSelected(int position) {
    mIndex = Math.abs(position - START_POSITION) % mStringList.length;
    if (position < START_POSITION && mIndex != 0){
        mIndex = mStringList.length - mIndex;
     }
    changePoints(mIndex);
​
}复制代码

这里为了方便,就直接使用这段代码了。有时间的同窗能够本身优化一下,提升复用率。

按照道理,如今应该就能够了。

3. 定时播放

轮播图的其中一个特色,就是定时播放。

咱们已经实现了这么多效果了,定时播放应该也是小菜一碟。

咱们可使用Handle调用setCurrentItem()便可。

如下代码思路来自Android ViewPager 无限循环左右滑动(可自动) 实现

修改咱们的MainActivity.java

private Handler mHandler = new Handler();
​
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
        mHandler = new Handler();
        mHandler.postDelayed(new TimerRunnable(),5000);
    }
​
    class TimerRunnable implements Runnable{
​
        @Override
        public void run() {
            int curItem = mViewPager.getCurrentItem();
            mViewPager.setCurrentItem(curItem+1);
            if (mHandler!=null){
                mHandler.postDelayed(this,5000);
            }
        }
    }
​
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler = null; //此处在Activity退出时及时 回收
}复制代码

4. 修改过渡动画

调用ViewPager.setPageTransformer()方法便可自行设置动画。

让咱们先新建一个动画类PhotoTransformer.java

package me.rosuh.android.viewpagernew;
​
import android.support.annotation.NonNull;
import android.support.v4.view.ViewPager;
import android.view.View;
​
​
public class PhotoTransformer implements ViewPager.PageTransformer {
    @Override
    public void transformPage(@NonNull View page, float position) {
        int pageWidth = page.getWidth();
​
        if (position < -1){
            page.setAlpha(1);
        }else if (position <= 1){
            page.setPivotX(position < 0f ? page.getWidth() : 0f);
            page.setPivotY(page.getHeight() * 0.5f);
            page.setRotationY(90f * position);
​
        }else {
            page.setAlpha(1);
        }
    }
}
​复制代码

而后为mViewPager设置动画:

...
FragmentManager fm = getSupportFragmentManager();
​
mViewPager.setPageTransformer(true, new PhotoTransformer());
​
mViewPager.setAdapter(...)复制代码

设置这个动画,最好把CardView的阴影属性设置为 0。而后稍微修改一下布局。(在此不列出,能够到代码仓库本身看一下)。下面是效果:

结语

本项目地址 ViewPagerDemo

目前为止,咱们的轮播图就已经作好了。

这两篇文章的目标读者是刚入门的同窗,因此有许多地方还有改进的空间。

可是不碍于咱们掌握。

文章做者毕竟经验很少,水平有限,因此缺漏在所不免,但愿路过读到本文的前辈们不吝赐教,谢谢~

感谢一下参考文章和资料:

相关文章
相关标签/搜索