package com.marshalchen.common.uimodule.listbuddies.views; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.support.v4.widget.AutoScrollHelper; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewStub; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.LinearLayout; import android.widget.ListView; import com.marshalchen.common.uimodule.R; import com.marshalchen.common.commonUtils.logUtils.Logs; import com.marshalchen.common.uimodule.listbuddies.adapters.CircularLoopAdapter; import com.marshalchen.common.uimodule.listbuddies.helpers.ListBuddiesAutoScrollHelper; import com.marshalchen.common.uimodule.listbuddies.provider.ScrollConfigOptions; /** * LinerLayout that contains 2 ListViews. This ListViews auto-scroll while the user is not interacting with them. * When the user interact with one of the ListViews a parrallax effect is created during its scroll. */ public class ListBuddiesLayout extends LinearLayout implements View.OnTouchListener, ObservableListView.ListViewObserverDelegate { private static final String TAG = ListBuddiesLayout.class.getSimpleName(); private static final long PRESS_DELAY = 100; private static final float CANCEL_CLICK_LIMIT = 8; public static final int DEFAULT_SPEED = 2; public static final int ATTR_NOT_SET = -1; private OnBuddyItemClickListener mItemBuddyListener; private ObservableListView mListViewLeft; private ObservableListView mListViewRight; private boolean mActionDown; private boolean isRightListEnabled = false; private boolean isLeftListEnabled = false; private boolean isUserInteracting = true; private boolean isAutoScrollLeftListFaster; private boolean isManualScrollLeftListFaster; private View mDownView; private View mGapView; private ViewStub mViewStubGap; private Rect mRect = new Rect(); private ListBuddiesAutoScrollHelper mScrollHelper; private int[] mListViewCoords = new int[2]; private int mGap; private int mGapColor; private int mSpeed = DEFAULT_SPEED; private int mSpeedLeft; private int mSpeedRight; private int mDividerHeight; private int mLastViewTouchId; private int mDownPosition; private int mChildCount; private float mDownEventY; private Drawable mDivider; public ListBuddiesLayout(Context context) { super(context); init(context); } public ListBuddiesLayout(Context context, AttributeSet attrs) { super(context, attrs); setListBuddiesAttributes(context, attrs); init(context); applyViewParams(); } private void setListBuddiesAttributes(Context context, AttributeSet attrs) { try { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListBuddiesOptions, 0, 0); mGap = a.getDimensionPixelSize(R.styleable.ListBuddiesOptions_gap, 12); mSpeed = a.getInteger(R.styleable.ListBuddiesOptions_speed, DEFAULT_SPEED); int configOptionFasterList = a.getInteger(R.styleable.ListBuddiesOptions_autoScrollFaster, ScrollConfigOptions.LEFT.getConfigValue()); isAutoScrollLeftListFaster = configOptionFasterList == ScrollConfigOptions.LEFT.getConfigValue(); isManualScrollLeftListFaster = a.getInteger(R.styleable.ListBuddiesOptions_scrollFaster, configOptionFasterList) == ScrollConfigOptions.LEFT.getConfigValue(); mDivider = a.getDrawable(R.styleable.ListBuddiesOptions_listsDivider); mDividerHeight = a.getDimensionPixelSize(R.styleable.ListBuddiesOptions_listsDividerHeight, ATTR_NOT_SET); mGapColor = a.getColor(R.styleable.ListBuddiesOptions_gapColor, ATTR_NOT_SET); a.recycle(); } catch (Resources.NotFoundException e) { e.printStackTrace(); Logs.e(e, ""); } } private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.listbuddies, this, true); mListViewLeft = (ObservableListView) findViewById(R.id.list_left); mListViewRight = (ObservableListView) findViewById(R.id.list_right); mLastViewTouchId = mListViewRight.getId(); calibrateSpeed(); setListeners(); setScrollHelpers(); startAutoScroll(); } private void calibrateSpeed() { //Parallax doesnt work with speed 1 if (mSpeed == 1) { mSpeed = DEFAULT_SPEED; } if (isAutoScrollLeftListFaster) { mSpeedLeft = mSpeed; mSpeedRight = mSpeed / 2; } else { mSpeedLeft = mSpeed / 2; mSpeedRight = mSpeed; } } private void setListeners() { mListViewLeft.setOnTouchListener(this); mListViewLeft.setObserver(this); setOnListScrollListener(mListViewLeft, true); mListViewRight.setOnTouchListener(this); mListViewRight.setObserver(this); setOnListScrollListener(mListViewRight, false); } private void setOnListScrollListener(ObservableListView list, final boolean isLeftList) { list.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int state) { switch (state) { case SCROLL_STATE_IDLE: setListState(true, isLeftList); forceScrollIfNeeded(isOtherListEnable(isLeftList)); break; case SCROLL_STATE_TOUCH_SCROLL: setListState(false, isLeftList); isUserInteracting = true; break; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { } }); } private void forceScrollIfNeeded(boolean isListEnabled) { if (isUserInteracting && isListEnabled) { isUserInteracting = false; if (!mActionDown) { forceScroll(); } } } private boolean isOtherListEnable(boolean isLeftList) { boolean result; if (isLeftList) { result = isRightListEnabled; } else { result = isLeftListEnabled; } return result; } private void setListState(boolean isEnabled, boolean isLeftList) { if (isLeftList) { isLeftListEnabled = isEnabled; } else { isRightListEnabled = isEnabled; } } private void setScrollHelpers() { mScrollHelper = new ListBuddiesAutoScrollHelper(mListViewLeft) { @Override public void scrollTargetBy(int deltaX, int deltaY) { mListViewLeft.smoothScrollBy(mSpeedLeft, 0); mListViewRight.smoothScrollBy(mSpeedRight, 0); } @Override public boolean canTargetScrollHorizontally(int i) { return false; } @Override public boolean canTargetScrollVertically(int i) { return true; } }; mScrollHelper.setEnabled(isEnable()); mScrollHelper.setEdgeType(AutoScrollHelper.EDGE_TYPE_OUTSIDE); } private boolean isEnable() { return mSpeed != 0; } private void startAutoScroll() { mListViewLeft.post(new Runnable() { @Override public void run() { forceScroll(); } }); } private void forceScroll() { MotionEvent event = MotionEvent.obtain(System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_MOVE, 570, -1, 0); mScrollHelper.onTouch(mListViewLeft, event); } private void applyViewParams() { setGap(); setDividerAndHeight(mDivider, mDividerHeight); } private void setDividerAndHeight(Drawable drawable, int dividerHeight) { setListsDivider(drawable); setHeightDivider(dividerHeight); } private void setListsDivider(Drawable drawable) { if (drawable != null) { mListViewLeft.setDivider(drawable); mListViewRight.setDivider(drawable); } } private void setHeightDivider(int dividerHeight) { if (dividerHeight > ATTR_NOT_SET) { mListViewLeft.setDividerHeight(dividerHeight); mListViewRight.setDividerHeight(dividerHeight); } } private void setGap() { if (mGapColor != ATTR_NOT_SET) { createGap(); setGapColor(); } else { emptyGap(); } } private void setGapColor() { if (mGapView != null && mGapColor != ATTR_NOT_SET) { mGapView.setBackgroundColor(mGapColor); } } private void emptyGap() { setLeftListMargin(mGap); if (mGapView != null) { mGapView.setVisibility(View.GONE); } } private void setLeftListMargin(int marginRigth) { if (mListViewLeft != null) { LayoutParams params = (LayoutParams) mListViewLeft.getLayoutParams(); params.setMargins(0, 0, marginRigth, 0); mListViewLeft.setLayoutParams(params); } } private void createGap() { if (mViewStubGap == null) { mViewStubGap = (ViewStub) findViewById(R.id.gap); } if (mGapView == null) { mGapView = mViewStubGap.inflate(); } setLeftListMargin(0); setGapWidth(mGap); if (mGapView != null) { mGapView.setVisibility(View.VISIBLE); } } private void setGapWidth(int width) { if (mGapView != null) { LayoutParams params = (LayoutParams) mGapView.getLayoutParams(); params.width = width; mGapView.setLayoutParams(params); } } @Override public boolean onTouch(View v, MotionEvent event) { ListView list = (ListView) v; switch (event.getAction()) { case MotionEvent.ACTION_DOWN: actionDown(list, event); break; case MotionEvent.ACTION_UP: actionUp(list); break; case MotionEvent.ACTION_MOVE: actionMove(event); break; } return mScrollHelper.onTouch(v, event); } private void actionDown(ListView list, MotionEvent event) { mActionDown = true; toogleListView(list); mLastViewTouchId = list.getId(); startClickSelection(event, list, event.getY()); } /** * Each time we touch the opposite ListView than the last one we have selected * we need to activate it as the enable one */ private void toogleListView(View v) { if (mLastViewTouchId != v.getId()) { if (mLastViewTouchId == mListViewLeft.getId()) { isLeftListEnabled = true; isRightListEnabled = false; } else { isLeftListEnabled = false; isRightListEnabled = true; } } } private void startClickSelection(MotionEvent event, ListView list, float eventY) { if (!isUserInteracting || mSpeed == 0) { findViewClicked(event, eventY, list); setSelectorPressed(list); } } private void findViewClicked(MotionEvent event, float eventY, ListView list) { mChildCount = list.getChildCount(); mListViewCoords = new int[2]; list.getLocationOnScreen(mListViewCoords); int x = (int) event.getRawX() - mListViewCoords[0]; int y = (int) event.getRawY() - mListViewCoords[1]; View child; for (int i = 0; i < mChildCount; i++) { child = list.getChildAt(i); child.getHitRect(mRect); if (mRect.contains(x, y)) { mDownView = child; mDownEventY = eventY; break; } } } private void setSelectorPressed(ListView list) { if (mDownView != null) { mDownPosition = list.getPositionForView(mDownView); mDownView.postDelayed(new Runnable() { @Override public void run() { if (isUserInteracting) { if (mDownView != null) { mDownView.setPressed(true); } } } }, PRESS_DELAY); } } private void actionUp(ListView list) { mActionDown = false; performClick(list); } private void actionMove(MotionEvent event) { cancelClick(event.getY()); } private void cancelClick(float eventY) { if (mDownView != null && (Math.abs(mDownEventY - eventY) > CANCEL_CLICK_LIMIT)) { mDownView.setPressed(false); mDownView = null; } } private void performClick(ListView list) { //In some occasions setOnListScrollListener-SCROLL_STATE_TOUCH_SCROLL //doesn't get called when we perform a click, so we need to double check if (!isUserInteracting) { isUserInteracting = true; } if (mDownView != null || mSpeed == 0) { mDownView.setPressed(false); if (mItemBuddyListener != null) { int buddy = 0; if (list.getId() != mListViewLeft.getId()) { buddy = 1; } int position = getPosition(list, mDownPosition); mItemBuddyListener.onBuddyItemClicked(list, mDownView, buddy, position, mDownView.getId()); } } } private int getPosition(ListView list, int position) { return ((CircularLoopAdapter) list.getAdapter()).getCircularPosition(position); } /** * Receives the distance scroll on listView. */ @Override public void onListScroll(View view, float deltaY) { int speed; if (view.getId() == mListViewLeft.getId() && !isLeftListEnabled) { speed = getSpeed(false, deltaY); mListViewRight.smoothScrollBy(speed, 0); } else if (view.getId() == mListViewRight.getId() && !isRightListEnabled) { speed = getSpeed(true, deltaY); mListViewLeft.smoothScrollBy(speed, 0); } } private int getSpeed(boolean isCalculationForLeft, float deltaY) { int speed; if (isManualScrollLeftListFaster && isCalculationForLeft || !isManualScrollLeftListFaster && !isCalculationForLeft) { speed = (int) -deltaY * 2; } else { speed = (int) -deltaY / 2; } return speed; } public interface OnBuddyItemClickListener { //Buddy corresponde with the list (0-left, 1-right) void onBuddyItemClicked(AdapterView<?> parent, View view, int buddy, int position, long id); } /** * Public setters */ public void setOnItemClickListener(OnBuddyItemClickListener listener) { mItemBuddyListener = listener; } public ListBuddiesLayout setGap(int value) { mGap = value; setGap(); return this; } public ListBuddiesLayout setSpeed(int value) { mSpeed = value; calibrateSpeed(); return this; } public ListBuddiesLayout setDividerHeight(int value) { mDividerHeight = value; setDividerAndHeight(mDivider, value); return this; } public ListBuddiesLayout setDivider(Drawable drawable) { mDivider = drawable; setDividerAndHeight(drawable, mDividerHeight); return this; } public ListBuddiesLayout setGapColor(int color) { mGapColor = color; setGap(); return this; } public ListBuddiesLayout setAutoScrollFaster(int option) { isAutoScrollLeftListFaster = option == 1; calibrateSpeed(); return this; } public ListBuddiesLayout setManualScrollFaster(int option) { isManualScrollLeftListFaster = option == 1; return this; } public void setAdapters(CircularLoopAdapter adapter, CircularLoopAdapter adapter2) { mListViewLeft.setAdapter(adapter); mListViewRight.setAdapter(adapter2); mListViewLeft.setSelection(Integer.MAX_VALUE / 2); mListViewRight.setSelection(Integer.MAX_VALUE / 2); } /** * Public getters */ public ListView getListViewLeft() { return mListViewLeft; } public ListView getListViewRight() { return mListViewRight; } public int getGapColor() { return mGapColor; } public int getSpeed() { return mSpeed; } public int getGap() { return mGap; } public int getDividerHeight() { return mDividerHeight; } public Drawable getDivider() { return mDivider; } }