首先,这个是在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 stubint 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);
}}
android 横向list特效——规格滑动