package com.basic.security.widget;
|
|
import android.annotation.SuppressLint;
|
import android.annotation.TargetApi;
|
import android.content.Context;
|
import android.content.res.TypedArray;
|
import android.database.DataSetObserver;
|
import android.graphics.Canvas;
|
import android.graphics.Rect;
|
import android.graphics.drawable.Drawable;
|
import android.os.Build;
|
import android.os.Bundle;
|
import android.os.Parcelable;
|
import android.support.v4.view.ViewCompat;
|
import android.support.v4.widget.EdgeEffectCompat;
|
import android.util.AttributeSet;
|
import android.view.GestureDetector;
|
import android.view.HapticFeedbackConstants;
|
import android.view.MotionEvent;
|
import android.view.View;
|
import android.view.ViewGroup;
|
import android.widget.AdapterView;
|
import android.widget.ListAdapter;
|
import android.widget.ListView;
|
import android.widget.ScrollView;
|
import android.widget.Scroller;
|
|
import java.util.ArrayList;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.Queue;
|
|
public class HorizontalListView extends AdapterView<ListAdapter> {
|
private static final int INSERT_AT_END_OF_LIST = -1;
|
private static final int INSERT_AT_START_OF_LIST = 0;
|
private static final float FLING_DEFAULT_ABSORB_VELOCITY = 30f;
|
private static final float FLING_FRICTION = 0.009f;
|
private static final String BUNDLE_ID_CURRENT_X = "BUNDLE_ID_CURRENT_X";
|
private static final String BUNDLE_ID_PARENT_STATE = "BUNDLE_ID_PARENT_STATE";
|
private final GestureListener mGestureListener = new GestureListener();
|
protected Scroller mFlingTracker = new Scroller(getContext());
|
protected ListAdapter mAdapter;
|
protected int mCurrentX;
|
protected int mNextX;
|
private GestureDetector mGestureDetector;
|
private int mDisplayOffset;
|
private List<Queue<View>> mRemovedViewsCache = new ArrayList<Queue<View>>();
|
private boolean mDataChanged = false;
|
private Rect mRect = new Rect();
|
private View mViewBeingTouched = null;
|
private int mDividerWidth = 0;
|
private Drawable mDivider = null;
|
private Integer mRestoreX = null;
|
private int mMaxX = Integer.MAX_VALUE;
|
private int mLeftViewAdapterIndex;
|
private int mRightViewAdapterIndex;
|
private int mCurrentlySelectedAdapterIndex;
|
private RunningOutOfDataListener mRunningOutOfDataListener = null;
|
private int mRunningOutOfDataThreshold = 0;
|
private boolean mHasNotifiedRunningLowOnData = false;
|
private OnScrollStateChangedListener mOnScrollStateChangedListener = null;
|
private OnScrollStateChangedListener.ScrollState mCurrentScrollState = OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE;
|
private EdgeEffectCompat mEdgeGlowLeft;
|
private EdgeEffectCompat mEdgeGlowRight;
|
private int mHeightMeasureSpec;
|
private boolean mBlockTouchAction = false;
|
private boolean mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = false;
|
private OnClickListener mOnClickListener;
|
private DataSetObserver mAdapterDataObserver = new DataSetObserver() {
|
public void onChanged() {
|
mDataChanged = true;
|
mHasNotifiedRunningLowOnData = false;
|
unpressTouchedChild();
|
invalidate();
|
requestLayout();
|
}
|
|
public void onInvalidated() {
|
mHasNotifiedRunningLowOnData = false;
|
unpressTouchedChild();
|
reset();
|
invalidate();
|
requestLayout();
|
}
|
};
|
private Runnable mDelayedLayout = () -> requestLayout();
|
|
public HorizontalListView(Context context, AttributeSet attrs) {
|
super(context, attrs);
|
mEdgeGlowLeft = new EdgeEffectCompat(context);
|
mEdgeGlowRight = new EdgeEffectCompat(context);
|
mGestureDetector = new GestureDetector(context, mGestureListener);
|
bindGestureDetector();
|
initView();
|
retrieveXmlConfiguration(context, attrs);
|
setWillNotDraw(false);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
HoneycombPlus.setFriction(mFlingTracker, FLING_FRICTION);
|
}
|
}
|
|
public void setMeasuredDimension1(int measuredWidth, int measuredHeight) {
|
setMeasuredDimension(measuredWidth, measuredHeight);
|
}
|
|
private void bindGestureDetector() {
|
final View.OnTouchListener gestureListenerHandler = (v, event) -> {
|
return mGestureDetector.onTouchEvent(event);
|
};
|
setOnTouchListener(gestureListenerHandler);
|
}
|
|
private void requestParentListViewToNotInterceptTouchEvents(Boolean disallowIntercept) {
|
if (mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent != disallowIntercept) {
|
View view = this;
|
while (view.getParent() instanceof View) {
|
if (view.getParent() instanceof ListView || view.getParent() instanceof ScrollView) {
|
view.getParent().requestDisallowInterceptTouchEvent(disallowIntercept);
|
mIsParentVerticiallyScrollableViewDisallowingInterceptTouchEvent = disallowIntercept;
|
return;
|
}
|
view = (View) view.getParent();
|
}
|
}
|
}
|
|
private void retrieveXmlConfiguration(Context context, AttributeSet attrs) {
|
if (attrs != null) {
|
TypedArray a = context.obtainStyledAttributes(attrs, com.basic.security.utils.RUtils.R_styleable_HorizontalListView);
|
final Drawable d = a.getDrawable(com.basic.security.utils.RUtils.R_styleable_HorizontalListView_android_divider);
|
if (d != null) {
|
setDivider(d);
|
}
|
final int dividerWidth = a.getDimensionPixelSize(com.basic.security.utils.RUtils.R_styleable_HorizontalListView_dividerWidth, 0);
|
if (dividerWidth != 0) {
|
setDividerWidth(dividerWidth);
|
}
|
a.recycle();
|
}
|
}
|
|
public Parcelable onSaveInstanceState() {
|
Bundle bundle = new Bundle();
|
bundle.putParcelable(BUNDLE_ID_PARENT_STATE, super.onSaveInstanceState());
|
bundle.putInt(BUNDLE_ID_CURRENT_X, mCurrentX);
|
return bundle;
|
}
|
|
public void onRestoreInstanceState(Parcelable state) {
|
if (state instanceof Bundle) {
|
Bundle bundle = (Bundle) state;
|
mRestoreX = Integer.valueOf((bundle.getInt(BUNDLE_ID_CURRENT_X)));
|
super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_ID_PARENT_STATE));
|
}
|
}
|
|
public void setDivider(Drawable divider) {
|
mDivider = divider;
|
if (divider != null) {
|
setDividerWidth(divider.getIntrinsicWidth());
|
} else {
|
setDividerWidth(0);
|
}
|
}
|
|
public void setDividerWidth(int width) {
|
mDividerWidth = width;
|
requestLayout();
|
invalidate();
|
}
|
|
private void initView() {
|
mLeftViewAdapterIndex = -1;
|
mRightViewAdapterIndex = -1;
|
mDisplayOffset = 0;
|
mCurrentX = 0;
|
mNextX = 0;
|
mMaxX = Integer.MAX_VALUE;
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
}
|
|
private void reset() {
|
initView();
|
removeAllViewsInLayout();
|
requestLayout();
|
}
|
|
public void setSelection(int position) {
|
mCurrentlySelectedAdapterIndex = position;
|
}
|
|
public View getSelectedView() {
|
return getChild(mCurrentlySelectedAdapterIndex);
|
}
|
|
public ListAdapter getAdapter() {
|
return mAdapter;
|
}
|
|
public void setAdapter(ListAdapter adapter) {
|
if (mAdapter != null) {
|
mAdapter.unregisterDataSetObserver(mAdapterDataObserver);
|
}
|
if (adapter != null) {
|
mHasNotifiedRunningLowOnData = false;
|
mAdapter = adapter;
|
mAdapter.registerDataSetObserver(mAdapterDataObserver);
|
}
|
initializeRecycledViewCache(mAdapter.getViewTypeCount());
|
reset();
|
}
|
|
private void initializeRecycledViewCache(int viewTypeCount) {
|
mRemovedViewsCache.clear();
|
for (int i = 0; i < viewTypeCount; i++) {
|
mRemovedViewsCache.add(new LinkedList<View>());
|
}
|
}
|
|
private View getRecycledView(int adapterIndex) {
|
int itemViewType = mAdapter.getItemViewType(adapterIndex);
|
if (isItemViewTypeValid(itemViewType)) {
|
return mRemovedViewsCache.get(itemViewType).poll();
|
}
|
return null;
|
}
|
|
private void recycleView(int adapterIndex, View view) {
|
int itemViewType = mAdapter.getItemViewType(adapterIndex);
|
if (isItemViewTypeValid(itemViewType)) {
|
mRemovedViewsCache.get(itemViewType).offer(view);
|
}
|
}
|
|
private boolean isItemViewTypeValid(int itemViewType) {
|
return itemViewType < mRemovedViewsCache.size();
|
}
|
|
private void addAndMeasureChild(final View child, int viewPos) {
|
LayoutParams params = getLayoutParams(child);
|
addViewInLayout(child, viewPos, params, true);
|
measureChild(child);
|
}
|
|
private void measureChild(View child) {
|
ViewGroup.LayoutParams childLayoutParams = getLayoutParams(child);
|
int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, getPaddingTop() + getPaddingBottom(), childLayoutParams.height);
|
int childWidthSpec;
|
if (childLayoutParams.width > 0) {
|
childWidthSpec = MeasureSpec.makeMeasureSpec(childLayoutParams.width, MeasureSpec.EXACTLY);
|
} else {
|
childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
|
}
|
child.measure(childWidthSpec, childHeightSpec);
|
}
|
|
private ViewGroup.LayoutParams getLayoutParams(View child) {
|
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
|
if (layoutParams == null) {
|
layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
}
|
return layoutParams;
|
}
|
|
@SuppressLint("WrongCall")
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
super.onLayout(changed, left, top, right, bottom);
|
if (mAdapter == null) {
|
return;
|
}
|
invalidate();
|
if (mDataChanged) {
|
int oldCurrentX = mCurrentX;
|
initView();
|
removeAllViewsInLayout();
|
mNextX = oldCurrentX;
|
mDataChanged = false;
|
}
|
if (mRestoreX != null) {
|
mNextX = mRestoreX;
|
mRestoreX = null;
|
}
|
if (mFlingTracker.computeScrollOffset()) {
|
mNextX = mFlingTracker.getCurrX();
|
}
|
if (mNextX < 0) {
|
mNextX = 0;
|
if (mEdgeGlowLeft.isFinished()) {
|
mEdgeGlowLeft.onAbsorb((int) determineFlingAbsorbVelocity());
|
}
|
mFlingTracker.forceFinished(true);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
} else if (mNextX > mMaxX) {
|
mNextX = mMaxX;
|
if (mEdgeGlowRight.isFinished()) {
|
mEdgeGlowRight.onAbsorb((int) determineFlingAbsorbVelocity());
|
}
|
mFlingTracker.forceFinished(true);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
}
|
int dx = mCurrentX - mNextX;
|
removeNonVisibleChildren(dx);
|
fillList(dx);
|
positionChildren(dx);
|
mCurrentX = mNextX;
|
if (determineMaxX()) {
|
onLayout(changed, left, top, right, bottom);
|
return;
|
}
|
if (mFlingTracker.isFinished()) {
|
if (mCurrentScrollState == OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING) {
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
}
|
} else {
|
ViewCompat.postOnAnimation(this, mDelayedLayout);
|
}
|
}
|
|
protected float getLeftFadingEdgeStrength() {
|
int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
|
if (mCurrentX == 0) {
|
return 0;
|
} else if (mCurrentX < horizontalFadingEdgeLength) {
|
return (float) mCurrentX / horizontalFadingEdgeLength;
|
} else {
|
return 1;
|
}
|
}
|
|
protected float getRightFadingEdgeStrength() {
|
int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
|
if (mCurrentX == mMaxX) {
|
return 0;
|
} else if ((mMaxX - mCurrentX) < horizontalFadingEdgeLength) {
|
return (float) (mMaxX - mCurrentX) / horizontalFadingEdgeLength;
|
} else {
|
return 1;
|
}
|
}
|
|
private float determineFlingAbsorbVelocity() {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
return IceCreamSandwichPlus.getCurrVelocity(mFlingTracker);
|
} else {
|
return FLING_DEFAULT_ABSORB_VELOCITY;
|
}
|
}
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
mHeightMeasureSpec = heightMeasureSpec;
|
}
|
|
private boolean determineMaxX() {
|
if (isLastItemInAdapter(mRightViewAdapterIndex)) {
|
View rightView = getRightmostChild();
|
if (rightView != null) {
|
int oldMaxX = mMaxX;
|
mMaxX = mCurrentX + (rightView.getRight() - getPaddingLeft()) - getRenderWidth();
|
if (mMaxX < 0) {
|
mMaxX = 0;
|
}
|
return mMaxX != oldMaxX;
|
}
|
}
|
return false;
|
}
|
|
private void fillList(final int dx) {
|
int edge = 0;
|
View child = getRightmostChild();
|
if (child != null) {
|
edge = child.getRight();
|
}
|
fillListRight(edge, dx);
|
edge = 0;
|
child = getLeftmostChild();
|
if (child != null) {
|
edge = child.getLeft();
|
}
|
fillListLeft(edge, dx);
|
}
|
|
private void removeNonVisibleChildren(final int dx) {
|
View child = getLeftmostChild();
|
while (child != null && child.getRight() + dx <= 0) {
|
mDisplayOffset += isLastItemInAdapter(mLeftViewAdapterIndex) ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
|
recycleView(mLeftViewAdapterIndex, child);
|
removeViewInLayout(child);
|
mLeftViewAdapterIndex++;
|
child = getLeftmostChild();
|
}
|
child = getRightmostChild();
|
while (child != null && child.getLeft() + dx >= getWidth()) {
|
recycleView(mRightViewAdapterIndex, child);
|
removeViewInLayout(child);
|
mRightViewAdapterIndex--;
|
child = getRightmostChild();
|
}
|
}
|
|
private void fillListRight(int rightEdge, final int dx) {
|
while (rightEdge + dx + mDividerWidth < getWidth() && mRightViewAdapterIndex + 1 < mAdapter.getCount()) {
|
mRightViewAdapterIndex++;
|
if (mLeftViewAdapterIndex < 0) {
|
mLeftViewAdapterIndex = mRightViewAdapterIndex;
|
}
|
View child = mAdapter.getView(mRightViewAdapterIndex, getRecycledView(mRightViewAdapterIndex), this);
|
addAndMeasureChild(child, INSERT_AT_END_OF_LIST);
|
rightEdge += (mRightViewAdapterIndex == 0 ? 0 : mDividerWidth) + child.getMeasuredWidth();
|
determineIfLowOnData();
|
}
|
}
|
|
private void fillListLeft(int leftEdge, final int dx) {
|
while (leftEdge + dx - mDividerWidth > 0 && mLeftViewAdapterIndex >= 1) {
|
mLeftViewAdapterIndex--;
|
View child = mAdapter.getView(mLeftViewAdapterIndex, getRecycledView(mLeftViewAdapterIndex), this);
|
addAndMeasureChild(child, INSERT_AT_START_OF_LIST);
|
leftEdge -= mLeftViewAdapterIndex == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
|
mDisplayOffset -= leftEdge + dx == 0 ? child.getMeasuredWidth() : mDividerWidth + child.getMeasuredWidth();
|
}
|
}
|
|
private void positionChildren(final int dx) {
|
int childCount = getChildCount();
|
if (childCount > 0) {
|
mDisplayOffset += dx;
|
int leftOffset = mDisplayOffset;
|
for (int i = 0; i < childCount; i++) {
|
View child = getChildAt(i);
|
int left = leftOffset + getPaddingLeft();
|
int top = getPaddingTop();
|
int right = left + child.getMeasuredWidth();
|
int bottom = top + child.getMeasuredHeight();
|
child.layout(left, top, right, bottom);
|
leftOffset += child.getMeasuredWidth() + mDividerWidth;
|
}
|
}
|
}
|
|
private View getLeftmostChild() {
|
return getChildAt(0);
|
}
|
|
private View getRightmostChild() {
|
return getChildAt(getChildCount() - 1);
|
}
|
|
private View getChild(int adapterIndex) {
|
if (adapterIndex >= mLeftViewAdapterIndex && adapterIndex <= mRightViewAdapterIndex) {
|
return getChildAt(adapterIndex - mLeftViewAdapterIndex);
|
}
|
return null;
|
}
|
|
private int getChildIndex(final int x, final int y) {
|
int childCount = getChildCount();
|
for (int index = 0; index < childCount; index++) {
|
getChildAt(index).getHitRect(mRect);
|
if (mRect.contains(x, y)) {
|
return index;
|
}
|
}
|
return -1;
|
}
|
|
private boolean isLastItemInAdapter(int index) {
|
return index == mAdapter.getCount() - 1;
|
}
|
|
/**
|
* Gets the height in px this view will be rendered. (padding removed)
|
*/
|
private int getRenderHeight() {
|
return getHeight() - getPaddingTop() - getPaddingBottom();
|
}
|
|
/**
|
* Gets the width in px this view will be rendered. (padding removed)
|
*/
|
private int getRenderWidth() {
|
return getWidth() - getPaddingLeft() - getPaddingRight();
|
}
|
|
/**
|
* Scroll to the provided offset
|
*/
|
public void scrollTo(int x) {
|
mFlingTracker.startScroll(mNextX, 0, x - mNextX, 0);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING);
|
requestLayout();
|
}
|
|
public int getFirstVisiblePosition() {
|
return mLeftViewAdapterIndex;
|
}
|
|
public int getLastVisiblePosition() {
|
return mRightViewAdapterIndex;
|
}
|
|
/**
|
* Draws the overscroll edge glow effect on the left and right sides of the horizontal list
|
*/
|
private void drawEdgeGlow(Canvas canvas) {
|
if (mEdgeGlowLeft != null && !mEdgeGlowLeft.isFinished() && isEdgeGlowEnabled()) {
|
|
final int restoreCount = canvas.save();
|
final int height = getHeight();
|
canvas.rotate(-90, 0, 0);
|
canvas.translate(-height + getPaddingBottom(), 0);
|
mEdgeGlowLeft.setSize(getRenderHeight(), getRenderWidth());
|
if (mEdgeGlowLeft.draw(canvas)) {
|
invalidate();
|
}
|
canvas.restoreToCount(restoreCount);
|
} else if (mEdgeGlowRight != null && !mEdgeGlowRight.isFinished() && isEdgeGlowEnabled()) {
|
|
final int restoreCount = canvas.save();
|
final int width = getWidth();
|
canvas.rotate(90, 0, 0);
|
canvas.translate(getPaddingTop(), -width);
|
mEdgeGlowRight.setSize(getRenderHeight(), getRenderWidth());
|
if (mEdgeGlowRight.draw(canvas)) {
|
invalidate();
|
}
|
canvas.restoreToCount(restoreCount);
|
}
|
}
|
|
/**
|
* Draws the dividers that go in between the horizontal list view items
|
*/
|
private void drawDividers(Canvas canvas) {
|
final int count = getChildCount();
|
|
final Rect bounds = mRect;
|
mRect.top = getPaddingTop();
|
mRect.bottom = mRect.top + getRenderHeight();
|
|
for (int i = 0; i < count; i++) {
|
|
if (!(i == count - 1 && isLastItemInAdapter(mRightViewAdapterIndex))) {
|
View child = getChildAt(i);
|
bounds.left = child.getRight();
|
bounds.right = child.getRight() + mDividerWidth;
|
|
if (bounds.left < getPaddingLeft()) {
|
bounds.left = getPaddingLeft();
|
}
|
|
if (bounds.right > getWidth() - getPaddingRight()) {
|
bounds.right = getWidth() - getPaddingRight();
|
}
|
|
drawDivider(canvas, bounds);
|
|
|
if (i == 0 && child.getLeft() > getPaddingLeft()) {
|
bounds.left = getPaddingLeft();
|
bounds.right = child.getLeft();
|
drawDivider(canvas, bounds);
|
}
|
}
|
}
|
}
|
|
/**
|
* Draws a divider in the given bounds.
|
*
|
* @param canvas The canvas to draw to.
|
* @param bounds The bounds of the divider.
|
*/
|
private void drawDivider(Canvas canvas, Rect bounds) {
|
if (mDivider != null) {
|
mDivider.setBounds(bounds);
|
mDivider.draw(canvas);
|
}
|
}
|
|
protected void onDraw(Canvas canvas) {
|
super.onDraw(canvas);
|
drawDividers(canvas);
|
}
|
|
protected void dispatchDraw(Canvas canvas) {
|
super.dispatchDraw(canvas);
|
drawEdgeGlow(canvas);
|
}
|
|
protected void dispatchSetPressed(boolean pressed) {
|
|
|
}
|
|
protected boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
mFlingTracker.fling(mNextX, 0, (int) -velocityX, 0, 0, mMaxX, 0, 0);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_FLING);
|
requestLayout();
|
return true;
|
}
|
|
protected boolean onDown(MotionEvent e) {
|
|
mBlockTouchAction = !mFlingTracker.isFinished();
|
|
mFlingTracker.forceFinished(true);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
unpressTouchedChild();
|
if (!mBlockTouchAction) {
|
|
final int index = getChildIndex((int) e.getX(), (int) e.getY());
|
if (index >= 0) {
|
|
mViewBeingTouched = getChildAt(index);
|
if (mViewBeingTouched != null) {
|
|
mViewBeingTouched.setPressed(true);
|
refreshDrawableState();
|
}
|
}
|
}
|
return true;
|
}
|
|
/**
|
* If a view is currently pressed then unpress it
|
*/
|
private void unpressTouchedChild() {
|
if (mViewBeingTouched != null) {
|
|
mViewBeingTouched.setPressed(false);
|
refreshDrawableState();
|
|
mViewBeingTouched = null;
|
}
|
}
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
if (event.getAction() == MotionEvent.ACTION_UP) {
|
|
if (mFlingTracker == null || mFlingTracker.isFinished()) {
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_IDLE);
|
}
|
|
requestParentListViewToNotInterceptTouchEvents(false);
|
releaseEdgeGlow();
|
} else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
|
unpressTouchedChild();
|
releaseEdgeGlow();
|
|
requestParentListViewToNotInterceptTouchEvents(false);
|
}
|
return super.onTouchEvent(event);
|
}
|
|
/**
|
* Release the EdgeGlow so it animates
|
*/
|
private void releaseEdgeGlow() {
|
if (mEdgeGlowLeft != null) {
|
mEdgeGlowLeft.onRelease();
|
}
|
if (mEdgeGlowRight != null) {
|
mEdgeGlowRight.onRelease();
|
}
|
}
|
|
/**
|
* Sets a listener to be called when the HorizontalListView has been scrolled to a point where it is
|
* running low on data. An example use case is wanting to auto download more data when the user
|
* has scrolled to the point where only 10 items are left to be rendered off the right of the
|
* screen. To get called back at that point just register with this function with a
|
* numberOfItemsLeftConsideredLow value of 10. <br>
|
* <br>
|
* This will only be called once to notify that the HorizontalListView is running low on data.
|
* Calling notifyDataSetChanged on the adapter will allow this to be called again once low on data.
|
*
|
* @param listener The listener to be notified when the number of array adapters items left to
|
* be shown is running low.
|
* @param numberOfItemsLeftConsideredLow The number of array adapter items that have not yet
|
* been displayed that is considered too low.
|
*/
|
public void setRunningOutOfDataListener(RunningOutOfDataListener listener, int numberOfItemsLeftConsideredLow) {
|
mRunningOutOfDataListener = listener;
|
mRunningOutOfDataThreshold = numberOfItemsLeftConsideredLow;
|
}
|
|
/**
|
* Determines if we are low on data and if so will call to notify the listener, if there is one,
|
* that we are running low on data.
|
*/
|
private void determineIfLowOnData() {
|
|
if (mRunningOutOfDataListener != null && mAdapter != null &&
|
mAdapter.getCount() - (mRightViewAdapterIndex + 1) < mRunningOutOfDataThreshold) {
|
|
if (!mHasNotifiedRunningLowOnData) {
|
mHasNotifiedRunningLowOnData = true;
|
mRunningOutOfDataListener.onRunningOutOfData();
|
}
|
}
|
}
|
|
public void setOnClickListener(OnClickListener listener) {
|
mOnClickListener = listener;
|
}
|
|
public void setOnScrollStateChangedListener(OnScrollStateChangedListener listener) {
|
mOnScrollStateChangedListener = listener;
|
}
|
|
private void setCurrentScrollState(OnScrollStateChangedListener.ScrollState newScrollState) {
|
|
if (mCurrentScrollState != newScrollState && mOnScrollStateChangedListener != null) {
|
mOnScrollStateChangedListener.onScrollStateChanged(newScrollState);
|
}
|
mCurrentScrollState = newScrollState;
|
}
|
|
private void updateOverscrollAnimation(final int scrolledOffset) {
|
if (mEdgeGlowLeft == null || mEdgeGlowRight == null) return;
|
|
int nextScrollPosition = mCurrentX + scrolledOffset;
|
|
if (mFlingTracker == null || mFlingTracker.isFinished()) {
|
|
if (nextScrollPosition < 0) {
|
|
int overscroll = Math.abs(scrolledOffset);
|
|
mEdgeGlowLeft.onPull((float) overscroll / getRenderWidth());
|
|
if (!mEdgeGlowRight.isFinished()) {
|
mEdgeGlowRight.onRelease();
|
}
|
} else if (nextScrollPosition > mMaxX) {
|
|
|
int overscroll = Math.abs(scrolledOffset);
|
|
mEdgeGlowRight.onPull((float) overscroll / getRenderWidth());
|
|
if (!mEdgeGlowLeft.isFinished()) {
|
mEdgeGlowLeft.onRelease();
|
}
|
}
|
}
|
}
|
|
/**
|
* Checks if the edge glow should be used enabled.
|
* The glow is not enabled unless there are more views than can fit on the screen at one time.
|
*/
|
private boolean isEdgeGlowEnabled() {
|
if (mAdapter == null || mAdapter.isEmpty()) return false;
|
|
return mMaxX > 0;
|
}
|
|
/**
|
* This listener is used to allow notification when the HorizontalListView is running low on data to display.
|
*/
|
public interface RunningOutOfDataListener {
|
/**
|
* Called when the HorizontalListView is running out of data and has reached at least the provided threshold.
|
*/
|
void onRunningOutOfData();
|
}
|
|
/**
|
* Interface definition for a callback to be invoked when the view scroll state has changed.
|
*/
|
public interface OnScrollStateChangedListener {
|
/**
|
* Callback method to be invoked when the scroll state changes.
|
*
|
* @param scrollState The current scroll state.
|
*/
|
void onScrollStateChanged(ScrollState scrollState);
|
|
enum ScrollState {
|
/**
|
* The view is not scrolling. Note navigating the list using the trackball counts as being
|
* in the idle state since these transitions are not animated.
|
*/
|
SCROLL_STATE_IDLE,
|
/**
|
* The user is scrolling using touch, and their finger is still on the screen
|
*/
|
SCROLL_STATE_TOUCH_SCROLL,
|
/**
|
* The user had previously been scrolling using touch and had performed a fling. The
|
* animation is now coasting to a stop
|
*/
|
SCROLL_STATE_FLING
|
}
|
}
|
|
@TargetApi(11)
|
/** Wrapper class to protect access to API version 11 and above features */
|
private static final class HoneycombPlus {
|
static {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
|
throw new RuntimeException("Should not get to HoneycombPlus class unless sdk is >= 11!");
|
}
|
}
|
|
/**
|
* Sets the friction for the provided scroller
|
*/
|
public static void setFriction(Scroller scroller, float friction) {
|
if (scroller != null) {
|
scroller.setFriction(friction);
|
}
|
}
|
}
|
|
@TargetApi(14)
|
/** Wrapper class to protect access to API version 14 and above features */
|
private static final class IceCreamSandwichPlus {
|
static {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
throw new RuntimeException("Should not get to IceCreamSandwichPlus class unless sdk is >= 14!");
|
}
|
}
|
|
/**
|
* Gets the velocity for the provided scroller
|
*/
|
public static float getCurrVelocity(Scroller scroller) {
|
return scroller.getCurrVelocity();
|
}
|
}
|
|
private class GestureListener extends GestureDetector.SimpleOnGestureListener {
|
public boolean onDown(MotionEvent e) {
|
return HorizontalListView.this.onDown(e);
|
}
|
|
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
|
return HorizontalListView.this.onFling(e1, e2, velocityX, velocityY);
|
}
|
|
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
|
requestParentListViewToNotInterceptTouchEvents(true);
|
setCurrentScrollState(OnScrollStateChangedListener.ScrollState.SCROLL_STATE_TOUCH_SCROLL);
|
unpressTouchedChild();
|
mNextX += (int) distanceX;
|
updateOverscrollAnimation(Math.round(distanceX));
|
requestLayout();
|
return true;
|
}
|
|
public boolean onSingleTapConfirmed(MotionEvent e) {
|
unpressTouchedChild();
|
OnItemClickListener onItemClickListener = getOnItemClickListener();
|
final int index = getChildIndex((int) e.getX(), (int) e.getY());
|
|
if (index >= 0 && !mBlockTouchAction) {
|
View child = getChildAt(index);
|
int adapterIndex = mLeftViewAdapterIndex + index;
|
if (onItemClickListener != null) {
|
onItemClickListener.onItemClick(HorizontalListView.this, child, adapterIndex, mAdapter.getItemId(adapterIndex));
|
return true;
|
}
|
}
|
if (mOnClickListener != null && !mBlockTouchAction) {
|
mOnClickListener.onClick(HorizontalListView.this);
|
}
|
return false;
|
}
|
|
public void onLongPress(MotionEvent e) {
|
unpressTouchedChild();
|
final int index = getChildIndex((int) e.getX(), (int) e.getY());
|
if (index >= 0 && !mBlockTouchAction) {
|
View child = getChildAt(index);
|
OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener();
|
if (onItemLongClickListener != null) {
|
int adapterIndex = mLeftViewAdapterIndex + index;
|
boolean handled = onItemLongClickListener.onItemLongClick(HorizontalListView.this, child, adapterIndex, mAdapter
|
.getItemId(adapterIndex));
|
if (handled) {
|
|
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
}
|
}
|
}
|
}
|
}
|
}
|