/**
* Copyright (c) 2013-2014, Rinc Liu (http://rincliu.com).
*
* 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 com.rincliu.library.widget.viewflow;
import java.util.EnumSet;
import java.util.LinkedList;
import com.rincliu.library.R;
import com.rincliu.library.util.RLAPICompat;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.AbsListView;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.Scroller;
@SuppressLint("HandlerLeak")
public class ViewFlow extends AdapterView<Adapter> {
private static final int SNAP_VELOCITY = 100;
private static final int INVALID_SCREEN = -1;
private static final int TOUCH_STATE_REST = 0;
private static final int TOUCH_STATE_SCROLLING = 1;
private LinkedList<View> mLoadedViews;
private LinkedList<View> mRecycledViews;
private int mCurrentBufferIndex;
private int mCurrentAdapterIndex;
private int mBufferedItemCount = 3;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
private int mTouchState = TOUCH_STATE_REST;
private float mLastMotionX;
private int TOUCH_SLOP;
private int MAX_VELOCITY;
private int mCurrentScreen;
private int mNextScreen = INVALID_SCREEN;
private boolean mFirstLayout = true;
private EnumSet<LazyInit> mLazyInit = EnumSet.allOf(LazyInit.class);
private Adapter mAdapter;
private int mLastScrollDirection;
private AdapterDataSetObserver mDataSetObserver;
private ViewSwitchListener viewSwitchListener;
private int mLastOrientation = -1;
private int mAutoScrollDuration = 1000;
private ScrollMode scrollMode;
private boolean isTaskRunning = false;
/**
* @param context
* @param attrs
*/
public ViewFlow(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ViewFlow);
setAutoScrollDuration(typedArray.getInt(R.styleable.ViewFlow_autoScrollDuration, 1000));
setBufferedItemCount(typedArray.getInt(R.styleable.ViewFlow_bufferedItemCount, 3));
typedArray.recycle();
mLoadedViews = new LinkedList<View>();
mRecycledViews = new LinkedList<View>();
mScroller = new Scroller(getContext());
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
TOUCH_SLOP = configuration.getScaledTouchSlop();
MAX_VELOCITY = configuration.getScaledMaximumFlingVelocity();
scrollMode = ScrollMode.MANUAL;
runTask();
}
private OnGlobalLayoutListener orientationChangeListener = new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
RLAPICompat.removeGlobalLayoutListener(getViewTreeObserver(), orientationChangeListener);
setSelection(mCurrentAdapterIndex);
}
};
private void runTask() {
if (!isTaskRunning) {
new Thread() {
@Override
public void run() {
isTaskRunning = true;
while (true) {
try {
Thread.sleep(mAutoScrollDuration);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (mAdapter != null && mAdapter.getCount() > 0 && scrollMode != ScrollMode.MANUAL) {
handler.sendEmptyMessage(scrollMode == ScrollMode.AUTO_PREVIOUS ? MSG_AUTO_PREVIOUS
: MSG_AUTO_NEXT);
}
}
}
}.start();
}
}
private static final int MSG_AUTO_PREVIOUS = 0x1111;
private static final int MSG_AUTO_NEXT = 0x2222;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_AUTO_PREVIOUS:
previous();
break;
case MSG_AUTO_NEXT:
next();
break;
}
}
};
/**
*
*/
public static interface ViewSwitchListener {
/**
* @param view
*/
public void setViewFlow(ViewFlow view);
/**
* @param h
* @param v
* @param oldh
* @param oldv
*/
public void onScrolled(int h, int v, int oldh, int oldv);
/**
* @param view
* @param position
*/
public void onSwitched(View view, int position);
}
/**
*/
public static interface ViewLazyInitializeListener {
void onViewLazyInitialize(View view, int position);
}
/**
*
*/
private enum LazyInit {
LEFT, RIGHT
}
/*
* (non-Javadoc)
* @see
* android.view.View#onConfigurationChanged(android.content.res.Configuration
* )
*/
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != mLastOrientation) {
mLastOrientation = newConfig.orientation;
getViewTreeObserver().addOnGlobalLayoutListener(orientationChangeListener);
}
}
/**
* @return
*/
public int getViewsCount() {
return mAdapter == null ? 0 : mAdapter.getCount();
}
/*
* (non-Javadoc)
* @see android.view.View#onMeasure(int,int)
*/
@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.");
}
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;
}
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#onLayout(boolean,int,int,int,int)
*/
@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;
}
}
}
/*
* (non-Javadoc)
* @see
* android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return handleTouchEvent(event, true);
}
/*
* (non-Javadoc)
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleTouchEvent(event, false);
}
private boolean handleTouchEvent(MotionEvent event, boolean isIntercept) {
if (getChildCount() == 0) {
return false;
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
final int action = event.getAction();
final float x = event.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
mLastMotionX = x;
mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
break;
case MotionEvent.ACTION_MOVE:
final int deltaX = (int) (mLastMotionX - x);
boolean xMoved = Math.abs(deltaX) > TOUCH_SLOP;
if (xMoved) {
mTouchState = TOUCH_STATE_SCROLLING;
}
if (mTouchState == TOUCH_STATE_SCROLLING) {
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastMotionX = x;
final int scrollX = getScrollX();
if (deltaX < 0) {
if (scrollX > 0) {
scrollBy(Math.max(-scrollX, deltaX), 0);
}
} else if (deltaX > 0) {
final int availableToScroll = getChildAt(getChildCount() - 1).getRight() - scrollX - getWidth();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX), 0);
}
}
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, MAX_VELOCITY);
int velocityX = (int) velocityTracker.getXVelocity();
if (velocityX > SNAP_VELOCITY) {
previous();
} else if (velocityX < -SNAP_VELOCITY) {
next();
} else {
snapToDestination();
}
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
mTouchState = TOUCH_STATE_REST;
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(false);
}
if (!isIntercept) {
snapToDestination();
}
break;
}
return !isIntercept;
}
/**
*
*/
public void next() {
if (mCurrentScreen < getChildCount() - 1) {
snapToScreen(mCurrentScreen + 1);
} else {
setSelection(0);
}
}
/**
*
*/
public void previous() {
if (mCurrentScreen > 0) {
snapToScreen(mCurrentScreen - 1);
} else {
setSelection(mAdapter.getCount() - 1);
}
}
/**
* @return
*/
public int getCurrentIndex() {
return mCurrentScreen;
}
/**
* @param duration
*/
public void setAutoScrollDuration(int duration) {
if (duration < 1000) {
throw new IllegalArgumentException("AutoScrollDuration should be larger than 1000.");
}
mAutoScrollDuration = duration;
}
/**
* @param count
*/
public void setBufferedItemCount(int count) {
if (count < 0) {
throw new IllegalArgumentException("BufferedItemCount should be larger than 0.");
}
mBufferedItemCount = count;
}
/**
*
*/
public enum ScrollMode {
MANUAL, AUTO_PREVIOUS, AUTO_NEXT;
}
/**
* @param scrollMode
*/
public void setScrollMode(ScrollMode scrollMode) {
this.scrollMode = scrollMode;
}
/**
* @return
*/
public ScrollMode getScrollMode() {
return scrollMode;
}
/*
* (non-Javadoc)
* @see android.view.View#onScrollChanged(int,int,int,int)
*/
@Override
protected void onScrollChanged(int h, int v, int oldh, int oldv) {
super.onScrollChanged(h, v, oldh, oldv);
if (viewSwitchListener != null) {
int hPerceived = h + (mCurrentAdapterIndex - mCurrentBufferIndex) * getWidth();
viewSwitchListener.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()) {
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
mNextScreen = whichScreen;
final int newX = whichScreen * getWidth();
final int delta = newX - getScrollX();
mScroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 2);
invalidate();
}
}
/*
* (non-Javadoc)
* @see android.view.View#computeScroll()
*/
@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);
}
}
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();
}
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#getAdapter()
*/
@Override
public Adapter getAdapter() {
return mAdapter;
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#setAdapter(android.widget.Adapter)
*/
@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 != null && mAdapter.getCount() > 0) {
setSelection(0);
}
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#getSelectedView()
*/
@Override
public View getSelectedView() {
return (mCurrentBufferIndex < mLoadedViews.size() ? mLoadedViews.get(mCurrentBufferIndex) : null);
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#getSelectedItemPosition()
*/
@Override
public int getSelectedItemPosition() {
return mCurrentAdapterIndex;
}
/**
* Set the FlowIndicator
*
* @param flowIndicator
*/
public void setIndicator(ViewSwitchListener indicator) {
viewSwitchListener = indicator;
viewSwitchListener.setViewFlow(this);
}
/**
*
*/
protected void recycleViews() {
while (!mLoadedViews.isEmpty()) {
recycleView(mLoadedViews.remove());
}
}
/**
* @param view
*/
protected void recycleView(View view) {
if (view != null) {
mRecycledViews.add(view);
detachViewFromParent(view);
}
}
/**
* @return
*/
protected View getRecycledView() {
return (mRecycledViews.isEmpty() ? null : mRecycledViews.remove(0));
}
/*
* (non-Javadoc)
* @see android.widget.AdapterView#setSelection(int)
*/
@Override
public void setSelection(int position) {
mNextScreen = INVALID_SCREEN;
mScroller.forceFinished(true);
if (mAdapter != null) {
position = Math.max(position, 0);
position = Math.min(position, mAdapter.getCount() - 1);
recycleViews();
View currentView = makeAndAddView(position, true);
mLoadedViews.addLast(currentView);
for (int offset = 1; mBufferedItemCount - offset >= 0; offset++) {
int leftIndex = position - offset;
int rightIndex = position + offset;
if (leftIndex >= 0) {
mLoadedViews.addFirst(makeAndAddView(leftIndex, false));
}
if (rightIndex < mAdapter.getCount()) {
mLoadedViews.addLast(makeAndAddView(rightIndex, true));
}
}
mCurrentBufferIndex = mLoadedViews.indexOf(currentView);
mCurrentAdapterIndex = position;
requestLayout();
setVisibleView(mCurrentBufferIndex, false);
if (viewSwitchListener != null) {
viewSwitchListener.onSwitched(currentView, mCurrentAdapterIndex);
}
}
}
private void resetFocus() {
recycleViews();
removeAllViewsInLayout();
mLazyInit.addAll(EnumSet.allOf(LazyInit.class));
for (int i = Math.max(0, mCurrentAdapterIndex - mBufferedItemCount); i < Math.min(mAdapter.getCount(),
mCurrentAdapterIndex + mBufferedItemCount + 1); i++) {
mLoadedViews.addLast(makeAndAddView(i, true));
if (i == mCurrentAdapterIndex) {
mCurrentBufferIndex = mLoadedViews.size() - 1;
}
}
requestLayout();
}
private void postViewSwitched(int direction) {
if (direction != 0) {
if (direction > 0) {
mCurrentAdapterIndex++;
mCurrentBufferIndex++;
mLazyInit.remove(LazyInit.LEFT);
mLazyInit.add(LazyInit.RIGHT);
if (mCurrentAdapterIndex > mBufferedItemCount) {
recycleView(mLoadedViews.removeFirst());
mCurrentBufferIndex--;
}
int newBufferIndex = mCurrentAdapterIndex + mBufferedItemCount;
if (newBufferIndex < mAdapter.getCount())
mLoadedViews.addLast(makeAndAddView(newBufferIndex, true));
} else {
mCurrentAdapterIndex--;
mCurrentBufferIndex--;
mLazyInit.add(LazyInit.LEFT);
mLazyInit.remove(LazyInit.RIGHT);
if (mAdapter.getCount() - 1 - mCurrentAdapterIndex > mBufferedItemCount) {
recycleView(mLoadedViews.removeLast());
}
int newBufferIndex = mCurrentAdapterIndex - mBufferedItemCount;
if (newBufferIndex > -1) {
mLoadedViews.addFirst(makeAndAddView(newBufferIndex, false));
mCurrentBufferIndex++;
}
}
requestLayout();
setVisibleView(mCurrentBufferIndex, true);
if (viewSwitchListener != null) {
viewSwitchListener.onSwitched(mLoadedViews.get(mCurrentBufferIndex), mCurrentAdapterIndex);
}
}
}
private View makeAndAddView(int position, boolean addToEnd) {
View convertView = getRecycledView();
View view = mAdapter.getView(position, convertView, this);
if (view != convertView) {
mRecycledViews.add(convertView);
}
ViewGroup.LayoutParams p = view.getLayoutParams();
if (p == null) {
p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT,
0);
}
if (view == convertView) {
attachViewToParent(view, (addToEnd ? -1 : 0), p);
} else {
addViewInLayout(view, (addToEnd ? -1 : 0), p, true);
}
return view;
}
/**
*
*/
private 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() {}
}
}