android可以無限迴圈滑動的ViewPager
前言:最近有需求需要某個頁面是可以無限滑動的,這個頁面是用ViewPager實現的,但是ViewPager本身並不能無限滑動,所以想在android現有ViewPager的基礎之上,實現這個功能,本文提供基於PagerAdapter和FragmentPagerAdapter的可以複用view和fragment的一種實現,github地址:https://github.com/ChenSWD/InfiniteViewPager。
分析:
ViewPager滑動邊界是怎麼判斷的?
ViewPager能不能滑動依賴於: mAdapter.getCount()和mCurItem變數,即:當前位置和item總個數。那我們直接讓mAdapter.getCount()返回一個很大的值(Integer.MAX_VALUE)就可以了,即有無限個item。
基於以上,這樣操作對效能上有多大的影響?其實ViewPager是有快取機制的,預設快取當前位置和其兩邊的各一個item,所以在記憶體上的問題是不存在的,但是我還是想自己快取一些可複用的view,向ListView那樣,因為一般在新建view時會執行LayoutInflater.inflate(),會解析xml格式的佈局檔案,會重複建立銷燬view,把view快取起來可以解決是個不錯的選擇。
基於PagerAdapter普通自定義佈局的分析:
因為要修改adapter和ViewPager的原始碼,所以把相關檔案拷貝一份:ViewPager
(ViewPagerCopy),PagerAdapter
(PagerAdapterCopy),FragmentPagerAdapter
(InfiniteFragmentPagerAdapter)。
我們假設ViewPager需要迴圈的有3個item,position分別是0 1 2
,把這個叫做item實際位置,在無限迴圈的ViewPager裡面,0的位置實際上對應的是Integer.MAX_VALUE/2
,1-Integer.MAX_VALUE/2+1
,2-Integer.MAX_VALUE/2+2
,把對映後的位置稱為item的擴充套件位置。
1、新建InfinitePagerAdapter
繼承PagerAdapterCopy
。在PagerAdapterCopy裡面新增方法 public abstract int getRealCount();
,該方法返回實際的item個數;新增方法:public int getRealPosition(int position){}
,該方法是給ViewPagerCopy裡OnPageChangeListener
使用的,根據擴充套件的位置返回實際的位置;新增方法:public abstract void onPageSelected(int extendPosition, int realPosition)
,該方法在每一次切換頁面時呼叫。
2、所有需要迴圈的adapter繼承自InfinitePagerAdapter,需要說明的是getExtendItem()方法,該方法在設定ViewPager初始的時候顯示第幾頁時使用,根據實際的位置,返回擴充套件後的位置,所有destroyItem()
和instantiateItem()
方法的position引數全部是擴充套件的位置:
public abstract class InfinitePagerAdapter extends PagerAdapterCopy {
public static final int INFINITE_COUNT = Integer.MAX_VALUE;
@Override
public int getCount() {
//返回無限個
return INFINITE_COUNT;
}
/**
* 在呼叫setCurrentItem()時使用該方法返回擴充套件後的位置
* 該方法需要在getRealCount()有返回值的時候使用,
* ViewPager的setCurrentItem()方法要在設定資料之後呼叫,因為不知道 RealCount,就不能算出擴充套件後的位置
*/
public int getExtendItem(int item) {
int real = getRealCount();
int total = getCount();
if (real <= 0) {
return item;
}
//找到大概中間位置的組號
int index = total / real / 2;
//返回中間組的第item個位置
return index * real + item;
}
}
3、下面給出具體使用的PagerActivity和PagerAdapter的程式碼,相關注釋在程式碼中都有,為什麼快取view的個數是limit*2+2,在程式碼中特別做了解釋:
public class PagerActivity extends AppCompatActivity {
private ViewPagerCopy mViewPager;
private PagerAdapter mAdapter;
List<Integer> text = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pager);
for (int i = 0; i < 3; i++) {
text.add(0);
}
//最多快取兩個(當前view一邊兩個,總共5個)
int limit = 2;
mViewPager = findViewById(R.id.view_pager);
//快取view的個數為limit*2 + 2
//為什麼是limit*2 + 2:正常情況下應該快取 limit*2 + 1 個view,因為左右兩邊各快取limit個,
//在從左向右滑動換頁時是沒有問題的,因為會先執行destroyItem()方法再執行instantiateItem()方法,
//這樣instantiateItem時就可以複用destroyItem時remove的view
//但是ViewPager在從右向左滑動的時候,會先執行instantiateItem()方法再執行destroyItem()方法,
// 這樣就必須多快取一個,即:limit*2 + 2個
mAdapter = new PagerAdapter(limit * 2 + 2);
mAdapter.setTextList(text);
mViewPager.setAdapter(mAdapter);
//設定快取的限制,預設會快取1個
mViewPager.setOffscreenPageLimit(limit);
//資料的設定(setTextList())要在setCurrentItem()之前,
mViewPager.setCurrentItem(mAdapter.getExtendItem(1));
mAdapter.notifyDataSetChanged();
}
}
public class PagerAdapter extends InfinitePagerAdapter {
View[] viewList;
List<Integer> textList = new ArrayList<>();
public PagerAdapter(int size) {
viewList = new View[size];
}
public void setTextList(List<Integer> textList) {
this.textList = textList;
}
@Override
public Object instantiateItem(ViewGroup container, final int position) {
final ViewHolder holder;
//這裡position是擴充套件後的位置,所以要根據位置,計算出當前要複用的item的下標位置
//迴圈複用viewList裡的view
int viewPos = position % viewList.length;
if (viewList[viewPos] == null) {
View view = LayoutInflater.from(container.getContext()).inflate(R.layout.pager_adapter_item, container, false);
holder = new ViewHolder(view);
view.setTag(holder);
viewList[viewPos] = view;
} else {
holder = (ViewHolder) viewList[viewPos].getTag();
}
final int realPos = position % textList.size();
holder.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
textList.set(realPos, textList.get(realPos) + 1);
holder.textView.setText("實際位置 " + realPos
+ "\n是第" + position + "個擴充套件的位置"
+ "\ncount = " + textList.get(realPos));
}
});
bindViewHolder(position);
container.addView(viewList[viewPos]);
return viewList[viewPos];
}
//每一次切換頁面的時候重新整理一下狀態
public void bindViewHolder(int extendPos) {
int viewPos = extendPos % viewList.length;
if (viewList[viewPos] != null) {
final ViewHolder holder = (ViewHolder) viewList[viewPos].getTag();
final int realPos = extendPos % textList.size();
holder.textView.setText("實際位置 " + realPos
+ "\n是第" + extendPos + "個擴充套件的位置"
+ "\ncount = " + textList.get(realPos));
}
}
@Override
public int getRealCount() {
return textList.size();
}
@Override
public void onPageSelected(int extendPosition, int realPosition) {
bindViewHolder(extendPosition);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
private static class ViewHolder {
private TextView textView;
public ViewHolder(View view) {
textView = view.findViewById(R.id.text);
}
}
}
基於FragmentPagerAdapter的Fragment無限迴圈的分析:
分析:因為ViewPager配合Fragment使用,很多時候Fragment都不相同,不能簡單的複用,一般來說實際Fragment是比較多的,直接在instantiateItem()方法中,把擴充套件position轉換成實際position就行(position%getRealCount
)。
但是當實際fragment個數小於limit*2+2的時候,會出現fragment快取不夠用的情況,例如真實fragment:0,1,2,需要快取的個數limit=2,那麼就需要6個真實的fragment才能完成迴圈複用,比如當前位置為2,快取的fragment需要是0,1,2,3,4。3的位置是不能複用0位置的,因為同一個fragment不能被attach兩次,這時候fragment是不夠用的,怎麼辦呢?我是在3的位置new了一個新的和0位置一樣的fragment,4的位置new了一個和1一樣的fragment,然後新增到fragment棧中的。
我是這樣做的(感覺還可以完善很多東西),定義一個Map:Map<Integer, List<AbstractFragment>> fragmentMap;
,key是fragment的實際位置,value是一個list,用於裝入在key這個位置重複的fragment,比如上面0和3都會被裝入到key為0的list中。
以下都是fragment很少,需要重複新增的情況下做的分析:
1、在AbstractFragment
中增加一個欄位表示當前fragment是不是能被複用(沒有被attach),因為用isAdded()和isDetached()方法似乎都不能標識:
public abstract class AbstractFragment<T> extends Fragment {
//是否可被新增,初始的true
private boolean isUsable = true;
public boolean isUsable() {
return isUsable;
}
public void setUsable(boolean isUsable) {
this.isUsable = isUsable;
}
//用來重新整理fragment的狀態
public abstract void refreshData(T data);
}
2、修改InfinitePagerAdapter的instantiateItem()
和destroyItem()
,這兩個方法主要給fragment設定可用的標識(呼叫setUsable()
方法),新增方法 protected abstract String findUsableFragmentTag(int pos)
是自定義FragmentAdapter需要實現的,這個方法是用來根據擴充套件的位置,返回可用的fragment的Tag,改變了原先Tag獲取的方式:
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
//先查詢有沒有可以複用的fragment,沒有的話要呼叫getItem()新建
String name = findUsableFragmentTag(position);
if (name == null) {
// Do we already have this fragment?
name = makeFragmentName(container.getId(), itemId);
}
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
if (fragment instanceof AbstractFragment) {
((AbstractFragment) fragment).setUsable(false);
}
return fragment;
}
protected abstract String findUsableFragmentTag(int pos);
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment) object).getView());
mCurTransaction.detach((Fragment) object);
if (object instanceof AbstractFragment) {
((AbstractFragment) object).setUsable(true);
}
}
3、自定義的FragmentPagerAdapter,主要是getItem()方法,該方法根據擴充套件位置,返回一個可用的實際位置的fragment,做法是根據實際位置去查詢Map,找到list裡面第一個可用的fragment返回,如果list中無當前位置可用的fragment,就去新建一個:
public class FragmentPagerAdapter extends InfiniteFragmentPagerAdapter {
private int realCount;
private Map<Integer, List<AbstractFragment>> fragmentMap = new HashMap<>();
private FragmentAdapterCallBack callBack;
private List<DataEntity> entityList;
public FragmentPagerAdapter(FragmentManager fm, List<AbstractFragment> fragments, FragmentAdapterCallBack callBack, List<DataEntity> entityList) {
super(fm);
realCount = fragments.size();
for (int i = 0; i < fragments.size(); i++) {
List<AbstractFragment> list = new ArrayList<>();
list.add(fragments.get(i));
fragmentMap.put(i, list);
}
this.callBack = callBack;
this.entityList = entityList;
}
@Override
public Fragment getItem(int position) {
int realPos = position % getRealCount();
List<AbstractFragment> fragments = fragmentMap.get(realPos);
//根據實際位置去查詢Map,找到list裡面第一個可用的fragment返回
for (AbstractFragment fragment : fragments) {
if (fragment.isUsable()) {
return fragment;
}
}
AbstractFragment fragment = null;
//如果list中無當前位置可用的fragment,就去新建一個
if (callBack != null) {
fragment = callBack.generateFragmentByPosition(realPos);
fragments.add(fragment);
}
if (fragment == null)
throw new NullPointerException("新生成的fragment不能為空");
return fragment;
}
@Override
protected String findUsableFragmentTag(int position) {
int realPos = position % getRealCount();
//根據實際位置去查詢Map,找到可用的就返回它的Tag
List<AbstractFragment> fragments = fragmentMap.get(realPos);
for (AbstractFragment fragment : fragments) {
if (fragment.isUsable()) {
return fragment.getTag();
}
}
return null;
}
@Override
public int getRealCount() {
return realCount;
}
@Override
public void onPageSelected(int extendPosition, int realPosition) {
//在換頁的時候,判斷是不是要重新整理擴充套件後的fragment的狀態
refreshFragmentsIfNeed(realPosition, entityList.get(realPosition));
}
//當前fragment是不是擴充套件新增的,用來判斷是否重新整理Fragment的狀態
public void refreshFragmentsIfNeed(int pos, DataEntity entity) {
//在當前位置有擴充套件的fragment的情況下,且當前位置的資料被更改過,查詢當前已經attach的fragment,重新整理它們的狀態
if (entity != null && entity.isRefresh()) {
List<AbstractFragment> fragments = fragmentMap.get(pos);
if (fragments != null && fragments.size() > 1) {
for (AbstractFragment fragment : fragments) {
if (!fragment.isUsable()) {
fragment.refreshData(entity);
}
}
}
entity.setRefresh(false);
}
}
}
總結:以上基本完成了自定義PagerAdapter和FragmentPagerAdapter實現無限滑動的功能,這裡只是提供一種解決方式,在具體使用中可能還要具體修改一些邏輯。在後續個人使用中,會完善一些程式碼,如果發現一些bug也會及時修復,如果有好的思路也請指教。說了這麼多可能都不如看一下程式碼:https://github.com/ChenSWD/InfiniteViewPager
相關文章
- ViewPager實現左右無限迴圈滑動Viewpager
- 左右迴圈滑動的viewpagerViewpager
- 直播系統app原始碼,垂直,水平無限迴圈滑動APP原始碼
- 無限for迴圈(死迴圈)
- javascript無限迴圈滾動JavaScript
- Android 禁止ViewPager左右滑動AndroidViewpager
- Flutter迴圈滑動的PageViewFlutterView
- Java無限迴圈問題Java
- iOS-無限迴圈輪播圖iOS
- IOS 無限迴圈小視訊播放iOS
- Vue無限滑動周選擇日期的元件Vue元件
- win10自動修復失敗無限迴圈怎麼辦_win10開機自動修復失敗無限迴圈如何解決Win10
- ViewPager最簡單的無限輪播Viewpager
- 實現抖音 “影片無限滑動“效果
- 原生js系列之無限迴圈輪播元件JS元件
- AlloyTouch之無限迴圈select外掛
- 無縫迴圈滾動
- AirDrop無限迴圈攻擊,你的iPhone還好嗎?AIiPhone
- win10無限迴圈自動修復怎麼辦_win10開機無限迴圈自帶修復失敗重啟如何解決Win10
- RecyclerView 、ViewPager 左右滑動衝突Viewpager
- iOS開發系列--無限迴圈的圖片瀏覽器iOS瀏覽器
- Android ViewPager Fragments滑動只重新整理當前頁AndroidViewpagerFragment
- 理解 vue-router的beforeEach無限迴圈的問題Vue
- 打造萬能的BannerView(ViewPager)無限輪播圖Viewpager
- 記錄---實現抖音 “影片無限滑動“效果
- iOS無限迴圈輪播圖(只使用三個imageView)iOSView
- ViewPager 禁止左右滑動完美解決Viewpager
- Android之ViewPager+GridView實現GridView介面滑動AndroidViewpager
- 【Android ViewPager】解決ViewPager巢狀時在API 13及其以下版本中不能滑動的問題AndroidViewpager巢狀API
- ThinkPHP 無限遞迴PHP遞迴
- 線上直播系統原始碼,橫向無限迴圈滾動的單行彈幕效果原始碼
- ViewPager兩種方式實現無限輪播Viewpager
- Android去掉SrollView、GrdiView、RecycleView、ViewPager等可滑動控制元件滑動到邊緣的光暈效果AndroidViewpager控制元件
- 無迴圈 JavaScriptJavaScript
- android 三種實現水平向滑動方式(ViewPager、ViewFilpper、ViewFlow)的比較AndroidViewpager
- 帶貨直播原始碼,確定ViewPager滑塊滑動方向原始碼Viewpager
- CSS實現迴圈無縫滾動CSS
- scrollview 的滑動衝突 viewpager等都適用Viewpager