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图片:
  • -

每天进步一点点!加油!

源码下载地址:安卓开发技术之下拉刷新的实现