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 = ; i < ; 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方法
@Override
public float getPageWidth(int position) {
return f / 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 = f / ONE_PAGE_ITEM_COUNT;
if (position <= ) {
page.setScaleX();
page.setScaleY();
} else if (position <= centerPosition) {
page.setScaleX( + (itemMaxScale - ) * position / centerPosition);
page.setScaleY( + (itemMaxScale - ) * position / centerPosition);
} else if (position <= centerPosition * ) {
page.setScaleX( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
page.setScaleY( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
} else {
page.setScaleX();
page.setScaleY();
}
}
}
其中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的情況。
@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));
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//這裡的position是指每一頁的第一個Item的position
viewPager.setCurrentItem(position == ? : position - , 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 = ; i < ; i++){
sourceData.add(i + "");
}
}
/**
* 為了能無限滾動,将末三項複制加入清單開頭,将前三項複制加入到清單末尾
*/
private void repairDataForLoop(){
repairData.clear();
List<String> beforeTemp = new ArrayList<String>();
List<String> afterTemp = new ArrayList<String>();
for(int i = ; i < ONE_PAGE_ITEM_COUNT; i++){
beforeTemp.add(, sourceData.get(sourceData.size() - - 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 == ){
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 * , false);
//居中的position比目前position多1
handleScale(getCount() - ONE_PAGE_ITEM_COUNT * + );
shouldToAfter = false;
}
if(shouldToBefore){
//這裡的position是指每一頁的第一個Item的position
viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false);
//居中的position比目前position多1
handleScale(ONE_PAGE_ITEM_COUNT + );
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 = ; 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 = ;
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 = ; i < ; i++){
sourceData.add(i + "");
}
}
/**
* 為了能無限滾動,将末三項複制加入清單開頭,将前三項複制加入到清單末尾
*/
private void repairDataForLoop(){
repairData.clear();
List<String> beforeTemp = new ArrayList<String>();
List<String> afterTemp = new ArrayList<String>();
for(int i = ; i < ONE_PAGE_ITEM_COUNT; i++){
beforeTemp.add(, sourceData.get(sourceData.size() - - 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 == ){
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 * , false);
handleScale(getCount() - ONE_PAGE_ITEM_COUNT * + );
shouldToAfter = false;
}
if(shouldToBefore){
//這裡的position是指每一頁的第一個Item的position
viewPager.setCurrentItem(ONE_PAGE_ITEM_COUNT, false);
handleScale(ONE_PAGE_ITEM_COUNT + );
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 = ; 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 == ? : position - , 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 f / 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 = f / ONE_PAGE_ITEM_COUNT;
if (position <= ) {
page.setScaleX();
page.setScaleY();
} else if (position <= centerPosition) {
page.setScaleX( + (itemMaxScale - ) * position / centerPosition);
page.setScaleY( + (itemMaxScale - ) * position / centerPosition);
} else if (position <= centerPosition * ) {
page.setScaleX( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
page.setScaleY( + (itemMaxScale - ) * ( * centerPosition - position) / centerPosition);
} else {
page.setScaleX();
page.setScaleY();
}
}
}
/**
* 配置ViewPager一些其他的屬性
*/
public void configViewPager(){
viewPager.setOffscreenPageLimit(ONE_PAGE_ITEM_COUNT + );
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, );
viewPager.setAdapter(adapter);
adapter.configViewPager();
}
}