// Copyright (C) 2012 LMIT Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.markupartist.android.widget;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.lmit.jenkins.android.activity.OnLoadMoreDataListener;
import com.lmit.jenkins.android.activity.R;
import com.lmit.jenkins.android.logger.Logger;
public class PullToRefreshListView extends ListView implements
OnScrollListener, OnIsBottomOverScrollListener {
private static final int TAP_TO_REFRESH = 1;
private static final int PULL_TO_REFRESH = 2;
private static final int RELEASE_TO_REFRESH = 3;
private static final int REFRESHING = 4;
private static final String TAG = "PullToRefreshListView";
private OnRefreshListener mOnRefreshListener;
private OnLoadMoreDataListener mOnLoadMoreDataListener;
private EndlessScrollListener mEndlessScrollListener;
private boolean isBottomOverscroll = false;
private boolean canDoLoadMoreData = false;
private boolean refreshing = false;
/**
* Listener that will receive notifications every time the list scrolls.
*/
private OnScrollListener mOnScrollListener;
private LayoutInflater mInflater;
private LinearLayout mRefreshView;
private TextView mRefreshViewText;
private ImageView mRefreshViewImage;
private ProgressBar mRefreshViewProgress;
private TextView mRefreshViewLastUpdated;
private int mCurrentScrollState;
private int mRefreshState;
private RotateAnimation mFlipAnimation;
private RotateAnimation mReverseFlipAnimation;
private int mRefreshViewHeight;
private int mRefreshOriginalTopPadding;
private int mLastMotionY;
private View lastFooterPad;
private static final int MAX_Y_OVERSCROLL_DISTANCE = 200;
private int mMaxYOverscrollDistance;
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);
}
@Override
public boolean performItemClick(View view, int position, long id) {
return super.performItemClick(view, position, id);
}
private void init(Context context) {
// Load all of the animations we need in code rather than through XML
mFlipAnimation =
new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
mFlipAnimation.setInterpolator(new LinearInterpolator());
mFlipAnimation.setDuration(250);
mFlipAnimation.setFillAfter(true);
mReverseFlipAnimation =
new RotateAnimation(-180, 0, RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
mReverseFlipAnimation.setDuration(250);
mReverseFlipAnimation.setFillAfter(true);
mInflater =
(LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mRefreshView =
(LinearLayout) mInflater.inflate(R.layout.pull_to_refresh_header, null);
mRefreshViewText =
(TextView) mRefreshView.findViewById(R.id.pull_to_refresh_text);
mRefreshViewImage =
(ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image);
mRefreshViewProgress =
(ProgressBar) mRefreshView.findViewById(R.id.pull_to_refresh_progress);
mRefreshViewLastUpdated =
(TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at);
mRefreshViewImage.setMinimumHeight(50);
mRefreshView.setOnClickListener(new OnClickRefreshListener());
mRefreshOriginalTopPadding = mRefreshView.getPaddingTop();
mRefreshState = TAP_TO_REFRESH;
addHeaderView(mRefreshView);
super.setOnScrollListener(this);
measureView(mRefreshView);
mRefreshViewHeight = mRefreshView.getMeasuredHeight();
final DisplayMetrics metrics = getResources().getDisplayMetrics();
final float density = metrics.density;
mMaxYOverscrollDistance = (int) (density * MAX_Y_OVERSCROLL_DISTANCE);
}
public void forcePullToRefreshViewHidden() {
invalidateViews();
setSelectionFromTop(1, 0);
}
public void forcePullToRefreshViewHidden(int size) {
addFooterView(size);
forcePullToRefreshViewHidden();
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
setSelectionFromTop(1, 0);
}
/**
* 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;
}
/**
* 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);
}
}
/**
* Smoothly scroll by distance pixels over duration milliseconds.
*
* <p>
* Using reflection internally to call smoothScrollBy for API Level 8
* otherwise scrollBy is called.
*
* @param distance Distance to scroll in pixels.
* @param duration Duration of the scroll animation in milliseconds.
*/
private void scrollListBy(int distance, int duration) {
try {
Method method =
ListView.class
.getMethod("smoothScrollBy", Integer.TYPE, Integer.TYPE);
method.invoke(this, distance + 1, duration);
} catch (NoSuchMethodException e) {
// If smoothScrollBy is not available (< 2.2)
setSelectionFromTop(1, 0);
} catch (IllegalArgumentException e) {
throw e;
} catch (IllegalAccessException e) {
System.err.println("unexpected " + e);
} catch (InvocationTargetException e) {
System.err.println("unexpected " + e);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if (!refreshing) {
if (canDoLoadMoreData) {
if (mOnLoadMoreDataListener != null) {
canDoLoadMoreData = false;
refreshing = true;
Logger.getInstance().debug("Calling laod more data");
mOnLoadMoreDataListener.onLoadMore(3000);
}
}
}
if (!isVerticalScrollBarEnabled()) {
setVerticalScrollBarEnabled(true);
}
if (getFirstVisiblePosition() == 0 && mRefreshState != REFRESHING) {
if (mRefreshView.getBottom() > mRefreshViewHeight
|| mRefreshView.getTop() >= 0
&& mRefreshState == RELEASE_TO_REFRESH) {
// Initiate the refresh
mRefreshState = REFRESHING;
prepareForRefresh();
onRefresh();
} else if (mRefreshView.getBottom() < mRefreshViewHeight) {
// Abort refresh and scroll down below the refresh view
resetHeader();
setSelectionFromTop(1, 0);
}
}
break;
case MotionEvent.ACTION_DOWN:
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
applyHeaderPadding(event);
break;
}
return super.onTouchEvent(event);
}
private void applyHeaderPadding(MotionEvent ev) {
final int historySize = ev.getHistorySize();
// Workaround for getPointerCount() which is unavailable in 1.5
// (it's always 1 in 1.5)
int pointerCount = 1;
try {
Method method = MotionEvent.class.getMethod("getPointerCount");
pointerCount = (Integer) method.invoke(ev);
} catch (NoSuchMethodException e) {
pointerCount = 1;
} catch (IllegalArgumentException e) {
throw e;
} catch (IllegalAccessException e) {
System.err.println("unexpected " + e);
} catch (InvocationTargetException e) {
System.err.println("unexpected " + e);
}
for (int h = 0; h < historySize; h++) {
for (int p = 0; p < pointerCount; p++) {
if (mRefreshState == RELEASE_TO_REFRESH) {
if (isVerticalFadingEdgeEnabled()) {
setVerticalScrollBarEnabled(false);
}
int historicalY = 0;
try {
// For Android > 2.0
Method method =
MotionEvent.class.getMethod("getHistoricalY", Integer.TYPE,
Integer.TYPE);
historicalY = ((Float) method.invoke(ev, p, h)).intValue();
} catch (NoSuchMethodException e) {
// For Android < 2.0
historicalY = (int) (ev.getHistoricalY(h));
} catch (IllegalArgumentException e) {
throw e;
} catch (IllegalAccessException e) {
System.err.println("unexpected " + e);
} catch (InvocationTargetException e) {
System.err.println("unexpected " + e);
}
// Calculate the padding to apply, we divide by 1.7 to
// simulate a more resistant effect during pull.
int topPadding =
(int) (((historicalY - mLastMotionY) - mRefreshViewHeight) / 1.7);
mRefreshView.setPadding(mRefreshView.getPaddingLeft(), topPadding,
mRefreshView.getPaddingRight(), mRefreshView.getPaddingBottom());
}
}
}
}
/**
* 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 != TAP_TO_REFRESH) {
mRefreshState = TAP_TO_REFRESH;
resetHeaderPadding();
// Set refresh view text to the pull label
mRefreshViewText.setText(R.string.pull_to_refresh_tap_label);
// Replace refresh drawable with arrow drawable
mRefreshViewImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
// Clear the full rotation animation
mRefreshViewImage.clearAnimation();
// Hide progress bar and arrow.
mRefreshViewImage.setVisibility(View.GONE);
mRefreshViewProgress.setVisibility(View.GONE);
}
}
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
int lpHeight = p.height;
int childHeightSpec;
if (lpHeight > 0) {
childHeightSpec =
MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
}
@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 != REFRESHING) {
if (firstVisibleItem == 0) {
mRefreshViewImage.setVisibility(View.VISIBLE);
if ((mRefreshView.getBottom() > mRefreshViewHeight + 20 || mRefreshView
.getTop() >= 0) && mRefreshState != RELEASE_TO_REFRESH) {
mRefreshViewText.setText(R.string.pull_to_refresh_release_label);
mRefreshViewImage.clearAnimation();
mRefreshViewImage.startAnimation(mFlipAnimation);
mRefreshState = RELEASE_TO_REFRESH;
} else if (mRefreshView.getBottom() < mRefreshViewHeight + 20
&& mRefreshState != PULL_TO_REFRESH) {
mRefreshViewText.setText(R.string.pull_to_refresh_pull_label);
if (mRefreshState != TAP_TO_REFRESH) {
mRefreshViewImage.clearAnimation();
mRefreshViewImage.startAnimation(mReverseFlipAnimation);
}
mRefreshState = PULL_TO_REFRESH;
}
} else {
mRefreshViewImage.setVisibility(View.GONE);
resetHeader();
}
} else if (mCurrentScrollState == SCROLL_STATE_FLING
&& firstVisibleItem == 0 && mRefreshState != REFRESHING) {
setSelectionFromTop(1, 0);
}
if (mOnScrollListener != null) {
mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount,
totalItemCount);
}
if (mEndlessScrollListener != null) {
mEndlessScrollListener.onScroll(view, firstVisibleItem, visibleItemCount,
totalItemCount);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChanged(view, scrollState);
}
if (mEndlessScrollListener != null) {
mEndlessScrollListener.onScrollStateChanged(view, scrollState);
}
}
public void prepareForRefresh() {
resetHeaderPadding();
mRefreshViewImage.setVisibility(View.GONE);
// We need this hack, otherwise it will keep the previous drawable.
mRefreshViewImage.setImageDrawable(null);
mRefreshViewProgress.setVisibility(View.VISIBLE);
// Set refresh view text to the refreshing label
mRefreshViewText.setText(R.string.pull_to_refresh_refreshing_label);
mRefreshState = REFRESHING;
}
public void onRefresh() {
Log.d(TAG, "onRefresh");
if (!refreshing) {
if (mOnRefreshListener != null) {
refreshing = true;
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");
refreshing = false;
resetHeader();
// If refresh view is visible when loading completes, scroll down to
// the next item.
if (mRefreshView.getBottom() > 0) {
invalidateViews();
setSelectionFromTop(1, 0);
}
}
/**
* 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 != REFRESHING) {
prepareForRefresh();
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();
}
@Override
protected void onSizeChanged(int w, final int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
addFooterView(getCount());
}
private void addFooterView(int count) {
Resources r = getResources();
Display display =
((Activity) getContext()).getWindowManager().getDefaultDisplay();
int height = display.getHeight();
float size =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, 60,
r.getDisplayMetrics());
int childViewHeight = (int) (count * size);
if (lastFooterPad != null) {
this.removeFooterView(lastFooterPad);
}
if (childViewHeight < height) {
View footerPad = new View(getContext());
lastFooterPad = footerPad;
footerPad.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
height - childViewHeight));
this.setFooterDividersEnabled(false);
this.addFooterView(footerPad, null, false);
}
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX,
int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX,
int maxOverScrollY, boolean isTouchEvent) {
// Logger.getInstance().debug("Called overScrollBy");
// Logger.getInstance().debug("scrollY=" + scrollY);
// Logger.getInstance().debug("deltaY=" + deltaY);
// Logger.getInstance().debug("scrollRangeY=" + scrollRangeY);
// Logger.getInstance().debug("maxOverScrollY=" + maxOverScrollY);
// Logger.getInstance().debug("isTouchEvent=" + isTouchEvent);
int __mMaxYOverscrollDistance = mMaxYOverscrollDistance;
canDoLoadMoreData = false;
if (mOnLoadMoreDataListener != null) {
if ((deltaY >= 1 || scrollY >= 1) && mOnLoadMoreDataListener != null) {
// Logger.getInstance().debug(
// "Enough overscroll detected: checking isBottonOverScroll='"
// + isBottomOverscroll + "'");
if (isBottomOverscroll && !refreshing) {
//Logger.getInstance().debug("Setting canDoLoadMoreData=true");
canDoLoadMoreData = true;
}
} else {
if (!isBottomOverscroll) {
//Logger.getInstance().debug("Not bottom overscroll: deny overscroll");
__mMaxYOverscrollDistance = 0;
}
}
} else {
if (!isBottomOverscroll) {
//Logger.getInstance().debug("Not bottom overscroll: deny overscroll");
__mMaxYOverscrollDistance = 0;
}
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
scrollRangeY, maxOverScrollX, __mMaxYOverscrollDistance, isTouchEvent);
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
// Logger.getInstance().debug("Called overScrolled");
// Logger.getInstance().debug("scrollY=" + scrollY);
// Logger.getInstance().debug("clampedY=" + clampedY);
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
public void setEndlessScrollListener(
EndlessScrollListener endlessScrollListener) {
this.mEndlessScrollListener = endlessScrollListener;
}
public void setOnLoadMoreDataListener(
OnLoadMoreDataListener mOnLoadMoreDataListener) {
this.mOnLoadMoreDataListener = mOnLoadMoreDataListener;
}
public static class EndlessScrollListener implements OnScrollListener {
private int visibleThreshold;
private OnIsBottomOverScrollListener listener;
public EndlessScrollListener(int visibleThreshold,
OnIsBottomOverScrollListener listener) {
this.visibleThreshold = visibleThreshold;
this.listener = listener;
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// Logger.getInstance().debug("totalItemCount=" + totalItemCount);
// Logger.getInstance().debug("visibleItemCount=" + visibleItemCount);
// Logger.getInstance().debug("firstVisibleItem=" + firstVisibleItem);
listener.onIsBottonOverscroll(false);
if (listener != null) {
if ((totalItemCount - visibleItemCount) <= (firstVisibleItem + visibleThreshold)) {
listener.onIsBottonOverscroll(true);
} else {
listener.onIsBottonOverscroll(false);
}
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//Logger.getInstance().debug(
// "onScrollStateChanged: scrollState=" + scrollState);
}
}
@Override
public void onIsBottonOverscroll(boolean isBottomOverscroll) {
//Logger.getInstance().debug("isBottomOverscroll" + isBottomOverscroll);
this.isBottomOverscroll = isBottomOverscroll;
}
}