最近工程需要使用下拉重新整理,但是使用網上流傳的各種版本均有或多或少的bug,或者效果不完美的地方。在使用QQ的時候,在消息清單界面的下拉重新整理,個人感覺效果比較棒,就做了一個高仿版,效果與QQ的基本保持一緻,有不足之處,歡迎指正。
源碼下載下傳位址:
http://download.csdn.net/detail/yutou58nian/6708851
又重構了一下代碼,目前接近完美版!
僅有的一個小問題是,滑開頭部,在下拉重新整理和松手立即重新整理狀态來回切換時,滑動的彈性效果會變小。原因是在一直滑動的過程中,根據手指滑動的距離,一直setPadding時,會導緻頭部的paddingTop值跟實際顯示在界面上的效果不一緻,暫時還不知道怎麼解決。
效果圖如下:
主要實作的特殊效果如下:
1. 下拉時縮減手指滑動距離,實作越拉越難的效果
2. 加載狀态時,上推界面遮擋部分頭部,頭部自動收回
3. 加載狀态時,界面依然可以下拉,松手自動收回,隻顯示頭部
4. 加載完成後,有加載完成的狀态,停留1秒之後自動收回
5. ListView中資料長度沒有充滿螢幕時,可以下拉重新整理
6. ListView中沒有資料時,可以下拉重新整理
7. 所有的下拉,回彈均有動畫效果
具體的實作思路跟網上的是一樣的,就是給ListView添加HeadView,預設隐藏,通過監聽OnTouch、OnScroll事件實作滑動時的各種效果。
隻說說在實作時遇到的幾個問題是怎麼解決的:
1. 實作越拉越難的效果
在實作拉動越來越難的效果時,通過監聽MotionEvent.ACTION_MOVE事件,取手指滑動距離的1/3,代碼如下:
if (currentHeaderState != REFRESH_BACED) { mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight + (int) ((mMoveY - mDownY) / 3), mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); } else if (currentHeaderState == REFRESH_BACED && headVisible == true) { mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), (int) ((mMoveY - mDownY) / 3), mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); } else if (currentHeaderState == REFRESH_BACED && headVisible == false) { mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight + (int) ((mMoveY - mDownY) / 3), mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); }
下拉時一共有三種狀态:A. 正常狀态、B. 加載狀态且頭部隐藏、C.加載狀态且頭部顯示。
其中A狀态和B狀态處理方式一樣,直接從手指滑動開始計算,拉開滑動距離1/3的效果
C狀态時,滑動時位置減去頭部的高度,再開始滑動。
直接通過setPadding來實作滑開的效果,且滑動距離縮減1/3,如果手指不松開,來回滑動的話,會導緻距離計算不正确,是以在設定回彈效果的時候,要做處理,不然會導緻界面收回之後,List中的部分條目也被遮擋。
2. 實作回彈動畫
這個因為ListView也是在主界面的線程中,是以可以使用Handler.postDelayed()來實作,每次縮減剩餘高度的1/4,5毫秒重新整理一次即可。
這裡主要實作了兩個動畫,一個是頭部隐藏動畫,用于未達到重新整理狀态,和遮擋部分加載中的頭部時的動畫
另外一個是頭部收回動畫,用于下拉高度超出頭部高度時,頭部的松手回彈動畫,代碼如下:
Runnable headHideAnimation = new Runnable() { public void run() { if (mHeaderLinearLayout.getBottom() > 0) { int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout .getPaddingTop() * 0.75f) - 1; if (paddingTop < -mHeaderHeight) { paddingTop = -mHeaderHeight; } mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), paddingTop, mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); handler.postDelayed(headHideAnimation, 5); } else { handler.removeCallbacks(headHideAnimation); mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight, mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); setSelection(1); headVisible = false; } } }; Runnable headBackAnimation = new Runnable() { public void run() { if (mHeaderLinearLayout.getPaddingTop() > 1) { mHeaderLinearLayout.setPadding( mHeaderLinearLayout.getPaddingLeft(), (int) (mHeaderLinearLayout.getPaddingTop() * 0.75f), mHeaderLinearLayout.getPaddingRight(), mHeaderLinearLayout.getPaddingBottom()); handler.postDelayed(headBackAnimation, 5); } else { headVisible = true; handler.removeCallbacks(headBackAnimation); } } };
3. 實作ListView中資料沒有充滿螢幕時的下拉
當ListView中的資料沒有充滿螢幕的時候,滑動ListView沒有内容的部分,監聽不到onScrollStateChanged()事件,隻能監聽到onTouchEvent()、onScroll()這兩個事件,如果不做特殊處理的話,會導緻下拉之後狀态不改變。
是以在onTouchEvent()中的Move事件中将界面的狀态由靜止改為滑動,即可解決問題。
全部的代碼實作如下:
public class RefreshListView extends ListView implements OnScrollListener {
private float mDownY;
private float mMoveY;
private int mHeaderHeight;
private int mCurrentScrollState;
private final static int NONE_PULL_REFRESH = 0; // 正常狀态
private final static int ENTER_PULL_REFRESH = 1; // 進入下拉重新整理狀态
private final static int OVER_PULL_REFRESH = 2; // 進入松手立即重新整理狀态
// 加載狀态下拉
private final static int PUSH_REFRESHING = 3; // 加載狀态中,隐藏部分正在加載
private final static int OVER_PULL_REFRESHING = 4; // 加載狀态中,滑開超出titlebar高度
private int mPullRefreshState = 0; // 記錄目前滑動狀态
// 松手後,界面狀态
private final static int REFRESH_BACED = 1; // 反彈結束,重新整理中
private final static int REFRESH_RETURN = 2; // 沒有達到重新整理界限,傳回
private final static int REFRESH_DONE = 3; // 加載資料結束
private final static int REFRESH_ORIGINAL = 4; // 最初的狀态
public int currentHeaderState = -1; // 記錄目前資料加載狀态
private boolean headVisible = false;
private LinearLayout mHeaderLinearLayout = null;
private TextView mHeaderTextView = null;
private ImageView mHeaderPullDownImageView = null;
private ImageView mHeaderProgressImage = null;
private ImageView mHeaderRefreshOkImage = null;
private RefreshListener mRefreshListener = null;
private RotateAnimation animation;
private RotateAnimation reverseAnimation;
private boolean isBack = false;
private Handler handler = new Handler();
public void setOnRefreshListener(RefreshListener refreshListener) {
this.mRefreshListener = refreshListener;
}
public RefreshListView(Context context) {
this(context, null);
}
public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public void init(final Context context) {
mHeaderLinearLayout = (LinearLayout) LayoutInflater.from(context)
.inflate(R.layout.refresh_list_header, null);
addHeaderView(mHeaderLinearLayout);
mHeaderTextView = (TextView) findViewById(R.id.refresh_list_header_text);
mHeaderPullDownImageView = (ImageView) findViewById(R.id.refresh_list_header_pull_down);
mHeaderProgressImage = (ImageView) findViewById(R.id.refresh_list_header_loading);
mHeaderRefreshOkImage = (ImageView) findViewById(R.id.refresh_list_header_success);
setSelection(1);
setOnScrollListener(this);
measureView(mHeaderLinearLayout);
mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();
mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
-mHeaderHeight, mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
animation = new RotateAnimation(0, 180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
animation.setInterpolator(new LinearInterpolator());
animation.setDuration(150);
animation.setFillAfter(true);// 箭頭翻轉動畫
reverseAnimation = new RotateAnimation(180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
reverseAnimation.setInterpolator(new LinearInterpolator());
reverseAnimation.setDuration(150);
reverseAnimation.setFillAfter(true);// 箭頭反翻轉動畫
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
handler.removeCallbacks(headHideAnimation);
handler.removeCallbacks(headBackAnimation);
break;
case MotionEvent.ACTION_MOVE:
mMoveY = ev.getY();
if (mCurrentScrollState == SCROLL_STATE_IDLE) {
mCurrentScrollState = SCROLL_STATE_TOUCH_SCROLL;
}
if (currentHeaderState != REFRESH_BACED) {
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
+ (int) ((mMoveY - mDownY) / 3),
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
} else if (currentHeaderState == REFRESH_BACED
&& headVisible == true) {
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(),
(int) ((mMoveY - mDownY) / 3),
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
} else if (currentHeaderState == REFRESH_BACED
&& headVisible == false) {
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
+ (int) ((mMoveY - mDownY) / 3),
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
}
break;
case MotionEvent.ACTION_UP:
if (mPullRefreshState == OVER_PULL_REFRESH) {
currentHeaderState = REFRESH_BACED;
handler.postDelayed(headBackAnimation, 5);
refreshViewByState();
} else if (mPullRefreshState == ENTER_PULL_REFRESH) {
currentHeaderState = REFRESH_RETURN;
handler.postDelayed(headHideAnimation, 5);
refreshViewByState();
} else if (mPullRefreshState == PUSH_REFRESHING) {
handler.postDelayed(headHideAnimation, 5);
} else if (mPullRefreshState == OVER_PULL_REFRESHING) {
handler.postDelayed(headBackAnimation, 5);
}
break;
}
return super.onTouchEvent(ev);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (currentHeaderState != REFRESH_BACED) {
if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
.getBottom() < mHeaderHeight)) {
mPullRefreshState = ENTER_PULL_REFRESH;
mHeaderTextView.setText(R.string.app_list_header_refresh_down);
mHeaderPullDownImageView.setVisibility(View.VISIBLE);
mHeaderRefreshOkImage.setVisibility(View.GONE);
if (isBack) {
isBack = false;
mHeaderPullDownImageView.clearAnimation();
mHeaderPullDownImageView.startAnimation(reverseAnimation);
}
} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
isBack = true;
if (mPullRefreshState == ENTER_PULL_REFRESH
|| mPullRefreshState == NONE_PULL_REFRESH) {
mPullRefreshState = OVER_PULL_REFRESH;
mHeaderTextView.setText(R.string.app_list_header_refresh);
mHeaderPullDownImageView.clearAnimation();
mHeaderPullDownImageView.startAnimation(animation);
}
} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem != 0) {
if (mPullRefreshState == ENTER_PULL_REFRESH) {
mPullRefreshState = NONE_PULL_REFRESH;
}
}
} else {
if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
.getBottom() < mHeaderHeight)) {
mPullRefreshState = PUSH_REFRESHING;
} else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& firstVisibleItem == 0
&& (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
mPullRefreshState = OVER_PULL_REFRESHING;
}
}
if (mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem == 0) {
setSelection(1);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
setSelection(1);
}
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_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);
}
public void refreshViewByState() {
switch (currentHeaderState) {
case REFRESH_BACED:
mHeaderTextView.setText(R.string.app_list_loading);
mHeaderProgressImage.setVisibility(View.VISIBLE);
mHeaderPullDownImageView.clearAnimation();
mHeaderPullDownImageView.setVisibility(View.GONE);
mPullRefreshState = NONE_PULL_REFRESH;
isBack = false;
if (mRefreshListener != null) {
mRefreshListener.refreshing();
}
break;
case REFRESH_RETURN:
mPullRefreshState = NONE_PULL_REFRESH;
currentHeaderState = REFRESH_ORIGINAL;
break;
case REFRESH_DONE:
mHeaderTextView.setText(R.string.app_list_refresh_done);
mHeaderProgressImage.setVisibility(View.INVISIBLE);
mHeaderRefreshOkImage.setVisibility(View.VISIBLE);
mPullRefreshState = NONE_PULL_REFRESH;
currentHeaderState = REFRESH_ORIGINAL;
mCurrentScrollState = SCROLL_STATE_IDLE;
handler.postDelayed(headHideAnimation, 700);
break;
default:
break;
}
}
Runnable headHideAnimation = new Runnable() {
public void run() {
if (mHeaderLinearLayout.getBottom() > 0) {
int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout
.getPaddingTop() * 0.75f) - 1;
if (paddingTop < -mHeaderHeight) {
paddingTop = -mHeaderHeight;
}
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(), paddingTop,
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
handler.postDelayed(headHideAnimation, 5);
} else {
handler.removeCallbacks(headHideAnimation);
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight,
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
setSelection(1);
headVisible = false;
}
}
};
Runnable headBackAnimation = new Runnable() {
public void run() {
if (mHeaderLinearLayout.getPaddingTop() > 1) {
mHeaderLinearLayout.setPadding(
mHeaderLinearLayout.getPaddingLeft(),
(int) (mHeaderLinearLayout.getPaddingTop() * 0.75f),
mHeaderLinearLayout.getPaddingRight(),
mHeaderLinearLayout.getPaddingBottom());
handler.postDelayed(headBackAnimation, 5);
} else {
headVisible = true;
handler.removeCallbacks(headBackAnimation);
}
}
};
public interface RefreshListener {
// 正在下拉重新整理
public void refreshing();
}
}
頭部的布局檔案如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/black"
android:gravity="center"
android:orientation="horizontal" >
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="50dp" >
<ImageView
android:id="@+id/refresh_list_header_loading"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginLeft="@dimen/refresh_title_margin_left"
android:contentDescription="@string/app_image_helper"
android:src="@drawable/refresh_loading"
android:visibility="gone" >
</ImageView>
<ImageView
android:id="@+id/refresh_list_header_pull_down"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginLeft="@dimen/refresh_title_margin_left"
android:contentDescription="@string/app_image_helper"
android:src="@drawable/refresh_arrow" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" >
<ImageView
android:id="@+id/refresh_list_header_success"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_centerVertical="true"
android:contentDescription="@string/app_image_helper"
android:src="@drawable/header_refresh_success"
android:visibility="gone" >
</ImageView>
<TextView
android:id="@+id/refresh_list_header_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="4dp"
android:layout_toRightOf="@id/refresh_list_header_success"
android:text="@string/app_list_header_refresh_down"
android:textColor="@android:color/white"
android:textSize="15sp" />
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
代碼中注釋不多,敬請諒解。