Android开发技术学习之下拉刷新功能的实现
- 好久没有写博客了,最近都在忙。有时候即使是有时间也会很懒,就会想玩一玩,放松放松!一直都没有什么时间更新我这个菜鸟的博客了。不过今天不一样,我要给大家讲讲怎么实现许多app中下拉刷新的功能。比如腾讯的QQ、新浪微博等等。为什么我会写这篇技术博客,是因为我热爱技术,平时喜欢学习一些比较好玩的demo。故而有了这篇技术博客的诞生。好了,废话不多说了,下面开始进入正题吧!
- 下拉刷新的功能,相信大家一定都接触过。比如一个界面上显示了一些内容,你只要按住手机的屏幕下拉一定的距离就可以实现刷新的功能,当然你也可以下拉一段距离后抬起你的手,此时就不会刷新了,继续回到界面上。
- 首先需要你新建一个项目,默认有MainActivity和activity_main.xml布局。且activity_main.xml布局如下所示:
- 该布局的代码很简单,就引用到自定义的一个view,在项目中的名字叫做MyListView,该类继承了ListView,需要重写构造方法。MyListView类的代码如下所示:
- -
package com.example.zq.pullfresh;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Created by zq on 2016/4/26.
*/
public class MyListView extends ListView {
private final View viewHeader;
private static final byte DONE = 1;
private static final byte PULL = 2;
private static final byte RELEASE = 3;
private static final byte RELEASE_NOT_FRESHING = 4;
private byte currentState;
private final int height;
private TextView tvState, tvTime;
private ImageView ivArrow;
private ProgressBar pBar;
private int downY, moveY, showY;
private onRefreshingListener onRefreshingListener;
// TODO: 2016/4/26 构造方法
public MyListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO: 2016/4/26 构建下拉刷新的布局
viewHeader = View.inflate(context, R.layout.layout_header, null);
setViews(viewHeader);// TODO: 2016/4/26 控件初始化
this.addHeaderView(viewHeader);// TODO: 2016/4/26 添加头部刷新布局
viewHeader.measure(0, 0);// TODO: 2016/4/26 精确测量
height = viewHeader.getMeasuredHeight();// TODO: 2016/4/26 获取布局的高度
viewHeader.setPadding(0, -height, 0, 0);// TODO: 2016/4/26 隐藏刷新布局
String strCurrentDate = GetCurrentDate();
tvTime.setText(strCurrentDate);// TODO: 2016/4/26 设置系统当前时间
currentState = DONE;
}
private void setViews(View viewHeader) {
tvState = (TextView) viewHeader.findViewById(R.id.tv_state);// TODO: 2016/4/26 下拉状态
tvTime = (TextView) viewHeader.findViewById(R.id.tv_updateTime);// TODO: 2016/4/26 刷新时间
ivArrow = (ImageView) viewHeader.findViewById(R.id.iv_arrow);// TODO: 2016/4/26 下拉时箭头
pBar = (ProgressBar) viewHeader.findViewById(R.id.progressBar);// TODO: 2016/4/26 进度条
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:// TODO: 2016/4/26 手势按下
if (currentState == DONE) {
currentState = PULL;
downY = (int) ev.getY();// TODO: 2016/4/26 获取按下时的Y坐标
}
break;
case MotionEvent.ACTION_MOVE:// TODO: 2016/4/27 手势移动
if (currentState == PULL) {
moveY = (int) ev.getY();// TODO: 2016/4/26 移动时的Y坐标
showY = moveY - downY + (-height);// TODO: 2016/4/26 获取下拉显示的长度
viewHeader.setPadding(0, showY, 0, 0); TODO: 2016/4/26 下拉的时候慢慢的将布局显示出来
// TODO: 2016/4/26 判断向下拉了很久
if (showY > height) {
ivArrow.setImageResource(R.mipmap.ic_launcher);// TODO: 2016/4/26 将下拉箭头设置为上拉箭头
tvState.setText("松开刷新");
currentState = RELEASE;
}
}
break;
case MotionEvent.ACTION_UP:// TODO: 2016/4/27 手势抬起
// TODO: 2016/4/26 下拉了一段距离但是没有刷新的情况
if (showY < height) {
ivArrow.setImageResource(R.drawable.arrow);// TODO: 2016/4/26 设置下拉箭头
ivArrow.setVisibility(View.VISIBLE);// TODO: 2016/4/26 设置下拉箭头可见
pBar.setVisibility(View.GONE);
tvState.setText("下拉刷新");
viewHeader.setPadding(0, -height, 0, 0);// TODO: 2016/4/26 隐藏下拉刷新布局
currentState = DONE;
}
if (currentState == RELEASE) {
tvState.setText("正在刷新");
pBar.setVisibility(View.VISIBLE);
ivArrow.setVisibility(View.GONE);
String strCurrentDate = GetCurrentDate();
tvTime.setText(strCurrentDate);// TODO: 2016/4/26 设置刷新时间,为系统当前时间
// TODO: 2016/4/26 比如联网获取数据,可以让别人调接口
if (onRefreshingListener != null) {
// TODO: 2016/4/27 主界面回调该方法,但在主界面对该方法做具体的实现
this.onRefreshingListener.onRefreshing(this);
}
}
break;
}
return super.onTouchEvent(ev);
}
// TODO: 2016/4/26 获取系统当前的日期 格式为:yyyy-MM-dd HH:mm:ss
private String GetCurrentDate() {
SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date currentData = new Date(System.currentTimeMillis()); TODO: 2016/4/26 获取系统当前时间
String strDate = format.format(currentData);// TODO: 2016/4/26 格式化系统当前时间
return strDate;// TODO: 2016/4/26 返回格式化后的日期
}
// TODO: 2016/4/26 接收接口实现类
public void setOnRefreshingListener(onRefreshingListener onRefreshingListener) {
this.onRefreshingListener = onRefreshingListener;
}
public void completeRefresh() {
ivArrow.setImageResource(R.drawable.arrow);
ivArrow.setVisibility(VISIBLE);
pBar.setVisibility(View.GONE);
tvState.setText("下拉刷新");
viewHeader.setPadding(0, -height, 0, 0);// TODO: 2016/4/26 隐藏下拉刷新布局
currentState = DONE;// TODO: 2016/4/26 用于重复刷新
}
}
- 代码很简单,就是自定义了一个view,MyListView继承ListView,重写构造方法。在构造方法中,构建下拉刷新的布局,在代码中构建的布局为viewHeader,即下拉刷新的布局;下拉刷新的xml布局如下所示:
xml的代码为:
<?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="#00000000" android:orientation="horizontal">
<!-- 内容 -->
<RelativeLayout
android:id="@+id/head_contentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="30dp">
<!-- 箭头图像、进度条 -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true">
<!-- 箭头 -->
<ImageView
android:id="@+id/iv_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/arrow" />
<!-- 进度条 -->
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleSmall"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>
<!-- 提示、最近更新 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center"
android:orientation="vertical">
<!-- 提示 -->
<TextView
android:id="@+id/tv_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="下拉刷新"
android:textColor="#FF000000"
android:textSize="20sp" />
<!-- 最近更新 -->
<TextView
android:id="@+id/tv_updateTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上次更新"
android:textColor="#FFFF0000"
android:textSize="15sp" />
</LinearLayout> </RelativeLayout>
</LinearLayout>
代码很简单,就是一个线性布局里面放置两个布局,两个布局再分别放置一些控件。其中第一个布局用的是帧布局,里面有一个下拉箭头的图片和一个进度条,此时需要将进度条隐藏,设置android:visibility=”gone”就可以了。第二个布局用的是一个线性布局,里面有两个TextView控件,一个是用于当前状态提示显示,另一个是用于最近更新提示显示。在MyListView类的构造方法中将下拉刷新布局构建好了之后,进行控件的一些初始化操作,就可以将该布局添加到头部了,this.addHeaderView(viewHeader);添加到头部的时候需要精确的测量布局的高度。高度测量完成后,设置padding,可以隐藏下拉刷新的布局,viewHeader.setPadding(0,-height, 0, 0);这里为什么要将top参数设置为-height,因为设置在头部需要将其隐藏。一些当前状态提示和时间提示的赋值我就不多说了,大家都看的懂,只要给对应的控件设置你需要让其显示的内容就可以了。下面开始关键的代码讲解:在MyListView类中重写了屏幕的触摸事件,为什么要重写该方法,因为下拉刷新这个功能是对手势做判断,执行相应操作实现的。如图方法:
即重写
public boolean onTouchEvent(MotionEvent ev) { ......
方法。
return super.onTouchEvent(ev); }在该方法中,首先获取手势动作action,手势动作有三个:手势按下,手势移动,手势抬起。
- 当手势按下的时候,currentState赋值为PULL,获取按下时的Y坐标。currentState是用于在不同的手势之间切换的一个参数标记。此时按下就执行以上操作就可以。
- 接下来需要下拉移动了,手势动作也相应的变成了手势移动。在手势移动的时候,判断currentState是否为PULL,为PULL的话,就获取移动时的Y坐标,根据手势按下时的Y坐标和移动时的Y坐标获取下拉显示的长度showY。获取的代码是:showY = moveY - downY + (-height);将下拉的布局慢慢显示出来就设置padding的top为当前获取的下拉显示的长度。这样你移动的时候就可以慢慢的显示出来了。这时再判断是否向下下拉了很久,如果是的话,就将下拉箭头ivArrow设置为上拉箭头,由于没有上拉箭头的图片资源,我就用ic_launcher代替了。当前状态显示提示tvState就设置为“松开刷新”,最后将currentState赋值为RELEASE。
- 手势移动就结束了,此时手势抬起的动作就来了,手势抬起的时候分两种情况。第一种下拉一段距离但是没有刷新的情况;另一种就是释放刷新的情况。下拉一段距离但没有刷新的情况用条件”showY < heght”来判断,如果该条件成立,就说明你此时你下还在下拉,下拉没有结束,此时松开,即手势抬起的时候,下拉刷新布局就会再一次的隐藏。有人会问这里为什么用showY < 0来做判断?当布局刚好全部显示出来的时候就提示你可以刷新了不是更好吗?如果你这样提问,恭喜你,我为什么这样写你应该懂了,我只是让其下拉的距离再长一点再执行刷新操作。只要是大于0以上的数值就可以,看你个人的喜好了。
- 而释放刷新的情况用条件currentState == RELEASE来判断,当此条件成立,即手势移动结束后(手势移动时下拉了足够长的距离),给当前状态显示提示tvState设置为“正在刷新”,进度条设置为显示,下拉箭头设置为不可见。当前刷新时间tvTime设置为系统当前时间,这个很简单,代码有注释,大家应该都能懂。接下来就是获取数据的时候了,刷新是为了干什么,是为了获取一些网络上的数据。关键的地方来了,这里你可以写个接口,让别人回调你接口的方法,在别人那里对该方法做具体的实现。给大家说具体点吧!可能有的人不是很懂。
- 第一步:创建一个接口,如下图所示:
- 第二步:在MyListView类写一个方法用于接收接口实现类,方法名称为:setOnRefreshingListener(onRefreshingListener onRefreshingListener)该方法用于接收一个接口参数,也就是刚刚定义的接口。如下图所示: *
- 第三步:在释放刷新,比如联网获取数据,可以让别人调你的接口,如下图所示: *
- 第四步:在MainActivity中用MyListView对象调用接收接口实现类方法,当调用该方法后需要执行接口中的onRefreshingListener.onRefreshing()方法,也就相当于在MainActivity中回调了第三步当中提到的方法this.RefreshingListener.onRefreshing(this)。但是具体的操作还是在MainActivity中实现。这其实也就是android中接口的回调机制,如果还不是很懂的就自己去百度“接口回调机制 ”,弄清楚它。
- 在MainActivity中具体的实现方式如下图所示:
- 以上图片中我标记了4处重要的地方,第一处是让线程休眠1s,模拟获取数据的操作;第二处是往集合中添加获取的数据,我这里也只用了简单的添加方式;第三处是adapter更新数据源的变化,因为往集合中添加了数据,所以执行该方法可以更新adapter中内容。但是更新UI的操作需要执行在主线程,故runOnUiThread(…);第四处是执行方法myListView.completeRefresh();用于完成数据的刷新操作。此时又回到了MyListView这个类中,在该类中去实现该方法。主要是联网完成了,隐藏下拉布局。如下图所示:
- 代码很简单,就是一些图片的显示与进度条的隐藏,布局的设置padding等,这里就不多说了。代码里都有给出。
- MainActivity中的代码如下所示:
package com.example.zq.pullfresh;
import android.support.v7.app.AppCompatActivity; import android.os.Bundle;
import java.util.ArrayList; import java.util.List; import java.util.Random;
public class MainActivity extends AppCompatActivity {
private List<String> lists = new ArrayList<>();// TODO: 2016/4/26 声明一个集合保存数据 private MyListView myListView;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: 2016/4/26 设置该Activity的使用布局
setContentView(R.layout.activity_main);
lists.add("a");// TODO: 2016/4/26 象征性的添加几个数据
lists.add("b");
lists.add("c");
// TODO: 2016/4/26 创建adapter
final MyAdapter adapter = new MyAdapter(MainActivity.this, lists);
// TODO: 2016/4/26 获取自定义的listView
myListView = (MyListView)findViewById(R.id.myListView);
// TODO: 2016/4/26 设置adapter
myListView.setAdapter(adapter);
myListView.setOnRefreshingListener(new onRefreshingListener() {
@Override
public void onRefreshing(final MyListView myListView) {
new Thread(){
@Override
public void run() {
super.run();
try {
Thread.sleep(1000);
lists.add(new Random().nextInt(100)+"联网获取的数据");
// TODO: 2016/4/26 主线程执行更新UI操作必须在主线程
runOnUiThread(new Runnable() {
@Override
public void run() {
adapter.notifyDataSetChanged();
// TODO: 2016/4/26 联网完成了,隐藏下拉刷新布局
myListView.completeRefresh();
}
});
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}); } }
- 代码很简单,获取MyListVIew对象,给对象设置adapter。这样就可以了。获取adapter只需要创建MyAdapter类的对象可以获取。MyAdapter类代码如下:
package com.example.zq.pullfresh;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* Created by zq on 2016/4/26.
*/
public class MyAdapter extends BaseAdapter {
private List<String> lists = new ArrayList<>();
private Context context;
public MyAdapter(Context context, List<String> lists) {
this.lists = lists;
this.context = context;
}
@Override
public int getCount() {
return lists.size();
}
@Override
public Object getItem(int position) {
return lists.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView tvData = new TextView(context);// TODO: 2016/4/26 新建一个TextView
tvData.setText(lists.get(position));// TODO: 2016/4/26 设置数据
tvData.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);// TODO: 2016/4/26 设置textView在listView中的字体显示位置
return tvData;// TODO: 2016/4/26 返回tvData
}
}
- 代码简单,继承BaseAdapter,重写相关方法,getView()方法中新建一个TextView,设置数据,返回TextView对象即可。
- 好了,到这里就结束了。
- 最后给大家显示一些演示的demo图片:
- -
每天进步一点点!加油!
源码下载地址:安卓开发技术之下拉刷新的实现