横向ListView-HorizontalListView

以前刚学习android不久时,参考第三方实现的一个横向listview。它模仿的是scrollView / gallery,用scroller控制滑动操作。它主要实现的功能是横向滑动的listview。它继承自AdapterView,AdapterView又继承自ViewGroup所以这两者公开的(即方法注释上没有@hide)公共方法和监听器它都可用。vertical listview类似,除特别说明的外。

监听器

  • RecyclerListener不可见的itemview放入回收站后回调。
  • OnScrollListener滑动过程中和滑动停止后回调,两者回调不同的方法。

方法

  • addHeaderView(View view) 给listview添加header view,如果header的高度少于item的高度,会存在下面的问题
  • addFooterView(View view) 给listview添加footer view
  • setDivider(Drawable/Color) 设置listview的itemview设置分隔线,不能通过xml文件配置,其它用法和vertical listview一样。(如根据itemview是否可以选中来定是否绘制divider)
  • setDividerWidth(int width) 设置divider的宽度,如果为0,则不绘制divider
  • setSelection(int position) 定位到目标itemview,不带动画效果
  • setSelectionFromTop(int position, int offset) 定位到目标itemview,目标itemview离可见区最左边offset
  • smoothScrollToPosition(int position) 定位到目标itemview,带动画效果
  • setSelector(Drawable/Color) 设置选中项的背景,不能通过xml文件配置。其它一些特点与vertical listview类似。(如果itemview原来有背景,则action up后,会还原成原来的背景,如果没有的话就是用户设置的selector)
  • setHeaderDividersEnabled(boolean) 设置是否给header views绘制divider

回收机制

这是模仿vertical listview的回收机制,将不可见的itemview放入回收站(RecyclBin)以便复用。此版本已经支持itemview定义不同样式布局,但在使用不同样式布局中,不能使用selection或者smoothscroll方法,因为scroller的滑动要设置滑动距离,不同布局时现在还没想到计算要滑动的长度的方法。如果使用多样式布局,在adapter中还要实现以下方法,这是为了让回收站根据类型来做不同处理,复用不同的itemview,模仿vertical listview。

源码

	package com.futurebits.instamessage.free.ui;

	import java.util.LinkedList;
	import java.util.Queue;
	import android.util.Log;
	import android.content.Context;
	import android.database.DataSetObserver;
	import android.graphics.Rect;
	import android.util.AttributeSet;
	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;
    	private int mRightViewIndex = 0;
    	protected int mCurrentX;
    	protected int mNextX;
    	private int mMaxX = Integer.MAX_VALUE;
    	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;
    
    	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);
    		mGesture.setIsLongpressEnabled(true);
    	}
    
    	@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() {
    			Log.d("test_tag", "function: DataSetObserver.onChanged");
    			synchronized (HorizontalListView.this) {
    				mDataChanged = true;
    			}
    			invalidate();
    			requestLayout();
    		}
    
    		@Override
    		public void onInvalidated() {
    			Log.d("test_tag", "function: DataSetObserver.onInvalidated");
    			reset();
    			invalidate();
    			requestLayout();
    		}
    
    	};
    
    	@Override
    	public ListAdapter getAdapter() {
    		return mAdapter;
    	}
    
    	@Override
    	public View getSelectedView() {
    		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) {
    		if (getChildCount() == 0)
    			return;
    		Log.d("test_tag", "function: firstTimeScroll(x=" + String.valueOf(position) + ")");
    		if (mScroller.computeScrollOffset()) {
    			int scrollx = mScroller.getCurrX();
    			mNextX = scrollx;
    		}
    		Log.d("test_tag", "mnextx=" + mNextX);
    		if (mNextX <= 0) {
    			mNextX = 0;
    			mScroller.forceFinished(true);
    		}
    		int childWidth = getChildAt(0).getMeasuredWidth();
    		int curScrollWidth = childWidth * position;
    		int maxScrollWidth = 0;
    		if (curScrollWidth >= mMaxX) {
    			maxScrollWidth = curScrollWidth - getWidth() / 2;
    		} else if ((curScrollWidth + getWidth()) >= mMaxX) {
    			maxScrollWidth = mMaxX - getWidth() / 2;
    		} else {
    			maxScrollWidth = curScrollWidth - getWidth() / 2 + childWidth / 2;
    		}
    		mScroller.startScroll(1, 0, maxScrollWidth, 0);
    		requestLayout();
    	}
    
    	private void addAndMeasureChild(final View child, int viewPos) {
    		Log.d("test_tag", "function: addAndMeasureChild");
    		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);
    		Log.d("test_tag",
    				"function: onLayout(" + String.valueOf(changed) + ", " + String.valueOf(left) + ", " + String.valueOf(top) + ", "
    						+ String.valueOf(right) + ", " + String.valueOf(bottom) + ")");
    
    		if (mAdapter == null) {
    			return;
    		}
    
    		if (mDataChanged) {
    			int oldCurrentX = mCurrentX;
    			initView();
    			removeAllViewsInLayout();
    			mNextX = oldCurrentX;
    			mDataChanged = false;
    		}
    
    		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);
    		}
    		Log.d("test_tag", "mCurrentX=" + mCurrentX + " mNext=" + mNextX);
    		int dx = mCurrentX - mNextX;
    
    		removeNonVisibleItems(dx);
    		fillList(dx);
    		positionItems(dx);
    
    		mCurrentX = mNextX;
    
    		if (!mScroller.isFinished()) {
    			post(new Runnable() {
    				@Override
    				public void run() {
    					requestLayout();
    				}
    			});
    
    		}
    	}
    
    	private void fillList(final int dx) {
    		Log.d("test_tag", "function: fillList(dx=" + String.valueOf(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) {
    		Log.d("test_tag", "function: fillListRight(rightEdge=" + String.valueOf(rightEdge) + ", dx=" + String.valueOf(dx) + ")");
    		int count = mAdapter.getCount();
    		while (rightEdge + dx < getWidth() && mRightViewIndex < count) {
    
    			View child = mAdapter.getView(mRightViewIndex, mRemovedViewQueue.poll(), this);
    			addAndMeasureChild(child, -1);
    			rightEdge += child.getMeasuredWidth();
    			Log.d("test_tag", "mRightViewIndex=" + mRightViewIndex + "   mCurrentX=" + mCurrentX + "  rightEdge=" + rightEdge
    					+ "  width=" + getWidth());
    			if (mRightViewIndex == mAdapter.getCount() - 1) {
    				// mMaxX = mCurrentX + rightEdge - getWidth();
    				int rightMax = mCurrentX + rightEdge - getWidth();
    				int childWidth = getChildAt(0).getMeasuredWidth();
    				int realMax = childWidth * count - getWidth();
    				if (rightMax <= realMax) {
    					mMaxX = realMax + 10;
    				}
    				else {
    					mMaxX = rightMax;
    				}
    				// mMaxX = childWidth * count - getWidth();
    				// mMaxX = rightMax > realMax ? rightMax : realMax;
    				Log.d("test_tag", "childWidth=" + childWidth + "screenWidth=" + getWidth());
    				Log.d("test_tag", "mRightViewIndex=" + mRightViewIndex + "  mMaxX=" + mMaxX);
    			}
    
    			if (mMaxX < 0) {
    				mMaxX = 0;
    			}
    			mRightViewIndex++;
    		}
    
    	}
    
    	private void fillListLeft(int leftEdge, final int dx) {
    		Log.d("test_tag", "function: fillListLeft(leftEdge=" + String.valueOf(leftEdge) + ", dx=" + String.valueOf(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();
    		}
    	}
    
    	private void removeNonVisibleItems(final int dx) {
    		Log.d("test_tag", "function: removeNonVisibleItems(dx=" + String.valueOf(dx) + ")");
    		View child = getChildAt(0);
    		while (child != null && child.getRight() + dx <= 0) {
    			mDisplayOffset += child.getMeasuredWidth();
    			mRemovedViewQueue.offer(child);
    			removeViewInLayout(child);
    			mLeftViewIndex++;
    			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) {
    		Log.d("test_tag", "function: positionItems(dx=" + String.valueOf(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) {
    		Log.d("test_tag", "function: scrollTo(x=" + String.valueOf(x) + ")");
    		mScroller.startScroll(mNextX, 0, x - mNextX, 0);
    		requestLayout();
    	}
    
    	/*
    	 * 第一次显示界面时,滚动到指定位置
    	 */
    	public synchronized void firstTimeScroll(int x) {
    		Log.d("test_tag", "function: firstTimeScroll(x=" + String.valueOf(x) + ")");
    		if (mScroller.computeScrollOffset()) {
    			int scrollx = mScroller.getCurrX();
    			mNextX = scrollx;
    		}
    		Log.d("test_tag", "mnextx=" + mNextX);
    		if (mNextX <= 0) {
    			mNextX = 0;
    			mScroller.forceFinished(true);
    		}
    		mScroller.startScroll(1, 0, x, 0);
    		requestLayout();
    	}
    
    	@Override
    	public boolean dispatchTouchEvent(MotionEvent ev) {
    		Log.d("test_tag", "function: dispatchTouchEvent");
    		boolean handled = super.dispatchTouchEvent(ev);
    		handled |= mGesture.onTouchEvent(ev);
    		return handled;
    	}
    
    	private float mLastMotionX;// 滑动过程中,x方向的初始坐标
    	private float mLastMotionY;// 滑动过程中,y方向的初始坐标
    	private int mTouchSlop;// 手指大小的距离
    	private boolean xMoved = false;
    
    	@Override
    	public boolean onTouchEvent(MotionEvent event) {
    		Log.d("test_tag", "function: onTouchEvent");
    		final int action = event.getAction();
    
    		final float x = event.getX();
    		final float y = event.getY();
    		switch (action) {
    		case MotionEvent.ACTION_DOWN:
    			xMoved = false;
    			mLastMotionX = x;// 初始化每次触摸事件的x方向的初始坐标,即手指按下的x方向坐标
    			mLastMotionY = y;// 初始化每次触摸事件的y方向的初始坐标,即手指按下的y方向坐标
    			break;
    
    		case MotionEvent.ACTION_MOVE:
    			final int deltaX = (int) (mLastMotionX - x);// 每次滑动事件x方向坐标与触摸事件x方向初始坐标的距离
    			final int deltaY = (int) (mLastMotionY - y);// 每次滑动事件y方向坐标与触摸事件y方向初始坐标的距离
    			xMoved = Math.abs(deltaX) > mTouchSlop && Math.abs(deltaY / deltaX) < 1;
    			Log.d("test_tag", "xMoved=" + xMoved + "  mTouchSlop=" + mTouchSlop);
    			// x方向的距离大于手指,并且y方向滑动的距离小于x方向的滑动距离时,Gallery消费掉此次触摸事件
    			if (xMoved) {// Gallery需要消费掉此次触摸事件
    				if (getParent() != null) {
    					getParent().requestDisallowInterceptTouchEvent(true);
    				}
    				return true;// 返回true就不会将此次触摸事件传递给子View了
    			}
    			break;
    		case MotionEvent.ACTION_UP:
    			xMoved = false;
    			if (getParent() != null) {
    				getParent().requestDisallowInterceptTouchEvent(false);
    			}
    			break;
    		}
    		return super.onTouchEvent(event);
    	}
    
    	protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    		Log.d("test_tag", "function: onFling mNextX=" + String.valueOf(mNextX) + ", mMaxX=" + String.valueOf(mMaxX));
    		synchronized (HorizontalListView.this) {
    			mScroller.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0);
    		}
    		requestLayout();
    
    		return true;
    	}
    
    	protected boolean onDown(MotionEvent e) {
    		Log.d("test_tag", "function: onDown");
    		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) {
    			return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
    		}
    
    		@Override
    		public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    
    			Log.d("test_tag",
    					"function: OnGestureListener.onScroll mNextX=" + String.valueOf(mNextX) + ", distanceX="
    							+ String.valueOf(distanceX));
    			synchronized (HorizontalListView.this) {
    				mNextX += (int) distanceX;
    			}
    			requestLayout();
    
    			return true;
    		}
    
    		@Override
    		public boolean onSingleTapConfirmed(MotionEvent e) {
    			Log.d("test_tag", "function: onSingleTapConfirmed");
    			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());
    		}
    	};

	}
文章目录
  1. 1. 监听器
  2. 2. 方法
  3. 3. 回收机制
  4. 4. 源码
|