Back to project page iPhoroidUI.
The source code is released under:
Apache License
If you think the Android project iPhoroidUI listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2011 Patrik kerfeldt// w w w . j a v a 2s . c o m * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.klab.iphoroid.widget.flowview; import java.util.ArrayList; import java.util.LinkedList; import android.content.Context; import android.content.res.TypedArray; import android.database.DataSetObserver; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.Scroller; import org.klab.iphoroid.widget.adpterview.OnScrollListener; import org.klab.iphoroid.R; /** * A horizontally scrollable {@link ViewGroup} with items populated from an * {@link Adapter}. The ViewFlow uses a buffer to store loaded {@link View}s in. * The default size of the buffer is 3 elements on both sides of the currently * visible {@link View}, making up a total buffer size of 3 * 2 + 1 = 7. The * buffer size can be changed using the {@code sidebuffer} xml attribute. * * @author Patrik kerfeldt * @author <a href="mailto:sano-n@klab.jp">Naohide Sano</a> (sano-n) * @version Patrik kerfeldt original * @version sano-n ????????????????{@link OnScrollListener} ??????????????????????? */ public class FlowView extends AdapterView<Adapter> { private static final int SNAP_VELOCITY = 1000; private static final int INVALID_SCREEN = -1; private final static int TOUCH_STATE_REST = 0; private final static int TOUCH_STATE_SCROLLING = 1; private LinkedList<View> mLoadedViews; private int mCurrentBufferIndex; private int mCurrentAdapterIndex; private int mSideBuffer = DEFAULT_SIDE_BUFFER; private Scroller mScroller; private VelocityTracker mVelocityTracker; private int mTouchState = TOUCH_STATE_REST; private float mLastMotionX; private float mLastMotionY; private int mTouchSlop; private int mMaximumVelocity; private int mCurrentScreen; private int mNextScreen = INVALID_SCREEN; private boolean mFirstLayout = true; private ViewSwitchListener mViewSwitchListener; private Adapter mAdapter; private int mLastScrollDirection; private AdapterDataSetObserver mDataSetObserver; private FlowIndicator mIndicator; private static final int DEFAULT_SIDE_BUFFER = 2; // TBD API level 8 ? // private OnGlobalLayoutListener orientationChangeListener = new OnGlobalLayoutListener() { // @Override // public void onGlobalLayout() { // getViewTreeObserver().removeGlobalOnLayoutListener(orientationChangeListener); // setSelection(mCurrentAdapterIndex); // } // }; /** * Receives call backs when a new {@link View} has been scrolled to. */ public static interface ViewSwitchListener { /** * This method is called when a new View has been scrolled to. * * @param view the {@link View} currently in focus. * @param position The position in the adapter of the {@link View} * currently in focus. */ void onSwitched(View view, int position); } public FlowView(Context context) { super(context); mSideBuffer = DEFAULT_SIDE_BUFFER; init(); } public FlowView(Context context, int sideBuffer) { super(context); mSideBuffer = sideBuffer; init(); } public FlowView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.org_klab_iphoroid_widget_flowview_FlowView); mSideBuffer = styledAttrs.getInt(R.styleable.org_klab_iphoroid_widget_flowview_FlowView_sidebuffer, DEFAULT_SIDE_BUFFER); Log.d("FlowView", "mSideBuffer: " + mSideBuffer); init(); } private void init() { mLoadedViews = new LinkedList<View>(); mScroller = new Scroller(getContext()); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); } // TBD API level 8 ? // @Override // protected void onConfigurationChanged(Configuration newConfig) { // super.onConfigurationChanged(newConfig); // getViewTreeObserver().addOnGlobalLayoutListener(orientationChangeListener); // } public int getViewsCount() { return mAdapter.getCount(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY && !isInEditMode()) { throw new IllegalStateException("ViewFlow can only be used in EXACTLY mode."); } final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && !isInEditMode()) { throw new IllegalStateException("ViewFlow can only be used in EXACTLY mode."); } // The children are given the same width and height as the workspace final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); } if (mFirstLayout) { mScroller.startScroll(0, 0, mCurrentScreen * width, 0, 0); mFirstLayout = false; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childLeft = 0; final int count = getChildCount(); for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != View.GONE) { final int childWidth = child.getMeasuredWidth(); child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight()); childLeft += childWidth; } } } private static enum Stage { None, Scroll, End } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (processTouchEvent(ev)) { case None: return false; case Scroll: return true; case End: return false; default: assert false; return false; } } @Override public boolean onTouchEvent(MotionEvent ev) { switch (processTouchEvent(ev)) { case None: return false; case Scroll: return true; case End: return true; default: assert false; return true; } } /** */ private Stage processTouchEvent(MotionEvent ev) { if (getChildCount() == 0) { return Stage.None; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: // If being flinged and user touches, stop the fling. isFinished // will be false if being flinged. if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // Remember where the motion event started mLastMotionX = x; mLastMotionY = y; mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; if (mTouchState == TOUCH_STATE_SCROLLING) { fireScrollStateChanged(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } break; case MotionEvent.ACTION_MOVE: final int xDiff = (int) Math.abs(x - mLastMotionX); // final int yDiff = (int) Math.abs(y - mLastMotionY); boolean xMoved = xDiff > mTouchSlop; if (xMoved) { // Scroll if the user moved far enough along the X axis mTouchState = TOUCH_STATE_SCROLLING; fireScrollStateChanged(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } if (mTouchState == TOUCH_STATE_SCROLLING) { // Scroll to follow the motion event final int deltaX = (int) (mLastMotionX - x); final int deltaY = (int) (mLastMotionY - y); mLastMotionX = x; mLastMotionY = y; final int scrollX = getScrollX(); final int scrollY = getScrollY(); Log.w("FlowView", "SCROLL: sx: " + scrollX + ", dx: " + deltaX + ", sy: " + scrollY + ", dy: " + deltaY); if (Math.abs(deltaX) > Math.abs(deltaY)) { if (deltaX < 0) { if (scrollX > 0) { scrollBy(Math.max(-scrollX, deltaX), 0); // y ???????????????? deltaY } } else if (deltaX > 0) { final int availableToScroll = getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth(); if (availableToScroll > 0) { scrollBy(Math.min(availableToScroll, deltaX), 0); // y ???????????????? deltaY } } return Stage.Scroll; } else { return Stage.None; } } break; case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_SCROLLING) { final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocityX = (int) velocityTracker.getXVelocity(); int velocityY = (int) velocityTracker.getYVelocity(); Log.d("FlowView", "velovity: " + velocityX + ", " + velocityY); if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0 && Math.abs(velocityX) > Math.abs(velocityY)) { // Fling hard enough to move left // fireScrollStateChanged(OnScrollListener.SCROLL_STATE_FLING); snapToScreen(mCurrentScreen - 1); } else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1 && Math.abs(velocityX) > Math.abs(velocityY)) { // Fling hard enough to move right // fireScrollStateChanged(OnScrollListener.SCROLL_STATE_FLING); snapToScreen(mCurrentScreen + 1); } else { snapToDestination(); } if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } mTouchState = TOUCH_STATE_REST; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST; fireScrollStateChanged(OnScrollListener.SCROLL_STATE_IDLE); } return Stage.End; } @Override protected void onScrollChanged(int h, int v, int oldh, int oldv) { super.onScrollChanged(h, v, oldh, oldv); if (mIndicator != null) { // The actual horizontal scroll origin does typically not match the // perceived one. Therefore, we need to calculate the perceived // horizontal scroll origin here, since we use a view buffer. int hPerceived = h + (mCurrentAdapterIndex - mCurrentBufferIndex) * getWidth(); mIndicator.onScrolled(hPerceived, v, oldh, oldv); } } private void snapToDestination() { final int screenWidth = getWidth(); final int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth; snapToScreen(whichScreen); } private void snapToScreen(int whichScreen) { mLastScrollDirection = whichScreen - mCurrentScreen; if (!mScroller.isFinished()) return; whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1)); mNextScreen = whichScreen; final int newX = whichScreen * getWidth(); final int delta = newX - getScrollX(); Log.w("FlowView", "scroll: " + getScrollX() + ", " + getScrollY()); mScroller.startScroll(getScrollX(), mScroller.getCurrY(), delta, 0, Math.abs(delta) * 2); invalidate(); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); postInvalidate(); } else if (mNextScreen != INVALID_SCREEN) { mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1)); mNextScreen = INVALID_SCREEN; postViewSwitched(mLastScrollDirection); } } /** * Scroll to the {@link View} in the view buffer specified by the index. * * @param indexInBuffer Index of the view in the view buffer. */ private void setVisibleView(int indexInBuffer, boolean uiThread) { mCurrentScreen = Math.max(0, Math.min(indexInBuffer, getChildCount() - 1)); int dx = (mCurrentScreen * getWidth()) - mScroller.getCurrX(); mScroller.startScroll(mScroller.getCurrX(), mScroller.getCurrY(), dx, 0, 0); if (dx == 0) { onScrollChanged(mScroller.getCurrX() + dx, mScroller.getCurrY(), mScroller.getCurrX() + dx, mScroller.getCurrY()); } if (uiThread) { invalidate(); } else { postInvalidate(); } } /** * Set the listener that will receive notifications every time the {code * ViewFlow} scrolls. * * @param l the scroll listener */ public void setOnViewSwitchListener(ViewSwitchListener l) { mViewSwitchListener = l; } @Override public Adapter getAdapter() { return mAdapter; } @Override public void setAdapter(Adapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(mDataSetObserver); } mAdapter = adapter; if (mAdapter != null) { mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); } if (mAdapter.getCount() == 0) return; for (int i = 0; i < Math.min(mAdapter.getCount(), mSideBuffer + 1); i++) { mLoadedViews.addLast(makeAndAddView(i, true, null)); } mCurrentAdapterIndex = 0; mCurrentBufferIndex = 0; requestLayout(); setVisibleView(mCurrentBufferIndex, false); if (mViewSwitchListener != null) { mViewSwitchListener.onSwitched(mLoadedViews.get(0), 0); } } @Override public View getSelectedView() { return (mCurrentBufferIndex < mLoadedViews.size() ? mLoadedViews.get(mCurrentBufferIndex) : null); } /** * Set the FlowIndicator * * @param flowIndicator */ public void setFlowIndicator(FlowIndicator flowIndicator) { mIndicator = flowIndicator; mIndicator.setViewFlow(this); } @Override public void setSelection(int position) { mNextScreen = INVALID_SCREEN; mScroller.forceFinished(true); if (mAdapter == null || position >= mAdapter.getCount()) { return; } ArrayList<View> recycleViews = new ArrayList<View>(); View recycleView; while (!mLoadedViews.isEmpty()) { recycleViews.add(recycleView = mLoadedViews.remove()); detachViewFromParent(recycleView); } for (int i = Math.max(0, position - mSideBuffer); i < Math.min(mAdapter.getCount(), position + mSideBuffer + 1); i++) { mLoadedViews.addLast(makeAndAddView(i, true, (recycleViews.isEmpty() ? null : recycleViews.remove(0)))); if (i == position) { mCurrentBufferIndex = mLoadedViews.size() - 1; } } mCurrentAdapterIndex = position; for (View view : recycleViews) { removeDetachedView(view, false); } requestLayout(); setVisibleView(mCurrentBufferIndex, false); if (mIndicator != null) { mIndicator.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex); } if (mViewSwitchListener != null) { mViewSwitchListener.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex); } } private void resetFocus() { logBuffer(); mLoadedViews.clear(); removeAllViewsInLayout(); for (int i = Math.max(0, mCurrentAdapterIndex - mSideBuffer); i < Math.min(mAdapter.getCount(), mCurrentAdapterIndex + mSideBuffer + 1); i++) { mLoadedViews.addLast(makeAndAddView(i, true, null)); if (i == mCurrentAdapterIndex) { mCurrentBufferIndex = mLoadedViews.size() - 1; } } logBuffer(); requestLayout(); } private void postViewSwitched(int direction) { if (direction == 0) { return; } if (direction > 0) { // to the right mCurrentAdapterIndex++; mCurrentBufferIndex++; View recycleView = null; // Remove view outside buffer range if (mCurrentAdapterIndex > mSideBuffer) { recycleView = mLoadedViews.removeFirst(); detachViewFromParent(recycleView); // removeView(recycleView); mCurrentBufferIndex--; } // Add new view to buffer int newBufferIndex = mCurrentAdapterIndex + mSideBuffer; if (newBufferIndex < mAdapter.getCount()) { mLoadedViews.addLast(makeAndAddView(newBufferIndex, true, recycleView)); } } else { // to the left mCurrentAdapterIndex--; mCurrentBufferIndex--; View recycleView = null; // Remove view outside buffer range if (mAdapter.getCount() - 1 - mCurrentAdapterIndex > mSideBuffer) { recycleView = mLoadedViews.removeLast(); detachViewFromParent(recycleView); } // Add new view to buffer int newBufferIndex = mCurrentAdapterIndex - mSideBuffer; if (newBufferIndex > -1) { mLoadedViews.addFirst(makeAndAddView(newBufferIndex, false, recycleView)); mCurrentBufferIndex++; } } requestLayout(); setVisibleView(mCurrentBufferIndex, true); if (mIndicator != null) { mIndicator.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex); } if (mViewSwitchListener != null) { mViewSwitchListener.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex); } logBuffer(); } private View setupChild(View child, boolean addToEnd, boolean recycle) { ViewGroup.LayoutParams p = child.getLayoutParams(); if (p == null) { p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } if (recycle) attachViewToParent(child, (addToEnd ? -1 : 0), p); else addViewInLayout(child, (addToEnd ? -1 : 0), p, true); return child; } private View makeAndAddView(int position, boolean addToEnd, View convertView) { View view = mAdapter.getView(position, convertView, this); return setupChild(view, addToEnd, convertView != null); } class AdapterDataSetObserver extends DataSetObserver { @Override public void onChanged() { View v = getChildAt(mCurrentBufferIndex); if (v != null) { for (int index = 0; index < mAdapter.getCount(); index++) { if (v.equals(mAdapter.getItem(index))) { mCurrentAdapterIndex = index; break; } } } resetFocus(); } @Override public void onInvalidated() { // Not yet implemented! } } private void logBuffer() { Log.d("FlowView", "Size of mLoadedViews: " + mLoadedViews.size() + ", X: " + mScroller.getCurrX() + ", Y: " + mScroller.getCurrY()); Log.d("FlowView", "IndexInAdapter: " + mCurrentAdapterIndex + ", IndexInBuffer: " + mCurrentBufferIndex); } /** */ private OnScrollListener onScrollListener; /** */ public void setOnScrollListener(OnScrollListener onScrollListener) { this.onScrollListener = onScrollListener; } /** */ private int scrollState = OnScrollListener.SCROLL_STATE_IDLE; /** */ protected void fireScrollStateChanged(int scrollState) { if (onScrollListener != null) { if (this.scrollState != scrollState) { onScrollListener.onScrollStateChanged(this, scrollState); this.scrollState = scrollState; } } } }