package edu.mit.mitmobile2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.Transformation;
import android.widget.HorizontalScrollView;
public class SliderView extends HorizontalScrollView {
// this factor is used to prevent overzealously
// going horizontally
private static int VERTICAL_FAVOR_FACTOR = 2;
protected int mLeftXforMiddle;
protected int mRightXforMiddle;
protected int mHeight;
protected Context mContext;
Adapter mSliderAdapter;
static final int SCROLL_DURATION_PER_SCREEN = 250;
static final int SCROLL_MAX_DURATION = 2500;
protected boolean isAnimatingScroll = false;
protected View.OnClickListener mClickListener = null;
protected View.OnTouchListener mTouchListener = null;
protected OnSeekListener mOnSeekListener = null;
private SliderViewLayout mSliderViewLayout;
private boolean mHasPreviousScreen;
private boolean mHasNextScreen;
private boolean mScrollNeedsResetting;
// keep track of which edge screen want to align
// true for left edge, false for right edge
private boolean mAlignLeftEdge = true;
// prevent accidentally starting a horizontal
// scroll when tapping on bottoms
private int mStaticFrictionThreshold;
/****************************************************/
public SliderView(Context context) {
super(context);
initSliderView(context);
mHeight = LayoutParams.MATCH_PARENT;
}
public SliderView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
initSliderView(context);
String layout_height = attributeSet.getAttributeValue("http://schemas.android.com/apk/res/android", "layout_height");
mHeight = AttributesParser.parseDimension(layout_height, mContext);
}
protected SliderViewLayout createSliderViewLayout(Context context) {
return new SliderViewLayout(context);
}
private void initSliderView(Context context) {
mContext = context;
mSliderViewLayout = createSliderViewLayout(context);
addView(mSliderViewLayout, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
setHorizontalScrollBarEnabled(false);
mLeftXforMiddle = mSliderViewLayout.getLeftXforMiddle();
setHorizontalFadingEdgeEnabled(false);
mStaticFrictionThreshold = AttributesParser.parseDimension("4dip", mContext);
setFillViewport(true);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mScrollNeedsResetting) {
resetScroll();
mScrollNeedsResetting = false;
}
}
@Override
protected void onSizeChanged (int w, int h, int oldw, int oldh) {
mLeftXforMiddle = mSliderViewLayout.getLeftXforMiddle();
mRightXforMiddle = mSliderViewLayout.getRightXforMiddle();
resetScroll();
mScrollNeedsResetting = true;
requestLayout();
}
public void setAdapter(Adapter sliderAdapter) {
mSliderAdapter = sliderAdapter;
refreshScreens();
}
// used to keep track of which page
// was on the screen at the last down event
private float mFingerX = -1;
private float mFingerY = -1;
private boolean mFingerIsStill;
private static float FINGER_MOTION_TOLERANCE = 15.0f;
private float mLastMotionX;
private float mLastX;
private float mLastY;
protected final static int TOUCH_STATE_REST = 0;
protected final static int TOUCH_STATE_HORIZONTAL_SCROLLING = 1;
protected final static int TOUCH_STATE_VERTICAL_SCROLLING = 2;
protected int mTouchState = TOUCH_STATE_REST;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if(mTouchListener != null) {
if(mTouchListener.onTouch(this, event)) {
return true;
}
}
switch (action) {
case MotionEvent.ACTION_MOVE:
if (Math.abs(event.getX() - mLastX) < mStaticFrictionThreshold) {
// do not process move events
// until the finger has moved a minimum distance
return false;
}
if (mTouchState == TOUCH_STATE_REST) {
if (mTouchState != TOUCH_STATE_HORIZONTAL_SCROLLING) {
if (VERTICAL_FAVOR_FACTOR * Math.abs(mLastY - event.getY()) > Math.abs(mLastX - event.getX())) {
// do not intercept vertical move events
mTouchState = TOUCH_STATE_VERTICAL_SCROLLING;
mLastX = event.getX();
mLastY = event.getY();
return false;
}
}
if (mTouchState != TOUCH_STATE_VERTICAL_SCROLLING) {
if (VERTICAL_FAVOR_FACTOR * Math.abs(mLastX - event.getX()) > Math.abs(mLastY - event.getY())) {
mTouchState = TOUCH_STATE_HORIZONTAL_SCROLLING;
mLastX = event.getX();
mLastY = event.getY();
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
if (mClickListener != null) {
if(mFingerIsStill) {
mClickListener.onClick(this);
}
}
case MotionEvent.ACTION_CANCEL:
mTouchState = TOUCH_STATE_REST;
break;
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
mLastMotionX = event.getX();
mFingerX = event.getX();
mFingerY = event.getY();
mTouchState = TOUCH_STATE_REST;
mFingerIsStill = true;
break;
}
mLastX = event.getX();
mLastY = event.getY();
updateFingerIsStillStatus(event);
return false;
}
private void updateFingerIsStillStatus(MotionEvent event) {
if(!mFingerIsStill) {
return;
}
// for purist this is not a Pythagorean distance, but is
// quicker to calculate
float motionDistance = Math.abs(event.getX() - mFingerX) + Math.abs(event.getY() - mFingerY);
if(motionDistance > FINGER_MOTION_TOLERANCE) {
mFingerIsStill = false;
}
}
/****************************************************/
@Override
public boolean onTouchEvent(MotionEvent event) {
if(mTouchListener != null) {
if(mTouchListener.onTouch(this, event)) {
return true;
}
}
int action = event.getAction();
updateFingerIsStillStatus(event);
if (action == MotionEvent.ACTION_MOVE) {
// Scroll to follow the motion event
final int deltaX = (int) (mLastMotionX - event.getX());
mLastMotionX = event.getX();
if (deltaX < 0) {
int availableToScroll = mHasPreviousScreen ? -getScrollX() : -getScrollX() + mLeftXforMiddle;
if (availableToScroll < 0) {
scrollBy(Math.max(availableToScroll, deltaX), 0);
}
} else if (deltaX > 0) {
int right = mHasNextScreen ? mSliderViewLayout.getWidth() : mRightXforMiddle;
final int availableToScroll = (right - mLeftXforMiddle) - (getScrollX() - mLeftXforMiddle) - getWidth();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX), 0);
}
}
return true;
}
if (action == MotionEvent.ACTION_UP) {
// calculate the closest position
mTouchState = TOUCH_STATE_REST;
if (shouldAlwaysSnapToPosition()) {
snapToPosition(nearestPosition());
} else {
int scrollX = getScrollX();
if (mLeftXforMiddle <= scrollX && scrollX <= mRightXforMiddle) {
int childWidth = mSliderViewLayout.getChildWidth();
if (scrollX >= (mRightXforMiddle - getWidth())) {
// user has scrolled beyond the screen, either let the user
// go to the next screen or scroll to be right justified
if (scrollX > mRightXforMiddle - childWidth/2) {
snapToPosition(ScreenPosition.Next);
} else {
smoothScrollTo(mRightXforMiddle - getWidth(), 0);
}
}
} else {
snapToPosition(nearestPosition());
}
}
if(mClickListener != null) {
if(mFingerIsStill) {
mClickListener.onClick(this);
}
}
return true;
}
if (action == MotionEvent.ACTION_CANCEL) {
mTouchState = TOUCH_STATE_REST;
}
if (action == MotionEvent.ACTION_DOWN) {
mLastMotionX = event.getX();
}
return true;
}
protected boolean shouldAlwaysSnapToPosition() {
return (getWidth() >= mSliderViewLayout.getChildWidth());
}
@Override
public void setOnClickListener(View.OnClickListener clickListener) {
mClickListener = clickListener;
}
@Override
public void setOnTouchListener(View.OnTouchListener touchListener) {
mTouchListener = touchListener;
}
private ScreenPosition nearestPosition() {
float screenFraction = (float)(getScrollX() - mLeftXforMiddle) / (float)(mSliderViewLayout.getChildWidth());
if(mSliderViewLayout.getChildCount()>2){
if (screenFraction < -0.50) {
return ScreenPosition.Previous;
}
if (screenFraction > 0.50) {
return ScreenPosition.Next;
}
return ScreenPosition.Current;
}else{
if (screenFraction <= 0.50) {
return ScreenPosition.Previous;
}else{
return ScreenPosition.Next;
}
}
}
private int scrollX(ScreenPosition screenPosition) {
if(mSliderViewLayout.getChildCount()>2){
switch (screenPosition) {
case Previous:
return mSliderViewLayout.getChildWidth() - getWidth();
case Current:
return mLeftXforMiddle;
case Next:
return mSliderViewLayout.getLeftXforRight();
}
}else if(mSliderViewLayout.getChildCount()==2){
switch (screenPosition) {
case Previous:
return mLeftXforMiddle;
case Next:
return mSliderViewLayout.getLeftXforRight();
case Current:
throw new RuntimeException("bad scroll position");
}
}
throw new RuntimeException("scroll position not found, must have received null for screen position");
}
protected void snapToPosition(final ScreenPosition screenPosition) {
isAnimatingScroll = true;
//final int finalX = scrollX(screenPosition);
final int finalX = scrollX(screenPosition);
final ScrollAnimation scrollAnimation = new ScrollAnimation(finalX);
scrollAnimation.setDuration(SCROLL_DURATION_PER_SCREEN);
scrollAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
scrollAnimation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation animation) {
isAnimatingScroll = false;
scrollAnimation.mEndAnimation = true;
seekPosition(screenPosition);
}
@Override
public void onAnimationRepeat(Animation animation) {}
@Override
public void onAnimationStart(Animation animation) {}
});
startAnimation(scrollAnimation);
}
/*
* This is not a typical Animation class, instead of using the builtin matrix transformation
* of the android animations, this just uses the scroll properties to animate a scrolling
*/
protected class ScrollAnimation extends Animation {
private int mToX;
private int mFromX;
public boolean mEndAnimation = false;
ScrollAnimation(int toX) {
mToX = toX;
mFromX = getScrollX();
}
@Override
protected void applyTransformation(float interpolation, Transformation t) {
if (!mEndAnimation) {
float nextXFloat = (mToX - mFromX) * interpolation + mFromX;
int nextX = Math.round(nextXFloat);
scrollTo(nextX, 0);
}
}
}
protected void seekPosition(ScreenPosition position) {
if (position != ScreenPosition.Current) {
mSliderAdapter.seek(position);
}
View newScreen = null;
if (mSliderAdapter.hasScreen(position)) {
newScreen = mSliderAdapter.getScreen(position);
}
if (position == ScreenPosition.Next) {
mHasPreviousScreen = true;
mHasNextScreen = (newScreen != null);
if (mSliderViewLayout.addViewToRight(newScreen) != null) {
mSliderAdapter.destroyScreen(ScreenPosition.Previous);
}
} else if (position == ScreenPosition.Previous) {
mHasNextScreen = true;
mHasPreviousScreen = (newScreen != null);
if (mSliderViewLayout.addViewToLeft(newScreen) != null) {
mSliderAdapter.destroyScreen(ScreenPosition.Next);
}
} else {
// nothing to do for seek to current screen
return;
}
mScrollNeedsResetting = true;
mAlignLeftEdge = (position != ScreenPosition.Previous);
if (mOnSeekListener != null) {
mOnSeekListener.onSeek(this, mSliderAdapter);
}
}
private void resetScroll() {
if (mAlignLeftEdge) {
scrollTo(mLeftXforMiddle, 0);
} else {
scrollTo(mRightXforMiddle - getWidth(), 0);
}
}
public void destroy() {
mSliderViewLayout.clear();
mSliderAdapter.destroy();
mSliderAdapter = null;
}
public void refreshScreens() {
/*
* really should change how SliderAdapter is notified about views being removed.
*/
mSliderViewLayout.clear();
View currentView = mSliderAdapter.getScreen(ScreenPosition.Current);
if(currentView!=null)
mSliderViewLayout.addViewToRight(currentView);
mHasNextScreen = false;
if (mSliderAdapter.hasScreen(ScreenPosition.Next)) {
mHasNextScreen = true;
View nextView = mSliderAdapter.getScreen(ScreenPosition.Next);
mSliderViewLayout.addViewToRight(nextView);
}
mHasPreviousScreen = false;
if (mSliderAdapter.hasScreen(ScreenPosition.Previous)) {
mHasPreviousScreen = true;
View previousView = mSliderAdapter.getScreen(ScreenPosition.Previous);
mSliderViewLayout.addViewToLeft(previousView);
}
mScrollNeedsResetting = true;
resetScroll();
mSliderAdapter.seek(ScreenPosition.Current);
if (mOnSeekListener != null) {
mOnSeekListener.onSeek(this, mSliderAdapter);
}
}
public void slideRight() {
if (mSliderAdapter.hasScreen(ScreenPosition.Next)) {
snapToPosition(ScreenPosition.Next);
}
}
public void slideLeft() {
if (mSliderAdapter.hasScreen(ScreenPosition.Previous)) {
snapToPosition(ScreenPosition.Previous);
}
}
public boolean isAnimatingScroll() {
return isAnimatingScroll;
}
public void setOnSeekListener(OnSeekListener seekListener) {
mOnSeekListener = seekListener;
}
public static enum ScreenPosition {
Previous,
Current,
Next
}
public boolean isAtBeginning() {
return !mHasPreviousScreen;
}
public boolean isAtEnd() {
return !mHasNextScreen;
}
public interface Adapter {
public boolean hasScreen(ScreenPosition screenPosition);
public View getScreen(ScreenPosition screenPosition);
public void destroyScreen(ScreenPosition screenPosition);
public void seek(ScreenPosition screenPosition);
public void destroy();
}
public interface OnSeekListener {
public void onSeek(SliderView view, Adapter adapter);
}
}