PS:以前也寫過關於圖片輪播這一塊的博客.不過寫的很爛,並且很多情況沒有考慮到(沒有支持無線輪播,和手勢點擊事件).因此這里寫一篇補上.也是當時太年輕了.
注:圖片請放大后再看.否則看不清楚.
學習內容:
1.自定義ViewPager
2.圖片無限輪播由於ViewPager的預加載機制所導致的問題.
以前也寫過關於圖片輪播的相關博客,不過總體寫的非常的爛,並且不能夠無線輪播,而且也無法對手勢事件進行相關的處理,因此在這里補上,也是屬於自定義View的一篇內容吧.並且通過這個過程發現了無限輪播由於ViewPager的預加載機制所導致的問題.也正趕上項目要上一個新的版本,發現了這個bug.我的同事想到了一個比較不錯的方案解決了這個問題.總體還是很不錯的.因此在這里記錄一下.
總體實現無限輪播的思想,其實和網上大部分的思路都是相同的,設置一個Integer的最大值的一半,然后根據position和圖片的數量去計算,來實現向左向右無限滑動這個功能.總體不是特別的難.自定義ViewPager之后,把相關的圖片和跟隨圖片滑動時的小圓點傳遞到ViewPager當中,然后設置相關的滑動監聽,Adapter就可以完美的實現圖片的無限輪播了.
i.初始化輪播.
初始化輪播需要線程和Handler配合來完成,設置一個無線循環的線程,讓這個線程按照一定的周期一直向Activity發送Message,Handler在接收到消息后會不斷的對消息進行處理.改變ViewPager當前顯示的CurrentItem就可以實現無限輪播了.
/** * 初始化輪播的線程 */ public void initLoop() { new Thread(new Runnable() { @Override public void run() { while (true) { try { Thread.sleep(3000); if (!stopLoopTag) { Message message = Message.obtain(); message.what = 10; message.arg1 = getCurrentItem() + 1; mHandler.sendMessage(message); } } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } /** * 處理輪播的Handler */ private Handler mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message msg) { if (msg.what == 10 && !stopLoopTag) { setCurrentItem(msg.arg1); } else { mHandler.removeMessages(10); } return true; } });
ii.圖片輪播時小圓點隨之變動
有了一個子線程和一個Handler就可以實現無線循環的一個過程,我們知道一般圖片輪播都需要伴隨小圓點的移動,小圓點一般是直接布局到ViewPager當中的,因為需要給用戶一種更好的體驗性,因此在圖片輪播的同時伴隨着小圓點也隨之變動.那么小圓點如何在ViewPager的Item改變的時候也隨之變動呢?這就需要addOnPageChangeListener()來實現了.
private OnPageChangeListener onPageChangeListener = new OnPageChangeListener() {
@Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { /** * 改變小圓點的狀態 * 三種情況: * 1.在初始位置 * 2.向左滑動 * 3.向右滑動 * 不要放在上一個方法中,會有明顯的延遲現象出現 * lastPointPosition 上一個點的位置,初始化為0 * lastPointIndex 上一個點在集合中的位置 * currentPointIndex 當前的點在集合中的位置 * */ if (lastPointPosition == 0) { currentPointIndex = 0; lastPointIndex = 0; } else if (lastPointPosition < position) { if (currentPointIndex == getImages().size() - 1) { currentPointIndex = 0; } else { currentPointIndex += 1; } } else if (lastPointPosition > position) { if (currentPointIndex == 0) { currentPointIndex = getImages().size() - 1; } else { currentPointIndex -= 1; } } dots.get(lastPointIndex).setImageResource(R.drawable.dot_normal); dots.get(currentPointIndex).setImageResource(R.drawable.dot_focus); lastPointPosition = position; lastPointIndex = currentPointIndex; } @Override public void onPageScrollStateChanged(int state) { } };
這里我們通過addOnPageChangeListener()綁定Page的改變監聽來改變小圓點隨着Page改變的同時隨之改變.圖片的改變需要在適配器里去設置,適配器我留到最后說,因為其中有很多的細節.這樣有了無限循環,原點移動,那么就需要說一下當我們手指停留在ViewPager的時候,如何使ViewPager停止播放.因為涉及到了手勢事件,因此就要重寫相關的方法.
iii.重寫手勢事件
/** * 手勢事件的重寫 */ @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: /** * 按下的時候停止輪播 * */ stopLoop(); xDown = (int) event.getX(); yDown = (int) event.getY(); break; case MotionEvent.ACTION_MOVE: /** * 這里不用做任何處理,移動的時候ViewPager內的圖片會自動滑動 * */ break; case MotionEvent.ACTION_UP: /** * 記錄按下時間 * */ if (timeThread == null) { touchTime = new Date().getTime(); timeTag = true; timeThread = new TimeThread(); timeThread.start(); } else { touchTime = new Date().getTime(); } /** * 判斷是否是點擊事件 * */ int xUp = (int) event.getX(); int yUp = (int) event.getY(); if (Math.abs(xDown - xUp) < 20 && Math.abs(yDown - yUp) < 20) { if (onImageItemClickListener != null) { onImageItemClickListener.onItemClick(currentPointIndex); } } break; } return super.onTouchEvent(event); }
手勢事件的重寫就非常的簡單了,當點擊的時候停止輪播,移動的時候我們不需要做任何的處理,在手指離開之后,我們需要計算離開的時間,離開的時間超過兩秒,就再次開啟輪播.關閉輪播只需要停止最開始的那個子線程,讓標記位StopLoopTag為true,再將消息隊列中的消息移除隊列.這樣Handler也接收不到消息了.這樣就停止了輪播的效果.計算手指按下和離開的時間也需要一個子線程去處理.
/** * 時間線程,用於記錄手指離開點擊ViewPager的時間 * 如果離開的時間 >= 2000毫秒,那么ViewPager繼續輪播 */ class TimeThread extends Thread { @Override public void run() { while (timeTag) { long currentTime = new Date().getTime(); if (currentTime - touchTime >= 2000) { openLoop(); timeTag = false; timeThread = null; } } } }
iv.PagerAdapter.
PagerAdapter也是最蛋疼的一塊.instantiateItem()方法里的那一大堆代碼是關鍵.很多人都用這樣的代碼來實現圖片的加載過程.
@Override public Object instantiateItem(ViewGroup container, int position) { position %= images.size(); if (position < 0) { position = position + images.size(); } ImageView imageView = images.get(initPosition); ViewParent parent = imageView.getParent(); if (parent != null) { ViewGroup viewGroup = (ViewGroup) parent; viewGroup.removeView(imageView); } container.addView(imageView); return imageView; }
其實這樣寫是有很大的問題的,因為這里沒有考慮到ViewPager的預加載機制的問題,這也是我們項目出現的一個bug,我們是從服務器上獲取相關的圖片數據,然后保存在集合當中,如果我們在將圖片加載到ViewPager中的時候,僅調用image.get(position),position為上面代碼計算出的position,這樣的話實際上會導致圖片錯位.雖然顯示的是5張圖片,但是實際上他們在ViewPager中顯示的順序是不對的.比如說我們在后台定義了這樣的順序, 1,2,3,4,5的順序發送給我們,但是如果我們按照上面的方法從集合中拿數據的時候,ViewPager顯示的不是1,2,3,4,5這樣的順序,這也是我們在項目中發現的問題.因為在點擊圖片的時候,是需要走不同的鏈接的,也正是這個原因我們發現了這個bug.因此這里做了很多的處理.
public class PictureAdapter extends PagerAdapter { private List<ImageView> images; /** * initPosition -1為初始化的位置,后面是當前的圖片索引位置 * topPosition 記錄上一次初始化的索引位,用於計算上次的position和本次position的偏移量 * * */ private int initPosition = -1; private int topPosition = -1; public PictureAdapter(List<ImageView> images) { this.images = images; } @Override public int getCount() { return Integer.MAX_VALUE; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { } /** * 實例化Item */ @Override public Object instantiateItem(ViewGroup container, int position) { if(images.size() <=1){ ImageView imageView = images.get(topPosition); container.addView(imageView); return imageView; }else{ /** * 初始化狀態 * 向左滑動 * 向右滑動 * 由於ViewPager有預加載機制,默認加載一頁,因此在第一次初始化的時候,會調用三次這個方法. * (第一次: position=1073741823 第二次: position=1073741822 第三次: position=1073741824) * * 而后續,這個方法僅被執行一次,並且執行的是預加載下一頁的請求. * */ Log.e("TAG","position="+position); if (initPosition == -1) { /** * 初始化狀態 * topPosition 記錄第一次初始化的索引位.用於后續作比較判斷下次是向右滑動還是向左滑動 * initPosition 初始化圖片集合的索引值 * */ topPosition = position; initPosition = 0; } else if (topPosition < position) { /** * 向左滑動 * 得出偏移量后比較是否超過圖片集合的大小 * */ int value = position - topPosition; initPosition += value; if (initPosition == images.size()) { /** * 滑動到了最后一頁 * */ initPosition = 0; } else if (initPosition > images.size()) { /** * 如果超出了圖片集合的大小,則 initPosition = 超過的數值 * */ initPosition = (initPosition - images.size()); } topPosition = position; } else if (topPosition > position) { int value = topPosition - position; initPosition -= value; if (initPosition == -1) { /** * 滑動到了第一頁 * */ initPosition = images.size() - 1; } else if (initPosition < -1) { /** * 當計算后的值小於了集合大小,則用集合大小減去小於的這部分 * */ initPosition = (images.size() - (Math.abs(initPosition))); } topPosition = position; } Log.e("TAG","topPosition="+topPosition); Log.e("TAG","initPosition="+initPosition); /** * 只用這句話應該會出現問題 * */ // position %= images.size(); // if (position < 0) { // position = position + images.size(); // } ImageView imageView = images.get(initPosition); ViewParent parent = imageView.getParent(); if (parent != null) { ViewGroup viewGroup = (ViewGroup) parent; viewGroup.removeView(imageView); } container.addView(imageView); return imageView; } } }
這就是我們定義的PagerAdapter.里面做了很多的邏輯處理.一張圖解釋一下其中的原理.
這張圖說明了一種的道理,上面代碼不難發現.會出現 (initPosition > images.size())和initPosition < -1這兩種情況.這種原因的導致就是因為position會由於這種預加載機制出現數值跳躍問題.這里大家可以去根據Log信息結合我說的原理,好好的研究一下.相信大家會研究明白的.
最后就是點擊事件了.對Activity暴露接口,讓Activity去實現就可以了.
/** * 對Activity暴露接口 */ private OnImageItemClickListener onImageItemClickListener; public interface OnImageItemClickListener { void onItemClick(int itemPosition); } public void setOnImageItemClickListener(OnImageItemClickListener onImageItemClickListener) { this.onImageItemClickListener = onImageItemClickListener; }
剩下的就是MainActivity了.這里就不粘貼代碼了.里面的內容比較的簡單.這樣就實現了圖片的無線輪播,並且支持點擊停止,松開繼續播放的功能,還有的圖片點擊事件.最后放一個源代碼:
源代碼地址:http://pan.baidu.com/s/1dFqig17