该方法,我们通过完全自定义的方式实现,不添加任何依赖库和jar包。纯原生。
首先看一下我们实现的效果
实现的关键点:
- 为
ListView
添加头布局和底布局。 - 通过改变头布局的
paddingTop
值,来控制控件的显示和隐藏 - 根据我们滑动的状态,动态修改头部布局和底部布局。
看一下代码:
1 public class CustomRefreshListView extends ListView implements OnScrollListener{ 2 3 /** 4 * 头布局 5 */ 6 private View headerView; 7 8 /** 9 * 头部布局的高度 10 */ 11 private int headerViewHeight; 12 13 /** 14 * 头部旋转的图片 15 */ 16 private ImageView iv_arrow; 17 18 /** 19 * 头部下拉刷新时状态的描述 20 */ 21 private TextView tv_state; 22 23 /** 24 * 下拉刷新时间的显示控件 25 */ 26 private TextView tv_time; 27 28 29 /** 30 * 底部布局 31 */ 32 private View footerView; 33 34 /** 35 * 底部旋转progressbar 36 */ 37 private ProgressBar pb_rotate; 38 39 40 /** 41 * 底部布局的高度 42 */ 43 private int footerViewHeight; 44 45 46 /** 47 * 按下时的Y坐标 48 */ 49 private int downY; 50 51 private final int PULL_REFRESH = 0;//下拉刷新的状态 52 private final int RELEASE_REFRESH = 1;//松开刷新的状态 53 private final int REFRESHING = 2;//正在刷新的状态 54 55 /** 56 * 当前下拉刷新处于的状态 57 */ 58 private int currentState = PULL_REFRESH; 59 60 /** 61 * 头部布局在下拉刷新改变时,图标的动画 62 */ 63 private RotateAnimation upAnimation,downAnimation; 64 65 /** 66 * 当前是否在加载数据 67 */ 68 private boolean isLoadingMore = false; 69 70 public CustomRefreshListView(Context context) { 71 this(context,null); 72 } 73 74 public CustomRefreshListView(Context context, AttributeSet attrs) { 75 super(context, attrs); 76 init(); 77 } 78 79 private void init(){ 80 //设置滑动监听 81 setOnScrollListener(this); 82 //初始化头布局 83 initHeaderView(); 84 //初始化头布局中图标的旋转动画 85 initRotateAnimation(); 86 //初始化为尾布局 87 initFooterView(); 88 } 89 90 91 /** 92 * 初始化headerView 93 */ 94 private void initHeaderView() { 95 headerView = View.inflate(getContext(), R.layout.head_custom_listview, null); 96 iv_arrow = (ImageView) headerView.findViewById(R.id.iv_arrow); 97 pb_rotate = (ProgressBar) headerView.findViewById(R.id.pb_rotate); 98 tv_state = (TextView) headerView.findViewById(R.id.tv_state); 99 tv_time = (TextView) headerView.findViewById(R.id.tv_time); 100 101 //测量headView的高度 102 headerView.measure(0, 0); 103 //获取高度,并保存 104 headerViewHeight = headerView.getMeasuredHeight(); 105 //设置paddingTop = -headerViewHeight;这样,该控件被隐藏 106 headerView.setPadding(0, -headerViewHeight, 0, 0); 107 //添加头布局 108 addHeaderView(headerView); 109 } 110 111 /** 112 * 初始化旋转动画 113 */ 114 private void initRotateAnimation() { 115 116 upAnimation = new RotateAnimation(0, -180, 117 RotateAnimation.RELATIVE_TO_SELF, 0.5f, 118 RotateAnimation.RELATIVE_TO_SELF, 0.5f); 119 upAnimation.setDuration(300); 120 upAnimation.setFillAfter(true); 121 122 downAnimation = new RotateAnimation(-180, -360, 123 RotateAnimation.RELATIVE_TO_SELF, 0.5f, 124 RotateAnimation.RELATIVE_TO_SELF, 0.5f); 125 downAnimation.setDuration(300); 126 downAnimation.setFillAfter(true); 127 } 128 129 //初始化底布局,与头布局同理 130 private void initFooterView() { 131 footerView = View.inflate(getContext(), R.layout.foot_custom_listview, null); 132 footerView.measure(0, 0); 133 footerViewHeight = footerView.getMeasuredHeight(); 134 footerView.setPadding(0, -footerViewHeight, 0, 0); 135 addFooterView(footerView); 136 } 137 138 @Override 139 public boolean onTouchEvent(MotionEvent ev) { 140 switch (ev.getAction()) { 141 case MotionEvent.ACTION_DOWN: 142 //获取按下时y坐标 143 downY = (int) ev.getY(); 144 break; 145 case MotionEvent.ACTION_MOVE: 146 147 if(currentState==REFRESHING){ 148 //如果当前处在滑动状态,则不做处理 149 break; 150 } 151 //手指滑动偏移量 152 int deltaY = (int) (ev.getY() - downY); 153 154 //获取新的padding值 155 int paddingTop = -headerViewHeight + deltaY; 156 if(paddingTop>-headerViewHeight && getFirstVisiblePosition()==0){ 157 //向下滑,且处于顶部,设置padding值,该方法实现了顶布局慢慢滑动显现 158 headerView.setPadding(0, paddingTop, 0, 0); 159 160 if(paddingTop>=0 && currentState==PULL_REFRESH){ 161 //从下拉刷新进入松开刷新状态 162 currentState = RELEASE_REFRESH; 163 //刷新头布局 164 refreshHeaderView(); 165 }else if (paddingTop<0 && currentState==RELEASE_REFRESH) { 166 //进入下拉刷新状态 167 currentState = PULL_REFRESH; 168 refreshHeaderView(); 169 } 170 171 172 return true;//拦截TouchMove,不让listview处理该次move事件,会造成listview无法滑动 173 } 174 175 176 break; 177 case MotionEvent.ACTION_UP: 178 if(currentState==PULL_REFRESH){ 179 //仍处于下拉刷新状态,未滑动一定距离,不加载数据,隐藏headView 180 headerView.setPadding(0, -headerViewHeight, 0, 0); 181 }else if (currentState==RELEASE_REFRESH) { 182 //滑倒一定距离,显示无padding值得headcView 183 headerView.setPadding(0, 0, 0, 0); 184 //设置状态为刷新 185 currentState = REFRESHING; 186 187 //刷新头部布局 188 refreshHeaderView(); 189 190 if(listener!=null){ 191 //接口回调加载数据 192 listener.onPullRefresh(); 193 } 194 } 195 break; 196 } 197 return super.onTouchEvent(ev); 198 } 199 200 /** 201 * 根据currentState来更新headerView 202 */ 203 private void refreshHeaderView(){ 204 switch (currentState) { 205 case PULL_REFRESH: 206 tv_state.setText("下拉刷新"); 207 iv_arrow.startAnimation(downAnimation); 208 break; 209 case RELEASE_REFRESH: 210 tv_state.setText("松开刷新"); 211 iv_arrow.startAnimation(upAnimation); 212 break; 213 case REFRESHING: 214 iv_arrow.clearAnimation();//因为向上的旋转动画有可能没有执行完 215 iv_arrow.setVisibility(View.INVISIBLE); 216 pb_rotate.setVisibility(View.VISIBLE); 217 tv_state.setText("正在刷新..."); 218 break; 219 } 220 } 221 222 /** 223 * 完成刷新操作,重置状态,在你获取完数据并更新完adater之后,去在UI线程中调用该方法 224 */ 225 public void completeRefresh(){ 226 if(isLoadingMore){ 227 //重置footerView状态 228 footerView.setPadding(0, -footerViewHeight, 0, 0); 229 isLoadingMore = false; 230 }else { 231 //重置headerView状态 232 headerView.setPadding(0, -headerViewHeight, 0, 0); 233 currentState = PULL_REFRESH; 234 pb_rotate.setVisibility(View.INVISIBLE); 235 iv_arrow.setVisibility(View.VISIBLE); 236 tv_state.setText("下拉刷新"); 237 tv_time.setText("最后刷新:"+getCurrentTime()); 238 } 239 } 240 241 /** 242 * 获取当前系统时间,并格式化 243 * @return 244 */ 245 private String getCurrentTime(){ 246 SimpleDateFormat format = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); 247 return format.format(new Date()); 248 } 249 250 private OnRefreshListener listener; 251 public void setOnRefreshListener(OnRefreshListener listener){ 252 this.listener = listener; 253 } 254 public interface OnRefreshListener{ 255 void onPullRefresh(); 256 void onLoadingMore(); 257 } 258 259 /** 260 * SCROLL_STATE_IDLE:闲置状态,就是手指松开 261 * SCROLL_STATE_TOUCH_SCROLL:手指触摸滑动,就是按着来滑动 262 * SCROLL_STATE_FLING:快速滑动后松开 263 */ 264 @Override 265 public void onScrollStateChanged(AbsListView view, int scrollState) { 266 if(scrollState==OnScrollListener.SCROLL_STATE_IDLE 267 && getLastVisiblePosition()==(getCount()-1) &&!isLoadingMore){ 268 isLoadingMore = true; 269 270 footerView.setPadding(0, 0, 0, 0);//显示出footerView 271 setSelection(getCount());//让listview最后一条显示出来,在页面完全显示出底布局 272 273 if(listener!=null){ 274 listener.onLoadingMore(); 275 } 276 } 277 } 278 279 280 @Override 281 public void onScroll(AbsListView view, int firstVisibleItem, 282 int visibleItemCount, int totalItemCount) { 283 } 284 285 }
代码的注释写的比较详细,在这里只说明主要的逻辑。
下拉刷新
下拉刷新是通过设置setOnTouchListener()
方法,监听触摸事件,通过手指滑动的不同处理实现相应逻辑。
实现比较复杂,分为了三个情况,初始状态(显示下拉刷新),释放刷新状态,刷新状态。
其中下拉刷新状态和释放状态的改变,是由于手指滑动的不同距离,是在MotionEvent.ACTION_MOVE
中进行判断,该判断不处理任何数据逻辑,只是根据手指滑动的偏移量该表UI的显示。
刷新状态的判断是在MotionEvent.ACTION_UP
手指抬起时判断的。这很好理解,因为最终下拉刷新是否加载数据的确定,是由我们手指离开屏幕时与初始值的偏移量确定的。如果我们的偏移量小于了头布局的高度,代表不刷新,继续隐藏头布局。如果偏移量大于了头布局的高度,代表刷新,修改UI,同时通过接口回调,让其持有者进行加载数据。
上拉加载
上拉加载和下拉刷新不同,他的视线较为简单,我们通过ListView
的滚动监听进行处理相应逻辑。即setOnScrollListener(this);
。
该方法需要实现两个回调方法
public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount)
:滚动监听的调用。public void onScrollStateChanged(AbsListView view, int scrollState)
:滑动状态改变的回调。其中scrollState
为回调的状态,可能值为
- SCROLL_STATE_IDLE:闲置状态,手指松开后的状态回调
- SCROLL_STATE_TOUCH_SCROLL:手指触摸滑动的状态回调
- SCROLL_STATE_FLING:手指松开后惯性滑动的回调
我们在onScrollStateChanged
中进行判断,主要判断一下条件;
- 是否是停止状态
- 是否滑倒最后
- 是否正在加载数据
如果符合条件,则开始加载数据,通过接口回调。