首先,这个是在github开源项目HorizontalListView上作得修改,这个开源项目的下载地址我忘记了,贴一个引用网址:http://code.eoe.cn/233。
首先来说一下什么叫规格滑动:
上图就是规格滑动的合法状态:恰好显示一定数量的item,并且没有item处于一半显示一半在屏幕外的状态。这样说还不是很清楚,那么再贴一张非法状态:
所谓规格滑动,就是每次滑动结束之后必然停留在合法状态,如果是非法状态,则会自动滑动到最近的合法状态位置上。一次滚动之后ListView要么没有位移,要么位移的距离为item宽度的整数倍,这样说理解了吧。如果滚动完成之后,一个图标有一半以上的距离滑出了屏幕,那么就再滑一点让它彻底移出屏幕,反之,如果这个图标滑出屏幕的距离不足一半宽度,就把它“拉回来”,让它完整显示出来。
下面贴出修改之后的HorizontalListView类代码:
package com.example.test3;
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.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 Queue<View> mRemovedLeftViewQueue = new LinkedList<View>();
private Queue<View> mRemovedRightViewQueue = 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的实际宽度
*/
public static final int MIN_WIDTH_IN_DP = 70;
private final int minItemWidth = (int) (metrics.density * MIN_WIDTH_IN_DP);
private final int itemCount = screenWidth / minItemWidth;
private final 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();
}
};
public boolean isOnDown = false;
@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.MATCH_PARENT,
LayoutParams.MATCH_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;
// by zhangchi
//在这个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();
}
});
}
/**
* isTouched是一个boolean变量,初始化默认为false。
* 在onTouch的ACTION_DOWN和ACTION_UP时会赋值为true
* 在onFling方法中会赋值为false。
* 也就是说,滑动分为两种:
* 1.手指迅速滑动后离开屏幕,会触发onFling方法,系统通过计算自动完成剩下的滑动,此时isTouched为false;
* 2.手指一直在屏幕滑动,滑到指定位置后才松开手指,不再触发onFling方法,此时isTouched为true;
* 下面的代码可以使情况1自动到达最近的合法位置
*/
else if (!isTouched) {
isTouched = true;
//下面几行计算dx:当前位置与最近合法位置之间的偏移量
dx = mScroller.getCurrX() % itemWidth;
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。
/**
* 下面这个mAdapter正是setAdapter传递进来的适配器,getView方法自然是自定义adapter时自己写的方法。
* 原程序只用了一个缓存队列,但我使用时不知道为什么会有图片错乱的情况,所以此处我增加了一个缓存队列,
* 两个队列分别缓存左右两边移除的view
*/
View child = mAdapter.getView(mRightViewIndex,
mRemovedRightViewQueue.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,
mRemovedLeftViewQueue.poll(), this);
addAndMeasureChild(child, 0);
leftEdge -= child.getMeasuredWidth();
mLeftViewIndex--;
mDisplayOffset -= child.getMeasuredWidth();
}
}
// 该方法从list两端找到不在屏幕内显示的子View,并作相应处理
private void removeNonVisibleItems(final int dx) {
// 从第一个子View开始,getChild()方法找到的是可见的第一个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组件队列之中。
mRemovedLeftViewQueue.offer(child);
// 将当前View从Layout中移除。
removeViewInLayout(child);
// 最左边View下标加一。
mLeftViewIndex++;
// child移动到下一个View
child = getChildAt(0);
}
// 右边的操作与左边类似
child = getChildAt(getChildCount() - 1);
while (child != null && child.getLeft() + dx >= getWidth()) {
mRemovedRightViewQueue.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) {
//如果滑动没有结束,把isOnDown设为true,在onItemClickListener中取出isOnDown做判断:
//1.如果isOnDown为true,则点击事件作用为停止滑动,不能响应onItemClick事件
//2.如果isOnDown为false,则点击之前滑动已经停止,可以响应onItemClick事件
isOnDown = !mScroller.isFinished();
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; //downX-upX计算手指滑动的距离
@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) {
return true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
upX = (int) event.getRawX();
int dx = (-(upX - downX) + mScroller.getCurrX()) % itemWidth;
// dx不为0表示当前不处于合法位置
if (dx != 0) {
if (dx > itemWidth / 2) {
dx = dx - itemWidth;
}
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);
}
}