Skip to content

Instantly share code, notes, and snippets.

@clowestab
Last active March 2, 2016 14:41
Show Gist options
  • Save clowestab/41ae46fca818f27f3209 to your computer and use it in GitHub Desktop.
Save clowestab/41ae46fca818f27f3209 to your computer and use it in GitHub Desktop.
A simple but complete Android pull to refresh/load more ListView implementation.
package com.package
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
/**
* Created by thomasclowes on 28/11/2014.
*/
public class PRListView extends ListView {
private static final float PULL_RESISTANCE = 1.7f;
private static final int ROTATE_ARROW_ANIMATION_DURATION = 250;
private static enum RefreshState{
PULL_TO_REFRESH,
RELEASE_TO_REFRESH,
REFRESHING
}
private static enum LoadMoreState{
SCROLL_TO_LOAD_MORE,
RELEASE_TO_LOAD_MORE,
LOADING_MORE
}
public PRListView(Context context) {
super(context);
init();
}
public PRListView(Context context, AttributeSet attrs){
super(context, attrs);
init();
}
//Will hold the heights of our header/footer so we don't have to requery it
private static int measuredHeaderHeight;
private static int measuredFooterHeight;
//The text views in which state messages are placed
TextView headerText;
TextView footerText;
//The text for the header in different states
private String pullToRefreshText = "Pull to refresh";
private String releaseToRefreshText = "Release to refresh";
private String refreshingText = "Refreshing..";
//The text for the footer in different states
private String scrollToLoadMoreText = "Scroll down to load more";
private String releaseToLoadMoreText = "Release to load more";
private String loadingMoreText = "Loading more..";
//Indicates if we have setup the footer..
Boolean footerSetup = false;
//Saves a reference to the last scroll y position
private float previousY;
//The header
private int headerPadding;
private RefreshState refreshState;
private LinearLayout headerContainer;
private RelativeLayout header;
//The footer
private int footerPadding;
private LoadMoreState loadMoreState;
private LinearLayout footerContainer;
private RelativeLayout footer;
//The animation definitions for spinning the image
private RotateAnimation flipAnimation;
private RotateAnimation reverseFlipAnimation;
//The image/spinner for the header/footer
private ImageView headerImage;
private ProgressBar headerSpinner;
private ImageView footerImage;
private ProgressBar footerSpinner;
//Listeners for doing the refresh/ load more
private OnRefreshListener onRefreshListener;
private OnLoadMoreListener onLoadMoreListener;
//The current page of data being shown
public Integer page = 1;
//Set up the refresh control state values
public void setUpRefreshControl(String pullText, String releaseText, String refreshing) {
pullToRefreshText = pullText;
releaseToRefreshText = releaseText;
refreshingText = refreshing;
}
//Set up the load more control state values
public void setUpLoadMoreControl(String scrollText, String releaseText, String loadingText) {
scrollToLoadMoreText = scrollText;
releaseToLoadMoreText = releaseText;
loadingMoreText = loadingText;
}
private void init(){
//Defines which edges should be faded on scrolling.
setVerticalFadingEdgeEnabled(false);
//Inflate the header and save the references
headerContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.ptr_header, null);
header = (RelativeLayout) headerContainer.findViewById(R.id.ptr_id_header);
headerText = (TextView) header.findViewById(R.id.ptr_id_text);
headerImage = (ImageView) header.findViewById(R.id.ptr_id_image);
headerSpinner = (ProgressBar) header.findViewById(R.id.ptr_id_spinner);
//We can reuse these animations for both header and footer
flipAnimation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
flipAnimation.setInterpolator(new LinearInterpolator());
flipAnimation.setDuration(ROTATE_ARROW_ANIMATION_DURATION);
flipAnimation.setFillAfter(true);
reverseFlipAnimation = new RotateAnimation(-180, 0, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
reverseFlipAnimation.setInterpolator(new LinearInterpolator());
reverseFlipAnimation.setDuration(ROTATE_ARROW_ANIMATION_DURATION);
reverseFlipAnimation.setFillAfter(true);
//Add the header and set its initial state
addHeaderView(headerContainer);
setRefreshState(RefreshState.PULL_TO_REFRESH);
//Inflate the footer and save the references
footerContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.load_more_control, null);
footer = (RelativeLayout) footerContainer.findViewById(R.id.ptr_id_header);
footerText = (TextView) footer.findViewById(R.id.ptr_id_text);
footerImage = (ImageView) footer.findViewById(R.id.ptr_id_image);
footerSpinner = (ProgressBar) footer.findViewById(R.id.ptr_id_spinner);
//Add the footer and set its initial state
addFooterView(footerContainer);
setLoadMoreState(LoadMoreState.SCROLL_TO_LOAD_MORE);
//This VTO makes sure the refresh control is not displayed at first
ViewTreeObserver vto = header.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new PTROnGlobalLayoutListener());
}
//The pull to refresh effect is accomplished by increasing the top padding of the header view so that it appears that you are pulling the list down
private void setHeaderPadding(int padding){
headerPadding = padding;
MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) header.getLayoutParams();
//left top right bottom
mlp.setMargins(0, Math.round(padding), 0, 0);
header.setLayoutParams(mlp);
}
//Bounce back the header to the appropriate place
private void bounceBackHeader(){
int yTranslation;
int newPadding;
int previousPadding = headerPadding;
//Container height is the height - the padding so header height - container height = padding
if (refreshState == RefreshState.REFRESHING) {
yTranslation = header.getHeight() - headerContainer.getHeight();
newPadding = 0;
} else {
yTranslation = -headerContainer.getHeight() - headerContainer.getTop() + getPaddingTop();
newPadding = -measuredHeaderHeight;
}
final int yTranslate = yTranslation;
final int newPad = newPadding;
//Proxy the header margin so it can be used with object animator
MarginProxy mp = new MarginProxy(header);
ObjectAnimator mover = ObjectAnimator.ofInt(mp, "topMargin", newPad);
mover.setDuration(400);
mover.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
//We have this because if you scroll again it flashes back to the previous padding
//I believe this may be to do with how android does animations with bitmaps etc
setHeaderPadding(newPad);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mover.start();
}
//The scroll to load more effect is accomplished by increasing the bottom padding of the header view so that it appears that you are scrolling further than is possible
public void setFooterPadding(int padding){
footerPadding = padding;
MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) header.getLayoutParams();
//left top right bottom
mlp.setMargins(0, 0, 0, Math.round(padding));
footer.setLayoutParams(mlp);
}
//Bounces back to the appropriate place
private void bounceBackFooter(){
int yTranslation;
int newPadding;
if (loadMoreState == LoadMoreState.LOADING_MORE) {
yTranslation = -(footer.getHeight() - footerContainer.getHeight());
newPadding = 0;
} else {
yTranslation = footerContainer.getHeight();
newPadding = -measuredFooterHeight;
}
final int yTranslate = yTranslation;
final int newPad = newPadding;
//Proxy the footer margin so it can be used with object animator
MarginProxy mp = new MarginProxy(footer);
ObjectAnimator mover = ObjectAnimator.ofInt(mp, "bottomMargin", newPad);
mover.setDuration(400);
mover.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
//We have this because if you scroll again it flashes back to the previous padding
//I believe this may be to do with how android does animations with bitmaps etc
setFooterPadding(newPad);
}
@Override
public void onAnimationCancel(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
});
mover.start();
}
//This is required to intercept touch down events
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event){
//The current y coordinate
float y = event.getY();
switch(event.getAction()){
//When we first touch down
case MotionEvent.ACTION_DOWN:
previousY = y;
break;
//When we let go of scroll
case MotionEvent.ACTION_UP:
if (loadMoreState == LoadMoreState.RELEASE_TO_LOAD_MORE || getLastVisiblePosition() == getCount() - 1) {
switch (loadMoreState) {
case RELEASE_TO_LOAD_MORE:
setLoadMoreState(LoadMoreState.LOADING_MORE);
bounceBackFooter();
break;
case SCROLL_TO_LOAD_MORE:
bounceBackFooter();
break;
}
}
else if (refreshState == RefreshState.RELEASE_TO_REFRESH || getFirstVisiblePosition() == 0) {
switch(refreshState){
case RELEASE_TO_REFRESH:
setRefreshState(RefreshState.REFRESHING);
bounceBackHeader();
break;
case PULL_TO_REFRESH:
bounceBackHeader();
break;
}
}
break;
case MotionEvent.ACTION_MOVE:
//y goes up as you scroll up
if (getFirstVisiblePosition() == 0){
float diff = y - previousY;
//resistance is simulated my only adding padding that is a percentage of the amount they actually moved
if(diff > 0) diff /= PULL_RESISTANCE;
//Header showing is normal. No padding.
//+ padding = extra space at the top
//- padding is equivalent to it being hidden
//if the negative padding equals its height it is totally hidden..
//when not enough -18, -180 for example
int potentialNewHeaderPadding = Math.round(headerPadding + diff);
//if the potential is less negative than -height then some of the header IS showing
int newHeaderPadding = Math.max(potentialNewHeaderPadding, -header.getHeight());
//Only show this 'over scroll' if not already refreshing
//newHeaderPadding != headerPadding essentially checks we are actually moving i think
if (newHeaderPadding != headerPadding && refreshState != RefreshState.REFRESHING){
setHeaderPadding(newHeaderPadding);
//if padding > 0 we are now over scrolling
if (refreshState == RefreshState.PULL_TO_REFRESH && headerPadding > 0){
setRefreshState(RefreshState.RELEASE_TO_REFRESH);
//Animate the image flipping
headerImage.clearAnimation();
headerImage.startAnimation(flipAnimation);
//If it is less than 0 we are partially showing
} else if (refreshState == RefreshState.RELEASE_TO_REFRESH && headerPadding < 0){
setRefreshState(RefreshState.PULL_TO_REFRESH);
headerImage.clearAnimation();
headerImage.startAnimation(reverseFlipAnimation);
}
}
//load more control
} else if (getLastVisiblePosition() == getCount() - 1) {
if (!footerSetup) {
//The header is set up in the global listener.. we set up the footer the first time in needs considering
int initialFooterHeight = footer.getHeight();
measuredFooterHeight = initialFooterHeight;
setFooterPadding(-initialFooterHeight);
footerSetup = true;
}
float diff = y - previousY;
//resistance is simulated by only adding padding that is a percentage of the amount they actually moved
if (diff < 0) diff /= PULL_RESISTANCE;
//Choose the max either the default or the calculated
//if its a - diff -- = +..
int potentialNewFooterPadding = Math.round(footerPadding - diff);
int newFooterPadding = Math.max(potentialNewFooterPadding, -measuredFooterHeight);
//Only show this 'over scroll' if not already refreshing
//and if the padding has changed
if (newFooterPadding != footerPadding && loadMoreState != LoadMoreState.LOADING_MORE) {
setFooterPadding(newFooterPadding);
if (loadMoreState == LoadMoreState.SCROLL_TO_LOAD_MORE && footerPadding > 0) {
setLoadMoreState(LoadMoreState.RELEASE_TO_LOAD_MORE);
//Animate the image flipping
footerImage.clearAnimation();
footerImage.startAnimation(flipAnimation);
} else if (loadMoreState == LoadMoreState.RELEASE_TO_LOAD_MORE && footerPadding < 0) {
setLoadMoreState(LoadMoreState.SCROLL_TO_LOAD_MORE);
footerImage.clearAnimation();
footerImage.startAnimation(reverseFlipAnimation);
}
}
}
//Always keep track of the last y coordinate while scrolling
previousY = y;
break;
}
return super.onTouchEvent(event);
}
//This method sets the state text, spinner visibility etc
//Also tells listener to do refreshing if in refresh state
private void setRefreshState(RefreshState state){
this.refreshState = state;
switch(state){
//Whilst we have to pull to refresh, no spinner visible
case PULL_TO_REFRESH:
headerSpinner.setVisibility(View.INVISIBLE);
headerImage.setVisibility(View.VISIBLE);
headerText.setText(pullToRefreshText);
break;
//Release state still no spinner
case RELEASE_TO_REFRESH:
headerSpinner.setVisibility(View.INVISIBLE);
headerImage.setVisibility(View.VISIBLE);
headerText.setText(releaseToRefreshText);
break;
//Refreshing - show spinner and tell listener to do refreshing
case REFRESHING:
//When a refresh has been triggered, hide image and show spinner/refreshing text
headerSpinner.setVisibility(View.VISIBLE);
headerImage.clearAnimation();
headerImage.setVisibility(View.INVISIBLE);
headerText.setText(refreshingText);
//If no listener the refresh is complete instantly..
if (onRefreshListener == null){
Log.w("Notice", "You have not implemented an onRefreshListener");
setRefreshState(RefreshState.PULL_TO_REFRESH);
//If there is a listener.. do the refresh and then tell the list view when its done
} else {
onRefreshListener.onRefresh();
}
break;
}
}
//This method sets the state text, spinner visibility etc
//Also tells listener to load more if in load more state
private void setLoadMoreState(LoadMoreState state){
this.loadMoreState = state;
switch(loadMoreState){
//Whilst we have to pull to refresh, no spinner visible
case SCROLL_TO_LOAD_MORE:
footerSpinner.setVisibility(View.INVISIBLE);
footerImage.setVisibility(View.VISIBLE);
footerText.setText(scrollToLoadMoreText);
break;
//Release state still no spinner
case RELEASE_TO_LOAD_MORE:
footerSpinner.setVisibility(View.INVISIBLE);
footerImage.setVisibility(View.VISIBLE);
footerText.setText(releaseToLoadMoreText);
break;
//Refreshing - show spinner and tell listener to do refreshing
case LOADING_MORE:
//When a refresh has been triggered, hide image and show spinner/refreshing text
footerSpinner.setVisibility(View.VISIBLE);
footerImage.clearAnimation();
footerImage.setVisibility(View.INVISIBLE);
footerText.setText(loadingMoreText);
//If no listener the refresh is complete instantly..
if (onLoadMoreListener == null){
Log.w("Notice", "You have not implemented an onLoadMoreListener");
setLoadMoreState(LoadMoreState.SCROLL_TO_LOAD_MORE);
//If there is a listener.. do the refresh and then tell the list view when its done
} else {
onLoadMoreListener.onLoadMore();
}
break;
}
}
public interface OnRefreshListener{
public void onRefresh();
}
public interface OnLoadMoreListener{
public void onLoadMore();
}
//Call this once you have finished refreshing data in listener
public void onRefreshComplete(){
setRefreshState(RefreshState.PULL_TO_REFRESH);
bounceBackHeader();
}
//Call this once you have finished loading more in listener
public void onLoadMoreComplete(){
setLoadMoreState(LoadMoreState.SCROLL_TO_LOAD_MORE);
bounceBackFooter();
}
//Define how you will refresh the data
public void setOnRefreshListener(OnRefreshListener onRefreshListener){
this.onRefreshListener = onRefreshListener;
}
//Define how you will load more data
public void setOnLoadMoreListener(OnLoadMoreListener onLoadMoreListener){
this.onLoadMoreListener = onLoadMoreListener;
}
//This listener makes sure that the Pull to Refresh control is hidden at the start.
private class PTROnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener{
@Override
public void onGlobalLayout(){
int initialHeaderHeight = header.getHeight();
if(initialHeaderHeight > 0){
measuredHeaderHeight = initialHeaderHeight;
if(measuredHeaderHeight > 0 && refreshState != RefreshState.REFRESHING){
setHeaderPadding(-measuredHeaderHeight);
//smoothScrollToPosition(1);
requestLayout();
}
}
Helpers.removeOnGlobalLayoutListener(getViewTreeObserver(), this);
}
}
}
@lalit9819
Copy link

Hi Thomas,
where is the MarginProxy Class ?
where is the Helper Class?

can you please share complete demo?
Thanks

Regards
Lalit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment