CycleScrollView.java
package com.example.test; import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.GestureDetector.OnGestureListener; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; @SuppressWarnings("deprecation") public class CycleScrollView<T> extends ViewGroup implements OnGestureListener { static final String TAG = "CycleScrollView"; Context mContext; /** * Scroll velocity. */ public static final long SCROLL_VELOCITY = 50; /** * Scroll offset. */ public static final int SCROLL_OFFSET = -1; /** * Touch delay. */ public static final long TOUCH_DELAYMILLIS = 2000; /** * Fling duration. */ public static final int FLING_DURATION = 2000; /** * Filing max velocity x. */ public static final int MAX_VELOCITY_X = 1000; private GestureDetector detector; private Handler mHandler; private Scroller mScroller; /** * Callback interface adapter and OnItemClick. */ private CycleScrollAdapter<T> mAdapter; private OnItemClickListener mOnItemClickListener; /** * Scroll index */ private int mPreIndex; private int mCurrentIndex; private int mNextIndex; private View mCurrentView; private View mPreView; private View mNextView; private float mLastMotionX; // The reLayout is false can not invoke onLayout. private boolean reLayout = false; // If the item count more than screen that can scroll. private boolean canScroll = false; // A flag for switch current view. private boolean mCurrentViewAtLeft = true; // Fling distance. private int mFlingX = 0; private boolean isMoveAction = false; private int defaultItemY = 10; private int maxItemCount = 7; private int initItemX = 20; /** * The screen width. */ private int screenWidth; /** * Item view height. */ private int itemHeight; /** * Item view width. */ private int itemWidth; /** * Item view layout x. */ private int itemX = getInitItemX(); /** * Item view layout y. */ private int itemY = defaultItemY; // Auto scroll view task. private final Runnable mScrollTask = new Runnable() { @Override public void run() { if (canScroll) { scrollView(SCROLL_OFFSET); mHandler.postDelayed(this, SCROLL_VELOCITY);// Loop self. } } }; public CycleScrollView(Context context) { super(context); onCreate(context); } public CycleScrollView(Context context, AttributeSet attrs) { super(context, attrs); onCreate(context); } public CycleScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); onCreate(context); } private void onCreate(Context context) { mContext = context; detector = new GestureDetector(this); mHandler = new Handler(); mScroller = new Scroller(context); } /** * Create scroll index. */ public void createIndex() { if (canScroll) { mPreIndex = maxItemCount - 1; mCurrentIndex = 0; mNextIndex = 1; mPreView = getChildAt(mPreIndex); mCurrentView = getChildAt(mCurrentIndex); mNextView = getChildAt(mNextIndex); } } /** * Set item click callback. * * @param onItemClickListener * The callback */ public void setOnItemClickListener(OnItemClickListener onItemClickListener) { mOnItemClickListener = onItemClickListener; } /** * Set itemAdapter for addItem and bindItem. * * @param itemAdapter */ public void setAdapter(CycleScrollAdapter<T> adapter) { mAdapter = adapter; } /** * Start auto scroll. */ public void startScroll() { if (canScroll) { mHandler.post(mScrollTask); } } /** * Stop auto scroll and filing scroll task. */ public void stopScroll() { mHandler.removeCallbacks(mScrollTask); } /** * Delay start auto scroll task. */ public void delayStartScroll() { if (canScroll) { mHandler.postDelayed(mScrollTask, TOUCH_DELAYMILLIS); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = this.getChildCount(); for (int i = 0; i < count; i++) { View child = this.getChildAt(i); child.measure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { /** * On layout set the child view layout by x y width and height. */ if (reLayout) {// Run one times. for (int i = 0; i < getChildCount(); i++) { View child = this.getChildAt(i); child.setVisibility(View.VISIBLE); child.layout(itemX, getItemY(), itemX + getItemWidth(), getItemY() + getItemHeight()); itemX += getItemMargin(); } reLayout = !reLayout; } } /** * When fling view run the fling task scroll view. */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (e1 == null || e2 == null) { return false; } // When deltaX and velocityX not good return false. if (Math.abs(velocityX) < MAX_VELOCITY_X) { return false; } // Get the delta x. float deltaX = (e1.getX() - e2.getX()); /** * If can fling stop other scroll task at first , delay the task after * fling. */ mHandler.removeCallbacks(mScrollTask); if (canScroll) { mHandler.postDelayed(mScrollTask, TOUCH_DELAYMILLIS + FLING_DURATION - 1000); } /** * The flingX is fling distance. */ mFlingX = (int) deltaX; // Start scroll with fling x. mScroller.startScroll(0, 0, mFlingX, 0, FLING_DURATION); return false; } @Override public void computeScroll() { if (canScroll && mScroller.computeScrollOffset()) { /** * The Scroller.getCurrX() approach mFlingX , the deltaX more and * more small. */ int deltaX = mFlingX - mScroller.getCurrX(); scrollView(-deltaX / 10); postInvalidate(); } } /** * When touch event is move scroll child view. */ @Override public boolean onTouchEvent(MotionEvent ev) { // Get event x,y at parent view. final float x = ev.getX(); /** * Get event x,y at screen. */ final int rawX = (int) ev.getRawX(); final int rawY = (int) ev.getRawY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // Reset isMoveAction. isMoveAction = false; // Get motionX. mLastMotionX = x; break; case MotionEvent.ACTION_MOVE: // When action move set isMoveAction true. isMoveAction = true; // Only support one pointer. if (ev.getPointerCount() == 1) { // Compute delta X. int deltaX = 0; deltaX = (int) (x - mLastMotionX); mLastMotionX = x; // When canScroll is true, scrollView width deltaX. if (canScroll) { scrollView(deltaX); } } break; case MotionEvent.ACTION_UP: /** * If not move find click item and invoke click event. */ if (!isMoveAction) { View view = getClickItem(rawX, rawY); if (view != null) { mOnItemClickListener.onItemClick(Integer.valueOf(view .getTag().toString())); } } break; } return this.detector.onTouchEvent(ev); } /** * Get click item view by rawX and rawY. * @param rawX the x at screen. * @param rawY the y at screen. * @return the click item view. */ private View getClickItem(final int rawX, final int rawY) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // Get item view rect. Rect rect = new Rect(); child.getGlobalVisibleRect(rect); // If click point on the item view, invoke the click event. if (rect.contains(rawX, rawY)) { return child; } } return null; } /** * Scroll view by delta x. * * @param deltaX * The scroll distance. */ private void scrollView(int deltaX) { // Move child view by deltaX. moveChildView(deltaX); // After move change index. if (deltaX < 0) {// move left // If current at right switch current view to left. switchCurrentViewToLeft(); // change previous current next index. moveToNext(); } else {// move right // If current at left switch current view to right. switchCurrentViewToRight(); // change previous current next index. moveToPre(); } invalidate(); } /** * Move view by delta x. * * @param deltaX * The move distance. */ private void moveChildView(int deltaX) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); child.layout(child.getLeft() + deltaX, child.getTop(), child.getRight() + deltaX, child.getBottom()); } } /** * Current event is move to left, if current view at right switch current * view to left. */ private void switchCurrentViewToLeft() { if (!mCurrentViewAtLeft) { mPreIndex = mCurrentIndex; mCurrentIndex = mNextIndex; mNextIndex++; if (mNextIndex > maxItemCount - 1) { mNextIndex = 0; } mCurrentView = getChildAt(mCurrentIndex); mPreView = getChildAt(mPreIndex); mNextView = getChildAt(mNextIndex); mCurrentViewAtLeft = !mCurrentViewAtLeft; } } /** * Current event is move to right, if current view at left switch current * view to right. */ private void switchCurrentViewToRight() { if (mCurrentViewAtLeft) { mNextIndex = mCurrentIndex; mCurrentIndex = mPreIndex; mPreIndex--; if (mPreIndex < 0) { mPreIndex = maxItemCount - 1; } mCurrentView = getChildAt(mCurrentIndex); mPreView = getChildAt(mPreIndex); mNextView = getChildAt(mNextIndex); mCurrentViewAtLeft = !mCurrentViewAtLeft; } } /** * Current event is move to left,if current view move out of screen move the * current view to right and reBind the item change index. */ private void moveToNext() { if (mCurrentView.getRight() < 0) { mCurrentView.layout(mPreView.getLeft() + getItemMargin(), getItemY(), mPreView.getLeft() + getItemMargin() + getItemWidth(), getItemY() + getItemHeight()); if (mCurrentView.getTag() != null) { int listIndex = (Integer) mCurrentView.getTag(); int index = (listIndex + maxItemCount) % mAdapter.getCount(); mAdapter.bindView(mCurrentView, mAdapter.get(index)); mCurrentView.setTag(index); } mPreIndex = mCurrentIndex; mCurrentIndex = mNextIndex; mNextIndex++; if (mNextIndex > maxItemCount - 1) { mNextIndex = 0; } mCurrentView = getChildAt(mCurrentIndex); mPreView = getChildAt(mPreIndex); mNextView = getChildAt(mNextIndex); moveToNext(); } } /** * Current event is move to right,if current view move out of screen move * the current view to left and reBind the item change index. */ private void moveToPre() { if (mCurrentView.getLeft() > getScreenWidth()) { mCurrentView.layout(mNextView.getLeft() - getItemMargin(), getItemY(), mNextView.getLeft() - getItemMargin() + getItemWidth(), getItemY() + getItemHeight()); if (mCurrentView.getTag() != null) { int listIndex = (Integer) mCurrentView.getTag(); int index = (listIndex - maxItemCount + mAdapter.getCount()) % mAdapter.getCount(); mAdapter.bindView(mCurrentView, mAdapter.get(index)); mCurrentView.setTag(index); } mNextIndex = mCurrentIndex; mCurrentIndex = mPreIndex; mPreIndex--; if (mPreIndex < 0) { mPreIndex = maxItemCount - 1; } mCurrentView = getChildAt(mCurrentIndex); mPreView = getChildAt(mPreIndex); mNextView = getChildAt(mNextIndex); moveToPre(); } } @Override public boolean onDown(MotionEvent e) { return true; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } @Override public void onLongPress(MotionEvent e) { } public int getMaxItemCount() { return maxItemCount; } public void setMaxItemCount(int maxItemCount) { this.maxItemCount = maxItemCount; } public void setReLayout(boolean reLayout) { this.reLayout = reLayout; } public void setCanScroll(boolean canScroll) { this.canScroll = canScroll; } public int getItemX() { return itemX; } public void setItemX(int itemX) { this.itemX = itemX; } public int getItemY() { return itemY; } public void setItemY(int itemY) { this.itemY = itemY; } public int getItemWidth() { return itemWidth; } public void setItemWidth(int itemWidth) { this.itemWidth = itemWidth; } public int getItemHeight() { return itemHeight; } public void setItemHeight(int itemHeight) { this.itemHeight = itemHeight; } public int getItemMargin() { return (screenWidth - itemWidth * (maxItemCount - 1) - initItemX * 2)/(maxItemCount - 2) + itemWidth; } public int getScreenWidth() { return screenWidth; } public void setScreenWidth(int screenWidth) { this.screenWidth = screenWidth; } public int getInitItemX() { return initItemX; } public void setInitItemX(int initItemX) { this.initItemX = initItemX; } /** * The interface for item click callback. */ interface OnItemClickListener { public boolean onItemClick(int position); } }
CycleScrollAdapter.java
package com.example.test; import java.util.List; import android.app.Activity; import android.content.Context; import android.util.DisplayMetrics; import android.view.View; public abstract class CycleScrollAdapter<T> { private List<T> list; private CycleScrollView<T> mCycleScrollView; Context mContext; /** * Initial CycleScrollAdapter bind list to view. * * @param list * The list data. * @param cycleScrollView * The CycleScrollView. * @param context * The Context. */ public CycleScrollAdapter(List<T> list, CycleScrollView<T> cycleScrollView, Context context) { this.list = list; mContext = context; mCycleScrollView = cycleScrollView; mCycleScrollView.setAdapter(this); GetScreenWidthPixels(); initView(list); } /** * Get screen width pixels. */ private void GetScreenWidthPixels() { DisplayMetrics dm = new DisplayMetrics(); Activity a = (Activity) mContext; a.getWindowManager().getDefaultDisplay().getMetrics(dm); mCycleScrollView.setScreenWidth(dm.widthPixels); } /** * Bind list to view. * * @param list * The list data. */ protected void initView(List<T> list) { if (list == null || list.size() == 0) { return; } // Clear all view from ViewGroup at first. mCycleScrollView.removeAllViewsInLayout(); // Loop list. for (int i = 0; i < list.size(); i++) { /** * If list size more than MaxItemCount break the loop, only create * view count is MaxItemCount. */ if (i == mCycleScrollView.getMaxItemCount()) { break; } /** * If list size less than MaxItemCount at the last loop reLayout * otherwise at the MaxItemCount index reLayout. */ if (i == list.size() - 1 || i == mCycleScrollView.getMaxItemCount() - 1) { mCycleScrollView.setItemX(mCycleScrollView.getInitItemX()); mCycleScrollView.setReLayout(true); } add(list.get(i), i); } /** * If list count more than MaxItemCount the view can scroll otherwise * can not scroll. */ if (list.size() >= mCycleScrollView.getMaxItemCount()) { mCycleScrollView.setCanScroll(true); } else { mCycleScrollView.setCanScroll(false); } /** * If list count more than MaxItemCount reBuild index. */ mCycleScrollView.createIndex(); } /** * Get list size. * * @return The list size. */ public int getCount() { return list.size(); } /** * Returns the element at the specified location in this * * @param index * the index of the element to return. * @return the element at the specified location. */ public T get(int index) { return list.get(index); } /** * Adds the specified object at the end of this and refresh view. * * @param t * the object to add. */ public void addItem(T t) { list.add(t); initView(list); } /** * Removes the first occurrence of the specified object from this and * refresh view. * * @param t * the object to remove. */ public void removeItem(T t) { list.remove(t); initView(list); } /** * Add the specified view to the index. * * @param t * The data to add. * @param index * the index. */ private void add(T t, int index) { View view = getView(t); ComputeItemSize(view); mCycleScrollView.addView(view); view.setTag(index); } /** * If item size is null compute item size. * * @param view * the item view. */ private void ComputeItemSize(View view) { if (mCycleScrollView.getItemWidth() == 0 || mCycleScrollView.getItemHeight() == 0) { int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); view.measure(w, h); int height = view.getMeasuredHeight(); int width = view.getMeasuredWidth(); mCycleScrollView.setItemHeight(height); mCycleScrollView.setItemWidth(width); } } /** * Get item view. * * @param t * the data need bind to view. * @return the view. */ public abstract View getView(T t); /** * Bind the item to view. * * @param child * the item view need bind. * @param t * the item. */ public abstract void bindView(View child, T t); }
以上两个是核心类,下面是测试代码。
实现CycleScrollAdapter
AppCycleScrollAdapter.java绑定视图和应用数据
package com.example.test; import java.util.List; import android.content.Context; import android.content.pm.PackageInfo; import android.view.View; import android.widget.ImageView; import android.widget.TextView; public class AppCycleScrollAdapter extends CycleScrollAdapter<PackageInfo> { public AppCycleScrollAdapter(List<PackageInfo> list, CycleScrollView<PackageInfo> cycleScrollView, Context context) { super(list, cycleScrollView, context); } @Override protected void initView(List<PackageInfo> list) { super.initView(list); } @Override public void bindView(View child, PackageInfo pi) { ImageView image = (ImageView) child.findViewById(R.id.item_image); TextView text = (TextView) child.findViewById(R.id.item_text); image.setImageDrawable(pi.applicationInfo.loadIcon(mContext .getPackageManager())); text.setText(pi.applicationInfo.loadLabel(mContext.getPackageManager())); } @Override public View getView(PackageInfo pi) { View view = View.inflate(mContext, R.layout.view_item, null); // inflate APP icon view ImageView image = (ImageView) view.findViewById(R.id.item_image); // inflate APP name view TextView text = (TextView) view.findViewById(R.id.item_text); image.setImageDrawable(pi.applicationInfo.loadIcon(mContext .getPackageManager())); text.setText(pi.applicationInfo.loadLabel(mContext.getPackageManager())); return view; } }
入口Activity
package com.example.test; import java.util.List; import android.app.Activity; import android.content.pm.PackageInfo; import android.os.Bundle; import android.view.Menu; public class MainActivity extends Activity{ private CycleScrollView<PackageInfo> mCycleScrollView; private AppCycleScrollAdapter mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mCycleScrollView = ((CycleScrollView<PackageInfo>) this.findViewById(R.id.cycle_scroll_view)); /** * Get APP list and sort by update time. */ List<PackageInfo> list = this.getPackageManager() .getInstalledPackages(0); mAdapter = new AppCycleScrollAdapter(list, mCycleScrollView, this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_main, menu); return true; } }
布局文件
<?xml version="1.0" encoding="utf-8"?> <AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" > <ImageView android:id="@+id/item_image" android:layout_width="60dip" android:layout_height="60dip" android:layout_y="5dip" android:layout_x="10dip" /> <TextView android:id="@+id/item_text" android:layout_width="80dip" android:layout_height="20dip" android:layout_y="65dip" android:layout_x="0dip" android:gravity="center_horizontal" /> </AbsoluteLayout>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/hello_world" tools:context=".MainActivity" /> <com.example.test.CycleScrollView android:id="@+id/cycle_scroll_view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#B9000000" /> </RelativeLayout>