package com.edisonwang.stackedview.view;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
public class StackedView extends RelativeLayout {
public static final boolean DEBUG = true;
private static final String TAG = "StackedViews";
private double threshold = 0.2;
private int duration = 250;
private float mLastMotionX;
private int mActivePointerId;
private View[] views;
private int size;
private int current;
private RelativeLayout root;
private boolean isScrolling;
private boolean isScrollingRight;
private boolean isPrepared;
private ScrollerRunner scroller;
private int topPage;
private OnPageChangeListener onPageChangeListener;
private boolean scrollingByTouch = true;
public StackedView(Context context) {
super(context);
initStackedViews(context, this, 0);
}
public StackedView(Context context, int initialPage) {
super(context);
initStackedViews(context, this, initialPage);
}
public StackedView(Context context, RelativeLayout root) {
super(context);
initStackedViews(context, root, 0);
if (root != this) {
addView(root);
}
}
public StackedView(Context context, RelativeLayout root, int initialPage) {
super(context);
initStackedViews(context, root, initialPage);
if (root != this) {
addView(root);
}
}
public StackedView(Context context, AttributeSet attrs) {
super(context, attrs);
initStackedViews(context, this, 0);
}
public StackedView(Context context, AttributeSet attrs, int initialPage) {
super(context);
initStackedViews(context, this, initialPage);
}
public StackedView(Context context, AttributeSet attrs, RelativeLayout root) {
super(context);
initStackedViews(context, root, 0);
if (root != this) {
addView(root);
}
}
public StackedView(Context context, AttributeSet attrs, RelativeLayout root, int initialPage) {
super(context);
initStackedViews(context, root, initialPage);
if (root != this) {
addView(root);
}
}
public void setRoot(RelativeLayout r) {
root = r;
initStackedViews(getContext(), root, 0);
}
public RelativeLayout getRoot() {
return root;
}
public StackedView setTopPage(int topPage) {
this.topPage = topPage;
return this;
}
public int getTopPage() {
return topPage;
}
public void setScrollingByTouch(boolean enabled) {
this.scrollingByTouch = enabled;
}
/**
* Currently only onPageSelected is implemented.
*
* @param onPageChangeListener
*/
public void setOnPageChangedListener(OnPageChangeListener onPageChangeListener) {
this.onPageChangeListener = onPageChangeListener;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
mLastMotionX = ev.getX();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
break;
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
mLastMotionX = x;
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
}
boolean intercept=super.onInterceptTouchEvent(ev);
debug("onInterceptTouchEvent: "+intercept);
this.onTouchEvent(ev);
return false;
}
public void setCurrent(int current){
this.current=current;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
boolean callSuper = false;
if (size == 0) {
debug("Size == 0");
return super.onTouchEvent(ev);
}
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
debug("Down detected");
mLastMotionX = ev.getX();
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
}
case MotionEventCompat.ACTION_POINTER_DOWN: {
debug("Pointer Down detected");
callSuper = true;
final int index = MotionEventCompat.getActionIndex(ev);
final float x = MotionEventCompat.getX(ev, index);
mLastMotionX = x;
mActivePointerId = MotionEventCompat.getPointerId(ev, index);
break;
}
case MotionEvent.ACTION_MOVE: {
debug("Pointer Move detected.");
if (!scrollingByTouch) {
break;
}
if ((!isScrolling) && mActivePointerId != -1) {
// Scroll to follow the motion event
final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float x = MotionEventCompat.getX(ev, activePointerIndex);
fixZzIndex(mLastMotionX - x);
mLastMotionX = x;
} else {
debug("Did not perform move because scrolling :" + isScrolling + " and pointerId: " + mActivePointerId);
}
scrollToInternal(false);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
debug("Pointer Up or Cancel detected");
mActivePointerId = -1;
scrollToInternal(true);
break;
}
case MotionEvent.ACTION_POINTER_UP: {
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastMotionX = ev.getX(newPointerIndex);
mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
callSuper = true;
}
break;
}
}
if (callSuper) {
// TODO
}
return true;
}
@Override
public void invalidate() {
initStackedViews(getContext(), root, current);
super.invalidate();
}
@Override
public void addView(View child) {
if (root == this) {
addStackedView(child);
} else {
super.addView(child);
}
}
/**
* Add view to root and reinitialize the view.
*
* @param child
*/
public void addStackedView(View child) {
if (root == this) {
super.addView(child);
} else {
root.addView(child);
}
initStackedViews(getContext(), getRoot(), current);
}
// INTERNAL *************************************
private void initStackedViews(Context context, RelativeLayout relativeLayout, int initialIndex) {
if (relativeLayout == null) {
return;
}
int cCount = relativeLayout.getChildCount();
this.views = new View[cCount];
for (int i = 0; i < cCount; i++) {
views[i] = relativeLayout.getChildAt(i);
}
size = views.length;
setInitialViewIndex(initialIndex);
setIsScrolling(false);
isPrepared = false;
root = relativeLayout;
topPage = -1;
}
private void setInitialViewIndex(int n) {
if (n != 0 && n >= size) {
throw new IllegalArgumentException("N is greater than the number of views");
}
for (int i = 0; i < size; i++) {
views[i].setVisibility(i == n ? View.VISIBLE : View.GONE);
}
current = n;
}
private void fixZzIndexLeft(float deltaX) {
debug("Fix to left, current: " + current);
RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
params.leftMargin -= deltaX;
params.rightMargin += deltaX;
views[current].setLayoutParams(params);
if (params.leftMargin < 0) {
debug("Change to scrolling to right.");
isScrollingRight = true;
prepareScrollingToRight();
}
}
private void prepareScrollingToRight() {
if (topPage == current - 1) {
// TODO
}
debug("Prepared Scrolling To Right");
RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
params.leftMargin = 0;
params.rightMargin = 0;
views[current].setLayoutParams(params);
if (current < size - 1) {
params = (LayoutParams)views[current + 1].getLayoutParams();
params.leftMargin = views[current].getWidth();
params.rightMargin = -params.leftMargin;
views[current].bringToFront();
views[current + 1].setLayoutParams(params);
views[current + 1].setVisibility(View.VISIBLE);
views[current + 1].bringToFront();
}
if (current >= 1) {
views[current - 1].setVisibility(View.GONE);
}
this.isPrepared = true;
}
private void fixZzIndexRight(float deltaX) {
debug("Fix to right, current: " + current);
// Move
if (current < size - 1) {
RelativeLayout.LayoutParams params = (LayoutParams)views[current + 1].getLayoutParams();
params.leftMargin -= deltaX;
params.rightMargin += deltaX;
views[current + 1].setLayoutParams(params);
// Test
if (params.leftMargin > views[current].getWidth()) {
isScrollingRight = false;
prepareScrollingToLeft();
}
} else {
isScrollingRight = false;
prepareScrollingToLeft();
}
}
private void prepareScrollingToLeft() {
debug("Prepared Scrolling To Left");
if (current > 0) {
views[current - 1].bringToFront();
views[current - 1].setVisibility(View.VISIBLE);
}
views[current].bringToFront();
if (current < size - 1) {
views[current + 1].setVisibility(View.GONE);
}
this.isPrepared = true;
}
private void fixZzIndex(float deltaX) {
debug("Moving and Fixing Index.");
boolean toLeft = deltaX < 0;
boolean toRight = deltaX > 0;
if (toRight) {
if (current >= size - 1) {
debug("Return because current>size-1");
return;
}
} else {
if (toLeft && current == 0) {
debug("Return because current==0");
return;
}
}
if (!isPrepared) {
if (toRight) {
isScrollingRight = true;
prepareScrollingToRight();
} else {
if (toLeft) {
isScrollingRight = false;
prepareScrollingToLeft();
}
}
}
if (isPrepared && isScrollingRight) {
fixZzIndexRight(deltaX);
} else {
fixZzIndexLeft(deltaX);
}
}
/**
*
* @return index if needs to snap, else return -1.
*/
private int scrollTestInternal() {
final int width = views[current].getWidth();
if (isScrollingRight) {
if (current >= size - 1) {
return -1;
}
RelativeLayout.LayoutParams params = (LayoutParams)views[current + 1].getLayoutParams();
if (Math.abs(params.leftMargin) <= width * (1 - threshold)) {
return current < size - 1 ? current + 1 : current;
}
} else {
RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
if (Math.abs(params.leftMargin) > width * threshold) {
return current > 0 ? current - 1 : current;
}
}
return -1;
}
private void setIsScrolling(boolean isScrolling) {
debug("setIsScrolling to " + isScrolling);
this.isScrolling = isScrolling;
}
private synchronized void scrollTo(int index) {
if (isScrolling) {
return;
}
debug("Scrolling to from current: " + current + " to " + index + " (" + (isScrollingRight ? "Right" : "Left") + ")");
new Thread(getScroller(index)).start();
}
public synchronized ScrollerRunner getScroller(int index) {
if (scroller == null) {
scroller = new ScrollerRunner();
}
scroller.index = index;
return scroller;
}
private void scrollToInternal(boolean canceled) {
int nextIndex = scrollTestInternal();
if (!canceled) {
if (nextIndex >= 0) {
scrollTo(nextIndex);
}
} else {
scrollTo(current);
}
}
private static void debug(String msg) {
Log.i(TAG, msg);
}
public class ScrollerRunner implements Runnable {
public int index;
@Override
public synchronized void run() {
final boolean isRestoring = current == index;
setIsScrolling(true);
if (isScrollingRight) {
if (current + 1 > size - 1) {
isPrepared = false;
setIsScrolling(false);
return;
}
debug((isRestoring ? " Restoring " : " Scrolling ") + (isRestoring ? "Right" : "Left") + " Currnet: " + current);
final RelativeLayout.LayoutParams params = (LayoutParams)views[current + 1].getLayoutParams();
int totalDistance = isRestoring ? (views[current].getWidth() - params.leftMargin) : (params.leftMargin); // >0
int distance = Math.abs(totalDistance / (isRestoring ? duration / 3 : duration));
if (distance == 0) {
distance = 1;
}
boolean needsMore;
if (isRestoring) {
needsMore = params.leftMargin < views[current].getWidth();
} else {
needsMore = params.leftMargin > 0;
}
while (needsMore) {
params.leftMargin += isRestoring ? distance : -distance;
params.rightMargin = -params.leftMargin;
if (isRestoring) {
needsMore = params.leftMargin < views[current].getWidth();
} else {
needsMore = params.leftMargin > 0;
}
views[current + 1].post(new Runnable() {
@Override
public void run() {
views[current + 1].setLayoutParams(params);
}
});
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
views[current + 1].post(new Runnable() {
@Override
public void run() {
// ScrollRight And Not restoring
if (isRestoring) {
RelativeLayout.LayoutParams params = (LayoutParams)views[current + 1].getLayoutParams();
params.leftMargin = views[current].getWidth();
params.rightMargin = -params.leftMargin;
views[current + 1].setLayoutParams(params);
} else {
debug("Set Current Index to " + index);
current = index;
if (onPageChangeListener != null) {
onPageChangeListener.onPageSelected(index);
}
RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
params.leftMargin = 0;
params.rightMargin = 0;
views[current].setLayoutParams(params);
views[current].bringToFront();
}
isPrepared = false;
setIsScrolling(false);
}
});
} else {
if (current < 1) {
isPrepared = false;
setIsScrolling(false);
return;
}
final RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
debug((isRestoring ? " Restoring " : " Scrolling ") + (isRestoring ? "Left" : "Right"));
int totalDistance = isRestoring ? (params.leftMargin) : (views[current - 1].getWidth() - params.leftMargin);
int distance = Math.abs(totalDistance / (isRestoring ? duration / 3 : duration));
if (distance == 0) {
distance = 1;
}
boolean needsMore;
if (isRestoring) {
needsMore = params.leftMargin > 0;
} else {
needsMore = params.leftMargin < views[current - 1].getWidth();
}
while (needsMore) {
params.leftMargin += isRestoring ? -distance : distance;
params.rightMargin = -params.leftMargin;
if (isRestoring) {
needsMore = params.leftMargin > 0;
} else {
needsMore = Math.abs(params.leftMargin) < views[current - 1].getWidth();
}
views[current].post(new Runnable() {
@Override
public void run() {
views[current].setLayoutParams(params);
}
});
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
debug("Finished scrolling.");
views[current].post(new Runnable() {
@Override
public void run() {
// ScrollRight And Not restoring
if (isRestoring) {
RelativeLayout.LayoutParams params = (LayoutParams)views[current].getLayoutParams();
params.leftMargin = 0;
params.rightMargin = 0;
views[current].setLayoutParams(params);
} else {
debug("Set Current Index to " + index);
views[current].setVisibility(View.GONE);
current = index;
if (onPageChangeListener != null) {
onPageChangeListener.onPageSelected(index);
}
views[current].setVisibility(View.VISIBLE);
views[current].bringToFront();
}
isPrepared = false;
setIsScrolling(false);
}
});
}
}
}
}