在讀本文以前,請先閱讀博文《橫向ListView(一) ——開篇,基礎邏輯實現》java
以前的文章已經介紹了橫向lListView的基礎實現邏輯,在這裏我將介紹快速滾動實現及item相關事件實現android
列表的快速滾動的實現主要依賴於android官方提供的android.widget.Scroller類,具體實現須要如下步驟:api
1.捕獲快速滑動事件,並啓動快速滑動計算(Scroller的功能)緩存
2.使用Scroller計算一次發生滾動的位移值,刷新視圖ide
3.若是總體滑動還未中止(即Scroller的滾動計算還未結束),則重複執行步驟2佈局
4.捕獲按下事件,實現當用戶按下時中止自動快速滾動操做post
如對Scroller的工做原理不瞭解的,能夠參考如下文章:ui
《Android Scroller徹底解析,關於Scroller你所需知道的一切》this
《ndroid 帶你從源碼的角度解析Scroller的滾動實現原理》spa
對於ListView,item須要響應的事件比較重要的就兩個,點擊和長按,具體實現以下:
1.點擊事件:在OnGestureListener中添加public boolean onSingleTapConfirmed(MotionEvent e)方法的實現,以響應點擊事件
2.長按事件:在OnGestureListener中添加public void onLongPress(MotionEvent e) 方法的實現,以響應長按事件
先上完整代碼:
package com.hss.os.horizontallistview.history_version; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.AttributeSet; import android.util.Log; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.Scroller; import java.util.LinkedList; import java.util.Queue; /** * 爲橫向ListView添加快速滾動功能及item相關事件實現 * * Created by sxyx on 2017/8/8. */ public class HorizontalListView2 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;//列表顯示的偏移值(用於矯正列表顯示的全部子項的顯示位置) private Scroller mScroller; private int firstItemLeftEdge=0;//第一個子項的左邊界 private int lastItemRightEdge=0;//最後一個子項的右邊界 public HorizontalListView2(Context context) { super(context); init(context); } public HorizontalListView2(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public HorizontalListView2(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public HorizontalListView2(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); mScroller=new Scroller(context); } private void initParams(){ mScroller.forceFinished(true);//避免在滑動過程當中變換視圖內容時,出現列表沒法滾動的狀況 removeAllViewsInLayout(); if(adapter!=null&&lastItemIndex<adapter.getCount()) hasToScrollValue=scrollValue;//保持顯示位置不變 else hasToScrollValue=0;//滾動到列表頭 scrollValue=0;//列表已經發生有效滾動的位移值 firstItemIndex = 0;//顯示的第一個子項的下標 lastItemIndex = -1;//顯示的最後的一個子項的下標 maxScrollValue=Integer.MAX_VALUE;//列表發生滾動所能達到的最大位移值(這個由最後顯示的列表項決定) displayOffset=0;//列表顯示的偏移值(用於矯正列表顯示的全部子項的顯示位置) firstItemLeftEdge=0;//第一個子項的左邊界 lastItemRightEdge=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 i) { } 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)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); Log.e("","============>>>>>left:"+left+" top:"+top+" right:"+right+" bottom:"+bottom); //須要先佈局列表項再根據餘下的空間佈局列表頭尾 //佈局列表項 /* 1.計算這一次總體滾動偏移量 2.根據偏移量提取須要緩存視圖 3.根據偏移量顯示新的列表項 4.根據總體偏移值整頓全部列表項位置 5.計算最大滾動位移值,記錄已經發生有效滾動的位移值 6.根據顯示的最終效果,判斷是否要居中顯示 */ int dx=calculateScrollValue(); removeNonVisibleItems(dx); showListItem(dx); adjustItems(); calculateMaxScrollValue(); //繼續滾動 if(!mScroller.isFinished()){ post(new Runnable(){ @Override public void run() { requestLayout(); } }); } } /** * 計算這一次總體滾動偏移量 * @return */ private int calculateScrollValue(){ int dx=0; if(mScroller.computeScrollOffset()){ hasToScrollValue = mScroller.getCurrX(); } if(hasToScrollValue<=0){ hasToScrollValue=0; mScroller.forceFinished(true); } if(hasToScrollValue >= maxScrollValue) { hasToScrollValue = maxScrollValue; mScroller.forceFinished(true); } dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; } /** * 計算最大滾動值 */ private void calculateMaxScrollValue(){ if(getListItemCount()>0) { if(lastItemIndex==adapter.getCount()-1) {//已經顯示了最後一項 if(getChildAt(getChildCount() - 1).getRight()>=getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); }else{ maxScrollValue=0; } } }else{ if(adapter!=null&&adapter.getCount()>0){ }else { if (getChildCount() > 0 && getChildAt(getChildCount() - 1).getRight() >= getShowEndEdge()) { maxScrollValue = scrollValue + getChildAt(getChildCount() - 1).getRight() - getShowEndEdge(); } else { maxScrollValue = 0; } } } } /** * 根據偏移量提取須要緩存視圖 * @param dx */ private void removeNonVisibleItems(int dx) { if(getListItemCount()>0) { //移除列表頭 View child = getChildAt(getStartItemIndex()); while (getListItemCount()>0&&child != null && child.getRight() + dx <= getShowStartEdge()) { displayOffset += child.getMeasuredWidth(); cacheView.offer(child); removeViewInLayout(child); firstItemIndex++; child = getChildAt(getStartItemIndex()); } //移除列表尾 child = getChildAt(getEndItemIndex()); while (getListItemCount()>0&&child != null && child.getLeft() + dx >= getShowEndEdge()) { cacheView.offer(child); removeViewInLayout(child); lastItemIndex--; child = getChildAt(getEndItemIndex()); } } } /** * 根據偏移量顯示新的列表項 * @param dx */ private void showListItem(int dx) { if(adapter==null)return; int firstItemEdge = getFirstItemLeftEdge()+dx; int lastItemEdge = getLastItemRightEdge()+dx; displayOffset+=dx;//計算偏移量 //顯示列表頭視圖 while(firstItemEdge > getShowStartEdge() && firstItemIndex-1 >= 0) { firstItemIndex--;//往前顯示一個列表項 View child = adapter.getView(firstItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getStartItemIndex()); firstItemEdge -= child.getMeasuredWidth(); displayOffset -= child.getMeasuredWidth(); } //顯示列表未視圖 while(lastItemEdge < getShowEndEdge() && lastItemIndex+1 < adapter.getCount()) { lastItemIndex++;//日後顯示一個列表項 View child = adapter.getView(lastItemIndex, cacheView.poll(), this); addAndMeasureChild(child, getEndItemIndex()+1); lastItemEdge += child.getMeasuredWidth(); } } /** * 調整各個item的位置 */ private void adjustItems() { if(getListItemCount() > 0){ int left = displayOffset+getShowStartEdge(); int top = getPaddingTop(); int endIndex = getEndItemIndex(); 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; } firstItemLeftEdge=getChildAt(getStartItemIndex()).getLeft(); lastItemRightEdge=getChildAt(getEndItemIndex()).getRight(); } } //如下八個方法爲概念性封裝方法,有助於日後的擴展和維護 /** * 得到列表視圖中item View的總數 * @return */ private int getListItemCount(){ int itemCount=getChildCount(); return itemCount; } /** * 得到列表視圖中第一個item View下標 * @return */ private int getStartItemIndex(){ return 0; } /** * 得到列表視圖中最後一個item View下標 * @return */ private int getEndItemIndex(){ return getChildCount()-1; } /** * 得到列表視圖中第一個item View左邊界值 * @return */ private int getFirstItemLeftEdge(){ if(getListItemCount()>0) { return firstItemLeftEdge; }else{ return 0; } } /** * 得到列表視圖中最後一個item View右邊界值 * @return */ private int getLastItemRightEdge(){ if(getListItemCount()>0) { return lastItemRightEdge; }else{ return 0; } } /** * 取得視圖可見區域的左邊界 * @return */ private int getShowStartEdge(){ return getPaddingLeft(); } /** * 取得視圖可見區域的右邊界 * @return */ private int getShowEndEdge(){ return getWidth()-getPaddingRight(); } /** * 取得視圖可見區域的寬度 * @return */ private int getShowWidth(){ return getWidth()-getPaddingLeft()-getPaddingRight(); } /** * 在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) { mScroller.forceFinished(true);//點擊時中止滾動 return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(hasToScrollValue, 0, (int)-velocityX, 0, 0, maxScrollValue, 0, 0); requestLayout(); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { synchronized(HorizontalListView2.this){ hasToScrollValue += (int)distanceX; } requestLayout(); return true; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); if (isEventWithinView(e, child)) { int position=firstItemIndex + i; if (getOnItemClickListener() != null) { getOnItemClickListener().onItemClick(HorizontalListView2.this, child, position, adapter.getItemId(position)); } if (getOnItemSelectedListener() != null) { getOnItemSelectedListener().onItemSelected(HorizontalListView2.this, child, position, adapter.getItemId(position)); } break; } } return true; } @Override public void onLongPress(MotionEvent e) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (isEventWithinView(e, child)) { int position=firstItemIndex + i; if (getOnItemLongClickListener() != null) { getOnItemLongClickListener().onItemLongClick(HorizontalListView2.this, child, position, adapter.getItemId(position)); } break; } } } private boolean isEventWithinView(MotionEvent e, View child) { Rect viewRect = new Rect(); int[] childPosition = new int[2]; child.getLocationOnScreen(childPosition); int left = childPosition[0]; int right = left + child.getWidth(); int top = childPosition[1]; int bottom = top + child.getHeight(); viewRect.set(left, top, right, bottom); return viewRect.contains((int) e.getRawX(), (int) e.getRawY()); } }; public synchronized void scrollTo(int x) { mScroller.startScroll(hasToScrollValue, 0, x - hasToScrollValue, 0); requestLayout(); } }
列表的快速滾動的實現主要依賴於android官方提供的android.widget.Scroller類,具體實現須要如下步驟:
1.捕獲快速滑動事件,並啓動快速滑動計算(Scroller的功能)
@Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mScroller.fling(hasToScrollValue, 0, (int)-velocityX, 0, 0, maxScrollValue, 0, 0); requestLayout(); return true; }
在OnGestureListener中添加public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)方法,以捕獲快速滑動事件,調用Scroller.fling()啓動快速滑動計算,而後調用requestLayout()要求從新佈局界面,已實現快速滑動效果。
2.使用Scroller計算一次發生滾動的位移值,刷新視圖
在未實現快速滑動以前calculateScrollValue()方法實現以下:
/** * 計算這一次總體滾動偏移量 * @return */ private int calculateScrollValue(){ int dx=0; hasToScrollValue=hasToScrollValue<0? 0:hasToScrollValue; hasToScrollValue=hasToScrollValue>maxScrollValue? maxScrollValue:hasToScrollValue; dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; }
如下代碼是爲了實現快速滑動而作出的修改:
/** * 計算這一次總體滾動偏移量 * @return */ private int calculateScrollValue(){ int dx=0; if(mScroller.computeScrollOffset()){ hasToScrollValue = mScroller.getCurrX(); } if(hasToScrollValue<=0){ hasToScrollValue=0; mScroller.forceFinished(true); } if(hasToScrollValue >= maxScrollValue) { hasToScrollValue = maxScrollValue; mScroller.forceFinished(true); } dx=hasToScrollValue-scrollValue; scrollValue=hasToScrollValue; return -dx; }
主要是調用Scroller計算須要發生滾動的位移值,以及在滾動到邊界上的時候,讓Scroller中止計算
3.若是總體滑動還未中止(即Scroller的滾動計算還未結束),則重複執行步驟2
這一步只須要在protected void onLayout(boolean changed, int left, int top, int right, int bottom)方法中添加如下代碼便可
//繼續滾動 if(!mScroller.isFinished()){ post(new Runnable(){ @Override public void run() { requestLayout(); } }); }
Scroller的總體滾動計算還未完成則調用 requestLayout()不斷重複刷新界面,直到總體滾動完成
4.捕獲按下事件,實現當用戶按下時中止自動快速滾動操做
@Override public boolean onDown(MotionEvent e) { mScroller.forceFinished(true);//點擊時中止滾動 return true; }
在OnGestureListener的public boolean onDown(MotionEvent e) 方法中添加mScroller.forceFinished(true); 用於告訴Scroller總體滾動計算須要中止,藉此中止整個界面的滑動
對於ListView,item須要響應的事件比較重要的就兩個,點擊和長按,具體實現以下:
1.點擊事件:在OnGestureListener中添加public boolean onSingleTapConfirmed(MotionEvent e)方法的實現,以響應點擊事件
@Override public boolean onSingleTapConfirmed(MotionEvent e) { for(int i=0;i<getChildCount();i++){ View child = getChildAt(i); if (isEventWithinView(e, child)) { int position=firstItemIndex + i; if (getOnItemClickListener() != null) { getOnItemClickListener().onItemClick(HorizontalListView2.this, child, position, adapter.getItemId(position)); } if (getOnItemSelectedListener() != null) { getOnItemSelectedListener().onItemSelected(HorizontalListView2.this, child, position, adapter.getItemId(position)); } break; } } return true; }
2.長按事件:在OnGestureListener中添加public void onLongPress(MotionEvent e) 方法的實現,以響應長按事件
@Override public void onLongPress(MotionEvent e) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (isEventWithinView(e, child)) { int position=firstItemIndex + i; if (getOnItemLongClickListener() != null) { getOnItemLongClickListener().onItemLongClick(HorizontalListView2.this, child, position, adapter.getItemId(position)); } break; } } }