package com.vt.vthacks.view; import com.vt.vthacks.R; import android.content.Context; import android.graphics.drawable.AnimationDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.widget.AbsListView; import android.widget.ImageView; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; /** * Based on https://github.com/johannilsson/android-pulltorefresh * @author wilsonmitchell * */ public class PullToRefreshListView extends ListView implements AbsListView.OnScrollListener { private static enum RefreshState { TAP_TO_REFRESH, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING } private static enum ScrollToBottomState { SCROLL_TO_ACTIVATE, ACTIVATED } private static final String TAG = "PullToRefreshListView"; private OnRefreshListener mOnRefreshListener; private OnScrollToBottomListener onScrollToBottomListener; /** * Listener that will receive notifications every time the list scrolls. */ private OnScrollListener mOnScrollListener; private LayoutInflater mInflater; private RelativeLayout mRefreshView; private ImageView mRefreshViewImage; private TextView mRefreshViewLastUpdated; private TextView mRefreshViewLabel; private AnimationDrawable refreshAnimationDrawable; private int mCurrentScrollState; private RefreshState mRefreshState; private ScrollToBottomState scrollToBottomState; private int mRefreshOriginalTopPadding; private boolean mBounceHack; public PullToRefreshListView(Context context) { super(context); init(context); } public PullToRefreshListView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public PullToRefreshListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context) { // Load all of the animations we need in code rather than through XML mInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mRefreshView = (RelativeLayout) mInflater.inflate( R.layout.pull_to_refresh_header, this, false); mRefreshViewImage = (ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image); mRefreshViewLastUpdated = (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at); mRefreshViewLabel = (TextView) mRefreshView.findViewById(R.id.pull_to_refresh_label); mRefreshViewImage.setImageResource(R.drawable.list_refresh_anim); refreshAnimationDrawable = (AnimationDrawable) mRefreshViewImage.getDrawable(); mRefreshView.setOnClickListener(new OnClickRefreshListener()); mRefreshOriginalTopPadding = mRefreshView.getPaddingTop(); setRefreshState(RefreshState.TAP_TO_REFRESH); scrollToBottomState = ScrollToBottomState.SCROLL_TO_ACTIVATE; addHeaderView(mRefreshView); super.setOnScrollListener(this); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); setSelection(1); } @Override public void setAdapter(ListAdapter adapter) { super.setAdapter(adapter); setSelection(1); } /** * Set the listener that will receive notifications every time the list * scrolls. * * @param l The scroll listener. */ @Override public void setOnScrollListener(AbsListView.OnScrollListener l) { mOnScrollListener = l; } /** * Register a callback to be invoked when this list should be refreshed. * * @param onRefreshListener The callback to run. */ public void setOnRefreshListener(OnRefreshListener onRefreshListener) { mOnRefreshListener = onRefreshListener; } public void setOnScrollToBottomListener(OnScrollToBottomListener onScrollToBottomListener) { this.onScrollToBottomListener = onScrollToBottomListener; } /** * Set a text to represent when the list was last updated. * @param lastUpdated Last updated at. */ public void setLastUpdated(CharSequence lastUpdated) { if (lastUpdated != null) { mRefreshViewLastUpdated.setVisibility(View.VISIBLE); mRefreshViewLastUpdated.setText(lastUpdated); } else { mRefreshViewLastUpdated.setVisibility(View.GONE); } } @Override public boolean onTouchEvent(MotionEvent event) { mBounceHack = false; switch (event.getAction()) { case MotionEvent.ACTION_UP: if (!isVerticalScrollBarEnabled()) { setVerticalScrollBarEnabled(true); } if (getFirstVisiblePosition() == 0 && mRefreshState != RefreshState.REFRESHING) { if ((mRefreshView.getTop() >= 0) && mRefreshState == RefreshState.RELEASE_TO_REFRESH) { // Initiate the refresh setRefreshState(RefreshState.REFRESHING); onRefresh(); } else if (mRefreshView.getTop() <= 0) { // Abort refresh and scroll down below the refresh view resetHeader(); setSelection(1); } } break; case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: if (mRefreshState == RefreshState.RELEASE_TO_REFRESH) { if (isVerticalFadingEdgeEnabled()) { setVerticalScrollBarEnabled(false); } } break; } return super.onTouchEvent(event); } /** * Sets the header padding back to original size. */ private void resetHeaderPadding() { mRefreshView.setPadding( mRefreshView.getPaddingLeft(), mRefreshOriginalTopPadding, mRefreshView.getPaddingRight(), mRefreshView.getPaddingBottom()); } /** * Resets the header to the original state. */ private void resetHeader() { if (mRefreshState != RefreshState.TAP_TO_REFRESH) { setRefreshState(RefreshState.TAP_TO_REFRESH); resetHeaderPadding(); } refreshAnimationDrawable.stop(); } private void setRefreshState(RefreshState state) { mRefreshState = state; switch (state) { case PULL_TO_REFRESH: mRefreshViewLabel.setText(R.string.pull_to_refresh); break; case REFRESHING: mRefreshViewLabel.setText(R.string.refreshing); break; case RELEASE_TO_REFRESH: mRefreshViewLabel.setText(R.string.release_to_refresh); break; case TAP_TO_REFRESH: mRefreshViewLabel.setText(R.string.tap_to_refresh); break; default: break; } } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // When the refresh view is completely visible, change the text to say // "Release to refresh..." and flip the arrow drawable. if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL && mRefreshState != RefreshState.REFRESHING) { if (firstVisibleItem == 0) { if ((mRefreshView.getTop() >= 0) && mRefreshState != RefreshState.RELEASE_TO_REFRESH) { refreshAnimationDrawable.start(); setRefreshState(RefreshState.RELEASE_TO_REFRESH); } else if (mRefreshView.getTop() < 0 && mRefreshState != RefreshState.PULL_TO_REFRESH) { if (mRefreshState != RefreshState.TAP_TO_REFRESH) { refreshAnimationDrawable.stop(); } setRefreshState(RefreshState.PULL_TO_REFRESH); } } else { resetHeader(); } } else if (mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem == 0 && mRefreshState != RefreshState.REFRESHING) { setSelection(1); mBounceHack = true; } else if (mBounceHack && mCurrentScrollState == SCROLL_STATE_FLING) { setSelection(1); } if (scrollToBottomState != ScrollToBottomState.ACTIVATED && firstVisibleItem + visibleItemCount == totalItemCount && onScrollToBottomListener != null) { scrollToBottomState = ScrollToBottomState.ACTIVATED; onScrollToBottomListener.onScrollToBottom(); } if (mOnScrollListener != null) { mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { mCurrentScrollState = scrollState; if (mCurrentScrollState == SCROLL_STATE_IDLE) { mBounceHack = false; } if (mOnScrollListener != null) { mOnScrollListener.onScrollStateChanged(view, scrollState); } } private void prepareForRefresh() { resetHeaderPadding(); refreshAnimationDrawable.start(); setRefreshState(RefreshState.REFRESHING); } public void onRefresh() { prepareForRefresh(); if (mOnRefreshListener != null) { mOnRefreshListener.onRefresh(); } } /** * Resets the list to a normal state after a refresh. * @param lastUpdated Last updated at. */ public void onRefreshComplete(CharSequence lastUpdated) { setLastUpdated(lastUpdated); onRefreshComplete(); } /** * Resets the list to a normal state after a refresh. */ public void onRefreshComplete() { Log.d(TAG, "onRefreshComplete"); resetHeader(); // If refresh view is visible when loading completes, scroll down to // the next item. if (getFirstVisiblePosition() == 0) { invalidateViews(); setSelection(1); } } public void onScrollToBottomComplete(CharSequence lastUpdated) { setLastUpdated(lastUpdated); onScrollToBottomComplete(); } public void onScrollToBottomComplete() { Log.d(TAG, "onScrollToBottomComplete"); scrollToBottomState = ScrollToBottomState.SCROLL_TO_ACTIVATE; } /** * Invoked when the refresh view is clicked on. This is mainly used when * there's only a few items in the list and it's not possible to drag the * list. */ private class OnClickRefreshListener implements OnClickListener { @Override public void onClick(View v) { if (mRefreshState != RefreshState.REFRESHING) { onRefresh(); } } } /** * Interface definition for a callback to be invoked when list should be * refreshed. */ public interface OnRefreshListener { /** * Called when the list should be refreshed. * <p> * A call to {@link PullToRefreshListView #onRefreshComplete()} is * expected to indicate that the refresh has completed. */ public void onRefresh(); } /** * Interface definition for a callback to be invoked when list has been * scrolled to the bottom. * @author wilsonmitchell */ public interface OnScrollToBottomListener { /** * Called when the list has been scrolled to the bottom. * <p> * A call to {@link PullToRefreshListView #onBottomScrollComplete()} is * expected to indicate that the action has been completed. */ public void onScrollToBottom(); } }