593 lines
21 KiB
Java
593 lines
21 KiB
Java
package com.hippo.ehviewer.widget;
|
|
|
|
import com.hippo.ehviewer.R;
|
|
import com.hippo.ehviewer.util.Ui;
|
|
|
|
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.animation.Animation;
|
|
import android.view.animation.LinearInterpolator;
|
|
import android.view.animation.RotateAnimation;
|
|
import android.view.animation.Transformation;
|
|
import android.widget.AbsListView;
|
|
import android.widget.ImageView;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.ListView;
|
|
import android.widget.ProgressBar;
|
|
import android.widget.TextView;
|
|
|
|
public class PullListView extends LinearLayout implements AbsListView.OnScrollListener{
|
|
@SuppressWarnings("unused")
|
|
private static final String TAG = "PullListView";
|
|
|
|
private final static int HEADER_PULL_TO_REFRESH = 0;
|
|
private final static int HEADER_RELEASE_TO_REFRESH = 1;
|
|
private final static int HEADER_REFRESHING = 2;
|
|
private final static int HEADER_DONE = 3;
|
|
private final static int HEADER_CANCEL = 4;
|
|
|
|
private final static int FOOTER_REFRESHING = 0;
|
|
private final static int FOOTER_SUCCESS = 1;
|
|
private final static int FOOTER_FAIL = 2;
|
|
|
|
// arrow animation
|
|
private RotateAnimation mAnimation;
|
|
private RotateAnimation mReverseAnimation;
|
|
|
|
// Header animation
|
|
private Animation mRefreshAnimation;
|
|
private Animation mOverAnimation;
|
|
|
|
private ListView mListView;
|
|
private LinearLayout mHeader;
|
|
private LinearLayout mFooter;
|
|
private TextView mTipTextView;
|
|
private ImageView mArrowImageView;
|
|
private ProgressBar mProgressBar;
|
|
|
|
private TextView mFooterTipTextView;
|
|
private ProgressBar mFooterProgressBar;
|
|
|
|
// Make sure only one pull event
|
|
private boolean isHeaderRecored = false;
|
|
private boolean isFooterRecored = false;
|
|
|
|
// Header String to show
|
|
private String mHeaderPullStr;
|
|
private String mHeaderReleaseStr;
|
|
private String mHeaderRefreshStr;
|
|
private String mHeaderDoneStr;
|
|
private String mHeaderCancelStr;
|
|
|
|
// Footer String to show
|
|
private String mFooterRefreshStr;
|
|
private String mFooterSuccessStr;
|
|
private String mFooterFailStr;
|
|
|
|
// Header
|
|
private int headerHeight;
|
|
private int headerOriginalLeftPadding;
|
|
private int headerOriginalTopPadding;
|
|
private int headerOriginalRightPadding;
|
|
private int headerOriginalBottomPadding;
|
|
private int releaseTopPadding;
|
|
private int doneTopPadding;
|
|
private int pullThreshold;
|
|
private int pullOffestMax;
|
|
|
|
// Footer
|
|
private boolean isActionWhenShow = true;
|
|
|
|
private int startY;
|
|
private int headerState = HEADER_DONE;
|
|
private boolean isBack;
|
|
|
|
private int footerState = FOOTER_SUCCESS;
|
|
|
|
private OnHeaderRefreshListener headerRefreshListener;
|
|
private OnFooterRefreshListener footerRefreshListener;
|
|
|
|
// To Check scroll to top
|
|
// Store y of last point
|
|
private int lastY;
|
|
|
|
private boolean mHeaderEnabled = true;
|
|
@SuppressWarnings("unused")
|
|
private boolean mFooterEnabled = true;
|
|
|
|
public interface OnHeaderRefreshListener {
|
|
public void onHeaderRefresh();
|
|
public void onHeaderRefresh(Object obj);
|
|
}
|
|
|
|
public interface OnFooterRefreshListener {
|
|
public void onFooterRefresh();
|
|
}
|
|
|
|
public PullListView(Context context) {
|
|
super(context);
|
|
init(context);
|
|
}
|
|
|
|
public PullListView(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
init(context);
|
|
}
|
|
public PullListView(Context context, AttributeSet attrs, int defStyle) {
|
|
super(context, attrs, defStyle);
|
|
init(context);
|
|
}
|
|
|
|
private void init(Context context) {
|
|
// Set arrow animation
|
|
mAnimation = new RotateAnimation(0, -180,
|
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
|
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
|
|
mAnimation.setInterpolator(new LinearInterpolator());
|
|
mAnimation.setDuration(100);
|
|
mAnimation.setFillAfter(true);
|
|
|
|
mReverseAnimation = new RotateAnimation(-180, 0,
|
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
|
|
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
|
|
mReverseAnimation.setInterpolator(new LinearInterpolator());
|
|
mReverseAnimation.setDuration(100);
|
|
mReverseAnimation.setFillAfter(true);
|
|
|
|
// Set header animation
|
|
mRefreshAnimation = new Animation() {
|
|
@Override
|
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
mHeader.setPadding(headerOriginalLeftPadding,
|
|
(int)((1 - interpolatedTime) * releaseTopPadding + (interpolatedTime * headerOriginalTopPadding)),
|
|
headerOriginalRightPadding, headerOriginalBottomPadding);
|
|
mHeader.invalidate();
|
|
}
|
|
};
|
|
mRefreshAnimation.setAnimationListener(new Animation.AnimationListener() {
|
|
@Override
|
|
public void onAnimationStart(Animation animation) {
|
|
setEnabled(false);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animation animation) {
|
|
setEnabled(true);
|
|
}
|
|
@Override
|
|
public void onAnimationRepeat(Animation animation) {}
|
|
});
|
|
mRefreshAnimation.setDuration(100);
|
|
|
|
mOverAnimation = new Animation() {
|
|
@Override
|
|
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
|
mHeader.setPadding(headerOriginalLeftPadding,
|
|
(int)((1 - interpolatedTime) * doneTopPadding - (interpolatedTime * headerHeight)),
|
|
headerOriginalRightPadding, headerOriginalBottomPadding);
|
|
mHeader.invalidate();
|
|
}
|
|
};
|
|
mOverAnimation.setAnimationListener(new Animation.AnimationListener() {
|
|
@Override
|
|
public void onAnimationStart(Animation animation) {
|
|
setEnabled(false);
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationEnd(Animation animation) {
|
|
setEnabled(true);
|
|
if (headerState == HEADER_CANCEL) {
|
|
headerState = HEADER_DONE;
|
|
isHeaderRecored = false;
|
|
isBack = false;
|
|
}
|
|
}
|
|
@Override
|
|
public void onAnimationRepeat(Animation animation) {}
|
|
});
|
|
mOverAnimation.setDuration(200);
|
|
|
|
View view = LayoutInflater.from(context).inflate(R.layout.pull_list_view, this);
|
|
mFooter = (LinearLayout)LayoutInflater.from(context).inflate(R.layout.pull_list_view_footer, null);
|
|
mFooter.setOnClickListener(new OnClickListener() {
|
|
@Override
|
|
public void onClick(View v) {
|
|
footerRefresh();
|
|
}
|
|
});
|
|
mListView = (ListView)view.findViewById(R.id.list_view);
|
|
mListView.setOnScrollListener(this);
|
|
mListView.addFooterView(mFooter);
|
|
mFooterTipTextView = (TextView)mFooter.findViewById(R.id.footer_tip_text);
|
|
mFooterProgressBar = (ProgressBar)mFooter.findViewById(R.id.footer_progressBar);
|
|
|
|
mHeader = (LinearLayout)view.findViewById(R.id.header);
|
|
mArrowImageView = (ImageView) mHeader.findViewById(R.id.header_arrow_image);
|
|
mArrowImageView.setMinimumWidth(50);
|
|
mArrowImageView.setMinimumHeight(50);
|
|
mProgressBar = (ProgressBar)mHeader.findViewById(R.id.header_progressBar);
|
|
mTipTextView = (TextView)mHeader.findViewById(R.id.header_tip_text);
|
|
|
|
headerOriginalLeftPadding = mHeader.getPaddingLeft();
|
|
headerOriginalTopPadding = mHeader.getPaddingTop();
|
|
headerOriginalRightPadding = mHeader.getPaddingRight();
|
|
headerOriginalBottomPadding = mHeader.getPaddingBottom();
|
|
|
|
Ui.measureView(mHeader);
|
|
headerHeight = mHeader.getMeasuredHeight();
|
|
pullThreshold = headerHeight;
|
|
pullOffestMax = (int)(headerHeight * 2.3f);
|
|
|
|
mHeader.setPadding(headerOriginalLeftPadding, -headerHeight, headerOriginalRightPadding, headerOriginalBottomPadding);
|
|
mHeader.invalidate();
|
|
}
|
|
|
|
@Override
|
|
public void onScrollStateChanged(AbsListView view, int scrollState) {}
|
|
|
|
@Override
|
|
public void onScroll(AbsListView view, int firstVisibleItem,
|
|
int visibleItemCount, int totalItemCount) {
|
|
if (firstVisibleItem + visibleItemCount == totalItemCount
|
|
&& isActionWhenShow && totalItemCount > 1)
|
|
footerRefresh();
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent event) {
|
|
if (!mHeaderEnabled || isHeaderRecored || isFooterRecored)
|
|
return false;
|
|
|
|
final int action = event.getAction() & MotionEvent.ACTION_MASK;
|
|
|
|
switch (action) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
lastY = (int)event.getY();
|
|
return false;
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
case MotionEvent.ACTION_CANCEL:
|
|
return false;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
int y = (int)event.getY();
|
|
int offsetY = y - lastY;
|
|
lastY = y;
|
|
|
|
if (offsetY > 0 && isListAtTop())
|
|
return true;
|
|
else
|
|
return false;
|
|
|
|
default:
|
|
return super.onInterceptTouchEvent(event);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent event) {
|
|
final int action = event.getAction() & MotionEvent.ACTION_MASK;
|
|
|
|
switch (action) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
if (mHeaderEnabled && !isHeaderRecored && !isFooterRecored) {
|
|
isHeaderRecored = true;
|
|
startY = (int) event.getY();
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
headerState = HEADER_DONE;
|
|
isHeaderRecored = false;
|
|
isBack = false;
|
|
changeHeaderViewByState();
|
|
break;
|
|
case MotionEvent.ACTION_UP:
|
|
if (headerState == HEADER_PULL_TO_REFRESH) {
|
|
headerState = HEADER_CANCEL;
|
|
changeHeaderViewByState();
|
|
}
|
|
else if (headerState == HEADER_RELEASE_TO_REFRESH) {
|
|
headerState = HEADER_REFRESHING;
|
|
changeHeaderViewByState();
|
|
onHeaderRefresh();
|
|
} else if (headerState == HEADER_DONE) {
|
|
isHeaderRecored = false;
|
|
isBack = false;
|
|
changeHeaderViewByState();
|
|
}
|
|
break;
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
|
int tempY = (int)event.getY();
|
|
if (mHeaderEnabled && !isHeaderRecored && !isFooterRecored) {
|
|
isHeaderRecored = true;
|
|
startY = tempY;
|
|
}
|
|
if (headerState != HEADER_REFRESHING && isHeaderRecored) {
|
|
int offsetY = getShowOffset(tempY - startY, pullOffestMax);
|
|
if (headerState == HEADER_RELEASE_TO_REFRESH) {
|
|
// 往上推,推到屏幕足够掩盖head的程度,但还没有全部掩盖
|
|
if (offsetY < pullThreshold && offsetY > 0) {
|
|
headerState = HEADER_PULL_TO_REFRESH;
|
|
changeHeaderViewByState();
|
|
}
|
|
// 一下子推到顶
|
|
else if (offsetY <= 0) {
|
|
headerState = HEADER_DONE;
|
|
startY = tempY;
|
|
}
|
|
} else if (headerState == HEADER_PULL_TO_REFRESH) {
|
|
// 下拉到可以进入RELEASE_TO_REFRESH的状态
|
|
if (offsetY >= pullThreshold) {
|
|
headerState = HEADER_RELEASE_TO_REFRESH;
|
|
isBack = true;
|
|
changeHeaderViewByState();
|
|
}
|
|
// 上推到顶了
|
|
else if (offsetY <= 0) {
|
|
headerState = HEADER_DONE;
|
|
startY = tempY;
|
|
}
|
|
} else if (headerState == HEADER_DONE) {
|
|
if (offsetY > 0) {
|
|
headerState = HEADER_PULL_TO_REFRESH;
|
|
changeHeaderViewByState();
|
|
} else {
|
|
startY = tempY;
|
|
}
|
|
}
|
|
|
|
// 更新headView的size
|
|
if (headerState == HEADER_PULL_TO_REFRESH) {
|
|
int topPadding = (int)(offsetY - headerHeight);
|
|
mHeader.setPadding(headerOriginalLeftPadding, topPadding, headerOriginalRightPadding, headerOriginalBottomPadding);
|
|
mHeader.invalidate();
|
|
}
|
|
|
|
// 更新headView的paddingTop
|
|
if (headerState == HEADER_RELEASE_TO_REFRESH) {
|
|
int topPadding = (int)(offsetY - headerHeight);
|
|
mHeader.setPadding(headerOriginalLeftPadding, topPadding, headerOriginalRightPadding, headerOriginalBottomPadding);
|
|
mHeader.invalidate();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private int getShowOffset(int eventOffset, int target) {
|
|
if (eventOffset <= 0) // avoid divide zero
|
|
return 0;
|
|
else
|
|
return (target * eventOffset) / (eventOffset + target);
|
|
}
|
|
|
|
// Refresh header UI
|
|
private void changeHeaderViewByState() {
|
|
switch (headerState) {
|
|
case HEADER_RELEASE_TO_REFRESH:
|
|
mArrowImageView.setVisibility(View.VISIBLE);
|
|
mProgressBar.setVisibility(View.GONE);
|
|
mTipTextView.setVisibility(View.VISIBLE);
|
|
mArrowImageView.clearAnimation();
|
|
mArrowImageView.startAnimation(mAnimation);
|
|
mTipTextView.setText(mHeaderReleaseStr);
|
|
break;
|
|
case HEADER_PULL_TO_REFRESH:
|
|
mProgressBar.setVisibility(View.GONE);
|
|
mTipTextView.setVisibility(View.VISIBLE);
|
|
mArrowImageView.clearAnimation();
|
|
mArrowImageView.setVisibility(View.VISIBLE);
|
|
if (isBack) {
|
|
isBack = false;
|
|
mArrowImageView.clearAnimation();
|
|
mArrowImageView.startAnimation(mReverseAnimation);
|
|
}
|
|
mTipTextView.setText(mHeaderPullStr);
|
|
break;
|
|
case HEADER_REFRESHING:
|
|
releaseTopPadding = mHeader.getPaddingTop();
|
|
mHeader.startAnimation(mRefreshAnimation);
|
|
mProgressBar.setVisibility(View.VISIBLE);
|
|
mArrowImageView.clearAnimation();
|
|
mArrowImageView.setVisibility(View.GONE);
|
|
mTipTextView.setText(mHeaderRefreshStr);
|
|
break;
|
|
case HEADER_DONE:
|
|
case HEADER_CANCEL:
|
|
doneTopPadding = mHeader.getPaddingTop();
|
|
mHeader.startAnimation(mOverAnimation);
|
|
mProgressBar.setVisibility(View.GONE);
|
|
mArrowImageView.setVisibility(View.GONE);
|
|
mArrowImageView.clearAnimation();
|
|
// 此处更换图标
|
|
mArrowImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow);
|
|
if (headerState == HEADER_DONE)
|
|
mTipTextView.setText(mHeaderDoneStr);
|
|
else
|
|
mTipTextView.setText(mHeaderCancelStr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Refresh footer UI
|
|
private void changeFooterViewByState() {
|
|
switch (footerState) {
|
|
case FOOTER_REFRESHING:
|
|
mFooterTipTextView.setText(mFooterRefreshStr);
|
|
mFooterProgressBar.setVisibility(View.VISIBLE);
|
|
break;
|
|
case FOOTER_SUCCESS:
|
|
mFooterTipTextView.setText(mFooterSuccessStr);
|
|
mFooterProgressBar.setVisibility(View.GONE);
|
|
break;
|
|
case FOOTER_FAIL:
|
|
mFooterTipTextView.setText(mFooterFailStr);
|
|
mFooterProgressBar.setVisibility(View.GONE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public ListView getListView() {
|
|
return mListView;
|
|
}
|
|
|
|
// Header
|
|
public void setHeaderEnabled(boolean enabled) {
|
|
mHeaderEnabled = enabled;
|
|
}
|
|
|
|
public void setHeaderString(String pullStr, String releaseStr, String refreshStr, String doneStr, String cancelStr) {
|
|
mHeaderPullStr = pullStr;
|
|
mHeaderReleaseStr = releaseStr;
|
|
mHeaderRefreshStr = refreshStr;
|
|
mHeaderDoneStr = doneStr;
|
|
mHeaderCancelStr = cancelStr;
|
|
|
|
switch (headerState) {
|
|
case HEADER_PULL_TO_REFRESH:
|
|
mTipTextView.setText(mHeaderPullStr);
|
|
break;
|
|
case HEADER_RELEASE_TO_REFRESH:
|
|
mTipTextView.setText(mHeaderReleaseStr);
|
|
break;
|
|
case HEADER_REFRESHING:
|
|
mTipTextView.setText(mHeaderRefreshStr);
|
|
break;
|
|
case HEADER_DONE:
|
|
mTipTextView.setText(mHeaderDoneStr);
|
|
break;
|
|
case HEADER_CANCEL:
|
|
mTipTextView.setText(mHeaderCancelStr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public boolean clickHeaderRefresh(Object obj) {
|
|
if (!mHeaderEnabled || isHeaderRecored || isFooterRecored)
|
|
return false;
|
|
|
|
isHeaderRecored = true;
|
|
headerState = HEADER_REFRESHING;
|
|
changeHeaderViewByState();
|
|
onHeaderRefresh(obj);
|
|
return true;
|
|
}
|
|
|
|
public void setOnRefreshListener(OnHeaderRefreshListener refreshListener) {
|
|
this.headerRefreshListener = refreshListener;
|
|
}
|
|
|
|
private void onHeaderRefresh() {
|
|
if (headerRefreshListener != null) {
|
|
headerRefreshListener.onHeaderRefresh();
|
|
}
|
|
}
|
|
|
|
private void onHeaderRefresh(Object obj) {
|
|
if (headerRefreshListener != null) {
|
|
headerRefreshListener.onHeaderRefresh(obj);
|
|
}
|
|
}
|
|
|
|
public boolean isHeaderRefreshing() {
|
|
return headerState == HEADER_REFRESHING;
|
|
}
|
|
|
|
public void onHeaderRefreshComplete() {
|
|
headerState = HEADER_DONE;
|
|
isHeaderRecored = false;
|
|
isBack = false;
|
|
changeHeaderViewByState();
|
|
}
|
|
|
|
// Footer
|
|
public void setFooterEnabled(boolean enabled) {
|
|
// TODO Remove footer if false
|
|
mFooterEnabled = enabled;
|
|
}
|
|
|
|
public void setFooterString(String refreshStr, String successStr, String failStr) {
|
|
mFooterRefreshStr = refreshStr;
|
|
mFooterSuccessStr = successStr;
|
|
mFooterFailStr = failStr;
|
|
|
|
switch (footerState) {
|
|
case FOOTER_REFRESHING:
|
|
mFooterTipTextView.setText(mFooterRefreshStr);
|
|
break;
|
|
case FOOTER_SUCCESS:
|
|
mFooterTipTextView.setText(mFooterSuccessStr);
|
|
break;
|
|
case FOOTER_FAIL:
|
|
mFooterTipTextView.setText(mFooterFailStr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void footerRefresh() {
|
|
if (!mHeaderEnabled || isHeaderRecored || isFooterRecored)
|
|
return;
|
|
|
|
isFooterRecored = true;
|
|
footerState = FOOTER_REFRESHING;
|
|
changeFooterViewByState();
|
|
|
|
if (footerRefreshListener != null) {
|
|
footerRefreshListener.onFooterRefresh();
|
|
}
|
|
}
|
|
|
|
public void setOnFooterRefreshListener(OnFooterRefreshListener footerRefreshListener) {
|
|
this.footerRefreshListener = footerRefreshListener;
|
|
}
|
|
|
|
public void onFooterRefreshComplete(boolean isSuccess) {
|
|
if (isSuccess) {
|
|
footerState = FOOTER_SUCCESS;
|
|
isActionWhenShow = true;
|
|
}
|
|
else {
|
|
footerState = FOOTER_FAIL;
|
|
isActionWhenShow = false;
|
|
}
|
|
isFooterRecored = false;
|
|
changeFooterViewByState();
|
|
}
|
|
|
|
public boolean isFooterRefreshing() {
|
|
return footerState == FOOTER_REFRESHING;
|
|
}
|
|
|
|
public void onFooterRefreshComplete(boolean isSuccess, boolean actionWhenShow) {
|
|
if (isSuccess)
|
|
footerState = FOOTER_SUCCESS;
|
|
else
|
|
footerState = FOOTER_FAIL;
|
|
isActionWhenShow = actionWhenShow;
|
|
isFooterRecored = false;
|
|
changeFooterViewByState();
|
|
}
|
|
|
|
public void setActionWhenShow(boolean actionWhenShow) {
|
|
isActionWhenShow = actionWhenShow;
|
|
}
|
|
|
|
private boolean isListAtTop() {
|
|
if(mListView.getChildCount() == 0)
|
|
return true;
|
|
return mListView.getFirstVisiblePosition() == 0
|
|
&& mListView.getChildAt(0).getTop() == 0;
|
|
}
|
|
|
|
// Get header width and height
|
|
|
|
}
|