首先,这个是在github开源项目HorizontalListView上作得修改,这个开源项目的下载地址我忘记了,贴一个引用网址:http://code.eoe.cn/233。
首先来说一下什么叫规格滑动:
上图就是规格滑动的合法状态:恰好显示一定数量的item,并且没有item处于一半显示一半在屏幕外的状态。这样说还不是很清楚,那么再贴一张非法状态:
所谓规格滑动,就是每次滑动结束之后必然停留在合法状态,如果是非法状态,则会自动滑动到最近的合法状态位置上。一次滚动之后ListView要么没有位移,要么位移的距离为item宽度的整数倍,这样说理解了吧。如果滚动完成之后,一个图标有一半以上的距离滑出了屏幕,那么就再滑一点让它彻底移出屏幕,反之,如果这个图标滑出屏幕的距离不足一半宽度,就把它“拉回来”,让它完整显示出来。
下面贴出修改之后的HorizontalListView类代码:
import java.util.LinkedList; import java.util.Queue; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.Scroller; public class HorizontalListView extends AdapterView<ListAdapter> { public boolean mAlwaysOverrideTouch = true; protected ListAdapter mAdapter; private int mLeftViewIndex = -1; // 左边View的下标 private int mRightViewIndex = 0; // 右边View的下标 protected int mCurrentX; // 当前x坐标 protected int mNextX; // 一次移动后x坐标 private int mMaxX = Integer.MAX_VALUE; // x坐标最大值 private int mDisplayOffset = 0; // 偏移量 protected Scroller mScroller; private GestureDetector mGesture; private Queue<View> mRemovedViewQueue = new LinkedList<View>(); private OnItemSelectedListener mOnItemSelected; private OnItemClickListener mOnItemClicked; private OnItemLongClickListener mOnItemLongClicked; private boolean mDataChanged = false; // 获取屏幕宽度 private DisplayMetrics metrics = getResources().getDisplayMetrics(); private int screenWidth = metrics.widthPixels; /** * minItemWidth:表示每个item的最小宽度 * itemCount :一屏能显示的item数量 * itemWidth :每个item的实际宽度 */ private final int minItemWidth = (int) (metrics.density * 70); private int itemCount = screenWidth / minItemWidth; private int itemWidth = (int) (screenWidth * 1.0 / itemCount); public HorizontalListView(Context context, AttributeSet attrs) { super(context, attrs); initView(); } private synchronized void initView() { mLeftViewIndex = -1; mRightViewIndex = 0; mDisplayOffset = 0; mCurrentX = 0; mNextX = 0; mMaxX = Integer.MAX_VALUE; mScroller = new Scroller(getContext()); mGesture = new GestureDetector(getContext(), mOnGesture); } @Override public void setOnItemSelectedListener( AdapterView.OnItemSelectedListener listener) { mOnItemSelected = listener; } @Override public void setOnItemClickListener(AdapterView.OnItemClickListener listener) { mOnItemClicked = listener; } @Override public void setOnItemLongClickListener( AdapterView.OnItemLongClickListener listener) { mOnItemLongClicked = listener; } private DataSetObserver mDataObserver = new DataSetObserver() { @Override public void onChanged() { synchronized (HorizontalListView.this) { mDataChanged = true; } invalidate(); requestLayout(); } @Override public void onInvalidated() { reset(); invalidate(); requestLayout(); } }; @Override public ListAdapter getAdapter() { return mAdapter; } @Override public View getSelectedView() { // TODO: implement return null; } @Override public void setAdapter(ListAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mDataObserver); } mAdapter = adapter; mAdapter.registerDataSetObserver(mDataObserver); reset(); } private synchronized void reset() { initView(); removeAllViewsInLayout(); requestLayout(); } @Override public void setSelection(int position) { // TODO: implement } private void addAndMeasureChild(final View child, int viewPos) { LayoutParams params = child.getLayoutParams(); if (params == null) { params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); } addViewInLayout(child, viewPos, params, true); child.measure( MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST)); } @Override protected synchronized void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mAdapter == null) { return; } if (mDataChanged) { int oldCurrentX = mCurrentX; initView(); removeAllViewsInLayout(); mNextX = oldCurrentX; //在这个if()语句中执行了initView()方法,表示如果adapter中数据变更则重新执行initView(); //initView()方法中重新初始化了mScroller,mScroller.getCurrX()的值将变为0; //但是这里我点击某个图标之后仅仅希望图标的背景改变,不希望listView回到起点,所以要执行下面语句 mScroller.setFinalX(oldCurrentX); // mDataChanged = false; } // computeScrollOffset()方法,如果返回true表示(滚动)动画还没有完成 // 当你想知道当前位置(location)时调用该方法。 if (mScroller.computeScrollOffset()) { int scrollx = mScroller.getCurrX(); mNextX = scrollx; } // 如果滑动位置要超过边界值,则把当前位置设为边界值,并强制终止滑动 if (mNextX <= 0) { mNextX = 0; mScroller.forceFinished(true); } if (mNextX >= mMaxX) { mNextX = mMaxX; mScroller.forceFinished(true); } //dx表示一次滑动的位移 int dx = mCurrentX - mNextX; removeNonVisibleItems(dx); fillList(dx); positionItems(dx); mCurrentX = mNextX; if (!mScroller.isFinished()) { post(new Runnable() { @Override public void run() { requestLayout(); } }); } else if (!isTouched) { isTouched = true; dx = mScroller.getCurrX() % itemWidth; // 从上面dx的计算方法可以看出,dx表示每次滑动结束之后与 if (dx != 0) { if (dx > itemWidth / 2) { dx = dx - itemWidth; } /** * void android.widget.Scroller.startScroll(int startX, int startY, int dx, int dy) * 该方法可以是Scroller从(startX,startY)滚动到(startX+dx,startY+dy),滚动时间为默认的250ms * startX:滚动起始x坐标 * startY:滚动起始y坐标 * dx:滚动x坐标偏移量,正直表示向左滚动 * dy:滚动y坐标偏移量,正直表示向上滚动 */ mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), -dx, 0); post(new Runnable() { @Override public void run() { // TODO Auto-generated method stub requestLayout(); } }); } } } // 填充列表,fillList()传入一次移动的偏移量dx private void fillList(final int dx) { int edge = 0; View child = getChildAt(getChildCount() - 1); if (child != null) { edge = child.getRight(); } fillListRight(edge, dx); edge = 0; child = getChildAt(0); if (child != null) { edge = child.getLeft(); } fillListLeft(edge, dx); } private void fillListRight(int rightEdge, final int dx) { // rightEdge是为了计算mMaxX(最大长度的X坐标) while (rightEdge + dx < getWidth() && mRightViewIndex < mAdapter.getCount()) { // Queue的Object poll()方法:获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。 View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this); addAndMeasureChild(child, -1); rightEdge += child.getMeasuredWidth(); // 如果右边View的下标已经是适配器的总数,则计算出mMaxX if (mRightViewIndex == mAdapter.getCount() - 1) { mMaxX = mCurrentX + rightEdge - getWidth(); } if (mMaxX < 0) { mMaxX = 0; } mRightViewIndex++; } } private void fillListLeft(int leftEdge, final int dx) { while (leftEdge + dx > 0 && mLeftViewIndex >= 0) { View child = mAdapter.getView(mLeftViewIndex, mRemovedViewQueue.poll(), this); addAndMeasureChild(child, 0); leftEdge -= child.getMeasuredWidth(); mLeftViewIndex--; mDisplayOffset -= child.getMeasuredWidth(); } } // 该方法从list两端找到不在屏幕内显示的子View,并作相应处理 private void removeNonVisibleItems(final int dx) { // 从第一个子View开始 View child = getChildAt(0); // 如果子View的右边界+偏移量<0,即该子View没在屏幕显示范围内(屏幕左边界之外) while (child != null && child.getRight() + dx <= 0) { mDisplayOffset += child.getMeasuredWidth(); // Queue接口中的boolean offer(Object e)方法:将指定元素加入此队列的尾部。 // 当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。 // 下面的语句把当前View加入removed组件队列之中。 mRemovedViewQueue.offer(child); // 将当前View从Layout中移除。 removeViewInLayout(child); // 最左边View下标加一。 mLeftViewIndex++; // child移动到下一个View child = getChildAt(0); } // 右边的操作与左边类似 child = getChildAt(getChildCount() - 1); while (child != null && child.getLeft() + dx >= getWidth()) { mRemovedViewQueue.offer(child); removeViewInLayout(child); mRightViewIndex--; child = getChildAt(getChildCount() - 1); } } private void positionItems(final int dx) { if (getChildCount() > 0) { mDisplayOffset += dx; int left = mDisplayOffset; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); int childWidth = child.getMeasuredWidth(); child.layout(left, 0, left + childWidth, child.getMeasuredHeight()); left += childWidth + child.getPaddingRight(); } } } public synchronized void scrollTo(int x) { mScroller.startScroll(mNextX, 0, x - mNextX, 0); requestLayout(); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = super.dispatchTouchEvent(ev); handled |= mGesture.onTouchEvent(ev);return handled; } protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { synchronized (HorizontalListView.this) { mScroller.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0); } requestLayout(); return true; } // 无论是否在滚动,如果有手指按下事件则立即停止滚动 protected boolean onDown(MotionEvent e) { mScroller.forceFinished(true); return true; } private OnGestureListener mOnGesture = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return HorizontalListView.this.onDown(e); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { isTouched = false; return HorizontalListView.this .onFling(e1, e2, velocityX, velocityY); } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { synchronized (HorizontalListView.this) { mNextX += (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)) { if (mOnItemClicked != null) { mOnItemClicked.onItemClick(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); } if (mOnItemSelected != null) { mOnItemSelected.onItemSelected(HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); } 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)) { if (mOnItemLongClicked != null) { mOnItemLongClicked.onItemLongClick( HorizontalListView.this, child, mLeftViewIndex + 1 + i, mAdapter.getItemId(mLeftViewIndex + 1 + i)); } 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()); } }; private boolean isTouched = false; private int downX, upX; private boolean cannotClick = false; @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub int ea = event.getAction(); switch (ea) { case MotionEvent.ACTION_DOWN: isTouched = true; downX = (int) event.getRawX(); if (mScroller.getCurrX() % itemWidth != 0) { Log.v("ACTION_DOWN", "ACTION_DOWN"); cannotClick = true; return true; } break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: Log.v("ACTION_UP", "ACTION_UP"); isTouched = true; upX = (int) event.getRawX(); int dx = (-(upX - downX) + mScroller.getCurrX()) % itemWidth; // dx为正则向右移动,否则向左移动 if (dx != 0) { Log.v("修正", "修正"); if (dx < itemWidth / 2) { // Log.v("ACTION_UP", "dx:"+dx); } else { dx = dx - itemWidth; // Log.v("ACTION_UP", "dx:"+dx); } mScroller.startScroll(mScroller.getCurrX() - (upX - downX), mScroller.getCurrY(), -dx, 0); post(new Runnable() { @Override public void run() { // TODO Auto-generated method stub requestLayout(); } }); return true; } break; default: break; } return super.onTouchEvent(event); } }