您的位置:首页 > 产品设计 > UI/UE

android UI进阶之实现listview的下拉刷新和加载

2014-08-01 15:46 267 查看



上下拉实现刷新和加载更多的ListView,如下:

[java] view
plaincopyprint?

package com.sin.android.ui;

import android.content.Context;

import android.util.AttributeSet;

import android.view.Gravity;

import android.view.MotionEvent;

import android.view.View;

import android.widget.AbsListView;

import android.widget.AbsListView.OnScrollListener;

import android.widget.LinearLayout;

import android.widget.ListView;

import android.widget.ProgressBar;

import android.widget.TextView;

/**

* 动态刷新和加载数据ListView

* @author RobinTang

*

*/

public class DynamicListView extends ListView implements OnScrollListener {

/**

* 监听器

* 监听控件的刷新或者加载更多事件

* 所有的条目事件都会有一个偏移量,也就是position应该减1才是你适配器中的条目

* @author RobinTang

*

*/

public interface DynamicListViewListener {

/**

*

* @param dynamicListView

* @param isRefresh 为true的时候代表的是刷新,为false的时候代表的是加载更多

* @return true:刷新或者加载更多动作完成,刷新或者加载更多的动画自动消失 false:刷新或者加载更多为完成,需要在数据加载完成之后去调用控件的doneRefresh()或者doneMore()方法

*/

public boolean onRefreshOrMore(DynamicListView dynamicListView, boolean isRefresh);

}

/**

* 状态控件(StatusView,列表头上和底端的)的状态枚举

* @author RobinTang

*

*/

enum RefreshStatus {

none, normal, willrefresh, refreshing

}

/**

* 状态控件

* @author RobinTang

*

*/

class StatusView extends LinearLayout {

public int height;

public int width;

private ProgressBar progressBar = null;

private TextView textView = null;

private RefreshStatus refreshStatus = RefreshStatus.none;

private String normalString = "下拉刷新";

private String willrefreshString = "松开刷新";

private String refreshingString = "正在刷新";

public StatusView(Context context, AttributeSet attrs) {

super(context, attrs);

initThis(context);

}

public StatusView(Context context) {

super(context);

initThis(context);

}

private void initThis(Context context) {

this.setOrientation(LinearLayout.HORIZONTAL);

this.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL);

progressBar = new ProgressBar(context);

progressBar.setLayoutParams(new LinearLayout.LayoutParams(30, 30));

textView = new TextView(context);

textView.setPadding(5, 0, 0, 0);

this.addView(progressBar);

this.addView(textView);

int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);

int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);

this.measure(w, h);

height = this.getMeasuredHeight();

width = this.getMeasuredWidth();

this.setRefreshStatus(RefreshStatus.normal);

}

public RefreshStatus getRefreshStatus() {

return refreshStatus;

}

public void setRefreshStatus(RefreshStatus refreshStatus) {

if (this.refreshStatus != refreshStatus) {

this.refreshStatus = refreshStatus;

if(refreshStatus == RefreshStatus.refreshing){

this.progressBar.setVisibility(View.VISIBLE);

}

else{

this.progressBar.setVisibility(View.GONE);

}

refreshStatusString();

this.invalidate();

}

}

private void refreshStatusString() {

switch (refreshStatus) {

case normal:

textView.setText(normalString);

progressBar.setProgress(0);

break;

case willrefresh:

textView.setText(willrefreshString);

break;

case refreshing:

textView.setText(refreshingString);

break;

default:

break;

}

}

/**

* 设置状态字符串

* @param normalString 平时的字符串

* @param willrefreshString 松开后刷新(或加载)的字符串

* @param refreshingString 正在刷新(或加载)的字符串

*/

public void setStatusStrings(String normalString, String willrefreshString, String refreshingString){

this.normalString = normalString;

this.willrefreshString = willrefreshString;

this.refreshingString = refreshingString;

this.refreshStatusString();

}

}

private StatusView refreshView;

private StatusView moreView;

private int itemFlag = -1;

private boolean isRecorded = false;

private int downY = -1;

private final float minTimesToRefresh = 1.5f;

private final static int ITEM_FLAG_FIRST = 1;

private final static int ITEM_FLAG_NONE = 0;

private final static int ITEM_FLAG_LAST = -1;

// 两个监听器

private DynamicListViewListener onRefreshListener;

private DynamicListViewListener onMoreListener;

// 滚动到低端的时候是否自动加载更多

private boolean doMoreWhenBottom = false;

public DynamicListView(Context context, AttributeSet attrs, int defStyle) {

super(context, attrs, defStyle);

initThis(context);

}

public DynamicListView(Context context, AttributeSet attrs) {

super(context, attrs);

initThis(context);

}

public DynamicListView(Context context) {

super(context);

initThis(context);

}

private void initThis(Context context) {

refreshView = new StatusView(context);

moreView = new StatusView(context);

refreshView.setStatusStrings("继续下拉刷新数据...", "松开之后刷新数据...", "正在刷新数据...");

moreView.setStatusStrings("继续上拉加载数据...", "松开之后加载数据...", "正在加载数据...");

this.addHeaderView(refreshView, null, false);

this.addFooterView(moreView, null, false);

this.setOnScrollListener(this);

doneRefresh();

doneMore();

}

// 监听器操作

public DynamicListViewListener getOnRefreshListener() {

return onRefreshListener;

}

public void setOnRefreshListener(DynamicListViewListener onRefreshListener) {

this.onRefreshListener = onRefreshListener;

}

public DynamicListViewListener getOnMoreListener() {

return onMoreListener;

}

public void setOnMoreListener(DynamicListViewListener onMoreListener) {

this.onMoreListener = onMoreListener;

}

// 设置

public boolean isDoMoreWhenBottom() {

return doMoreWhenBottom;

}

public void setDoMoreWhenBottom(boolean doMoreWhenBottom) {

this.doMoreWhenBottom = doMoreWhenBottom;

}

@Override

public void onScroll(AbsListView l, int t, int oldl, int count) {

// log("%d %d %d", t, oldl, count);

if (t == 0)

itemFlag = ITEM_FLAG_FIRST;

else if ((t + oldl) == count){

itemFlag = ITEM_FLAG_LAST;

if(doMoreWhenBottom && onMoreListener != null && moreView.getRefreshStatus() != RefreshStatus.refreshing){

doMore();

}

}

else {

itemFlag = ITEM_FLAG_NONE;

// isRecorded = false;

}

}

@Override

public void onScrollStateChanged(AbsListView arg0, int arg1) {

}

@Override

public boolean onTouchEvent(MotionEvent ev) {

switch (ev.getAction()) {

case MotionEvent.ACTION_DOWN:

if (isRecorded == false && (itemFlag == ITEM_FLAG_FIRST && onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.normal || itemFlag == ITEM_FLAG_LAST && onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.normal)) {

downY = (int) ev.getY(0);

isRecorded = true;

// log("按下,记录:%d flag:%d", downY, itemFlag);

}

break;

case MotionEvent.ACTION_UP: {

isRecorded = false;

if (onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.willrefresh) {

doRefresh();

} else if (refreshView.getRefreshStatus() == RefreshStatus.normal) {

refreshView.setPadding(0, -1 * refreshView.height, 0, 0);

}

if (onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.willrefresh) {

doMore();

} else if (moreView.getRefreshStatus() == RefreshStatus.normal) {

moreView.setPadding(0, 0, 0, -1 * moreView.height);

}

break;

}

case MotionEvent.ACTION_MOVE: {

if (isRecorded == false && (itemFlag == ITEM_FLAG_FIRST && onRefreshListener != null && refreshView.getRefreshStatus() == RefreshStatus.normal ||

itemFlag == ITEM_FLAG_LAST && onMoreListener != null && moreView.getRefreshStatus() == RefreshStatus.normal)) {

downY = (int) ev.getY(0);

isRecorded = true;

// log("按下,记录:%d flag:%d", downY, itemFlag);

} else if (isRecorded) {

int nowY = (int) ev.getY(0);

int offset = nowY - downY;

if (offset > 0 && itemFlag == ITEM_FLAG_FIRST) {

// 下拉

setSelection(0);

if (offset >= (minTimesToRefresh * refreshView.height)) {

refreshView.setRefreshStatus(RefreshStatus.willrefresh);

} else {

refreshView.setRefreshStatus(RefreshStatus.normal);

}

refreshView.setPadding(0, -1 * (refreshView.height - offset), 0, 0);

} else if(itemFlag == ITEM_FLAG_LAST){

// 上拉

setSelection(this.getCount());

if (offset <= -1 * (minTimesToRefresh * moreView.height)) {

moreView.setRefreshStatus(RefreshStatus.willrefresh);

} else {

moreView.setRefreshStatus(RefreshStatus.normal);

}

moreView.setPadding(0, 0, 0, -1 * (moreView.height + offset));

}

// log("位移:%d", offset);

}

break;

}

default:

break;

}

return super.onTouchEvent(ev);

}

/**

* 开始刷新

*/

private void doRefresh(){

// log("开始刷新");

refreshView.setRefreshStatus(RefreshStatus.refreshing);

refreshView.setPadding(0, 0, 0, 0);

if(onRefreshListener.onRefreshOrMore(this, true))

doneRefresh();

}

/**

* 开始加载更多

*/

private void doMore(){

// log("加载更多");

moreView.setRefreshStatus(RefreshStatus.refreshing);

moreView.setPadding(0, 0, 0, 0);

if(onMoreListener.onRefreshOrMore(this, false))

doneMore();

}

/**

* 刷新完成之后调用,用于取消刷新的动画

*/

public void doneRefresh() {

// log("刷新完成!");

refreshView.setRefreshStatus(RefreshStatus.normal);

refreshView.setPadding(0, -1 * refreshView.height, 0, 0);

}

/**

* 加载更多完成之后调用,用于取消加载更多的动画

*/

public void doneMore() {

// log("加载完成!");

moreView.setRefreshStatus(RefreshStatus.normal);

moreView.setPadding(0, 0, 0, -1 * moreView.height);

}

/**

* 获取刷新的状态

* @return 一般 将要刷新 刷新完成

*/

public RefreshStatus getRefreshStatus(){

return refreshView.getRefreshStatus();

}

/**

* 获取加载更多的状态

* @return 一般 将要加载 加载完成

*/

public RefreshStatus getMoreStatus(){

return moreView.getRefreshStatus();

}

// private void log(Object obj) {

// log("%s", obj.toString());

// }

//

// private void log(String format, Object... args) {

// Log.i("DynamicListView", String.format(format, args));

// }

}

使用例子:

[java] view
plaincopyprint?

package com.sin.android.ui;

import java.util.ArrayList;

import java.util.Date;

import android.annotation.SuppressLint;

import android.app.Activity;

import android.os.Bundle;

import android.os.Handler;

import android.os.Message;

import android.util.Log;

import android.view.View;

import android.widget.AdapterView;

import android.widget.AdapterView.OnItemClickListener;

import android.widget.ArrayAdapter;

import android.widget.Toast;

import com.sin.android.ui.DynamicListView.DynamicListViewListener;

@SuppressLint("HandlerLeak")

public class MainActivity extends Activity implements DynamicListViewListener {

DynamicListView listView;

ArrayList<String> data;

ArrayAdapter<String> adapter;

// 用于刷新控件状态

Handler handler = new Handler() {

@Override

public void handleMessage(Message msg) {

if (msg.what == 0) {

adapter.notifyDataSetChanged();

listView.doneRefresh();

Toast.makeText(MainActivity.this, "新加载"+msg.arg1+"条数据!", Toast.LENGTH_LONG).show();

} else if (msg.what == 1) {

adapter.notifyDataSetChanged();

listView.doneMore();

} else {

super.handleMessage(msg);

}

}

};

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

listView = new DynamicListView(this);

setContentView(listView);

data = new ArrayList<String>();

for (int i = 1; i < 10; ++i) {

data.add("原始数据" + i);

}

adapter = new ArrayAdapter<String>(this, android.R.layout.simple_expandable_list_item_1, data);

listView.setAdapter(adapter);

listView.setOnItemClickListener(new OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {

Log.i("",data.get(arg2-1));

}

});

listView.setDoMoreWhenBottom(false); // 滚动到低端的时候不自己加载更多

listView.setOnRefreshListener(this);

listView.setOnMoreListener(this);

}

@Override

public boolean onRefreshOrMore(DynamicListView dynamicListView, boolean isRefresh) {

if (isRefresh) {

new Thread(new Runnable() {

@Override

public void run() {

// 刷新

ArrayList<String> temp = new ArrayList<String>();

for (int i = 0; i < 3; ++i) {

temp.add(0, new Date().toLocaleString());

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

synchronized (data) {

data.addAll(0, temp);

}

Message message = new Message();

message.what = 0;

message.arg1 = temp.size();

handler.sendMessage(message);

}

}).start();

} else {

new Thread(new Runnable() {

@Override

public void run() {

// 加载更多

ArrayList<String> temp = new ArrayList<String>();

for (int i = 0; i < 3; ++i) {

temp.add(new Date().toLocaleString());

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

synchronized (data) {

data.addAll(temp);

}

handler.sendEmptyMessage(1);

}

}).start();

}

return false;

}

}

截图:









Android下拉刷新完全解析,教你如何一分钟实现下拉刷新功能

最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到。因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下。相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能。

首先讲一下实现原理。这里我们将采取的方案是使用组合View的方式,先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。原理示意图如下:



那我们现在就来动手实现一下,新建一个项目起名叫PullToRefreshTest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:

[html] view
plaincopy

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:tools="http://schemas.android.com/tools"

android:id="@+id/pull_to_refresh_head"

android:layout_width="fill_parent"

android:layout_height="60dip" >

<LinearLayout

android:layout_width="200dip"

android:layout_height="60dip"

android:layout_centerInParent="true"

android:orientation="horizontal" >

<RelativeLayout

android:layout_width="0dip"

android:layout_height="60dip"

android:layout_weight="3"

>

<ImageView

android:id="@+id/arrow"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_centerInParent="true"

android:src="@drawable/arrow"

/>

<ProgressBar

android:id="@+id/progress_bar"

android:layout_width="30dip"

android:layout_height="30dip"

android:layout_centerInParent="true"

android:visibility="gone"

/>

</RelativeLayout>

<LinearLayout

android:layout_width="0dip"

android:layout_height="60dip"

android:layout_weight="12"

android:orientation="vertical" >

<TextView

android:id="@+id/description"

android:layout_width="fill_parent"

android:layout_height="0dip"

android:layout_weight="1"

android:gravity="center_horizontal|bottom"

android:text="@string/pull_to_refresh" />

<TextView

android:id="@+id/updated_at"

android:layout_width="fill_parent"

android:layout_height="0dip"

android:layout_weight="1"

android:gravity="center_horizontal|top"

android:text="@string/updated_at" />

</LinearLayout>

</LinearLayout>

</RelativeLayout>

在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。

布局中所有引用的字符串我们都放在strings.xml中,如下所示:

[html] view
plaincopy

<?xml version="1.0" encoding="utf-8"?>

<resources>

<string name="app_name">PullToRefreshTest</string>

<string name="pull_to_refresh">下拉可以刷新</string>

<string name="release_to_refresh">释放立即刷新</string>

<string name="refreshing">正在刷新…</string>

<string name="not_updated_yet">暂未更新过</string>

<string name="updated_at">上次更新于%1$s前</string>

<string name="updated_just_now">刚刚更新</string>

<string name="time_error">时间有问题</string>

</resources>

然后新建一个RefreshableView继承自LinearLayout,代码如下所示:

[java] view
plaincopy

public class RefreshableView extends LinearLayout implements OnTouchListener {

/**

* 下拉状态

*/

public static final int STATUS_PULL_TO_REFRESH = 0;

/**

* 释放立即刷新状态

*/

public static final int STATUS_RELEASE_TO_REFRESH = 1;

/**

* 正在刷新状态

*/

public static final int STATUS_REFRESHING = 2;

/**

* 刷新完成或未刷新状态

*/

public static final int STATUS_REFRESH_FINISHED = 3;

/**

* 下拉头部回滚的速度

*/

public static final int SCROLL_SPEED = -20;

/**

* 一分钟的毫秒值,用于判断上次的更新时间

*/

public static final long ONE_MINUTE = 60 * 1000;

/**

* 一小时的毫秒值,用于判断上次的更新时间

*/

public static final long ONE_HOUR = 60 * ONE_MINUTE;

/**

* 一天的毫秒值,用于判断上次的更新时间

*/

public static final long ONE_DAY = 24 * ONE_HOUR;

/**

* 一月的毫秒值,用于判断上次的更新时间

*/

public static final long ONE_MONTH = 30 * ONE_DAY;

/**

* 一年的毫秒值,用于判断上次的更新时间

*/

public static final long ONE_YEAR = 12 * ONE_MONTH;

/**

* 上次更新时间的字符串常量,用于作为SharedPreferences的键值

*/

private static final String UPDATED_AT = "updated_at";

/**

* 下拉刷新的回调接口

*/

private PullToRefreshListener mListener;

/**

* 用于存储上次更新时间

*/

private SharedPreferences preferences;

/**

* 下拉头的View

*/

private View header;

/**

* 需要去下拉刷新的ListView

*/

private ListView listView;

/**

* 刷新时显示的进度条

*/

private ProgressBar progressBar;

/**

* 指示下拉和释放的箭头

*/

private ImageView arrow;

/**

* 指示下拉和释放的文字描述

*/

private TextView description;

/**

* 上次更新时间的文字描述

*/

private TextView updateAt;

/**

* 下拉头的布局参数

*/

private MarginLayoutParams headerLayoutParams;

/**

* 上次更新时间的毫秒值

*/

private long lastUpdateTime;

/**

* 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分

*/

private int mId = -1;

/**

* 下拉头的高度

*/

private int hideHeaderHeight;

/**

* 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,

* STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED

*/

private int currentStatus = STATUS_REFRESH_FINISHED;;

/**

* 记录上一次的状态是什么,避免进行重复操作

*/

private int lastStatus = currentStatus;

/**

* 手指按下时的屏幕纵坐标

*/

private float yDown;

/**

* 在被判定为滚动之前用户手指可以移动的最大值。

*/

private int touchSlop;

/**

* 是否已加载过一次layout,这里onLayout中的初始化只需加载一次

*/

private boolean loadOnce;

/**

* 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉

*/

private boolean ableToPull;

/**

* 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。

*

* @param context

* @param attrs

*/

public RefreshableView(Context context, AttributeSet attrs) {

super(context, attrs);

preferences = PreferenceManager.getDefaultSharedPreferences(context);

header = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh, null, true);

progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);

arrow = (ImageView) header.findViewById(R.id.arrow);

description = (TextView) header.findViewById(R.id.description);

updateAt = (TextView) header.findViewById(R.id.updated_at);

touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();

refreshUpdatedAtValue();

setOrientation(VERTICAL);

addView(header, 0);

}

/**

* 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。

*/

@Override

protected void onLayout(boolean changed, int l, int t, int r, int b) {

super.onLayout(changed, l, t, r, b);

if (changed && !loadOnce) {

hideHeaderHeight = -header.getHeight();

headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();

headerLayoutParams.topMargin = hideHeaderHeight;

listView = (ListView) getChildAt(1);

listView.setOnTouchListener(this);

loadOnce = true;

}

}

/**

* 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。

*/

@Override

public boolean onTouch(View v, MotionEvent event) {

setIsAbleToPull(event);

if (ableToPull) {

switch (event.getAction()) {

case MotionEvent.ACTION_DOWN:

yDown = event.getRawY();

break;

case MotionEvent.ACTION_MOVE:

float yMove = event.getRawY();

int distance = (int) (yMove - yDown);

// 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件

if (distance <= 0 && headerLayoutParams.topMargin <= hideHeaderHeight) {

return false;

}

if (distance < touchSlop) {

return false;

}

if (currentStatus != STATUS_REFRESHING) {

if (headerLayoutParams.topMargin > 0) {

currentStatus = STATUS_RELEASE_TO_REFRESH;

} else {

currentStatus = STATUS_PULL_TO_REFRESH;

}

// 通过偏移下拉头的topMargin值,来实现下拉效果

headerLayoutParams.topMargin = (distance / 2) + hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

}

break;

case MotionEvent.ACTION_UP:

default:

if (currentStatus == STATUS_RELEASE_TO_REFRESH) {

// 松手时如果是释放立即刷新状态,就去调用正在刷新的任务

new RefreshingTask().execute();

} else if (currentStatus == STATUS_PULL_TO_REFRESH) {

// 松手时如果是下拉状态,就去调用隐藏下拉头的任务

new HideHeaderTask().execute();

}

break;

}

// 时刻记得更新下拉头中的信息

if (currentStatus == STATUS_PULL_TO_REFRESH

|| currentStatus == STATUS_RELEASE_TO_REFRESH) {

updateHeaderView();

// 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态

listView.setPressed(false);

listView.setFocusable(false);

listView.setFocusableInTouchMode(false);

lastStatus = currentStatus;

// 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件

return true;

}

}

return false;

}

/**

* 给下拉刷新控件注册一个监听器。

*

* @param listener

* 监听器的实现。

* @param id

* 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突, 请不同界面在注册下拉刷新监听器时一定要传入不同的id。

*/

public void setOnRefreshListener(PullToRefreshListener listener, int id) {

mListener = listener;

mId = id;

}

/**

* 当所有的刷新逻辑完成后,记录调用一下,否则你的ListView将一直处于正在刷新状态。

*/

public void finishRefreshing() {

currentStatus = STATUS_REFRESH_FINISHED;

preferences.edit().putLong(UPDATED_AT + mId, System.currentTimeMillis()).commit();

new HideHeaderTask().execute();

}

/**

* 根据当前ListView的滚动状态来设定 {@link #ableToPull}

* 的值,每次都需要在onTouch中第一个执行,这样可以判断出当前应该是滚动ListView,还是应该进行下拉。

*

* @param event

*/

private void setIsAbleToPull(MotionEvent event) {

View firstChild = listView.getChildAt(0);

if (firstChild != null) {

int firstVisiblePos = listView.getFirstVisiblePosition();

if (firstVisiblePos == 0 && firstChild.getTop() == 0) {

if (!ableToPull) {

yDown = event.getRawY();

}

// 如果首个元素的上边缘,距离父布局值为0,就说明ListView滚动到了最顶部,此时应该允许下拉刷新

ableToPull = true;

} else {

if (headerLayoutParams.topMargin != hideHeaderHeight) {

headerLayoutParams.topMargin = hideHeaderHeight;

header.setLayoutParams(headerLayoutParams);

}

ableToPull = false;

}

} else {

// 如果ListView中没有元素,也应该允许下拉刷新

ableToPull = true;

}

}

/**

* 更新下拉头中的信息。

*/

private void updateHeaderView() {

if (lastStatus != currentStatus) {

if (currentStatus == STATUS_PULL_TO_REFRESH) {

description.setText(getResources().getString(R.string.pull_to_refresh));

arrow.setVisibility(View.VISIBLE);

progressBar.setVisibility(View.GONE);

rotateArrow();

} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {

description.setText(getResources().getString(R.string.release_to_refresh));

arrow.setVisibility(View.VISIBLE);

progressBar.setVisibility(View.GONE);

rotateArrow();

} else if (currentStatus == STATUS_REFRESHING) {

description.setText(getResources().getString(R.string.refreshing));

progressBar.setVisibility(View.VISIBLE);

arrow.clearAnimation();

arrow.setVisibility(View.GONE);

}

refreshUpdatedAtValue();

}

}

/**

* 根据当前的状态来旋转箭头。

*/

private void rotateArrow() {

float pivotX = arrow.getWidth() / 2f;

float pivotY = arrow.getHeight() / 2f;

float fromDegrees = 0f;

float toDegrees = 0f;

if (currentStatus == STATUS_PULL_TO_REFRESH) {

fromDegrees = 180f;

toDegrees = 360f;

} else if (currentStatus == STATUS_RELEASE_TO_REFRESH) {

fromDegrees = 0f;

toDegrees = 180f;

}

RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);

animation.setDuration(100);

animation.setFillAfter(true);

arrow.startAnimation(animation);

}

/**

* 刷新下拉头中上次更新时间的文字描述。

*/

private void refreshUpdatedAtValue() {

lastUpdateTime = preferences.getLong(UPDATED_AT + mId, -1);

long currentTime = System.currentTimeMillis();

long timePassed = currentTime - lastUpdateTime;

long timeIntoFormat;

String updateAtValue;

if (lastUpdateTime == -1) {

updateAtValue = getResources().getString(R.string.not_updated_yet);

} else if (timePassed < 0) {

updateAtValue = getResources().getString(R.string.time_error);

} else if (timePassed < ONE_MINUTE) {

updateAtValue = getResources().getString(R.string.updated_just_now);

} else if (timePassed < ONE_HOUR) {

timeIntoFormat = timePassed / ONE_MINUTE;

String value = timeIntoFormat + "分钟";

updateAtValue = String.format(getResources().getString(R.string.updated_at), value);

} else if (timePassed < ONE_DAY) {

timeIntoFormat = timePassed / ONE_HOUR;

String value = timeIntoFormat + "小时";

updateAtValue = String.format(getResources().getString(R.string.updated_at), value);

} else if (timePassed < ONE_MONTH) {

timeIntoFormat = timePassed / ONE_DAY;

String value = timeIntoFormat + "天";

updateAtValue = String.format(getResources().getString(R.string.updated_at), value);

} else if (timePassed < ONE_YEAR) {

timeIntoFormat = timePassed / ONE_MONTH;

String value = timeIntoFormat + "个月";

updateAtValue = String.format(getResources().getString(R.string.updated_at), value);

} else {

timeIntoFormat = timePassed / ONE_YEAR;

String value = timeIntoFormat + "年";

updateAtValue = String.format(getResources().getString(R.string.updated_at), value);

}

updateAt.setText(updateAtValue);

}

/**

* 正在刷新的任务,在此任务中会去回调注册进来的下拉刷新监听器。

*

* @author guolin

*/

class RefreshingTask extends AsyncTask<Void, Integer, Void> {

@Override

protected Void doInBackground(Void... params) {

int topMargin = headerLayoutParams.topMargin;

while (true) {

topMargin = topMargin + SCROLL_SPEED;

if (topMargin <= 0) {

topMargin = 0;

break;

}

publishProgress(topMargin);

sleep(10);

}

currentStatus = STATUS_REFRESHING;

publishProgress(0);

if (mListener != null) {

mListener.onRefresh();

}

return null;

}

@Override

protected void onProgressUpdate(Integer... topMargin) {

updateHeaderView();

headerLayoutParams.topMargin = topMargin[0];

header.setLayoutParams(headerLayoutParams);

}

}

/**

* 隐藏下拉头的任务,当未进行下拉刷新或下拉刷新完成后,此任务将会使下拉头重新隐藏。

*

* @author guolin

*/

class HideHeaderTask extends AsyncTask<Void, Integer, Integer> {

@Override

protected Integer doInBackground(Void... params) {

int topMargin = headerLayoutParams.topMargin;

while (true) {

topMargin = topMargin + SCROLL_SPEED;

if (topMargin <= hideHeaderHeight) {

topMargin = hideHeaderHeight;

break;

}

publishProgress(topMargin);

sleep(10);

}

return topMargin;

}

@Override

protected void onProgressUpdate(Integer... topMargin) {

headerLayoutParams.topMargin = topMargin[0];

header.setLayoutParams(headerLayoutParams);

}

@Override

protected void onPostExecute(Integer topMargin) {

headerLayoutParams.topMargin = topMargin;

header.setLayoutParams(headerLayoutParams);

currentStatus = STATUS_REFRESH_FINISHED;

}

}

/**

* 使当前线程睡眠指定的毫秒数。

*

* @param time

* 指定当前线程睡眠多久,以毫秒为单位

*/

private void sleep(int time) {

try {

Thread.sleep(time);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

/**

* 下拉刷新的监听器,使用下拉刷新的地方应该注册此监听器来获取刷新回调。

*

* @author guolin

*/

public interface PullToRefreshListener {

/**

* 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。

*/

void onRefresh();

}

}

这个类是整个下拉刷新功能中最重要的一个类,注释已经写得比较详细了,我再简单解释一下。首先在RefreshableView的构造函数中动态添加了刚刚定义的pull_to_refresh这个布局作为下拉头,然后在onLayout方法中将下拉头向上偏移出了屏幕,再给ListView注册了touch事件。之后每当手指在ListView上滑动时,onTouch方法就会执行。在onTouch方法中的第一行就调用了setIsAbleToPull方法来判断ListView是否滚动到了最顶部,只有滚动到了最顶部才会执行后面的代码,否则就视为正常的ListView滚动,不做任何处理。当ListView滚动到了最顶部时,如果手指还在向下拖动,就会改变下拉头的偏移值,让下拉头显示出来,下拉的距离设定为手指移动距离的1/2,这样才会有拉力的感觉。如果下拉的距离足够大,在松手的时候就会执行刷新操作,如果距离不够大,就仅仅重新隐藏下拉头。

具体的刷新操作会在RefreshingTask中进行,其中在doInBackground方法中回调了PullToRefreshListener接口的onRefresh方法,这也是大家在使用RefreshableView时必须要去实现的一个接口,因为具体刷新的逻辑就应该写在onRefresh方法中,后面会演示使用的方法。
另外每次在下拉的时候都还会调用updateHeaderView方法来改变下拉头中的数据,比如箭头方向的旋转,下拉文字描述的改变等。更加深入的理解请大家仔细去阅读RefreshableView中的代码。

现在我们已经把下拉刷新的所有功能都完成了,接下来就要看一看如何在项目中引入下拉刷新了。打开或新建activity_main.xml作为程序主界面的布局,加入如下代码:

[html] view
plaincopy

<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.example.pulltorefreshtest.RefreshableView

android:id="@+id/refreshable_view"

android:layout_width="fill_parent"

android:layout_height="fill_parent" >

<ListView

android:id="@+id/list_view"

android:layout_width="fill_parent"

android:layout_height="fill_parent" >

</ListView>

</com.example.pulltorefreshtest.RefreshableView>

</RelativeLayout>

可以看到,我们在自定义的RefreshableView中加入了一个ListView,这就意味着给这个ListView加入了下拉刷新的功能,就是这么简单!

然后我们再来看一下程序的主Activity,打开或新建MainActivity,加入如下代码:

[java] view
plaincopy

public class MainActivity extends Activity {

RefreshableView refreshableView;

ListView listView;

ArrayAdapter<String> adapter;

String[] items = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L" };

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

requestWindowFeature(Window.FEATURE_NO_TITLE);

setContentView(R.layout.activity_main);

refreshableView = (RefreshableView) findViewById(R.id.refreshable_view);

listView = (ListView) findViewById(R.id.list_view);

adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items);

listView.setAdapter(adapter);

refreshableView.setOnRefreshListener(new PullToRefreshListener() {

@Override

public void onRefresh() {

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

refreshableView.finishRefreshing();

}

}, 0);

}

}

可以看到,我们通过调用RefreshableView的setOnRefreshListener方法注册了一个监听器,当ListView正在刷新时就会回调监听器的onRefresh方法,刷新的具体逻辑就在这里处理。而且这个方法已经自动开启了线程,可以直接在onRefresh方法中进行耗时操作,比如向服务器请求最新数据等,在这里我就简单让线程睡眠3秒钟。另外在onRefresh方法的最后,一定要调用RefreshableView中的finishRefreshing方法,这个方法是用来通知RefreshableView刷新结束了,不然我们的ListView将一直处于正在刷新的状态。

不知道大家有没有注意到,setOnRefreshListener这个方法其实是有两个参数的,我们刚刚也是传入了一个不起眼的0。那这第二个参数是用来做什么的呢?由于RefreshableView比较智能,它会自动帮我们记录上次刷新完成的时间,然后下拉的时候会在下拉头中显示距上次刷新已过了多久。这是一个非常好用的功能,让我们不用再自己手动去记录和计算时间了,但是却存在一个问题。如果当前我们的项目中有三个地方都使用到了下拉刷新的功能,现在在一处进行了刷新,其它两处的时间也都会跟着改变!因为刷新完成的时间是记录在配置文件中的,由于在一处刷新更改了配置文件,导致在其它两处读取到的配置文件时间已经是更改过的了。那解决方案是什么?就是每个用到下拉刷新的地方,给setOnRefreshListener方法的第二个参数中传入不同的id就行了。这样各处的上次刷新完成时间都是单独记录的,相互之间就不会再有影响。

好了,全部的代码都在这里了,让我们来运行一下,看看效果吧。



效果看起来还是非常不错的。我们最后再来总结一下,在项目中引入ListView下拉刷新功能只需三步:

1. 在Activity的布局文件中加入自定义的RefreshableView,并让ListView包含在其中。

2. 在Activity中调用RefreshableView的setOnRefreshListener方法注册回调接口。

3. 在onRefresh方法的最后,记得调用RefreshableView的finishRefreshing方法,通知刷新结束。

从此以后,在项目的任何地方,一分钟引入下拉刷新功能妥妥的。

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里


android UI进阶之实现listview的下拉加载

关于listview的操作五花八门,有下拉刷新,分级显示,分页列表,逐页加载等,以后会陆续和大家分享这些技术,今天讲下下拉加载这个功能的实现。

最初的下拉加载应该是ios上的效果,现在很多应用如新浪微博等都加入了这个操作。即下拉listview刷新列表,这无疑是一个非常友好的操作。今天就和大家分享下这个操作的实现。

先看下运行效果:











代码参考国外朋友Johan Nilsson的实现,http://johannilsson.com/2011/03/13/android-pull-to-refresh-update.html。

主要原理为监听触摸和滑动操作,在listview头部加载一个视图。那要做的其实很简单:1.写好加载到listview头部的view 2.重写listview,实现onTouchEvent方法和onScroll方法,监听滑动状态。计算headview全部显示出来即可实行加载动作,加载完成即刷新列表。重新隐藏headview。

首先写下headview的xml代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingTop="10dip"
android:paddingBottom="15dip"
android:gravity="center"
android:id="@+id/pull_to_refresh_header"
>
<ProgressBar
android:id="@+id/pull_to_refresh_progress"
android:indeterminate="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dip"
android:layout_marginRight="20dip"
android:layout_marginTop="10dip"
android:visibility="gone"
android:layout_centerVertical="true"
style="?android:attr/progressBarStyleSmall"
/>
<ImageView
android:id="@+id/pull_to_refresh_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dip"
android:layout_marginRight="20dip"
android:visibility="gone"
android:layout_gravity="center"
android:gravity="center"
android:src="@drawable/ic_pulltorefresh_arrow"
/>
<TextView
android:id="@+id/pull_to_refresh_text"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
android:paddingTop="5dip"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
/>
<TextView
android:id="@+id/pull_to_refresh_updated_at"
android:layout_below="@+id/pull_to_refresh_text"
android:visibility="gone"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
/>
</RelativeLayout>

代码比较简单,即headview包括一个进度条一个箭头和两段文字(一个显示加载状态,另一个显示最后刷新时间,本例就不设置了)。

而后重写listview,代码如下:
package com.notice.pullrefresh;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;

public class PullToRefreshListView extends ListView implements OnScrollListener {

// 状态
private static final int TAP_TO_REFRESH = 1;
private static final int PULL_TO_REFRESH = 2;
private static final int RELEASE_TO_REFRESH = 3;
private static final int REFRESHING = 4;

private OnRefreshListener mOnRefreshListener;

// 监听对listview的滑动动作
private OnScrollListener mOnScrollListener;
private LayoutInflater mInflater;

//顶部刷新时出现的控件
private RelativeLayout mRefreshView;
private TextView mRefreshViewText;
private ImageView mRefreshViewImage;
private ProgressBar mRefreshViewProgress;
private TextView mRefreshViewLastUpdated;

// 当前滑动状态
private int mCurrentScrollState;
// 当前刷新状态
private int mRefreshState;

// 箭头动画效果
private RotateAnimation mFlipAnimation;
private RotateAnimation mReverseFlipAnimation;

private int mRefreshViewHeight;
private int mRefreshOriginalTopPadding;
private int mLastMotionY;

private boolean mBounceHack;

public PullToRefreshListView(Context context) {
super(context);
init(context);
}

public PullToRefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}

public PullToRefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}

/**
* 初始化控件和箭头动画(这里直接在代码中初始化动画而不是通过xml)
*/
private void init(Context context) {
mFlipAnimation = new RotateAnimation(0, -180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
mFlipAnimation.setInterpolator(new LinearInterpolator());
mFlipAnimation.setDuration(250);
mFlipAnimation.setFillAfter(true);
mReverseFlipAnimation = new RotateAnimation(-180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
mReverseFlipAnimation.setInterpolator(new LinearInterpolator());
mReverseFlipAnimation.setDuration(250);
mReverseFlipAnimation.setFillAfter(true);

mInflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);

mRefreshView = (RelativeLayout) mInflater.inflate(
R.layout.pull_to_refresh_header, this, false);
mRefreshViewText =
(TextView) mRefreshView.findViewById(R.id.pull_to_refresh_text);
mRefreshViewImage =
(ImageView) mRefreshView.findViewById(R.id.pull_to_refresh_image);
mRefreshViewProgress =
(ProgressBar) mRefreshView.findViewById(R.id.pull_to_refresh_progress);
mRefreshViewLastUpdated =
(TextView) mRefreshView.findViewById(R.id.pull_to_refresh_updated_at);

mRefreshViewImage.setMinimumHeight(50);
mRefreshOriginalTopPadding = mRefreshView.getPaddingTop();

mRefreshState = TAP_TO_REFRESH;

//为listview头部增加一个view
addHeaderView(mRefreshView);

super.setOnScrollListener(this);

measureView(mRefreshView);
mRefreshViewHeight = mRefreshView.getMeasuredHeight();
}

@Override
protected void onAttachedToWindow() {
setSelection(1);
}

@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);

setSelection(1);
}

/**
* 设置滑动监听器
*
*/
@Override
public void setOnScrollListener(AbsListView.OnScrollListener l) {
mOnScrollListener = l;
}

/**
* 注册一个list需要刷新时的回调接口
*
*/
public void setOnRefreshListener(OnRefreshListener onRefreshListener) {
mOnRefreshListener = onRefreshListener;
}

/**
* 设置标签显示何时最后被刷新
*
* @param lastUpdated
*            Last updated at.
*/
public void setLastUpdated(CharSequence lastUpdated) {
if (lastUpdated != null) {
mRefreshViewLastUpdated.setVisibility(View.VISIBLE);
mRefreshViewLastUpdated.setText(lastUpdated);
} else {
mRefreshViewLastUpdated.setVisibility(View.GONE);
}
}

// 实现该方法处理触摸
@Override
public boolean onTouchEvent(MotionEvent event) {
final int y = (int) event.getY();
mBounceHack = false;

switch (event.getAction()) {

case MotionEvent.ACTION_UP:
if (!isVerticalScrollBarEnabled()) {
setVerticalScrollBarEnabled(true);
}
if (getFirstVisiblePosition() == 0 && mRefreshState != REFRESHING) {
// 拖动距离达到刷新需要
if ((mRefreshView.getBottom() >= mRefreshViewHeight
|| mRefreshView.getTop() >= 0)
&& mRefreshState == RELEASE_TO_REFRESH) {
// 把状态设置为正在刷新
mRefreshState = REFRESHING;
// 准备刷新
prepareForRefresh();
// 刷新
onRefresh();
} else if (mRefreshView.getBottom() < mRefreshViewHeight
|| mRefreshView.getTop() <= 0) {
// 中止刷新
resetHeader();
setSelection(1);
}
}
break;
case MotionEvent.ACTION_DOWN:
// 获得按下y轴位置
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算边距
applyHeaderPadding(event);
break;
}
return super.onTouchEvent(event);
}

// 获得header的边距
private void applyHeaderPadding(MotionEvent ev) {

int pointerCount = ev.getHistorySize();

for (int p = 0; p < pointerCount; p++) {
if (mRefreshState == RELEASE_TO_REFRESH) {
if (isVerticalFadingEdgeEnabled()) {
setVerticalScrollBarEnabled(false);
}

int historicalY = (int) ev.getHistoricalY(p);

// 计算申请的边距,除以1.7使得拉动效果更好
int topPadding = (int) (((historicalY - mLastMotionY)
- mRefreshViewHeight) / 1.7);

mRefreshView.setPadding(
mRefreshView.getPaddingLeft(),
topPadding,
mRefreshView.getPaddingRight(),
mRefreshView.getPaddingBottom());
}
}
}

/**
* 将head的边距重置为初始的数值
*/
private void resetHeaderPadding() {
mRefreshView.setPadding(
mRefreshView.getPaddingLeft(),
mRefreshOriginalTopPadding,
mRefreshView.getPaddingRight(),
mRefreshView.getPaddingBottom());
}

/**
* 重置header为之前的状态
*/
private void resetHeader() {
if (mRefreshState != TAP_TO_REFRESH) {
mRefreshState = TAP_TO_REFRESH;

resetHeaderPadding();

// 将刷新图标换成箭头
mRefreshViewImage.setImageResource(R.drawable.ic_pulltorefresh_arrow);
// 清除动画
mRefreshViewImage.clearAnimation();
// 隐藏图标和进度条
mRefreshViewImage.setVisibility(View.GONE);
mRefreshViewProgress.setVisibility(View.GONE);
}
}

// 估算headview的width和height
private void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.FILL_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);
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {

// 在refreshview完全可见时,设置文字为松开刷新,同时翻转箭头
if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
&& mRefreshState != REFRESHING) {
if (firstVisibleItem == 0) {
mRefreshViewImage.setVisibility(View.VISIBLE);
if ((mRefreshView.getBottom() >= mRefreshViewHeight + 20
|| mRefreshView.getTop() >= 0)
&& mRefreshState != RELEASE_TO_REFRESH) {
mRefreshViewText.setText("松开加载...");
mRefreshViewImage.clearAnimation();
mRefreshViewImage.startAnimation(mFlipAnimation);
mRefreshState = RELEASE_TO_REFRESH;
} else if (mRefreshView.getBottom() < mRefreshViewHeight + 20
&& mRefreshState != PULL_TO_REFRESH) {
mRefreshViewText.setText("下拉刷新...");
if (mRefreshState != TAP_TO_REFRESH) {
mRefreshViewImage.clearAnimation();
mRefreshViewImage.startAnimation(mReverseFlipAnimation);
}
mRefreshState = PULL_TO_REFRESH;
}
} else {
mRefreshViewImage.setVisibility(View.GONE);
resetHeader();
}
} else if (mCurrentScrollState == SCROLL_STATE_FLING
&& firstVisibleItem == 0
&& mRefreshState != REFRESHING) {
setSelection(1);
mBounceHack = true;
} else if (mBounceHack && mCurrentScrollState == SCROLL_STATE_FLING) {
setSelection(1);
}

if (mOnScrollListener != null) {
mOnScrollListener.onScroll(view, firstVisibleItem,
visibleItemCount, totalItemCount);
}
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;

if (mCurrentScrollState == SCROLL_STATE_IDLE) {
mBounceHack = false;
}

if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChanged(view, scrollState);
}
}

public void prepareForRefresh() {
resetHeaderPadding();// 恢复header的边距

mRefreshViewImage.setVisibility(View.GONE);
// 注意加上,否则仍然显示之前的图片
mRefreshViewImage.setImageDrawable(null);
mRefreshViewProgress.setVisibility(View.VISIBLE);

// 设置文字
mRefreshViewText.setText("加载中...");

mRefreshState = REFRESHING;
}

public void onRefresh() {

if (mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
}

/**
* 重置listview为普通的listview,该方法设置最后更新时间
*
* @param lastUpdated
*            Last updated at.
*/
public void onRefreshComplete(CharSequence lastUpdated) {
setLastUpdated(lastUpdated);
onRefreshComplete();
}

/**
* 重置listview为普通的listview,不设置最后更新时间
*/
public void onRefreshComplete() {

resetHeader();

// 如果refreshview在加载结束后可见,下滑到下一个条目
if (mRefreshView.getBottom() > 0) {
invalidateViews();
setSelection(1);
}
}

/**
* 刷新监听器接口
*/
public interface OnRefreshListener {
/**
* list需要被刷新时调用
*/
public void onRefresh();
}
}

相信我注释已经写的比较详细了,主要注意onTouchEvent和onScroll方法,在这里面计算头部边距,从而通过用户的手势实现“下拉刷新”到“松开加载”以及“加载”三个状态的切换。其中还有一系列和header有关的方法,用来设置header的显示以及取得header的边距。于此同时,代码留出了接口以供调用。

那么现在写一个测试Activity来试验下效果:
package com.notice.pullrefresh;

import java.util.Arrays;
import java.util.LinkedList;

import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.ArrayAdapter;

import com.notice.pullrefresh.PullToRefreshListView.OnRefreshListener;

public class PullrefreshActivity extends ListActivity {
private LinkedList<String> mListItems;
ArrayAdapter<String> adapter;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.pull_to_refresh);

// list需要刷新时调用
((PullToRefreshListView) getListView())
.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh() {
// 在这执行后台工作
new GetDataTask().execute();
}
});

mListItems = new LinkedList<String>();
mListItems.addAll(Arrays.asList(mStrings));

adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, mListItems);

setListAdapter(adapter);
}

private class GetDataTask extends AsyncTask<Void, Void, String[]> {

@Override
protected String[] doInBackground(Void... params) {
// 在这里可以做一些后台工作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return mStrings;
}

@Override
protected void onPostExecute(String[] result) {
// 下拉后增加的内容
mListItems.addFirst("Added after refresh...");

// 刷新完成调用该方法复位
((PullToRefreshListView) getListView()).onRefreshComplete();

super.onPostExecute(result);
}
}

private String[] mStrings = { "normal data1", "normal data2",
"nomal data3", "normal data4", "norma data5", "normal data6" };
}

代码通过asyncTask实现一个异步操作,并通过设置onRefreshListener监听器调用onRefresh方法实现下拉时刷新,并在刷新完成后调用onRefreshComplete做复位处理。


android UI进阶之实现listview的分页加载

上篇博文和大家分享了下拉刷新,这是一个用户体验非常好的操作方式。新浪微薄就是使用这种方式的典型。

还有个问题,当用户从网络上读取微薄的时候,如果一下子全部加载用户未读的微薄这将耗费比较长的时间,造成不好的用户体验,同时一屏的内容也不足以显示如此多的内容。这时候,我们就需要用到另一个功能,那就是listview的分页了。通过分页分次加载数据,用户看多少就去加载多少。

通常这也分为两种方式,一种是设置一个按钮,用户点击即加载。另一种是当用户滑动到底部时自动加载。今天我就和大家分享一下这个功能的实现。

首先,写一个xml文件,moredata.xml,该文件即定义了放在listview底部的视图:

<?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="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/bt_load"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="加载更多数据" />
<ProgressBar
android:id="@+id/pg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:visibility="gone"
/>
</LinearLayout>

可以看到是一个按钮和一个进度条。因为只做一个演示,这里简单处理,通过设置控件的visibility,未加载时显示按钮,加载时就显示进度条。

写一个item.xml,大家应该很熟悉了。用来定义listview的每个item的视图。

<?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="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/tv_title"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
/>
<TextView
android:textSize="12sp"
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
/>

</LinearLayout>

main.xml就不贴了,整个主界面就一个listview。

直接先看下Activity的代码,在里面实现分页效果。

package com.notice.moredate;

import java.util.ArrayList;
import java.util.HashMap;

import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.SimpleAdapter;
import android.widget.Toast;

public class MoreDateListActivity extends Activity implements OnScrollListener {

// ListView的Adapter
private SimpleAdapter mSimpleAdapter;
private ListView lv;
private Button bt;
private ProgressBar pg;
private ArrayList<HashMap<String,String>> list;
// ListView底部View
private View moreView;
private Handler handler;
// 设置一个最大的数据条数,超过即不再加载
private int MaxDateNum;
// 最后可见条目的索引
private int lastVisibleIndex;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

MaxDateNum = 22; // 设置最大数据条数

lv = (ListView) findViewById(R.id.lv);

// 实例化底部布局
moreView = getLayoutInflater().inflate(R.layout.moredate, null);

bt = (Button) moreView.findViewById(R.id.bt_load);
pg = (ProgressBar) moreView.findViewById(R.id.pg);
handler = new Handler();

// 用map来装载数据,初始化10条数据
list = new ArrayList<HashMap<String,String>>();
for (int i = 0; i < 10; i++) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("ItemTitle", "第" + i + "行标题");
map.put("ItemText", "第" + i + "行内容");
list.add(map);
}
// 实例化SimpleAdapter
mSimpleAdapter = new SimpleAdapter(this, list, R.layout.item,
new String[] { "ItemTitle", "ItemText" },
new int[] { R.id.tv_title, R.id.tv_content });
// 加上底部View,注意要放在setAdapter方法前
lv.addFooterView(moreView);
lv.setAdapter(mSimpleAdapter);
// 绑定监听器
lv.setOnScrollListener(this);

bt.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
pg.setVisibility(View.VISIBLE);// 将进度条可见
bt.setVisibility(View.GONE);// 按钮不可见

handler.postDelayed(new Runnable() {

@Override
public void run() {
loadMoreDate();// 加载更多数据
bt.setVisibility(View.VISIBLE);
pg.setVisibility(View.GONE);
mSimpleAdapter.notifyDataSetChanged();// 通知listView刷新数据
}

}, 2000);
}
});

}

private void loadMoreDate() {
int count = mSimpleAdapter.getCount();
if (count + 5 < MaxDateNum) {
// 每次加载5条
for (int i = count; i < count + 5; i++) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("ItemTitle", "新增第" + i + "行标题");
map.put("ItemText", "新增第" + i + "行内容");
list.add(map);
}
} else {
// 数据已经不足5条
for (int i = count; i < MaxDateNum; i++) {
HashMap<String, String> map = new HashMap<String, String>();
map.put("ItemTitle", "新增第" + i + "行标题");
map.put("ItemText", "新增第" + i + "行内容");
list.add(map);
}
}

}

@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// 计算最后可见条目的索引
lastVisibleIndex = firstVisibleItem + visibleItemCount - 1;

// 所有的条目已经和最大条数相等,则移除底部的View
if (totalItemCount == MaxDateNum + 1) {
lv.removeFooterView(moreView);
Toast.makeText(this, "数据全部加载完成,没有更多数据!", Toast.LENGTH_LONG).show();
}

}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// 滑到底部后自动加载,判断listview已经停止滚动并且最后可视的条目等于adapter的条目
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE
&& lastVisibleIndex == mSimpleAdapter.getCount()) {
// 当滑到底部时自动加载
// pg.setVisibility(View.VISIBLE);
// bt.setVisibility(View.GONE);
// handler.postDelayed(new Runnable() {
//
// @Override
// public void run() {
// loadMoreDate();
// bt.setVisibility(View.VISIBLE);
// pg.setVisibility(View.GONE);
// mSimpleAdapter.notifyDataSetChanged();
// }
//
// }, 2000);

}

}

}

通过注释,大家应该很容易理解了。这里做下简单的解析。首先要注意的是,addFootView方法一定要在setAdapter方法之前,否则会无效。addFootView方法为listview底部加入一个视图,在本例中就是那个Button加progressbar的视图。当用户点击按钮时,调用loadmoreDate方法,为listview绑定更多的数据,通过adapter的notifyDataSetChanged方法通知listview刷新,显示刚加入的数据。

这里用handler异步延迟2秒操作,模仿加载过程。同时listview绑定了onScrollListener监听器,并且实现了onScroll和onScrollStateChanged方法。在后者方法中,我们通过判断listview已经停止滚动并且最后可视的条目等于adapter的条目,可以知道用户已经滑动到底部并且自动加载,代码中将这部分代码注释掉了,大家可以自己试下。

代码中还加入了一个MaxDateNum变量,用来记录最大的数据数量。也就是说网络或者其他地方一共的数据。通过onScroll方法判断用户加载完这些数据后,移除listview底部视图,不让继续加载。同时在loadmoreDate方法中也对最大数据量做相应的操作来判断加载数量。(默认加载5条,不足5条时加载剩余的)。

看下效果图:









就写这么多了,总的来说还是很简单的,但是确实非常有用的一个效果。


android UI进阶之实现listview中checkbox的多选与记录

今天继续和大家分享涉及到listview的内容。在很多时候,我们会用到listview和checkbox配合来提供给用户一些选择操作。比如在一个清单页面,我们需要记录用户勾选了哪些条目。这个的实现并不太难,但是有很多朋友来问我如何实现,他们有遇到各种各样的问题,这里就一并写出来和大家一起分享。

ListView的操作就一定会涉及到item和Adapter,我们还是先来实现这部分内容。

首先,写个item的xml布局,里面放置一个TextView和一个CheckBox。要注意的时候,这里我设置了CheckBox没有焦点,这样的话,无法单独点击checkbox,而是在点击listview的条目后,Checkbox会响应操作。

<?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="fill_parent"
android:orientation="horizontal" >

<TextView
android:id="@+id/item_tv"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
/>

<CheckBox
android:id="@+id/item_cb"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
/>

</LinearLayout>

下面就写一个Adapter类,我们依然继承BaseAdapter类。这里我们使用一个HashMap<Integer,boolean>的键值来记录checkbox在对应位置的选中状况,这是本例的实现的基础。
package com.notice.listcheck;

import java.util.ArrayList;
import java.util.HashMap;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.TextView;

public class MyAdapter extends BaseAdapter{
// 填充数据的list
private ArrayList<String> list;
// 用来控制CheckBox的选中状况
private static HashMap<Integer,Boolean> isSelected;
// 上下文
private Context context;
// 用来导入布局
private LayoutInflater inflater = null;

// 构造器
public MyAdapter(ArrayList<String> list, Context context) {
this.context = context;
this.list = list;
inflater = LayoutInflater.from(context);
isSelected = new HashMap<Integer, Boolean>();
// 初始化数据
initDate();
}

// 初始化isSelected的数据
private void initDate(){
for(int i=0; i<list.size();i++) {
getIsSelected().put(i,false);
}
}

@Override
public int getCount() {
return list.size();
}

@Override
public Object getItem(int position) {
return list.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (convertView == null) {
// 获得ViewHolder对象
holder = new ViewHolder();
// 导入布局并赋值给convertview
convertView = inflater.inflate(R.layout.listviewitem, null);
holder.tv = (TextView) convertView.findViewById(R.id.item_tv);
holder.cb = (CheckBox) convertView.findViewById(R.id.item_cb);
// 为view设置标签
convertView.setTag(holder);
} else {
// 取出holder
holder = (ViewHolder) convertView.getTag();
}

// 设置list中TextView的显示
holder.tv.setText(list.get(position));
// 根据isSelected来设置checkbox的选中状况
holder.cb.setChecked(getIsSelected().get(position));
return convertView;
}

public static HashMap<Integer,Boolean> getIsSelected() {
return isSelected;
}

public static void setIsSelected(HashMap<Integer,Boolean> isSelected) {
MyAdapter.isSelected = isSelected;
}

}

注释已经写的非常详尽了,通过

holder.cb.setChecked(getIsSelected().get(position)); 这行代码我们实现了设置CheckBox的选中状况。

那么我们只需要在点击事件中,控制isSelected的键值即可控制对应位置checkbox的选中了。

在Activity中我们除了放置一个ListView外,还放置了三个按钮,分别实现全选,取消和反选。

看下Activity类的代码:
package com.notice.listcheck;

import java.util.ArrayList;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;

public class Ex_checkboxActivity extends Activity {

private ListView lv;
private MyAdapter mAdapter;
private ArrayList<String> list;
private Button bt_selectall;
private Button bt_cancel;
private Button bt_deselectall;
private int checkNum; // 记录选中的条目数量
private TextView tv_show;// 用于显示选中的条目数量

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
/* 实例化各个控件 */
lv = (ListView) findViewById(R.id.lv);
bt_selectall = (Button) findViewById(R.id.bt_selectall);
bt_cancel = (Button) findViewById(R.id.bt_cancelselectall);
bt_deselectall = (Button) findViewById(R.id.bt_deselectall);
tv_show = (TextView) findViewById(R.id.tv);
list = new ArrayList<String>();
// 为Adapter准备数据
initDate();
// 实例化自定义的MyAdapter
mAdapter = new MyAdapter(list, this);
// 绑定Adapter
lv.setAdapter(mAdapter);

// 全选按钮的回调接口
bt_selectall.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// 遍历list的长度,将MyAdapter中的map值全部设为true
for (int i = 0; i < list.size(); i++) {
MyAdapter.getIsSelected().put(i, true);
}
// 数量设为list的长度
checkNum = list.size();
// 刷新listview和TextView的显示
dataChanged();

}
});
// 取消按钮的回调接口
bt_cancel.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// 遍历list的长度,将已选的按钮设为未选
for (int i = 0; i < list.size(); i++) {
if (MyAdapter.getIsSelected().get(i)) {
MyAdapter.getIsSelected().put(i, false);
checkNum--;// 数量减1
}
}
// 刷新listview和TextView的显示
dataChanged();

}
});

// 反选按钮的回调接口
bt_deselectall.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
// 遍历list的长度,将已选的设为未选,未选的设为已选
for (int i = 0; i < list.size(); i++) {
if (MyAdapter.getIsSelected().get(i)) {
MyAdapter.getIsSelected().put(i, false);
checkNum--;
} else {
MyAdapter.getIsSelected().put(i, true);
checkNum++;
}

}
// 刷新listview和TextView的显示
dataChanged();
}
});

// 绑定listView的监听器
lv.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// 取得ViewHolder对象,这样就省去了通过层层的findViewById去实例化我们需要的cb实例的步骤
          ViewHolder holder = (ViewHolder) arg1.getTag();
// 改变CheckBox的状态
holder.cb.toggle();
// 将CheckBox的选中状况记录下来
MyAdapter.getIsSelected().put(arg2, holder.cb.isChecked());
// 调整选定条目
if (holder.cb.isChecked() == true) {
checkNum++;
} else {
checkNum--;
}
// 用TextView显示
tv_show.setText("已选中"+checkNum+"项");

}
});
}

// 初始化数据
private void initDate() {
for (int i = 0; i < 15; i++) {
list.add("data" + "   " + i);
}
}

// 刷新listview和TextView的显示
private void dataChanged() {
// 通知listView刷新
mAdapter.notifyDataSetChanged();
// TextView显示最新的选中数目
tv_show.setText("已选中" + checkNum + "项");
}

}

代码中在item的点击事件中,直接调用

holder.cb.toggle();

先改变CheckBox的状态,然后将值存进map记录下来

MyAdapter.getIsSelected().put(arg2, holder.cb.isChecked());而其他几个Button的点击事件,都是通过遍历list的长度来设置isSelected的值,进而通知listview根据已经变化的adapter刷新,来实现Checkbox的对应选中状态。因为对listview的处理中我们仍然使用了ViewHolder来优化ListView的效率(通过findViewById层层查找是比较耗时的,这里不了解的朋友可以看我另一篇博客android应用开发全程实录-你有多熟悉listview?,全面解析listview的)。

最后,来看下运行效果:





好了,就写到这里。相信大家都能明白了。这里要说下一个问题,有很多朋友留言或者发邮件要博客中的一些源码。我在这里声明下,我不会去发任何我觉得已经在博客里介绍的非常清楚的实例的源码,有些实例我已经把所有代码都贴出来了,还是有人要源码。。。我希望看我博客的朋友都能真正理解这个实例,能学到更多的知识,最好能有自己的改进然后再和大家一起分享。很多朋友现在已经习惯了拿别人的源码,功能类似的就直接搬到自己项目里,这是非常不好的习惯。动动手,多写写,你会学到更多。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: