Android中ViewPager支持一屏多个View、切换动画以及无限滚动
来源:互联网 发布:安卓编程权威指南第3版 编辑:程序博客网 时间:2024/06/06 02:41
1. 首先看一下最终的效果图
2. 需求拆解
第一眼看见上面的效果,是不是有些朋友觉得这个效果很酷,有的高手会觉得这个效果很简单。笔者昨天刚拿到需求的时候,最开始也是觉得这个很简单,可是越分析越发现好像实现出来并不是那么容易。单个的效果可能很简单,但是这么多的效果叠在一起,可能就比较复杂了。我简单得将这个效果任务拆解一下:
- 一屏要展示3个View,支持左右滑动
- 屏幕中间两侧的View移向中间时,会有一个放大的效果;中间的View移向两侧时,会有一个缩小的效果
- 点击屏幕两侧的View,会自动滚动到屏幕的中间,同理效果要满足第2点
- 当滑动到最左侧或者最右侧时,需要立马衔接最末尾的View和最开头的View,也就是无限滚动
- 考虑性能问题,必须有一个缓存机制。否则控件的count大于30的时候,会有非常明显的卡顿
起初我想过用HorizontalScrollView来做,但是在性能问题和无限滚动的实现上有一点困难;然后我想过用RecyclerView来做横向的列表,但是在无限滚动和居中放大的实现上有一点困难;然后我考虑直接重写View来做,但是这样的风险和成本实在太高,我不能不负责任地把这种代码投放到实际项目中。最后想来想去,我觉得用ViewPager来实现是最稳妥的,虽然里面仍然有不少坑,不过都被我解决了。
3. 实现一个普通的ViewPager
这里实现一个最最最普通的ViewPager,一页一个Item,支持横向滑动:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="wrap_content" /></LinearLayout>
MainActivity.java
public class MainActivity extends Activity { private ViewPager viewPager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.viewPager); OnePageThreeItemAdapter adapter = new OnePageThreeItemAdapter(this); viewPager.setAdapter(adapter); }}
OnePageThreeItemAdapter.java
public class OnePageThreeItemAdapter extends PagerAdapter { private Context context; private List<String> sourceData = new ArrayList<String>(); public OnePageThreeItemAdapter(Context context) { this.context = context; initData(); } /** * 初始化原始数据 */ public void initData(){ sourceData.clear(); for(int i = 0 ; i < 5; i++){ sourceData.add(i + ""); } } @Override public int getCount() { return sourceData.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, final int position) { View view = View.inflate(context, R.layout.item, null); view.setTag(String.valueOf(position)); TextView txt = (TextView) view.findViewById(R.id.txt); txt.setText(sourceData.get(position)); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); }}
item.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:gravity="center" android:textColor="@android:color/black" android:textSize="15sp" /></LinearLayout>
4. 实现一屏三个View
这个功能网上已经有很多了,相信大家都已经看见过了,所以我也就简单得说一下
A. 首先ViewPager和包含ViewPager的ViewGroup设置clipChildren属性
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:layerType="software" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" /></LinearLayout>
clipChildren的意思是是否裁剪,当设置为false时,一屏有多个View的时候不裁剪多余的View;layerType的关闭硬件加速,具体原因我也不知道,网友是这样给的,我这样弄了能跑起来就没管了,若有知情者可以告知我哟。
B. 重写PagerAdapter的getPageWidth方法
@Overridepublic float getPageWidth(int position) { return 1.0f / ONE_PAGE_ITEM_COUNT; //ONE_PAGE_ITEM_COUNT = 3}
这个方法返回的是每一个Item所占当前屏幕的比例,我们需要一屏展示三个View,所以这里返回1/3。
5. 实现居中时放大,左右滑动时缩小的动画效果
提起ViewPager的动画效果,大家第一反应肯定是PageTransformer,没错,本文的确是用PageTransformer来实现的,不过因为是一屏展示多个View,所以这里的实现上跟网上的大部分都有很大的差异。首先我们了解一下PageTransformer的transformPage方法的参数
A. 参数讲解
transformPage方法有两个参数,第一个View代表当前的子View,这个没什么好说的。主要是第二个参数position,是一个float类型的。这个position是什么类型呢,我们分两种情况演示一下:
如果一屏只有一个View,也就是最普通最简单的情况
一屏有三个View
可以看出,如果一屏只有一个View,那么当前View就是0;如果一屏有多个View,那么当前页的第一个View就是0。然后向左依次递减每一个View的宽度;向右依次递增每一个View的宽度。
B. 一屏三个View的实现
通过上面的图,我们可以知道,我们需要处理的区间有(-INFINITE, 0)、(0, 1/3)、(1/3, 2/3)、(2/3, +INFINITE)
public class OnePageThreeItemTransformer implements ViewPager.PageTransformer { @Override public void transformPage(View page, float position) { //这里的position是指每一个Item相对于Page所占的比例,当前页的第一个Item是0,左边的依次减去Item的宽度,右边的依次加上Item的宽度 float centerPosition = 1.0f / ONE_PAGE_ITEM_COUNT; if (position <= 0) { page.setScaleX(1); page.setScaleY(1); } else if (position <= centerPosition) { page.setScaleX(1 + (itemMaxScale - 1) * position / centerPosition); page.setScaleY(1 + (itemMaxScale - 1) * position / centerPosition); } else if (position <= centerPosition * 2) { page.setScaleX(1 + (itemMaxScale - 1) * (2 * centerPosition - position) / centerPosition); page.setScaleY(1 + (itemMaxScale - 1) * (2 * centerPosition - position) / centerPosition); } else { page.setScaleX(1); page.setScaleY(1); } }}
其中itemMaxScale代表最大放大的比例,这里我暂定的2倍,值可以随便修改。
然后设置ViewPager的PageTransformer就可以了
viewPager.setPageTransformer(true, new OnePageThreeItemTransformer());
6. 点击屏幕中的View,实现居中效果
网上很多种实现,原理大约是通过TouchEvent获取内存中的View所在的index,然后再调用setCurrentItem方法平滑过去。在这里,我愤怒的告诉大家:这种做法是完全错误的。顺带黑一句,中国的博客写手很多都不严谨,代码都没有经过分析与验证,就直接发布出来或者转发。大家都知道ViewPager和ListView等都是有缓存原理的,内存中的View所在的index,跟setCurrentItem的参数position,是完全两码事的东西。
后来经过琢磨,我找到了一种实现思路:
在PagerAdapter的instantiateItem方法中,给每一个View设置点击监听器,instantiateItem的position是指每一个View的position,这个position跟数据源的position是保持一致的。在View的点击监听器中调用setCurrentItem方法,注意到setCurrentItem的position参数的意义:如果一屏只有一个View,那么position就是当前页的position;如果一屏有三个View,那么position是指当前页的第一个View的position。我举个例子:
如图所示,左边一页的position为3;中间一页的position为6;右边一页的position为9。
也就是说,如果我想要指定position居中,那么我只需要调用setCurrentItem(position - 1)就可以了,这里要考虑position为0的情况。
@Overridepublic Object instantiateItem(ViewGroup container, final int position) { View view = View.inflate(context, R.layout.item, null); view.setTag(String.valueOf(position)); TextView txt = (TextView) view.findViewById(R.id.txt); txt.setText(sourceData.get(position)); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(position == 0 ? 0 : position - 1, true); } }); container.addView(view); return view;}
这里设置Tag是为了下一节实现无限循环滚动做了必要准备。
7. 实现无限循环滚动
A. 修复添加数据源
网上已经有很多实现ViewPager无限滚动的原理了,但是因为我们这儿是一屏有三个View,因此这里的逻辑需要变动一点。
在数据源的前后各加3个数据源,前面加入原数据源的末三位;后面加入原数据源的前三位。如图:
3-7是指我们原来的5个数据源;0-2是添加的头数据源,具体的索引指向5-7;8-10是添加的尾数据源,具体的索引指向3-5。代码如下
/** * 初始化原始数据 */public void initData(){ sourceData.clear(); for(int i = 0 ; i < 5; i++){ sourceData.add(i + ""); }}/** * 为了能无限滚动,将末三项复制加入列表开头,将前三项复制加入到列表末尾 */private void repairDataForLoop(){ repairData.clear(); List<String> beforeTemp = new ArrayList<String>(); List<String> afterTemp = new ArrayList<String>(); for(int i = 0; i < ONE_PAGE_ITEM_COUNT; i++){ beforeTemp.add(0, sourceData.get(sourceData.size() - 1 - i)); afterTemp.add(sourceData.get(i)); } repairData.addAll(beforeTemp); repairData.addAll(sourceData); repairData.addAll(afterTemp);}
我们将原生的数据源放入sourceData,便于数据检索。而将修复后的数据源放入repairData。
B. 无限滚动的要领
然后当界面滚动到position=0时,立马调用setCurrentItem滚动到position=5的位置;当界面滚动到position=8时,立马调用setCurrentItem滚动到position=3的位置。注意这里的position是指当前页的第一个View的position,上面已经讲过了。这里需要有几点需要注意一下:
- setCurrentItem的第二个参数要传false,这样才能无动画滚动,才能对用户无感。
- 在OnPageChangeListener中,需要在onPageScrollStateChanged中处理而不是在onPageSelected。因为onPageScrollStateChanged的SCROLL_STATE_IDLE状态比onPageSelected晚调用。否则会出现界面突然跳动没有平滑动画的问题。
- 调用setCurrentItem(position, false)时,设置的PageTransformer动画效果会失效。也就是说中间的View不会有放大的效果。所以必须手动设置放大的效果。这里就需要前面instantiateItem中设置Tag的帮助了。
/** * 真正处理无限滚动的逻辑,在滑动到边界时,立马跳转到对应的位置 */private void handleLoop(){ viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { // 是否应该强制跳转到首页 boolean shouldToBefore = false; // 是否应该强制跳转到末页 boolean shouldToAfter = false; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { //这里的position是指每一页的第一个Item的position if (position == 0){ shouldToAfter = true; }else if(position == getCount() - ONE_PAGE_ITEM_COUNT){ shouldToBefore = true; } } @Override public void onPageScrollStateChanged(int state) { //onPageScrollStateChanged的SCROLL_STATE_IDLE比onPageSelected晚调用,如果在onPageSelected中处理方法,则不会有滑动动画效果 if(state == ViewPager.SCROLL_STATE_IDLE){ if(shouldToAfter){ //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(getCount() - ONE_PAGE_ITEM_COUNT * 2, false); //居中的position比当前position多1 handleScale(getCount() - ONE_PAGE_ITEM_COUNT * 2 + 1); shouldToAfter = false; } if(shouldToBefore){ //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false); //居中的position比当前position多1 handleScale(ONE_PAGE_ITEM_COUNT + 1); shouldToBefore = false; } } } });}/** * 调用了setCurrentItem(position, false)后,设置的PageTransformer不会生效, * 也就是说中间需要放大的项不会放大,所以手动将这一项放大 */private void handleScale(int position){ View view = findCenterView(position); if(view == null){ return; } view.setScaleX(itemMaxScale); view.setScaleY(itemMaxScale);}/** * 通过在instantiateItem方法给每个View设置的Tag来标记,找出内存中的View */private View findCenterView(int position){ for(int i = 0; i < viewPager.getChildCount(); i++){ View view = viewPager.getChildAt(i); if(String.valueOf(position).equals(view.getTag())){ return view; } } return null;}
8. 完整的代码:
item.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:orientation="vertical"> <ImageView android:id="@+id/img" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/txt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:gravity="center" android:textColor="@android:color/black" android:textSize="15sp" /></LinearLayout>
activity_main.xml
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" android:layerType="software" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="wrap_content" android:clipChildren="false" /></LinearLayout>
OnePageThreeItemAdapter.java
/** * Created by chenchen on 2017/8/15. * * 代码中有几个position需要注意一些 * 1. setCurrentItem的position参数,是指当前页的第一个Item的所在position * 2. OnPageChangeListener的position参数,同样也是指当前页的第一个Item的所在position * 3. instantiateItem的position参数,是指每一个Item的所在position * 4. PageTransformer的position参数,是指每一个Item相对于Page所占的比例,当前页的第一个Item是0,左边的依次减去Item的宽度,右边的依次加上Item的宽度 * 比如一页只有一个Item。那么当前页的position为0。左边的position分别为-1。右边的position分别为1; * 比如一页有三个Item。那么当前页的Item的position分别为0、1/3、2/3。左边的position分别为-1、-2/3、-1/3。右边的position分别为1、4/3、5/3。 */public class OnePageThreeItemAdapter extends PagerAdapter { //一屏最多3个item,不能随便改变这个值,改这个数量必须要改动很多逻辑 private static final int ONE_PAGE_ITEM_COUNT = 3; private Context context; private ViewPager viewPager; // 居中的Item的最大放大值 private float itemMaxScale; //在原始的数据前后各加了3项数据 private List<String> repairData = new ArrayList<String>(); //原始的数据,用于数据检索等 private List<String> sourceData = new ArrayList<String>(); public OnePageThreeItemAdapter(ViewPager viewPager, float itemMaxScale) { this.viewPager = viewPager; this.context = viewPager.getContext(); this.itemMaxScale = itemMaxScale; initData(); repairDataForLoop(); handleLoop(); } /** * 调用之前保证initData方法已被调用 */ @Override public void notifyDataSetChanged() { repairDataForLoop(); super.notifyDataSetChanged(); } /** * 初始化原始数据 */ public void initData(){ sourceData.clear(); for(int i = 0 ; i < 5; i++){ sourceData.add(i + ""); } } /** * 为了能无限滚动,将末三项复制加入列表开头,将前三项复制加入到列表末尾 */ private void repairDataForLoop(){ repairData.clear(); List<String> beforeTemp = new ArrayList<String>(); List<String> afterTemp = new ArrayList<String>(); for(int i = 0; i < ONE_PAGE_ITEM_COUNT; i++){ beforeTemp.add(0, sourceData.get(sourceData.size() - 1 - i)); afterTemp.add(sourceData.get(i)); } repairData.addAll(beforeTemp); repairData.addAll(sourceData); repairData.addAll(afterTemp); } /** * 真正处理无限滚动的逻辑,在滑动到边界时,立马跳转到对应的位置 */ private void handleLoop(){ viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { // 是否应该强制跳转到首页 boolean shouldToBefore = false; // 是否应该强制跳转到末页 boolean shouldToAfter = false; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { //这里的position是指每一页的第一个Item的position if (position == 0){ shouldToAfter = true; }else if(position == getCount() - ONE_PAGE_ITEM_COUNT){ shouldToBefore = true; } } @Override public void onPageScrollStateChanged(int state) { //onPageScrollStateChanged的SCROLL_STATE_IDLE比onPageSelected晚调用,如果在onPageSelected中处理方法,则不会有滑动动画效果 if(state == ViewPager.SCROLL_STATE_IDLE){ if(shouldToAfter){ //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(getCount() - ONE_PAGE_ITEM_COUNT * 2, false); handleScale(getCount() - ONE_PAGE_ITEM_COUNT * 2 + 1); shouldToAfter = false; } if(shouldToBefore){ //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false); handleScale(ONE_PAGE_ITEM_COUNT + 1); shouldToBefore = false; } } } }); } /** * 调用了setCurrentItem(position, false)后,设置的PageTransformer不会生效, * 也就是说中间需要放大的项不会放大,所以手动将这一项放大 */ private void handleScale(int position){ View view = findCenterView(position); if(view == null){ return; } view.setScaleX(itemMaxScale); view.setScaleY(itemMaxScale); } /** * 通过在instantiateItem方法给每个View设置的Tag来标记,找出内存中的View */ private View findCenterView(int position){ for(int i = 0; i < viewPager.getChildCount(); i++){ View view = viewPager.getChildAt(i); if(String.valueOf(position).equals(view.getTag())){ return view; } } return null; } @Override public int getCount() { return repairData.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public Object instantiateItem(ViewGroup container, final int position) { View view = View.inflate(context, R.layout.item, null); view.setTag(String.valueOf(position)); TextView txt = (TextView) view.findViewById(R.id.txt); txt.setText(repairData.get(position)); view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //这里的position是指每一页的第一个Item的position viewPager.setCurrentItem(position == 0 ? 0 : position - 1, true); Toast.makeText(context, "点击了" + repairData.get(position), Toast.LENGTH_SHORT).show(); } }); container.addView(view); return view; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView((View) object); } @Override public float getPageWidth(int position) { return 1.0f / ONE_PAGE_ITEM_COUNT; } public class OnePageThreeItemTransformer implements ViewPager.PageTransformer { @Override public void transformPage(View page, float position) { //这里的position是指每一个Item相对于Page所占的比例,当前页的第一个Item是0,左边的依次减去Item的宽度,右边的依次加上Item的宽度 float centerPosition = 1.0f / ONE_PAGE_ITEM_COUNT; if (position <= 0) { page.setScaleX(1); page.setScaleY(1); } else if (position <= centerPosition) { page.setScaleX(1 + (itemMaxScale - 1) * position / centerPosition); page.setScaleY(1 + (itemMaxScale - 1) * position / centerPosition); } else if (position <= centerPosition * 2) { page.setScaleX(1 + (itemMaxScale - 1) * (2 * centerPosition - position) / centerPosition); page.setScaleY(1 + (itemMaxScale - 1) * (2 * centerPosition - position) / centerPosition); } else { page.setScaleX(1); page.setScaleY(1); } } } /** * 配置ViewPager一些其他的属性 */ public void configViewPager(){ viewPager.setOffscreenPageLimit(ONE_PAGE_ITEM_COUNT + 2); viewPager.setPageTransformer(true, new OnePageThreeItemTransformer()); viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT); }}
MainActivity.java
public class MainActivity extends Activity { private ViewPager viewPager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewPager = (ViewPager) findViewById(R.id.viewPager); OnePageThreeItemAdapter adapter = new OnePageThreeItemAdapter(viewPager, 2); viewPager.setAdapter(adapter); adapter.configViewPager(); }}
- Android中ViewPager支持一屏多个View、切换动画以及无限滚动
- Android 无限循环且支持自动滚动的ViewPager
- Android ViewPager多页面滑动切换以及动画效果---换view
- android ViewPager无限滚动、轮播图
- Android无限循环滚动ViewPager
- Android 中ViewPager 实现动画效果切换
- android viewpager 切换动画
- [Android实例] ViewPager多页面滑动切换以及动画效果
- [Android实例] ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- [Android实例] ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- Android ViewPager多页面滑动切换以及动画效果
- 2017816
- anaconda多环境配置
- vue-schart : vue.js 的图表组件
- tf.contrib.learn快速入门
- :app:transformResourcesWithMergeJavaResForDebug Duplicate files copied in APK META-INF错误的解决办法
- Android中ViewPager支持一屏多个View、切换动画以及无限滚动
- hdoj6129 Just do it(三种方法加详解)
- cookie与session的区别
- TensorFlow Ubuntu 安装笔记
- Metropolis Light Transport学习与实现
- 剑指OFFER 算法练习
- POJ 2785 4 Values whose Sum is 0
- intent-filter的action,category,data匹配规则
- 使用tf.contrib.learn构建输入函数