/**
* Copyright 2012-2013 Jeremie Martinez (jeremiemartinez@gmail.com)
*
* 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.github.jeremiemartinez.refreshlistview;
import java.text.DateFormat;
import java.util.Date;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.view.animation.Transformation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.github.jeremiemartinez.refreslistview.R;
/**
* @author jmartinez
*
* Simple Android ListView that enables pull to refresh as Twitter or Facebook apps ListView. Developers must implement OnRefreshListener interface and set it to the list. They also have to
* call finishRefreshing when their task is done. See <a href="https://github.com/jeremiemartinez/RefreshListView">Project Site</a> for more information.
*
*/
public class RefreshListView extends ListView {
private static final int RESISTANCE = 3;
private static final int HEADER_HEIGHT = 60;
private static final int DURATION = 300;
private OnRefreshListener refreshListener;
private View container;
private RelativeLayout header;
private ProgressBar progress;
private TextView comment;
private ImageView arrow;
private TextView date;
private LayoutInflater inflater;
private float currentY;
private int headerHeight;
private boolean enabledDate;
private Date lastUpdateDate;
private DateFormat formatter;
private State currentState;
private enum State {
PULLDOWN, RELEASE, UPDATING;
}
private enum Rotation {
CLOCKWISE, ANTICLOCKWISE;
}
public RefreshListView(Context context) {
super(context);
init(context);
}
public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
/**
* initializing method. Call in constructors to set up the headers.
*
* @param context
* activity context, got by constructors
*/
private void init(Context context) {
formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT);
inflater = LayoutInflater.from(context);
container = inflater.inflate(R.layout.layout_refreshlistview_header, null);
header = (RelativeLayout) container.findViewById(R.id.header);
arrow = (ImageView) container.findViewById(R.id.arrow);
progress = (ProgressBar) container.findViewById(R.id.progress);
date = (TextView) container.findViewById(R.id.date);
comment = (TextView) container.findViewById(R.id.comment);
container.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
header.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
addHeaderView(container);
headerHeight = (int) (HEADER_HEIGHT * getContext().getResources().getDisplayMetrics().density);
changeHeaderHeight(0);
comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
currentState = State.PULLDOWN;
}
/**
* Call to perform item click. Reset the position without the header
*
* @see android.widget.AbsListView#performItemClick(android.view.View, int, long)
*/
@Override
public boolean performItemClick(View view, int position, long id) {
if (position == 0) {
return true;
} else {
return super.performItemClick(view, position - getHeaderViewsCount(), id);
}
}
/**
* Set up first touch to later calculations.
*
* @see android.widget.AbsListView#onInterceptTouchEvent(android.view.MotionEvent )
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
currentY = ev.getY();
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* Handle what to do when the user release the touch.
*
* @see android.widget.AbsListView#onTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean onTouchEvent(final MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_UP:
if (currentState != State.UPDATING) {
if (currentState == State.RELEASE) {
header.startAnimation(new ResizeHeaderAnimation(headerHeight));
startRefreshing();
} else {
header.startAnimation(new ResizeHeaderAnimation(0));
}
} else {
header.startAnimation(new ResizeHeaderAnimation(headerHeight));
}
}
return super.onTouchEvent(ev);
}
/**
* Used to show header when scrolling down.
*
* @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent)
*/
@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE && getFirstVisiblePosition() == 0) {
if (isAllowedToShowHeader(ev.getY())) {
changeHeaderHeight(getHeightWithScrollResistance(ev.getY()));
}
}
return super.dispatchTouchEvent(ev);
}
/**
* Call to update UI when the refresh started.
*/
private void startRefreshing() {
arrow.clearAnimation();
arrow.setVisibility(View.INVISIBLE);
progress.setVisibility(View.VISIBLE);
comment.setText(getResources().getString(R.string.refreshlistview_updating));
if (refreshListener != null) {
refreshListener.onRefresh(this);
}
currentState = State.UPDATING;
}
/**
* Call when refreshing task is done. Must be called by the developer.
*/
public void finishRefreshing() {
finishRefreshing(null);
}
/**
* Call when refreshing task is done. Must be called by the developer.
*
* @param updateDate
* allow developer to set the last updateDate
*/
public void finishRefreshing(Date updateDate) {
header.startAnimation(new ResizeHeaderAnimation(0));
progress.setVisibility(View.INVISIBLE);
arrow.setVisibility(View.VISIBLE);
if (updateDate == null)
lastUpdateDate = new Date();
else
lastUpdateDate = updateDate;
date.setText(getFormattedDate(lastUpdateDate));
comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
currentState = State.PULLDOWN;
invalidate();
}
/**
* Change the header height while scrolling down by making it visible and increasing topMargin of the header.
*
* @param height
* the height of the header
*/
private void changeHeaderHeight(int height) {
hideOrShowHeader(height);
LayoutParams layoutParams = (LayoutParams) container.getLayoutParams();
layoutParams.height = height;
container.setLayoutParams(layoutParams);
LinearLayout.LayoutParams headerLayoutParams = (LinearLayout.LayoutParams) header.getLayoutParams();
headerLayoutParams.topMargin = height - headerHeight;
header.setLayoutParams(headerLayoutParams);
if (currentState != State.UPDATING) {
if (height > headerHeight && currentState == State.PULLDOWN) {
arrow.startAnimation(getRotationAnimation(Rotation.ANTICLOCKWISE));
comment.setText(getResources().getString(R.string.refreshlistview_release));
currentState = State.RELEASE;
} else if (height < headerHeight && currentState == State.RELEASE) {
arrow.startAnimation(getRotationAnimation(Rotation.CLOCKWISE));
comment.setText(getResources().getString(R.string.refreshlistview_pulldown));
currentState = State.PULLDOWN;
}
}
}
/**
* Check whether or not the header should be shown.
*
* @param newY
* just acquired Y event
* @return true if it is, false otherwise
*/
private boolean isAllowedToShowHeader(float newY) {
return isScrollingEnough(newY) && (currentState != State.UPDATING || (currentState == State.UPDATING && (newY - currentY) > 0));
}
/**
* Check if the scroll is enough to be taken into acccount.
*
* @param newY
* just acquired Y event
* @return true if it is, false otherwise
*/
private boolean isScrollingEnough(float newY) {
float deltaY = Math.abs(currentY - newY);
ViewConfiguration config = ViewConfiguration.get(getContext());
return deltaY > config.getScaledTouchSlop();
}
/**
* Calculate height header when scrolling down.
*
* @param newY
* just acquired Y event
* @return the height of the header to set
*/
private int getHeightWithScrollResistance(float newY) {
return Math.max((int) (newY - currentY) / RESISTANCE, 0);
}
/**
* Hide or show the header according to its height.
*
* @param height
* current height of the header
*/
private void hideOrShowHeader(int height) {
if (height <= 0) {
header.setVisibility(View.GONE);
} else {
header.setVisibility(View.VISIBLE);
}
}
/**
* Getter to know if date is enabled
*
* @return true if date is enabled, false otherwise
*/
public boolean isEnabledDate() {
return enabledDate;
}
/**
* Set enabled date, the first date to show will just be "No past update".
*
* @param enabledDate
*/
public void setEnabledDate(boolean enabledDate) {
setEnabledDate(enabledDate, null);
}
public void setEnabledDate(boolean enabledDate, Date firstDate) {
this.enabledDate = enabledDate;
lastUpdateDate = firstDate;
if (enabledDate) {
date.setVisibility(View.VISIBLE);
if (firstDate != null) {
date.setText(getFormattedDate(firstDate));
} else {
date.setText(getResources().getString(R.string.refreshlistview_no_update));
}
} else {
date.setVisibility(View.GONE);
}
}
/**
* Getter for last update date.
*
* @return the last update date or null is it has never been updated yet.
*/
public Date getLastUpdateDate() {
return lastUpdateDate;
}
private String getFormattedDate(Date date) {
return formatter.format(date);
}
public void setRefreshListener(OnRefreshListener listener) {
this.refreshListener = listener;
}
/**
* Callback. Call when user asks to refresh the list. Required to be implemented by developer.
*/
public interface OnRefreshListener {
public void onRefresh(RefreshListView listView);
}
private Animation getRotationAnimation(Rotation rotation) {
int fromAngle = 0;
int toAngle = 0;
if (rotation == Rotation.ANTICLOCKWISE)
toAngle = 180;
else
fromAngle = 180;
Animation animation = new RotateAnimation(fromAngle, toAngle, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(DURATION);
animation.setFillAfter(true);
return animation;
}
/**
* Animation to resize the header's height
*/
public class ResizeHeaderAnimation extends Animation {
private int toHeight;
public ResizeHeaderAnimation(int toHeight) {
this.toHeight = toHeight;
setDuration(DURATION);
}
/**
* Animation core, animate the height of the header to a specific value.
*
* @see android.view.animation.Animation#applyTransformation(float, android.view.animation.Transformation)
*/
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float height = (toHeight - container.getHeight()) * interpolatedTime + container.getHeight();
LayoutParams lp = (LayoutParams) container.getLayoutParams();
LinearLayout.LayoutParams headerlp = (LinearLayout.LayoutParams) header.getLayoutParams();
headerlp.topMargin = (int) height - headerHeight;
lp.height = (int) height;
lp.width = (int) container.getWidth();
container.requestLayout();
}
/**
* Used at the end of the animation to hide completely the header if it's required (toHeight == 0).
*
* @see android.view.animation.Animation#getTransformation(long, android.view.animation.Transformation)
*/
@Override
public boolean getTransformation(long currentTime, Transformation outTransformation) {
hideOrShowHeader(toHeight);
return super.getTransformation(currentTime, outTransformation);
}
}
/**
* Display a toast when there is an error in refresh task
*
* @param errorMessage
* error message to display
*/
public void errorInRefresh(String errorMessage) {
Toast.makeText(getContext(), errorMessage, Toast.LENGTH_SHORT).show();
}
}