天天看點

Android自定義垂直滾動自動選擇日期控件

------------------本部落格如未明正聲明轉載,皆為原創,轉載請注明出處!------------------        

      項目中需要一個日期選擇控件,該日期選擇控件是垂直滾動,停止滾動時需要校正日期數字位置,自動選擇離中心位置最近的數字。效果如下:

Android自定義垂直滾動自動選擇日期控件

     利用繼承LinearLayout實作,模仿Android帶資料的控件的一般做法,加入擴充卡接口,選擇事件監聽接口,另外簡單實作了子View的緩存,對應這樣簡單應用的情況下,應該是可以的,本人隻用過TextView來做子控件,其他适配尚未測試,不知道效果如何。可能有其他的應用場景,分享給各位,可以修改或應用于你自己的項目。

下面貼代碼,有點長O(∩_∩)O~:

/**
 * 内容垂直滾動的一個控件,内容子項項垂直滾動後,将自動把目前離中心最近的項復原至中心位置。
 * 可根據{@link #getSelectedItem()} 擷取目前選擇的項,使用{@link #setSelection(int)}
 * 設定目前選擇項,另外可添加選中事件監聽器 {@link #setOnItemSelectedListener(OnItemSelectedListener)}
 * 
 * This component may contains several sub items, and the items are able to
 * scroll up and down, by once released the scroll bar, the closest item will be 
 * rolled back to the center of component. use {@link #getSelectedItem()} to 
 * get the current selected item, use {@link #setSelection(int)} to set selected item,
 * and you can always use {@link #setOnItemSelectedListener(OnItemSelectedListener)}
 * to do something after the item is selected.
 * 
 * @date 2013/09/26
 * @author Wison
 *
 */
public class VerticalScrollAutoSelector extends LinearLayout {

	private ScrollView mContentScrollView;
	private OnItemSelectedListener mOnItemSelectedListener;
	
	private AutoSelectorAdapter mAdapter;
	private LinearLayout mItemsContainer;
	private ViewGroup.LayoutParams mItemLayoutParams;
	private TextView mStartBlankView;
	private TextView mEndBlankView;
	private List<View> mCachedSubViewList = new ArrayList<View>();
	
	private int[] mItemViewsScrollYArr;
	private Point mTouchedPoint = new Point();
	private ScrollPointChecker mScrollPointChecker;
	private int mSelectedPosition = -1;
	
	public VerticalScrollAutoSelector(Context context) {
		this(context, null);
	}

	@SuppressWarnings("deprecation")
	public VerticalScrollAutoSelector(Context context, AttributeSet attrs) {
		super(context, attrs);
		
		mContentScrollView = new ScrollView(context);
		LinearLayout.LayoutParams linearLP = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
		mContentScrollView.setLayoutParams(linearLP);
		mContentScrollView.setVerticalScrollBarEnabled(false);
		addView(mContentScrollView);
		
		mStartBlankView = new TextView(context);
		mEndBlankView = new TextView(context);
		mItemLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
		mItemsContainer = new LinearLayout(context);
		mItemsContainer.setOrientation(LinearLayout.VERTICAL);
		mItemsContainer.setGravity(Gravity.CENTER);
		mItemsContainer.setLayoutParams(mItemLayoutParams);
		mContentScrollView.addView(mItemsContainer);
		mContentScrollView.setOnTouchListener(new ScrollViewOnTouchListener());
	}
	
    /**
     * Register a callback to be invoked when an item in this VerticalScrollAutoSelector has been selected.
     * @param listener The callback that will run
     */
	public void setOnItemSelectedListener(OnItemSelectedListener listener) {
		mOnItemSelectedListener = listener;
	}
	
	/**
	 * Sets the data behind this VerticalScrollAutoSelector.
	 * @param adapter
	 */
	public void setAdapter(AutoSelectorAdapter adapter) {
		mAdapter = adapter;
		mSelectedPosition = -1;
		mItemViewsScrollYArr = null;
		mItemsContainer.removeAllViews();
		if (mAdapter == null || mAdapter.getCount() <= 0) {
			return;
		}
		
		if (getHeight() == 0) {
			// Waiting for component initialization finished
			postDelayed(new Thread() {
				@Override
				public void run() {
					attachAdapter();
				}
			}, 1);
		} else {
			attachAdapter();
		}
	}
	
	private void attachAdapter() {
		if (getHeight() == 0) {
			// try again!
			setAdapter(mAdapter);
			return;
		}
		
		final int itemCount = mAdapter.getCount();
		int itemGroup = mAdapter.getItemsCountPerGroup();
		if (itemGroup < 3) {
			itemGroup = 3;
		}

		final float height = getHeight();
		
		final int itemHeight = (int) (height / itemGroup);
		int additionHeight = (int) (height - itemHeight * itemGroup);
		
		int itemPosition = 0;
		final int totalItems = itemCount + 2;
		
		for (int i = 0; i < totalItems; i++) {
			
			if (i == 0 || i == totalItems - 1) {
				TextView tv = (i == 0 ? mStartBlankView : mEndBlankView);
				mItemLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
				mItemLayoutParams.height = (--additionHeight >= 0) ? itemHeight + 1 : itemHeight;
				tv.setLayoutParams(mItemLayoutParams);
				mItemsContainer.addView(tv);
			} else {
				View convertView = null;
				boolean isCached = true;
				if (itemPosition < mCachedSubViewList.size()) {
					convertView = mCachedSubViewList.get(itemPosition);
				} else {
					isCached = false;
				}
				
				View view = mAdapter.getView(itemPosition, convertView, this);
				view.setId(mAdapter.getItemId(itemPosition));
				mItemLayoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
				mItemLayoutParams.height = (--additionHeight >= 0) ? itemHeight + 1 : itemHeight;
				view.setLayoutParams(mItemLayoutParams);
				mItemsContainer.addView(view);
				
				if (!isCached) {
					mCachedSubViewList.add(itemPosition, view);
				} else {
					if (view != convertView) {
						mCachedSubViewList.remove(itemPosition);
						mCachedSubViewList.add(itemPosition, view);
					}
				}
				itemPosition++;
			}
		}
	} 
	
	
	/**
	 * Returns the adapter currently in use in this VerticalScrollAutoSelector.
	 * @return
	 */
	public AutoSelectorAdapter getAdapter() {
		return mAdapter;
	}
	
	/**
	 * Get the selected item.
	 * @return
	 */
	public Object getSelectedItem() {
		if (mAdapter == null || mSelectedPosition < 0) return null; 
		return mAdapter.getItem(mSelectedPosition);
	}
	
	/**
	 * Get the position of currently selected item.
	 * @return
	 */
	public int getSelectedItemPosition() {
		return mSelectedPosition;
	}
	
	/**
	 * Sets the currently selected item.
	 * @param position
	 */
	public void setSelection(final int position) {
		if (mAdapter == null || mAdapter.getCount() < 1) {
			throw new NullPointerException("Currently no items!");
		}
		if (position < 0 || position >= mAdapter.getCount()) {
			throw new IllegalArgumentException("Position out of index");
		}
		if (mContentScrollView.getHeight() == 0) {
			mContentScrollView.postDelayed(new Runnable() {
				@Override
				public void run() {
					setNextSelectedPositionInt(position);
				}
			}, 1);
		} else {
			setNextSelectedPositionInt(position);
		}
    }
	
	private void setNextSelectedPositionInt(int position) {
		if (mContentScrollView.getHeight() == 0) {
			setSelection(position);
			return;
		}
		
		mSelectedPosition = position;
		
		initItemViewsScrollYArr();
		mContentScrollView.scrollTo(mContentScrollView.getScrollX(), mItemViewsScrollYArr[mSelectedPosition]);
		if (mOnItemSelectedListener != null) {
			mOnItemSelectedListener.onItemSelected(this, mCachedSubViewList.get(mSelectedPosition), 
											mSelectedPosition, mAdapter.getItemId(mSelectedPosition));
		}
	}
	
	private void initItemViewsScrollYArr() {
		if (mAdapter == null || mAdapter.getCount() < 1) {
			return;
		}
		if (mItemViewsScrollYArr == null) {
			int maxY = getMaxScrollY();
			
			final int itemsCount = mAdapter.getCount();
			mItemViewsScrollYArr = new int[itemsCount];
			mItemViewsScrollYArr[0] = 0;
			mItemViewsScrollYArr[itemsCount - 1] = maxY;
			
			for (int i = 0; i < itemsCount - 2; i++) {
				mItemViewsScrollYArr[i + 1] = (int) (1f * (i + 1) * maxY / (itemsCount - 1));
			}
		}
	}
	
	private class ScrollViewOnTouchListener implements OnTouchListener {
		
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			if (mAdapter == null || mAdapter.getCount() < 1) {
				return false;
			}
			
			switch (event.getAction()) {
			case MotionEvent.ACTION_DOWN:
				initItemViewsScrollYArr();
				
				mTouchedPoint.x = mContentScrollView.getScrollX();
				mTouchedPoint.y = mContentScrollView.getScrollY();
				break;
			case MotionEvent.ACTION_MOVE:
				break;
			case MotionEvent.ACTION_CANCEL:
			case MotionEvent.ACTION_UP:
				if (mScrollPointChecker == null) {
					mScrollPointChecker = new ScrollPointChecker();
					mScrollPointChecker.execute(mTouchedPoint);
				} else {
					mScrollPointChecker.cancel(true);
					mScrollPointChecker = new ScrollPointChecker();
					mScrollPointChecker.execute(mTouchedPoint);
				}
				break;
			default:
				break;
			}
			return false;
		}
	}
	
	/**
	 * 獲得ScrollView最大垂直滾動距離
	 * @param scrollView
	 * @return
	 */
	private int getMaxScrollY() {
		int tmpY = mContentScrollView.getScrollY();
		
		mContentScrollView.scrollTo(getScrollX(), 5000);
		int maxY = mContentScrollView.getScrollY();
		
		mContentScrollView.scrollTo(mContentScrollView.getScrollX(), tmpY);
		return maxY;
	}
	
	
	private class ScrollPointChecker extends AsyncTask<Point, Integer, Integer> {

		private int oldScrollY = -1;
		
		@Override
		protected Integer doInBackground(Point... params) {
			if (params == null || params.length < 1) {
				return -1;
			}
			Point originalPoint = params[0];
			int scrollView_y = mContentScrollView.getScrollY();
			if (scrollView_y == originalPoint.y) {
				return -1;
			}
			
			int currentPosition = -1;
			
			while (true) {
				scrollView_y = mContentScrollView.getScrollY();
				
				if (oldScrollY == scrollView_y) {
					int tempPosition = -1;
					
					for (int i = 0; i < getAdapter().getCount(); i++) {
						Rect visibleRect = new Rect();
						boolean flag = mCachedSubViewList.get(i).getGlobalVisibleRect(visibleRect);
						int[] location = new int[2];
						mCachedSubViewList.get(i).getLocationOnScreen(location);
						
						if (flag && Math.abs(visibleRect.top - visibleRect.bottom) == mCachedSubViewList.get(i).getHeight()) {
							if (tempPosition != -1) {
								// compare with previous item, get the closer one.
								if (Math.abs(mItemViewsScrollYArr[tempPosition] - scrollView_y) > Math.abs(mItemViewsScrollYArr[i] - scrollView_y)) {
									tempPosition = i;
								}
							} else {
								tempPosition = i;
							}
						}
					}
					
					currentPosition = tempPosition;
					break;
				} else {
					oldScrollY = scrollView_y;
				}
				try {
					TimeUnit.MILLISECONDS.sleep(100);
				} catch (InterruptedException e) {
					return -1;
				}
			}
			return currentPosition;
		}
		
		@Override
		protected void onPostExecute(Integer result) {
			if (result != -1) {
				mSelectedPosition = result;
				mContentScrollView.scrollTo(mContentScrollView.getScrollX(), mItemViewsScrollYArr[mSelectedPosition]);
				if (mOnItemSelectedListener != null) {
					mOnItemSelectedListener.onItemSelected(VerticalScrollAutoSelector.this,
							mCachedSubViewList.get(mSelectedPosition), mSelectedPosition, mAdapter.getItemId(mSelectedPosition));
				}
			} else {
				if (mOnItemSelectedListener != null) {
					mOnItemSelectedListener.onNothingSelected(VerticalScrollAutoSelector.this);
				}
			}
		}
	}
	
	
	/**
     * Interface definition for a callback to be invoked when
     * an item in this view has been selected.
     */
    public interface OnItemSelectedListener {

    	void onItemSelected(VerticalScrollAutoSelector parent, View view, int position, int id);

        void onNothingSelected(VerticalScrollAutoSelector parent);
    }
	
    
	/**
	 * Adapter for VerticalScrollAutoSelector
	 * @author Wison
	 */
	public abstract static class AutoSelectorAdapter {

	    public boolean isEmpty() {
	        return getCount() == 0;
	    }

	    /**
	     * Get the count of visible items when the VerticalScrollAutoSelector is displayed on the screen.
	     * @return
	     */
	    public abstract int getItemsCountPerGroup();
	    
	    /**
	     * Get the count of items
	     * @return
	     */
		public abstract int getCount();

		/**
		 * Get the data item associated with the specified position in the data set.
		 * @param position
		 * @return
		 */
		public abstract Object getItem(int position);

		/**
		 * Get the row id associated with the specified position in the list.
		 * @param position
		 * @return
		 */
		public abstract int getItemId(int position);

		/**
		 * Get a View that displays the data at the specified position in the data set.
		 * @param position
		 * @param convertView
		 * @param parent
		 * @return
		 */
		public abstract View getView(int position, View convertView, ViewGroup parent);
		
	}
}