第一次寫博文,寫得很差的地方還望各位看客見諒html
爲了學習自定義軟件開發,且定製出知足本身需求的控件(不須要將就地使用第三方源碼),本人花了一週的時間開發了個橫向ListView,寫博客是爲了記錄整個開發過程及思路,也能和各位看客一塊兒學習和探討。java
這一系列文章是針對的讀者是已經瞭解listview緩存和工做原理的android開發人員,若是對listview緩存和工做原理還不瞭解的讀者,能夠查看如下文章:android
《Android研究院之ListView原理學習與優化總結》api
目前橫向ListView的可替代方案有如下三種:緩存
1.HorizontalScrollView——android官方提供ide
2.RecyclerView——android6.0提供的佈局
3.第三方開源控件學習
儘管有衆多的選擇,但感受仍是本身會實現比較酷一些,還有就是,本身的東西能夠隨便改改改改改。優化
本篇文章將介紹橫向ListView的實現基本思路,在接下來的一系列文章中將不斷介紹整個控件的完善思路(包括:實現快速滾動、添加頭/尾視圖、添加滾動條、實現下拉刷新/上拉加載等)。ui
參考文章: 《Android UI開發: 橫向ListView(HorizontalListView)及一個簡單相冊的完整實現》
橫向ListView的基礎邏輯:
1.新建java類,類名:HorizontalListView
2.繼承AdapterView
3.實現setAdapter()和getAdapter()方法(須要爲adapter註冊數據觀察器)
4.實現onTouchEvent()方法響應事件(採用android提供的手勢解析器GestureDetector解析事件)
5.實現onLayout方法,佈局列表項
1).計算當前列表發生滾動的滾動「位移值」,記錄已經發生有效滾動的「位移累加值」
2).根據「位移值」提取須要緩存的視圖(已經滾動到可視區域外的列表項)
3).根據「位移值」設置須要顯示的的列表項
4).根據總體列表「顯示偏移值」整頓全部列表項位置(調用子view的列表項)
5).計算能夠發生滾動的「最大位移值」
先上代碼:
package com.hss.os.horizontallistview.history_version; import android.content.Context; import android.database.DataSetObserver; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ListAdapter; import java.util.LinkedList; import java.util.Queue; /** * 橫向ListView的基礎邏輯 * 1.繼承AdapterView * 2.實現setAdapter()和getAdapter()方法(須要爲adapter註冊數據觀察器) * 3.實現onTouchEvent()方法響應事件(採用android提供的手勢解析器GestureDetector解析事件) * 4.實現onLayout方法,佈局列表項 1).計算當前列表發生滾動的滾動「位移值」,記錄已經發生有效滾動的「位移累加值」 2).根據「位移值」提取須要緩存的視圖(已經滾動到可視區域外的列表項) 3).根據「位移值」設置須要顯示的的列表項 4).根據總體列表「顯示偏移值」整頓全部列表項位置(調用子view的列表項) 5).計算能夠發生滾動的「最大位移值」 * * Created by hss on 2017/7/17. */ public class HorizontalListView1 extends AdapterView<ListAdapter> { private ListAdapter adapter = null; private GestureDetector mGesture; private Queue<View> cacheView = new LinkedList<>();//列表項緩存視圖 private int firstItemIndex = 0;//顯示的第一個子項的下標 private int lastItemIndex = -1;//顯示的最後的一個子項的下標 private int scrollValue=0;//列表已經發生有效滾動的位移值 private int hasToScrollValue=0;//接下來列表發生滾動所要達到的位移值 private int maxScrollValue=Integer.MAX_VALUE;//列表發生滾動所能達到的最大位移值(這個由最後顯示的列表項決定) private int displayOffset=0;//列表顯示的偏移值(用於矯正列表顯示的全部子項的顯示位置) public HorizontalListView1(Context context) { super(context); init(context); } public HorizontalListView1(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public HorizontalListView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public HorizontalListView1(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); } private void init(Context context){ mGesture = new GestureDetector(getContext(), mOnGesture); } private void initParams(){ removeAllViewsInLayout(); if(adapter!=null&&lastItemIndex<adapter.getCount()) hasToScrollValue=scrollValue;//保持顯示位置不變 else hasToScrollValue=0;//滾動到列表頭 scrollValue=0;//列表已經發生有效滾動的位移值 firstItemIndex = 0;//顯示的第一個子項的下標 lastItemIndex = -1;//顯示的最後的一個子項的下標 maxScrollValue=Integer.MAX_VALUE;//列表發生滾動所能達到的最大位移值(這個由最後顯示的列表項決定) displayOffset=0;//列表顯示的偏移值(用於矯正列表顯示的全部子項的顯示位置) requestLayout(); } private DataSetObserver mDataObserver = new DataSetObserver() { @Override public void onChanged() { //執行Adapter數據改變時的邏輯 initParams(); } @Override public void onInvalidated() { //執行Adapter數據失效時的邏輯 initParams(); } }; @Override public ListAdapter getAdapter() { return adapter; } @Override public void setAdapter(ListAdapter adapter) { if(adapter!=null){ adapter.registerDataSetObserver(mDataObserver); } if(this.adapter!=null){ this.adapter.unregisterDataSetObserver(mDataObserver); } this.adapter=adapter; requestLayout(); } @Override public View getSelectedView() { return null; } @Override public void setSelection(int position) { } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); /* 1).計算當前列表發生滾動的滾動「位移值」,記錄已經發生有效滾動的「位移累加值」 2).根據「位移值」提取須要緩存的視圖(已經滾動到可視區域外的列表項) 3).根據「位移值」設置須要顯示的的列表項 4).根據總體列表「顯示偏移值」整頓全部列表項位置(調用子view的列表項) 5).計算能夠發生滾動的「最大位移值」 */ int dx=calculateScrollValue(); removeNonVisibleItems(dx); showListItem(dx); adjustItems(); calculateMaxScrollValue(); } /** * 計算這一次總體滾動偏移量 * @return */ private int calculateScrollValue(){ int dx=0; hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue; hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue; dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; } /** * 計算最大滾動值 */ private void calculateMaxScrollValue(){ if(adapter==null) return; if(lastItemIndex==adapter.getCount()-1) {//已經顯示了最後一項 if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); }else{ maxScrollValue = 0; } } } /** * 根據偏移量提取須要緩存視圖 * @param dx */ private void removeNonVisibleItems(int dx) { if(getChildCount()>0) { //移除列表頭 View child = getChildAt(getChildCount()); while (getChildCount()>0&&child != null && child.getRight() + dx <= 0) { displayOffset += child.getMeasuredWidth(); cacheView.offer(child); removeViewInLayout(child); firstItemIndex++; child = getChildAt(0); } //移除列表尾 child = getChildAt(getChildCount()-1); while (getChildCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) { cacheView.offer(child); removeViewInLayout(child); lastItemIndex--; child = getChildAt(getChildCount()-1); } } } /** * 根據偏移量顯示新的列表項 * @param dx */ private void showListItem(int dx) { if(adapter==null)return; int firstItemEdge = getFirstItemLeftEdge()+dx; int lastItemEdge = getLastItemRightEdge()+dx; displayOffset+=dx;//計算偏移量 //顯示列表頭視圖 while(firstItemEdge > getPaddingLeft() && firstItemIndex-1 >= 0) { firstItemIndex--;//往前顯示一個列表項 View child = adapter.getView(firstItemIndex, cacheView.poll(), this); addAndMeasureChild(child, 0); firstItemEdge -= child.getMeasuredWidth(); displayOffset -= child.getMeasuredWidth(); } //顯示列表未視圖 while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) { lastItemIndex++;//日後顯示一個列表項 View child = adapter.getView(lastItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getChildCount()); lastItemEdge += child.getMeasuredWidth(); } } /** * 調整各個item的位置 */ private void adjustItems() { if(getChildCount() > 0){ int left = displayOffset+getPaddingLeft(); int endIndex = getChildCount()-1; for(int i=0;i<=endIndex;i++){ View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); child.layout(left, getPaddingTop(), left + childWidth, child.getMeasuredHeight()+getPaddingTop()); left += childWidth + child.getPaddingRight(); } } } /** * 取得視圖可見區域的右邊界 * @return */ private int getShowEndEdge(){ return getWidth()-getPaddingRight(); } private int getFirstItemLeftEdge(){ if(getChildCount()>0) { return getChildAt(0).getLeft(); }else{ return 0; } } private int getLastItemRightEdge(){ if(getChildCount()>0) { return getChildAt(getChildCount()-1).getRight(); }else{ return 0; } } private void addAndMeasureChild(View child, int viewIndex) { LayoutParams params = child.getLayoutParams(); params = params==null ? new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT):params; addViewInLayout(child, viewIndex, params, true); child.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.UNSPECIFIED)); } /** * 在onTouchEvent處理事件,讓子視圖優先消費事件 * @param event * @return */ @Override public boolean onTouchEvent(MotionEvent event) { return mGesture.onTouchEvent(event); } private GestureDetector.OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { synchronized(HorizontalListView1.this){ hasToScrollValue += (int)distanceX; } requestLayout(); return true; } }; }
如下是具體實現解析:
第1-3步是總體實現的準備工做,比較簡單,這裏就不作講解
4.實現onTouchEvent()方法響應事件(採用android提供的手勢解析器GestureDetector解析事件)
處理觸摸事件的方法有三個(如下說法針對當前使用的GestureDetector實現):
1.dispatchTouchEvent() —— 若是在這裏處理,子視圖和當前視圖能夠同時響應事件
2.onInterceptTouchEvent() —— 若是在這裏處理,子視圖沒法響應事件
3.onTouchEvent() —— 優先子視圖響應事件
以上三個方法涉及到事件分發機制,若是對這方面不是很懂也想學習的,可參考如下文章:
《《Android深刻透析》之Android事件分發機制 》
在實現GestureDetector.OnGestureListener時,必需實現onDown()和onScroll()兩個方法
onScroll()方法用於獲取用戶的滾動行爲所產生的滾動值
onDown()方法必須實現且返回值必須是true,不然onScroll()方法沒法執行,具體緣由還未深究
5.實現onLayout方法,佈局列表項
1).計算當前列表發生滾動的滾動「位移值」,記錄已經發生有效滾動的「位移累加值」
private int calculateScrollValue(){ int dx=0; hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue; hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue; dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; }
在這裏採用了三個變量:
private int scrollValue=0;//列表已經發生有效滾動的位移值
private int hasToScrollValue=0;//接下來列表發生滾動所要達到的位移值
private int maxScrollValue=Integer.MAX_VALUE;//列表發生滾動所能達到的最大位移值(這個由最後顯示的列表項決定)
在這個時候就有個問題,爲何要採用這三個變量而不是直接使用用戶滾動行爲所產生的偏移值(onScroll()方法中的distanceX);直接使用distanceX去計算也是能夠實現咱們所須要的功能的,不過這樣處理起來,各部分的邏輯代碼耦合度就會很高,沒法切分出各個步驟,這個對於代碼的維護工做帶來很大的不便,代碼的可讀性也很差,邏輯也不夠清晰,採用這三個變量能很好的解決以上問題(這個思路是借用別人的,具體是誰最初想到的,我也不清楚,不過挺佩服的)
2).根據「位移值」提取須要緩存的視圖(已經滾動到可視區域外的列表項)
/** * 根據偏移量提取須要緩存視圖 * @param dx */ private void removeNonVisibleItems(int dx) { if(getChildCount()>0) { //移除列表頭 View child = getChildAt(getChildCount()); while (getChildCount()>0&&child != null && child.getRight() + dx <= 0) { displayOffset += child.getMeasuredWidth(); cacheView.offer(child); removeViewInLayout(child); firstItemIndex++; child = getChildAt(0); } //移除列表尾 child = getChildAt(getChildCount()-1); while (getChildCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) { cacheView.offer(child); removeViewInLayout(child); lastItemIndex--; child = getChildAt(getChildCount()-1); } } }
這一步是在列表發生滾動以後根據發生滾動的位移值dx計算滾動後第一個和最後一個列表項是否已經滾動到不可見的區域(注意:可見的區域寬度 =(控件的寬度 - 左padding - 右padding))
3).根據「位移值」設置須要顯示的的列表項
/** * 根據偏移量顯示新的列表項 * @param dx */ private void showListItem(int dx) { if(adapter==null)return; int firstItemEdge = getFirstItemLeftEdge()+dx; int lastItemEdge = getLastItemRightEdge()+dx; displayOffset+=dx;//計算偏移量 //顯示列表頭視圖 while(firstItemEdge > getPaddingLeft() && firstItemIndex-1 >= 0) { firstItemIndex--;//往前顯示一個列表項 View child = adapter.getView(firstItemIndex, cacheView.poll(), this); addAndMeasureChild(child, 0); firstItemEdge -= child.getMeasuredWidth(); displayOffset -= child.getMeasuredWidth(); } //顯示列表未視圖 while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) { lastItemIndex++;//日後顯示一個列表項 View child = adapter.getView(lastItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getChildCount()); lastItemEdge += child.getMeasuredWidth(); } }
這一步根據列表滾動的「位移值dx」計算是否須要在列表中添加新的item View,若是列表在移動的過程當中,第一個顯示的item View的左邊界出如今總體視圖可見區域的左邊界內即(firstItemEdge > getPaddingLeft() ),則在列表頭添加一個新的item View,同時記錄下整個列表顯示的左邊偏移值(displayOffset -= child.getMeasuredWidth(); ),該值十分重要,是體現整個列表顯示狀態的值;若是最後一個顯示的item View的右邊界出如今總體視圖可見區域的右邊界內即(lastItemEdge < getShowEndEdge() ) ,則在列表尾添加一個新的item View;第一次顯示列表時,是以追加的方式顯示item View的
注意:
1.代碼中採用while() {}循環操做而不是採用if()直接判斷是爲了代碼邏輯的嚴密性,實際上這裏採用if()進行判斷操做效果是同樣的,可這樣作整個代碼的邏輯就不夠嚴密,可能在之後的擴展中留下隱患(bug),在removeNonVisibleItems(int dx)方法中的while操做也是基於以上考慮
2.firstItemEdge 和lastItemEdge 的值採用如下方法計算,不只是爲了加強代碼的可讀性,更是爲了日後的擴展作準備
private int getFirstItemLeftEdge(){ if(getChildCount()>0) { return getChildAt(0).getLeft(); }else{ return 0; } } private int getLastItemRightEdge(){ if(getChildCount()>0) { return getChildAt(getChildCount()-1).getRight(); }else{ return 0; } }
4).根據總體列表「顯示偏移值」整頓全部列表項位置(調用子view的列表項)
/** * 調整各個item的位置 */ private void adjustItems() { if(getChildCount() > 0){ int left = displayOffset+getPaddingLeft(); int top = getPaddingTop(); int endIndex = getChildCount()-1; int childWidth,childHeight; for(int i=0;i<=endIndex;i++){ View child = getChildAt(i); childWidth = child.getMeasuredWidth(); childHeight = child.getMeasuredHeight(); child.layout(left, top, left + childWidth, top + childHeight); left += childWidth; } } }
在這裏是對視圖項進行正確的佈局排列,把各個列表項安放到合適的位置上;這個列表如何顯示,整體依賴displayOffset這個值;值得注意的是,child.layout()中的right和bottom的值須要在寬和高的基礎上分別加上left和top的值,不然整個item View沒法徹底顯示。
5).計算能夠發生滾動的「最大位移值」
/** * 計算最大滾動值 */ private void calculateMaxScrollValue(){ if(adapter==null) return; if(lastItemIndex==adapter.getCount()-1) {//已經顯示了最後一項 if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); }else{ maxScrollValue = 0; } } }
當列表滾動到最後一個列表項時,則可計算整個列表可滾動最大值;scrollValue 表示已經發生滾動的距離,getChildAt(getChildCount() - 1).getRight() - getShowEndEdge()表示還能夠發生滾動的距離,也表示最後一個列表項(item View)未顯示出來的部分;若是顯示項過少而沒法鋪滿整個控件,最大滾動位移值爲0,即maxScrollValue = 0;