一、前言:
很多应用都会用到ListView,当然如果是iOS就会用UITableViewController,这两个控件在不同的OS上,功能是一样的,只是有些细微的不同(iOS的UITableViewController支持静态与动态两种),不过,大多数应用都用的是动态属性,那么,这里就涉及到一个问题:刷新及加载更多内容。
目前网上流行的有两种方式:
1. 通用的方法,即将ListView, GridView和ScrollView当成ChildView,在这顶部及底部各加一个Layout,但是,一但出现了,就一直显示在顶部或底部,并不会随着ChildView的滚动而滚动,功能实用,就是有点破坏美感;
2. 各自实现,即如果需要实现ListView的下拉刷新和上拉更多,那么就得去继承ListView,并对它的HeaderView和FooterView做一些扩展,同理,GridView和ScrollView;
本篇将使用第二种方法来实现,如果通过继承ListView的方式,来实现下拉刷新,以及上拉更多,或者是点击底部加载更多的。
二、实现:
2.1 HeaderView的布局,以及代码实现
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#ffffff" android:gravity="bottom"> <RelativeLayout android:id="@+id/header_content" android:layout_width="match_parent" android:layout_height="60dip"> <LinearLayout android:id="@+id/layoutTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="center" android:orientation="vertical"> <TextView android:id="@+id/refresh_tips" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="15sp" android:text="@string/pull_down_for_refresh"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_marginTop="4dip"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" android:text="@string/label_update"/> <TextView android:id="@+id/refresh_last_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" android:text="@string/label_last_time"/> </LinearLayout> </LinearLayout> <ImageView android:id="@+id/ivArrow" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_toLeftOf="@id/layoutTitle" android:layout_centerInParent="true" android:layout_marginRight="30dip" android:contentDescription="@string/image_desc" android:src="@drawable/refresh_arrow_down"/> <ProgressBar android:id="@+id/pbWaiting" android:visibility="gone" android:layout_height="wrap_content" android:layout_width="wrap_content" android:layout_toLeftOf="@id/layoutTitle" android:layout_centerInParent="true" android:layout_marginRight="30dip" style="?android:attr/progressBarStyleSmall"/> </RelativeLayout> </LinearLayout>
布局很简单,一些TextView,一个ImageView和一个ProgressBar。再来看看代码实现
package com.chris.list.refresh; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.view.animation.RotateAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; public class HeaderView extends LinearLayout { public final static int STATE_NORMAL = 0; public final static int STATE_WILL_RELEASE = 1; public final static int STATE_REFRESHING = 2; private int mState = STATE_NORMAL; private View mHeader = null; private ImageView mArrow = null; private ProgressBar mProgressBar = null; private TextView mRefreshTips = null; //private TextView mRefreshLastTime = null; private RotateAnimation mRotateUp = null; private RotateAnimation mRotateDown = null; private final static int ROTATE_DURATION = 250; public HeaderView(Context context) { this(context, null); } public HeaderView(Context context, AttributeSet attrs) { super(context, attrs); initHeaderView(context); } private void initHeaderView(Context context){ LinearLayout.LayoutParams lp = new LayoutParams( LayoutParams.MATCH_PARENT, 0); mHeader = LayoutInflater.from(context).inflate(R.layout.refresh_header, null); addView(mHeader, lp); setGravity(Gravity.BOTTOM); mArrow = (ImageView) mHeader.findViewById(R.id.ivArrow); mProgressBar = (ProgressBar) mHeader.findViewById(R.id.pbWaiting); mRefreshTips = (TextView) mHeader.findViewById(R.id.refresh_tips); //mRefreshLastTime = (TextView) mHeader.findViewById(R.id.refresh_last_time); mRotateUp = new RotateAnimation(0.0f, -180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateUp.setDuration(ROTATE_DURATION); mRotateUp.setFillAfter(true); mRotateDown = new RotateAnimation(-180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateDown.setDuration(ROTATE_DURATION); mRotateDown.setFillAfter(true); } public void setHeaderState(int state){ if(mState == state){ return; } mArrow.clearAnimation(); if(state == STATE_REFRESHING){ mProgressBar.setVisibility(View.VISIBLE); mArrow.setVisibility(View.GONE); }else{ mProgressBar.setVisibility(View.GONE); mArrow.setVisibility(View.VISIBLE); } switch(state){ case STATE_NORMAL: mArrow.startAnimation(mRotateDown); mRefreshTips.setText(R.string.pull_down_for_refresh); break; case STATE_WILL_RELEASE: mArrow.startAnimation(mRotateUp); mRefreshTips.setText(R.string.release_for_refresh); break; case STATE_REFRESHING: mRefreshTips.setText(R.string.refreshing); break; default: break; } mState = state; } public int getCurrentState(){ return mState; } public void setHeaderHeight(int height){ if(height <= 0){ height = 0; } LayoutParams lp = (LayoutParams) mHeader.getLayoutParams(); lp.height = height; mHeader.setLayoutParams(lp); } public int getHeaderHeight(){ return mHeader.getHeight(); } }
这个代码中,主要就两个函数:setHeaderState 和 setHeaderHeight。 前者是根据TouchEvent,以及当前移动的距离,来设置状态,同时,移动的距离去设置HeaderView的高度,达到一点一点的显示出来。
2.2 FooterView的布局,以及代码实现
看了HeaderView的布局与实现后,FooterView的布局与实现也差不多,咱们一起来看看吧
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="top" > <RelativeLayout android:id="@+id/footer_content" android:layout_width="match_parent" android:layout_height="60dip" > <TextView android:id="@+id/loader_tips" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/pull_up_for_more" android:textSize="15sp" /> <ImageView android:id="@+id/ivLoaderArrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_marginRight="30dip" android:layout_toLeftOf="@id/loader_tips" android:contentDescription="@string/image_desc" android:src="@drawable/refresh_arrow_up" /> <ProgressBar android:id="@+id/pbLoaderWaiting" style="?android:attr/progressBarStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_marginRight="30dip" android:layout_toLeftOf="@id/loader_tips" android:visibility="gone" /> </RelativeLayout> </LinearLayout>
哇,这个布局比HeaderView布局还要简单!?这个布局涵盖了两部分,不过,在布局中无法体现出来,但在代码实现中体现出来了:
1. 上拉更多,这个布局全部显示;
2. 如果是滑到底部点击加载,就不会有ImageView;
还是来看看代码实现吧
package com.chris.list.refresh; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.animation.Animation; import android.view.animation.RotateAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; public class FooterView extends LinearLayout { public final static int FOOTER_OPTIONS_PULL = 0; public final static int FOOTER_OPTIONS_CLICK = 1; private static int sFooterOps = FOOTER_OPTIONS_PULL; public final static int STATE_NORMAL = 0; public final static int STATE_WILL_RELEASE = 1; public final static int STATE_LOADING = 2; private int mState = STATE_NORMAL; private View mFooter = null; private ImageView mArrow = null; private ProgressBar mProgressBar = null; private TextView mLoaderTips = null; private RotateAnimation mRotateUp = null; private RotateAnimation mRotateDown = null; private final static int ROTATE_DURATION = 250; public FooterView(Context context) { this(context, null); } public FooterView(Context context, AttributeSet attrs) { super(context, attrs); initFooterView(context); } private void initFooterView(Context context){ LinearLayout.LayoutParams lp = new LayoutParams( LayoutParams.MATCH_PARENT, 0); mFooter = LayoutInflater.from(context).inflate(R.layout.loader_footer, null); addView(mFooter, lp); mArrow = (ImageView) mFooter.findViewById(R.id.ivLoaderArrow); mProgressBar = (ProgressBar) mFooter.findViewById(R.id.pbLoaderWaiting); mLoaderTips = (TextView) mFooter.findViewById(R.id.loader_tips); mRotateDown = new RotateAnimation(0.0f, 180.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateDown.setDuration(ROTATE_DURATION); mRotateDown.setFillAfter(true); mRotateUp = new RotateAnimation(180.0f, 0.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mRotateUp.setDuration(ROTATE_DURATION); mRotateUp.setFillAfter(true); setFooterViewOptions(FOOTER_OPTIONS_CLICK); } public void setFooterViewOptions(int options){ sFooterOps = options; switch(sFooterOps){ case FOOTER_OPTIONS_PULL: hide(); break; case FOOTER_OPTIONS_CLICK: show(); break; default: break; } } public int getFooterViewOptions(){ return sFooterOps; } public void setFooterState(int state){ if(mState == state){ return; } mArrow.clearAnimation(); if(state == STATE_LOADING){ mProgressBar.setVisibility(View.VISIBLE); mArrow.setVisibility(View.GONE); }else{ mProgressBar.setVisibility(View.GONE); mArrow.setVisibility(View.VISIBLE); } switch(state){ case STATE_NORMAL: mArrow.startAnimation(mRotateUp); mLoaderTips.setText(R.string.pull_up_for_more); break; case STATE_WILL_RELEASE: mArrow.startAnimation(mRotateDown); mLoaderTips.setText(R.string.release_for_more); break; case STATE_LOADING: mLoaderTips.setText(R.string.loading); break; default: break; } mState = state; } public int getCurrentState(){ return mState; } public void setFooterHeight(int height){ if(height <= 0){ height = 0; } LayoutParams lp = (LayoutParams) mFooter.getLayoutParams(); lp.height = height; mFooter.setLayoutParams(lp); } public int getFooterHeight(){ return mFooter.getHeight(); } public void hide(){ mArrow.clearAnimation(); mArrow.setVisibility(View.VISIBLE); mLoaderTips.setText(R.string.pull_up_for_more); setFooterHeight(0); } public void show(){ mArrow.clearAnimation(); mArrow.setVisibility(View.GONE); mLoaderTips.setText(R.string.click_for_more); LayoutParams lp = (LayoutParams) mFooter.getLayoutParams(); lp.height = LayoutParams.WRAP_CONTENT; mFooter.setLayoutParams(lp); } }
代码中,有个Options函数,用来提供设置:上拉或点击。同样,也有设置状态,和设计高度。
2.3 扩展ListView的实现
package com.chris.list.refresh; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.DecelerateInterpolator; import android.widget.AbsListView; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.Scroller; import android.widget.AbsListView.OnScrollListener; public class ListViewExt extends ListView implements OnScrollListener { private final static String TAG = "ChrisLV"; private HeaderView mHeaderView = null; private RelativeLayout mHeaderContent = null; private int iHeaderHeight = 0; private FooterView mFooterView = null; private RelativeLayout mFooterContent = null; private int iFooterHeight = 0; private final static int SCROLL_HEADER = 0; private final static int SCROLL_FOOTER = 1; private int iScrollWhich = SCROLL_HEADER; private Scroller mScroller = null; private final static float OFFSET_Y = 0.7f; private float iLastY = 0; private int mTotalNumber = 0; public ListViewExt(Context context) { this(context, null, 0); } public ListViewExt(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ListViewExt(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context); } private void initView(Context context){ /* * mScroller用来回弹下拉刷新/上拉更多 * 配合computerScroll来使用 */ mScroller = new Scroller(context, new DecelerateInterpolator()); super.setOnScrollListener(this); initHeaderView(context); initFooterView(context); } @Override public void setAdapter(ListAdapter adapter) { if(getFooterViewsCount() == 0){ addFooterView(mFooterView); } super.setAdapter(adapter); } @Override public boolean onTouchEvent(MotionEvent ev) { switch(ev.getAction()){ case MotionEvent.ACTION_DOWN: iLastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: float deltaY = ev.getY() - iLastY; iLastY = ev.getY(); if(canHeaderPull() && getFirstVisiblePosition() == 0 && (deltaY > 0 || mHeaderView.getHeaderHeight() > 0)){ updateHeaderState(deltaY * OFFSET_Y); }else if(canFooterPull() && getLastVisiblePosition() == mTotalNumber - 1 && (deltaY < 0 || mFooterView.getFooterHeight() > 0)){ updateFooterState(-deltaY * OFFSET_Y); } break; case MotionEvent.ACTION_UP: if(getFirstVisiblePosition() == 0){ if(mHeaderView.getHeaderHeight() > iHeaderHeight){ mHeaderView.setHeaderState(HeaderView.STATE_REFRESHING); if(mFooterView.getFooterViewOptions() == FooterView.FOOTER_OPTIONS_CLICK){ mFooterView.hide(); } } resetHeader(); }else if(getLastVisiblePosition() == mTotalNumber - 1){ if(mFooterView.getFooterHeight() > iFooterHeight){ mFooterView.setFooterState(FooterView.STATE_LOADING); } resetFooter(); } break; default: break; } return super.onTouchEvent(ev); } @Override public void computeScroll() { if(mScroller.computeScrollOffset()){ if(iScrollWhich == SCROLL_HEADER){ mHeaderView.setHeaderHeight(mScroller.getCurrY()); }else if(iScrollWhich == SCROLL_FOOTER){ mFooterView.setFooterHeight(mScroller.getCurrY()); } } super.computeScroll(); } /* * 获取ListView有多少个item: * 1. 在init中,需要设置super.setOnScrollListener; * 2. 重载以下两个函数; * 3. 在onScroll中取得totalItemCount即可; */ @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { mTotalNumber = totalItemCount; } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } ///////////////////////////////////////////////////////////////////////////// private boolean canHeaderPull(){ if(mFooterView.getCurrentState() == FooterView.STATE_NORMAL){ return true; } return false; } private boolean canFooterPull(){ if(mHeaderView.getCurrentState() == HeaderView.STATE_NORMAL){ return true; } return false; } ///////////////////////////////////// Header //////////////////////////////// public void stopRefresh(){ if(mHeaderView.getCurrentState() == HeaderView.STATE_REFRESHING){ mHeaderView.setHeaderState(HeaderView.STATE_NORMAL); resetHeader(); if(mFooterView.getFooterViewOptions() == FooterView.FOOTER_OPTIONS_CLICK){ mFooterView.show(); } } } private void initHeaderView(Context context){ mHeaderView = new HeaderView(context); mHeaderContent = (RelativeLayout) mHeaderView.findViewById(R.id.header_content); addHeaderView(mHeaderView); mHeaderView.getViewTreeObserver() .addOnGlobalLayoutListener(new OnGlobalLayoutListener(){ @Override public void onGlobalLayout() { iHeaderHeight = mHeaderContent.getHeight(); Log.d(TAG, "iHeaderHeight = " + iHeaderHeight); getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); } private void updateHeaderState(float delta){ mHeaderView.setHeaderHeight((int)(delta + mHeaderView.getHeaderHeight())); if(mHeaderView.getCurrentState() != HeaderView.STATE_REFRESHING){ if(mHeaderView.getHeaderHeight() > iHeaderHeight){ mHeaderView.setHeaderState(HeaderView.STATE_WILL_RELEASE); }else{ mHeaderView.setHeaderState(HeaderView.STATE_NORMAL); } } setSelection(0); } private void resetHeader(){ int height = mHeaderView.getHeaderHeight(); if(height == 0){ return; } int finalHeight = 0; if(height > iHeaderHeight){ /* * 如果超过HeaderView高度,则回滚到HeaderView高度即可 */ finalHeight = iHeaderHeight; }else if(mHeaderView.getCurrentState() == HeaderView.STATE_REFRESHING){ /* * 如果HeaderView未完全显示 * 1. 处于正在刷新中,则不管; * 2. 回滚HeaderView当前可视高度 */ return; } iScrollWhich = SCROLL_HEADER; mScroller.startScroll(0, height, 0, finalHeight - height, 250); invalidate(); } ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////// Footer //////////////////////////////// public void setFooterMode(int options){ mFooterView.setFooterViewOptions(options); } public void stopLoad(){ if(mFooterView.getCurrentState() == FooterView.STATE_LOADING){ mFooterView.setFooterState(FooterView.STATE_NORMAL); resetFooter(); } } private void initFooterView(Context context){ mFooterView = new FooterView(context); mFooterContent = (RelativeLayout) mFooterView.findViewById(R.id.footer_content); mFooterContent.setOnClickListener(new OnClickListener(){ @Override public void onClick(View v) { if(mFooterView.getFooterViewOptions() == FooterView.FOOTER_OPTIONS_CLICK && mFooterView.getCurrentState() == FooterView.STATE_NORMAL){ mFooterView.setFooterState(FooterView.STATE_LOADING); } } }); mFooterView.getViewTreeObserver() .addOnGlobalLayoutListener(new OnGlobalLayoutListener(){ @Override public void onGlobalLayout() { iFooterHeight = mFooterContent.getHeight(); Log.d(TAG, "iFooterHeight = " + iFooterHeight); getViewTreeObserver().removeGlobalOnLayoutListener(this); } }); } private void updateFooterState(float delta){ if(mFooterView.getFooterViewOptions() == FooterView.FOOTER_OPTIONS_CLICK){ return; } mFooterView.setFooterHeight((int)(delta + mFooterView.getFooterHeight())); if(mFooterView.getCurrentState() != FooterView.STATE_LOADING){ if(mFooterView.getFooterHeight() > iFooterHeight){ mFooterView.setFooterState(FooterView.STATE_WILL_RELEASE); }else{ mFooterView.setFooterState(FooterView.STATE_NORMAL); } } } private void resetFooter(){ int height = mFooterView.getFooterHeight(); if(height == 0){ return; } if(mFooterView.getFooterViewOptions() == FooterView.FOOTER_OPTIONS_CLICK){ return; } int finalHeight = 0; if(height > iFooterHeight){ finalHeight = iFooterHeight; }else if(mFooterView.getCurrentState() == FooterView.STATE_LOADING){ return; } iScrollWhich = SCROLL_FOOTER; mScroller.startScroll(0, height, 0, finalHeight - height, 250); invalidate(); } ///////////////////////////////////////////////////////////////////////////// }
代码结构比较清楚,相关的都集中在一起,大致流程是:
1. down时,记住坐标;
2. move时,判断当前可见是否是第一个或是最后一个,如果是,则将移动的距离去设置HeaderView或FooterView的高度,达到一点一点的显示出来;
3. up时,判断HeaderView或FooterView是否滚动的距离超过它们的高度,如果是,则表示是刷新或加载,且回弹到移动的距离-高度;
4. 代码还提供了冲突设置,即如果当前正在刷新中,则不允许滚动到底部上拉更多,或者显示“点击加载更多”,同样,如果是底部正在加载,则不允许滚动到顶多,下拉刷新;
2.4 使用举例
首页布局
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <com.chris.list.refresh.ListViewExt android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="#000000" android:dividerHeight="0.5dip"/> </RelativeLayout>
首页Activity代码实现,和一般的使用ListView方法一样
package com.chris.list.refresh; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.app.Activity; public class MainActivity extends Activity { private final static String TAG = "ChrisLV"; private ListViewExt mListView = null; private String[] mList = { "abcd1", "abcd2", "abcd3", "abcd4", "abcd5", "abcd6", "abcd7", "abcd8", "abcd9", "abcd10", "abcd11", "abcd12", "abcd13", "abcd14", "abcd15", "abcd16", "abcd17", "abcd18", "abcd19", "abcd20", "abcd21", "abcd22", "abcd23", "abcd24" }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListViewExt) findViewById(R.id.listview); mListView.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mList)); mListView.setOnItemClickListener(new OnItemClickListener(){ @Override public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) { Log.d(TAG, "arg2 = " + arg2); if(arg2 > 0){ mListView.stopRefresh(); mListView.stopLoad(); } mListView.setFooterMode(arg2 % 2); } }); } }
在onItemClick中,只是做了简单的将HeaderView或FooterView停止,并设置FooterView的加载模式:是上拉更多,还是点击加载更多。
三、小结
本篇文章,大致就这么多,虽然,为了UI体验友好,花了很多精力,但是一通百通,其它的也不外乎是这些,所以大家学习后,希望能举一反三,同时,咱们也交流交流。