package com.huewu.pla.lib; import java.text.SimpleDateFormat; import java.util.Date; import com.youxiachai.onexlistview.R; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.view.animation.TranslateAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; /** * A generic, customizable Android ListView implementation that has 'Pull to Refresh' functionality. * <p/> * This ListView can be used in place of the normal Android android.widget.ListView class. * <p/> * Users of this class should implement OnRefreshListener and call setOnRefreshListener(..) * to get notified on refresh events. The using class should call onRefreshComplete() when * refreshing is finished. * <p/> * The using class can call setRefreshing() to set the state explicitly to refreshing. This * is useful when you want to show the spinner and 'Refreshing' text when the * refresh was not triggered by 'Pull to Refresh', for example on start. * <p/> * For more information, visit the project page: * https://github.com/erikwt/PullToRefresh-ListView * * @author Erik Wallentinsen <dev+ptr@erikw.eu> * @version 1.0.0 */ public class MultiColumnPullToRefreshListView extends MultiColumnListView { private static final float PULL_RESISTANCE = 1.7f; private static final int BOUNCE_ANIMATION_DURATION = 200; private static final int BOUNCE_ANIMATION_DELAY = 0; private static final int ROTATE_ARROW_ANIMATION_DURATION = 250; private static enum State{ PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING } /** * Interface to implement when you want to get notified of 'pull to refresh' * events. * Call setOnRefreshListener(..) to activate an OnRefreshListener. */ public interface OnRefreshListener{ /** * Method to be called when a refresh is requested */ void onRefresh(); } private static int measuredHeaderHeight; private boolean scrollbarEnabled; private boolean bounceBackHeader; private boolean lockScrollWhileRefreshing; private boolean showLastUpdatedText; private String pullToRefreshText; private String releaseToRefreshText; private String refreshingText; private String lastUpdatedText; private SimpleDateFormat lastUpdatedDateFormat = new SimpleDateFormat("dd/MM HH:mm"); private float previousY; private int headerPadding; private boolean hasResetHeader; private long lastUpdated = -1; private State state; private LinearLayout headerContainer; private RelativeLayout header; private RotateAnimation flipAnimation; private RotateAnimation reverseFlipAnimation; private RotateAnimation refreshingAnimation; private ImageView image; private View refreshingIcon; private TextView text; private TextView lastUpdatedTextView; private OnRefreshListener onRefreshListener; private TranslateAnimation bounceAnimation; public MultiColumnPullToRefreshListView(Context context){ super(context); init(); } public MultiColumnPullToRefreshListView(Context context, AttributeSet attrs){ super(context, attrs); init(); } public MultiColumnPullToRefreshListView(Context context, AttributeSet attrs, int defStyle){ super(context, attrs, defStyle); init(); } /** * Activate an OnRefreshListener to get notified on 'pull to refresh' * events. * * @param onRefreshListener The OnRefreshListener to get notified */ public void setOnRefreshListener(OnRefreshListener onRefreshListener){ this.onRefreshListener = onRefreshListener; } /** * @return If the list is in 'Refreshing' state */ public boolean isRefreshing(){ return state == State.REFRESHING; } /** * Default is false. When lockScrollWhileRefreshing is set to true, the list * cannot scroll when in 'refreshing' mode. It's 'locked' on refreshing. * * @param lockScrollWhileRefreshing */ public void setLockScrollWhileRefreshing(boolean lockScrollWhileRefreshing){ this.lockScrollWhileRefreshing = lockScrollWhileRefreshing; } /** * Default is false. Show the last-updated date/time in the 'Pull ro Refresh' * header. See 'setLastUpdatedDateFormat' to set the date/time formatting. * * @param showLastUpdatedText */ public void setShowLastUpdatedText(boolean showLastUpdatedText){ this.showLastUpdatedText = showLastUpdatedText; if(!showLastUpdatedText) lastUpdatedTextView.setVisibility(View.GONE); } /** * Default: "dd/MM HH:mm". Set the format in which the last-updated * date/time is shown. Meaningless if 'showLastUpdatedText == false (default)'. * See 'setShowLastUpdatedText'. * * @param lastUpdatedDateFormat */ public void setLastUpdatedDateFormat(SimpleDateFormat lastUpdatedDateFormat){ this.lastUpdatedDateFormat = lastUpdatedDateFormat; } /** * Explicitly set the state to refreshing. This * is useful when you want to show the spinner and 'Refreshing' text when * the refresh was not triggered by 'pull to refresh', for example on start. */ public void setRefreshing(){ state = State.REFRESHING; setUiRefreshing(); //setHeaderPadding(0); //scrollTo(0, 0); } /** * Set the state back to 'pull to refresh'. Call this method when refreshing * the data is finished. */ public void onRefreshComplete(){ state = State.PULL_TO_REFRESH; resetHeader(); lastUpdated = System.currentTimeMillis(); } /** * Change the label text on state 'Pull to Refresh' * * @param pullToRefreshText Text */ public void setTextPullToRefresh(String pullToRefreshText){ this.pullToRefreshText = pullToRefreshText; if(state == State.PULL_TO_REFRESH){ text.setText(pullToRefreshText); } } /** * Change the label text on state 'Release to Refresh' * * @param releaseToRefreshText Text */ public void setTextReleaseToRefresh(String releaseToRefreshText){ this.releaseToRefreshText = releaseToRefreshText; if(state == State.RELEASE_TO_REFRESH){ text.setText(releaseToRefreshText); } } /** * Change the label text on state 'Refreshing' * * @param refreshingText Text */ public void setTextRefreshing(String refreshingText){ this.refreshingText = refreshingText; if(state == State.REFRESHING){ text.setText(refreshingText); } } private void init(){ setVerticalFadingEdgeEnabled(false); headerContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.ptr_header, null); header = (RelativeLayout) headerContainer.findViewById(R.id.ptr_id_header); text = (TextView) header.findViewById(R.id.ptr_id_text); lastUpdatedTextView = (TextView) header.findViewById(R.id.ptr_id_last_updated); image = (ImageView) header.findViewById(R.id.ptr_id_image); refreshingIcon = header.findViewById(R.id.ptr_id_spinner); pullToRefreshText = getContext().getString(R.string.ptr_pull_to_refresh); releaseToRefreshText = getContext().getString(R.string.ptr_release_to_refresh); refreshingText = getContext().getString(R.string.ptr_refreshing); lastUpdatedText = getContext().getString(R.string.ptr_last_updated); 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); refreshingAnimation = new RotateAnimation(0, 720, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); refreshingAnimation.setDuration(1200); refreshingAnimation.setInterpolator(new LinearInterpolator()); refreshingAnimation.setRepeatCount(Integer.MAX_VALUE); refreshingAnimation.setRepeatMode(Animation.RESTART); addHeaderView(headerContainer); setState(State.PULL_TO_REFRESH); scrollbarEnabled = isVerticalScrollBarEnabled(); ViewTreeObserver vto = header.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new PTROnGlobalLayoutListener()); //super.setOnItemClickListener(new PTROnItemClickListener()); //super.setOnItemLongClickListener(new PTROnItemLongClickListener()); } private void setHeaderPadding(int padding){ headerPadding = padding; MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) header.getLayoutParams(); mlp.setMargins(0, Math.round(padding), 0, 0); header.setLayoutParams(mlp); } private boolean enablePulling = true; private boolean enablePull(MotionEvent event){ return enablePulling; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if(lockScrollWhileRefreshing && (state == State.REFRESHING || getAnimation() != null && !getAnimation().hasEnded())){ return true; //consume touch event here.. } switch(event.getAction()){ case MotionEvent.ACTION_DOWN: if( getFirstVisiblePosition() == 0 ) previousY = event.getY(); break; case MotionEvent.ACTION_MOVE: if( getFirstVisiblePosition() == 0 && event.getY() - previousY > 0 ) { enablePulling = true; return true; }else{ enablePulling = false; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: enablePulling = false; break; } return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event){ if(lockScrollWhileRefreshing && (state == State.REFRESHING || getAnimation() != null && !getAnimation().hasEnded())){ return true; } switch(event.getAction()){ case MotionEvent.ACTION_UP: if(enablePull(event) && (state == State.RELEASE_TO_REFRESH || getFirstVisiblePosition() == 0)){ switch(state){ case RELEASE_TO_REFRESH: setState(State.REFRESHING); bounceBackHeader(); break; case PULL_TO_REFRESH: resetHeader(); break; default: break; } } break; case MotionEvent.ACTION_MOVE: if(enablePull(event)){ float y = event.getY(); float diff = y - previousY; if(diff > 0) diff /= PULL_RESISTANCE; previousY = y; int newHeaderPadding = Math.max(Math.round(headerPadding + diff), -header.getHeight()); if(newHeaderPadding != headerPadding && state != State.REFRESHING){ setHeaderPadding(newHeaderPadding); if(state == State.PULL_TO_REFRESH && headerPadding > 0){ setState(State.RELEASE_TO_REFRESH); image.clearAnimation(); image.startAnimation(flipAnimation); }else if(state == State.RELEASE_TO_REFRESH && headerPadding < 0){ setState(State.PULL_TO_REFRESH); image.clearAnimation(); image.startAnimation(reverseFlipAnimation); } } } break; } return super.onTouchEvent(event); } private void bounceBackHeader(){ int yTranslate = state == State.REFRESHING ? header.getHeight() - headerContainer.getHeight() : -headerContainer.getHeight() - headerContainer.getTop(); bounceAnimation = new TranslateAnimation( TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, 0, TranslateAnimation.ABSOLUTE, yTranslate); bounceAnimation.setDuration(BOUNCE_ANIMATION_DURATION); bounceAnimation.setFillEnabled(true); bounceAnimation.setFillAfter(false); bounceAnimation.setFillBefore(true); // bounceAnimation.setInterpolator(new OvershootInterpolator(BOUNCE_OVERSHOOT_TENSION)); bounceAnimation.setAnimationListener(new HeaderAnimationListener(yTranslate)); startAnimation(bounceAnimation); } private void resetHeader(){ if(getFirstVisiblePosition() > 0){ setHeaderPadding(-header.getHeight()); setState(State.PULL_TO_REFRESH); return; } if(getAnimation() != null && !getAnimation().hasEnded()){ bounceBackHeader = true; }else{ bounceBackHeader(); } } private void setUiRefreshing(){ image.clearAnimation(); image.setVisibility(View.GONE); refreshingIcon.setVisibility(View.VISIBLE); refreshingIcon.startAnimation(refreshingAnimation); text.setText(refreshingText); } private void stopRefreshing(){ refreshingIcon.clearAnimation(); refreshingIcon.setVisibility(View.GONE); } private void setState(State state){ this.state = state; switch(state){ case PULL_TO_REFRESH: stopRefreshing(); image.setVisibility(View.VISIBLE); text.setText(pullToRefreshText); if(showLastUpdatedText && lastUpdated != -1){ lastUpdatedTextView.setVisibility(View.VISIBLE); lastUpdatedTextView.setText(String.format(lastUpdatedText, lastUpdatedDateFormat.format(new Date(lastUpdated)))); } break; case RELEASE_TO_REFRESH: stopRefreshing(); image.setVisibility(View.VISIBLE); text.setText(releaseToRefreshText); break; case REFRESHING: setUiRefreshing(); lastUpdated = System.currentTimeMillis(); if(onRefreshListener == null){ setState(State.PULL_TO_REFRESH); }else{ onRefreshListener.onRefresh(); } break; } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt){ super.onScrollChanged(l, t, oldl, oldt); if(!hasResetHeader){ if(measuredHeaderHeight > 0 && state != State.REFRESHING){ setHeaderPadding(-measuredHeaderHeight); } hasResetHeader = true; } } private class HeaderAnimationListener implements AnimationListener{ private int height, translation; private State stateAtAnimationStart; public HeaderAnimationListener(int translation){ this.translation = translation; } @Override public void onAnimationStart(Animation animation){ stateAtAnimationStart = state; android.view.ViewGroup.LayoutParams lp = getLayoutParams(); height = lp.height; lp.height = getHeight() - translation; setLayoutParams(lp); if(scrollbarEnabled){ setVerticalScrollBarEnabled(false); } } @Override public void onAnimationEnd(Animation animation){ setHeaderPadding(stateAtAnimationStart == State.REFRESHING ? 0 : -measuredHeaderHeight - headerContainer.getTop()); //setSelection(0); android.view.ViewGroup.LayoutParams lp = getLayoutParams(); lp.height = height; setLayoutParams(lp); if(scrollbarEnabled){ setVerticalScrollBarEnabled(true); } if(bounceBackHeader){ bounceBackHeader = false; postDelayed(new Runnable(){ @Override public void run(){ resetHeader(); } }, BOUNCE_ANIMATION_DELAY); }else if(stateAtAnimationStart != State.REFRESHING){ setState(State.PULL_TO_REFRESH); } } @Override public void onAnimationRepeat(Animation animation){} } private class PTROnGlobalLayoutListener implements OnGlobalLayoutListener{ @Override public void onGlobalLayout(){ int initialHeaderHeight = header.getHeight(); if(initialHeaderHeight > 0){ measuredHeaderHeight = initialHeaderHeight; if(measuredHeaderHeight > 0 && state != State.REFRESHING){ setHeaderPadding(-measuredHeaderHeight); requestLayout(); } } getViewTreeObserver().removeGlobalOnLayoutListener(this); } } // private class PTROnItemClickListener implements OnItemClickListener { // // @Override // public void onItemClick(AdapterView<?> adapterView, View view, int position, long id){ // hasResetHeader = false; // // if(onItemClickListener != null && state == State.PULL_TO_REFRESH){ // // Passing up onItemClick. Correct position with the number of header views // onItemClickListener.onItemClick(adapterView, view, position - getHeaderViewsCount(), id); // } // } // } // // private class PTROnItemLongClickListener implements OnItemLongClickListener{ // // @Override // public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long id){ // hasResetHeader = false; // // if(onItemLongClickListener != null && state == State.PULL_TO_REFRESH){ // // Passing up onItemLongClick. Correct position with the number of header views // return onItemLongClickListener.onItemLongClick(adapterView, view, position - getHeaderViewsCount(), id); // } // // return false; // } // } }