Android 自定義 MarqueeView 實現跑馬燈 —— 原理篇
本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
ofollow,noindex">Android 自定義 MarqueeView 實現跑馬燈效果 - 使用說明
Android 自定義 MarqueeView 實現跑馬燈 —— 原理篇
前言
在上一篇部落格 Android 自定義 MarqueeView 實現跑馬燈效果 - 使用說明 中,我們已經講解了 MarqueeView 的各種用法。這篇部落格,讓我們一起來看一下 MarqueeView 的實現原理。
在上一篇部落格中,我們知道我們是通過給 MarqueeView setAdapter 來重新整理介面的。因此,讓我們一起先來看一下 MultiItemTypeAdapter。
MultiItemTypeAdapter 講解
講解 MultiItemTypeAdapter 之前,我們先來看一下相應的介面 ItemViewDelegate 和類 ItemViewDelegateManager
ItemViewDelegate
而 ItemViewDelegateManager 主要是管理 ItemViewDelegate 的。
public interface ItemViewDelegate<T> { public abstract int getItemViewLayoutId(); public abstract boolean isForViewType(T item, int position); public abstract void convert(ViewHolder holder, T t, int position); }
ItemViewDelegate 主要有三個方法,getItemViewLayoutId 方法表示獲取 ItemViewLayoutId,isForViewType 會根據 item 即 position 判斷當前的 item 是不是屬於當前的 ItemViewDelegate,convert 在重新整理當前 item 的時候會呼叫。
ItemViewDelegateManager
ItemViewDelegateManager,沒錯,從字面意思來看,就是來管理 ItemViewDelegate 的。
接下來我們來看 ItemViewDelegateManager 裡面幾個比較重要的方法,
-
當有指定 viewType會先去快取裡面查詢是否存在相應的 delegate,如果存在,不合法,丟擲異常。因為同一時刻只有一個 delegate 能處理該 position;
-
當沒有指定 viewType 的時候,我們會以當前 delegates 的容量作為 key 存進 SparseArrayCompat 中。
SparseArrayCompat<ItemViewDelegate<T>> delegates = new SparseArrayCompat(); public ItemViewDelegateManager<T> addDelegate(int viewType, ItemViewDelegate<T> delegate) { if (delegates.get(viewType) != null) { throw new IllegalArgumentException("An ItemViewDelegate is already registered for the" + " viewType = " + viewType + ". Already registered ItemViewDelegate is " + delegates.get(viewType)); } delegates.put(viewType, delegate); return this; } public ItemViewDelegateManager<T> addDelegate(ItemViewDelegate<T> delegate) { int viewType = delegates.size(); if (delegate != null) { delegates.put(viewType, delegate); viewType++; } return this; }
因此,我們如果想獲取對應 position 的 viewType,可以通過 delegate 在 delegates 中對應的 key
於是衍生出以下方法:
即根據當前 postion,去查詢相應的 delegate,然後再獲取通過 delegate 在 delegates 陣列中對應的 key,即我們的 viewType
public int getItemViewType(T item, int position) { int delegatesCount = delegates.size(); for (int i = delegatesCount - 1; i >= 0; i--) { ItemViewDelegate<T> delegate = delegates.valueAt(i); if (delegate.isForViewType(item, position)) { return delegates.keyAt(i); } } throw new IllegalArgumentException("No ItemViewDelegate added that matches position=" + position + " in data source"); }
MultiItemTypeAdapter 講解
主要有幾個重要的方法:
public View createItemView(ItemViewDelegate<T> itemViewDelegate, ViewGroup parent) { int layoutId = itemViewDelegate.getItemViewLayoutId(); ViewHolder viewHolder = null; View convertView = LayoutInflater.from(mContext).inflate(layoutId, parent, false); viewHolder = new ViewHolder(mContext, convertView, parent, -1); viewHolder.mLayoutId = layoutId; onViewHolderCreated(viewHolder, viewHolder.getConvertView()); return convertView; } public View createItemView(int position, View convertView, ViewGroup parent) { ItemViewDelegate itemViewDelegate = mItemViewDelegateManager.getItemViewDelegate(mDatas .get(position), position); int layoutId = itemViewDelegate.getItemViewLayoutId(); ViewHolder viewHolder = null; if (convertView == null) { convertView = LayoutInflater.from(mContext).inflate(layoutId, parent, false); viewHolder = new ViewHolder(mContext, convertView, parent, position); viewHolder.mLayoutId = layoutId; onViewHolderCreated(viewHolder, viewHolder.getConvertView()); } else { viewHolder = (ViewHolder) convertView.getTag(); viewHolder.mPosition = position; } convert(viewHolder, getItem(position), position); return convertView; } private void convert(ViewHolder viewHolder, T item, int position) { mItemViewDelegateManager.convert(viewHolder, item, position); } public SparseArrayCompat<View> getAllTyeView(ViewGroup parent) { SparseArrayCompat<ItemViewDelegate<T>> itemViewDelegates = getItemViewDelegate(); int size = itemViewDelegates.size(); SparseArrayCompat<View> viewSparseArrayCompat = new SparseArrayCompat<>(); for (int i = 0; i < size; i++) { ItemViewDelegate delegate = itemViewDelegates.valueAt(i); View itemView = createItemView(delegate, parent); int itemViewType = getItemViewType(itemViewDelegates, i); Log.i(TAG, "getAllTyeView: itemViewType = " + itemViewType); viewSparseArrayCompat.put(itemViewType, itemView); } return viewSparseArrayCompat; }
- 第一個方法: createItemView(ItemViewDelegate<T> itemViewDelegate, ViewGroup parent),會根據傳遞的 itemViewDelegate 建立相應的 convertView,並呼叫 onViewHolderCreated() 方法
- 第二個方法:createItemView 會根據傳遞進來的 position 建立相應的 convertView
- 若 convertView 為 null,從佈局中 load 進來
- 若 convertView 不為空,取出來 viewHolder,並重新整理 viewHolder 裡面的 position
最後呼叫 convert 方法去重新整理介面資料。
而這個 convertView 什麼時候為 null,什麼時候不為 null,這個必須要外部呼叫來管理,MultiItemTypeAdapter 管理不了,也不應該管理。
- 第三個方法: getAllTyeView ,這個方法會遍歷所有的 itemViewDelegate 並建立相應的 View 及 ViewHolder
接下來我們來看一下在 MarqueeView 裡面是怎樣實現 convertView 的快取的,標重點了。
MarqueeView
首先我們來看一下 getItemView
private SparseArray<View> mViews; private View getItemView(int index) { int itemViewType = mMultiItemTypeAdapter.getItemViewType(index); // 獲取快取的 convertView View convertView = mViews.get(itemViewType); View itemView = mMultiItemTypeAdapter.createItemView(index, convertView, MarqueeView.this); return itemView; }
從程式碼中可以看出我們是從 mViews 裡面根據當前位置 index 的 itemViewType 取出 convertView 的。那我們的 mViews 是什麼時候賦值的呢?
是在 addAllTypeView 方法中
private void addAllTypeView() { int viewTypeCount = mMultiItemTypeAdapter.getViewTypeCount(); if (viewTypeCount < 1) { return; } mViews.clear(); SparseArrayCompat<View> allTyeView = mMultiItemTypeAdapter.getAllTyeView(MarqueeView.this); int curItemViewType = mMultiItemTypeAdapter.getItemViewType(mPosition); for (int i = 0; i < allTyeView.size(); i++) { int key = allTyeView.keyAt(i); View view = allTyeView.valueAt(i); mViews.put(key, view); LayoutParams layoutParams = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); layoutParams.gravity = mGravity; addView(view, layoutParams); // 設定當前 itemView 可見,其他不可見 if (key == curItemViewType) { view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.INVISIBLE); } } }
在 addAllTypeView 的時候,會呼叫 mMultiItemTypeAdapter.getAllTyeView 初始化所有型別的 itemView,並新增到 mViews 快取,key 為 viewType,value 為 itemView。
MarqueeView 是怎樣與 MultiItemTypeAdapter 建立關聯的
我們來看一下 setAdapter 這個方法:
有一個引數,MultiItemTypeAdapter ,這個 MultiItemTypeAdapter 主要是用來實現 View 的複用以及根據不同的 viewType 新增不同的 View 的。這裡先大概有個印象。下面會講解到。
public void setAdapter(MultiItemTypeAdapter multiItemTypeAdapter) { if (multiItemTypeAdapter == null) { return; } mMultiItemTypeAdapter = multiItemTypeAdapter; start(mInAnimResId, mOutAnimResId); } private void start(final @AnimRes int inAnimResId, final @AnimRes int outAnimResID) { // 第一步:做一些重置的工作,mPosition 終止,清除所有 View,清除動畫; mPosition = 0; clearAnimation(); removeAllViews(); // 第二步:根據 MultiItemTypeAdapter ,把所有型別的 typeView 載入進來,並根據 mPosition 設定可見性 addAllTypeView(); // 第三步:初始化當前 position 的 View,並呼叫 mMultiItemTypeAdapter 的相關方法 int itemViewType = mMultiItemTypeAdapter.getItemViewType(mPosition); View convertView = mViews.get(itemViewType); View itemView = mMultiItemTypeAdapter.createItemView(mPosition, convertView, MarqueeView.this); mCurView = itemView; mLastView = mCurView; // 利用 handle 傳送訊息,執行動畫 post(new Runnable() { @Override public void run() { sendAppear(); } }); }
在 setAdapter 方法中,會先用 start 方法。而在 start 方法中主要做即將事情
-
第一步:做一些重置的工作,mPosition 終止,清除所有 View,清除動畫;
-
第二步:根據 MultiItemTypeAdapter ,把所有型別的 typeView 載入進來,並根據 mPosition 設定可見性
- 第三步:初始化當前 position 的 View,並呼叫 mMultiItemTypeAdapter 的 createItemView 去初始化對應 postion 的 View
int itemViewType = mMultiItemTypeAdapter.getItemViewType(mPosition); View convertView = mViews.get(itemViewType); View itemView = mMultiItemTypeAdapter.createItemView(mPosition, convertView, MarqueeView.this); mCurView = itemView; mLastView = mCurView;
- 第四步:利用 handle 傳送訊息,執行進場動畫
post(new Runnable() { @Override public void run() { sendAppear(); } }); private void sendAppear() { mHandler.removeMessages(APPEAR); if (!isStart) { return; } mHandler.sendEmptyMessageDelayed(APPEAR, 0); }
MarqueeView 是怎樣輪詢執行動畫的
實質是用 hanlde 不斷髮送訊息
接受到 APPEAR 訊息的時候:
首先獲取當前位置的 ItemView,接著執行動畫,執行完動畫之後,mLastView = mCurView; 。接著,判斷當前是否還需要執行 flip 動畫,如果需要的話,會發送併發送延時訊息,告訴下一次執行小時動畫的時間。如果,不需要,則不會發送 DIS_APPEAR 訊息
private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case APPEAR: handleAppearMes(); break; ---- } private void handleAppearMes() { mLastView = mCurView; mCurView = getItemView(mPosition); Animation inAnimation = getInAnimation(); inAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mLastView.setVisibility(View.GONE); mCurView.setVisibility(View.VISIBLE); if (mIFlipListener != null) { mIFlipListener.onFilpStart(mPosition, mCurView); } } @Override public void onAnimationEnd(Animation animation) { mLastView = mCurView; mCurView = getItemView(mPosition); if (mIFlipListener != null) { mIFlipListener.onFilpSelect(mPosition, mCurView); } sendDisappear(); } @Override public void onAnimationRepeat(Animation animation) { } }); mCurView.startAnimation(inAnimation); }
接受到 DIS_APPEAR 訊息的時候:
當執行完動畫的時候,mPosition++; 並檢驗 mPosition 合法性。接著,判斷當前是否還需要執行 flip 動畫,如果需要的話,會發送 APPEAR 訊息。不需要,則不傳送。
case DIS_APPEAR: handleDisappearMes(); break; private void handleDisappearMes() { Animation animation = getOutAnimation(); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mLastView.setVisibility(View.VISIBLE); } @Override public void onAnimationEnd(Animation animation) { mLastView.setVisibility(View.GONE); mPosition++; int count = mMultiItemTypeAdapter.getCount(); if (mPosition >= count) { mPosition = 0; } sendAppear(); } @Override public void onAnimationRepeat(Animation animation) { } }); mLastView.startAnimation(animation); }
OK ,我們回過頭來梳理一下我們的 MarqueeView 是怎樣實現 View 的輪播的?

image
- Handler 接受到 APPEAR 訊息,執行進場動畫之後,根據標誌位isStart 判斷是否還需要 執行 動畫,需要的話,傳送延時的 DIS_APPEAR 訊息
- Handler 接收到 DIS_APPEAR 訊息,執行完退出動畫之後,根據標誌位isStart 判斷是否還需要 執行 動畫,需要的話,傳送延時的 APPEAR 訊息。從而形成一個迴圈。
到此,MarqueeView 的核心原理已講完。
感謝
https://github.com/hongyangAndroid/baseAdapter
參考了鴻洋大佬 baseAdapter 的大部分用法
https://github.com/sunfusheng/MarqueeView
裡面 View 的複用也給了我相應的思路。不過 ViewFliper 無法實現多種 ViewType 的複用,最終捨棄了該方案,採用自定義 FrameLayout 的方式。
關於我
GitHub: gdutxiaoxu
CSDN 部落格:https://blog.csdn.net/gdutxiaoxu
簡書主頁: https://www.jianshu.com/u/ca9b3e19f454
個人微信公眾號:

image
如果覺得效果還不錯,請 star,謝謝。