这是一个下拉刷新的ListView,它不是一个复合控件,而是继承了ListView然后进行了改写.
这里要说明一点,其实用复合控件去实现下拉刷新是最简单的方式,但由于复合控件继承的通常是layout,所以并不是真正意义上的ListView.而且多个相同的复合控件在同一个fragment页面里是会出问题的.
所以说,直接继承ListView去制作一个下拉控件,是相对比较复杂,但比较安全的方式.它的重用性更强.
现在大部分下拉刷新控件是不隔离下拉操作和列表滚动操作的,也就是说当列表拉到顶部之后,如果继续拉就直接刷新了.
我个人不是很喜欢这样的操作,我希望滚到顶的时候能停住,就像一个普通的ListView一样.只有松开手指再次下拉,才能刷新.这也是我写这个控件的初衷.
源码及Demo在这里:
http://download.youkuaiyun.com/detail/qyc898/7556559
源码里"加载更多"功能没有更新进去,但下面的代码是更新了的.可以直接复制下面ListPullDown类的代码,然后替换掉Demo里的代码.
头部ITEM的XML布局,注意,这个布局一定要LinearLayout,用相对布局在拖动时会被自动排版,失去效果
pulldownlist_top.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" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="下拉刷新"
android:textColor="#000000"
android:textSize="14sp" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/textView1"
android:layout_centerHorizontal="true"
android:text="上一次更新时间:未更新"
android:textSize="12sp" />
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="70dp"
android:src="@drawable/refresh_down" />
<ProgressBar
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="70dp" />
</RelativeLayout>
</LinearLayout>
底部ITEM布局
pulldownlist_bottom.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/st_pulldownlist_click"
android:paddingBottom="6dp"
android:paddingTop="6dp" >
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="#FF000000"
android:textSize="16sp" />
<ProgressBar
android:id="@+id/progressBar1"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="70dp" />
</RelativeLayout>
自定义属性attrs.xml
<declare-styleable name="PullDownList">
<attr name="loadingMoreTextColor" format="color" />
<attr name="loadingMoreTextSize" format="dimension" />
<attr name="loadingMoreBKColor" format="reference|color" />
<attr name="refreshTextColor" format="color" />
<attr name="refreshTextSize" format="dimension" />
<attr name="refreshTimeColor" format="color" />
<attr name="refreshTimeSize" format="dimension" />
<attr name="refreshArrowUp" format="reference" />
<attr name="refreshArrowDown" format="reference" />
</declare-styleable>
代码:
package qyc.library.control.list_pulldown;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import qyc.library.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
/**
* <p>
* 作者:QYC
* <p>
* 邮件:68452263@qq.com
* <p>
* */
public class ListPullDown extends ListView {
public ListPullDown(Context context) {
super(context);
if (isInEditMode()) {
inEditMode();
} else {
init(null);
}
}
public ListPullDown(Context context, AttributeSet attrs) {
super(context, attrs);
if (isInEditMode()) {
inEditMode();
} else {
init(attrs);
}
}
public ListPullDown(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
if (isInEditMode()) {
inEditMode();
} else {
init(attrs);
}
}
// 在可视化编辑器里面显示的内容
private void inEditMode() {
if (isInEditMode()) {
setBackgroundColor(Color.rgb(134, 154, 190));
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
getContext(), android.R.layout.simple_list_item_1);
adapter.add("PullDownList");
adapter.add("作者:QYC");
adapter.add("邮箱:68452263@qq.com");
setAdapter(adapter);
return;
}
}
/*
* ==================列表顶部的控件集合==================
*/
private ProgressBar progressBar_top = null;
private ImageView imageView_top = null;
private TextView txtTime_top, txtRefresh_top;
/** 列表顶部的视图 */
private View topViewItem = null;
/** 列表顶部视图的高 */
private int topViewItemHeight = 0;
/** 刷新向下箭头 */
private int imgRefreshDown = R.drawable.png_listpulldown_refreshdown;
/** 刷新向上箭头 */
private int imgRefreshUp = R.drawable.png_listpulldown_refreshup;
/*
* =====================================================
*/
/*
* ==================列表底部的控件集合==================
*/
private View bottomViewItem = null;
private ProgressBar progressBar_bottom = null;
private TextView txtBottom = null;
/*
* =====================================================
*/
/*
* ==================用于逻辑判断的4个变量==================
*/
/** 第一行是否在屏幕上 */
private boolean isFirstItemShow = false;// 第一行即加载行,只要把列表滚动到顶,不管加载行被压缩了还是被拉开了,都会认为显示在了屏幕上
/** 是否有加载意图,即加载第一行的意图 */
private boolean isWantToExtrude = true;
/** 是否已经拉开 */
private boolean isExtruded = false;
/** 是否已经在加载 */
private boolean isLoading = false;
/*
* =====================================================
*/
/** 时间格式化 */
private static final SimpleDateFormat sdf = new SimpleDateFormat(
"HH:mm:ss", Locale.CHINA);
/*
* =====================================================
*/
/** 初始化 */
private void init(AttributeSet attrs) {
setOnScrollListener(onScrollListener);
LayoutInflater layoutInflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// 初始化列表底部工具条
bottomViewItem = layoutInflater.inflate(R.layout.listpulldown_bottom,
this, false);
progressBar_bottom = (ProgressBar) bottomViewItem
.findViewById(R.id.progressBar1);
txtBottom = (TextView) bottomViewItem.findViewById(R.id.textView1);
bottomViewItem.setOnClickListener(onClickListener);// 设置点击监听
bottomViewItem.setVisibility(View.GONE);// 默认状态下不显示加载更多的动作条
// 加入底部工具条(加载更多)
addFooterView(bottomViewItem);
// 初始化列表顶部工具条
topViewItem = layoutInflater.inflate(R.layout.listpulldown_top, this,
false);
topViewItem.setOnClickListener(null);// 使它不能作为list的一个item来点击
progressBar_top = (ProgressBar) topViewItem
.findViewById(R.id.progressBar1);
progressBar_top.setVisibility(View.INVISIBLE);
imageView_top = (ImageView) topViewItem.findViewById(R.id.imageView1);
txtTime_top = (TextView) topViewItem.findViewById(R.id.textView2);
txtTime_top.setText("上一次更新时间:无");
txtRefresh_top = (TextView) topViewItem.findViewById(R.id.textView1);
// 根据兹定于属性,对控件样式进行调整
setResValue(attrs);
/*
* 3步隐藏该topViewItem: 1.测出其加入屏幕后的长和宽,因为现在它没加入屏幕,直接获取其长宽是无效的 2.得到测出的高度
* 3.将上边距设置为负高度,相当于把高度变成了0,达到隐藏的效果(顺带一提:topViewItem即使通过padding设置其高度为0
* ,在列表中,它仍然会占据一行的像素,列表仍然认为它是第0行)
*/
// 第一步
measureView(topViewItem);
// 第二步
topViewItemHeight = topViewItem.getMeasuredHeight();
// 第三步
topViewItem.setPadding(0, 0 - topViewItemHeight, 0, 0);
// 加入到列表中(点击刷新)
addHeaderView(topViewItem);
}
/** 根据自定义属性,对控件样式进行调整 */
private void setResValue(AttributeSet attrs) {
// 获取控件自定义属性值
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.ListPullDown);
int N = a.getIndexCount();// 自定义属性被使用的数量
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
case R.styleable.ListPullDown_loadingMoreBKColor:
// 底部工具条背景色
int loadingMoreBKColorID = a.getResourceId(attr, 0);
if (loadingMoreBKColorID != 0) {
bottomViewItem.setBackgroundResource(loadingMoreBKColorID);
} else {
int loadingMoreBKColor = a.getColor(attr, 0);
if (loadingMoreBKColor != 0) {
bottomViewItem.setBackgroundColor(loadingMoreBKColor);
}
}
break;
case R.styleable.ListPullDown_loadingMoreTextColor:
// 底部工具条文字颜色
int loadingMoreTextColor = a.getColor(attr, 0);
if (loadingMoreTextColor != 0) {
txtBottom.setTextColor(loadingMoreTextColor);
}
break;
case R.styleable.ListPullDown_loadingMoreTextSize:
// 底部工具条文字大小
float loadingMoreTextSize = a.getDimension(attr, 0);
if (loadingMoreTextSize != 0) {
txtBottom.setTextSize(TypedValue.COMPLEX_UNIT_PX,
loadingMoreTextSize);
}
break;
case R.styleable.ListPullDown_refreshTextColor:
// 顶部工具条提示文字颜色
int refreshTextColor = a.getColor(attr, 0);
if (refreshTextColor != 0) {
txtRefresh_top.setTextColor(refreshTextColor);
}
break;
case R.styleable.ListPullDown_refreshTextSize:
// 顶部工具条提示文字大小
float refreshTextSize = a.getDimension(attr, 0);
if (refreshTextSize != 0) {
txtRefresh_top.setTextSize(TypedValue.COMPLEX_UNIT_PX,
refreshTextSize);
}
break;
case R.styleable.ListPullDown_refreshTimeColor:
// 顶部工具条时间文字颜色
int refreshTimeColor = a.getColor(attr, 0);
if (refreshTimeColor != 0) {
txtTime_top.setTextColor(refreshTimeColor);
}
break;
case R.styleable.ListPullDown_refreshTimeSize:
// 顶部工具条时间文字大小
float refreshTimeSize = a.getDimension(attr, 0);
if (refreshTimeSize != 0) {
txtTime_top.setTextSize(TypedValue.COMPLEX_UNIT_PX,
refreshTimeSize);
}
break;
case R.styleable.ListPullDown_refreshArrowUp:
// 顶部工具条向上箭头
int refreshArrowUp = a.getResourceId(attr, 0);
if (refreshArrowUp != 0) {
this.imgRefreshUp = refreshArrowUp;
}
break;
case R.styleable.ListPullDown_refreshArrowDown:
// 顶部工具条向下箭头
int refreshArrowDown = a.getResourceId(attr, 0);
if (refreshArrowDown != 0) {
this.imgRefreshDown = refreshArrowDown;
}
break;
default:
break;
}
}
a.recycle();
}
// 列表滚动事件
private android.widget.AbsListView.OnScrollListener onScrollListener = new OnScrollListener() {
/*
* 当列表以任何形式(自动滚或者手拖着滚)停止滚动后,如果第一行已经出现了,那么认为已经有了加载的意图
* 如果第一行没有出现,那么认为没有加载的意图
*/
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (isLoading)// 已经在加载则不作任何处理
{
return;
}
// 当列表以任何形式(自动滚或者手拖着滚)停止滚动时
if (scrollState == SCROLL_STATE_IDLE) {
if (isFirstItemShow) {
isWantToExtrude = true;
// System.out.println("意图被onScrollStateChanged置为true");
} else {
isWantToExtrude = false;
// System.out.println("意图被onScrollStateChanged置为false");
}
}
}
/*
* 在列表滚动过程中,根据屏幕最顶行是不是第0行,来判断:第0行是否出现在屏幕上,是否存在加载的意图
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if (isLoading)// 已经在加载则不作任何处理
{
return;
}
if (firstVisibleItem == 0) {
isFirstItemShow = true;
// System.out.println("isFirstItemShow被onScroll置为true");
} else {
isFirstItemShow = false;
isWantToExtrude = false;
// System.out.println("意图被onScroll置为false");
}
}
};
/** 上一次手指触摸屏幕的Y轴坐标 */
private float lastActionDown_Y = 0;
// 触摸事件
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (isLoading)// 已经在加载则不作任何处理
{
return super.onTouchEvent(ev);
}
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
// 按下手指时,获得按下y轴位置,用于在拖动时计算手指拖动距离
lastActionDown_Y = ev.getY();
} else if (action == MotionEvent.ACTION_MOVE) {
/*
* 1.如果有加载的意图,则根据触摸屏Y轴的移动来判断.
* 2.Y轴移动值为正说明在有拉伸意图的情况下,操作者进行了拉伸,所以应该根据拉伸的幅度来拉伸第一行,并且屏蔽其他控件的触摸屏事件。
* 3.Y轴移动值变成负,说明操作者已经顶回去,第一行也已经被顶没..这时不再屏蔽触摸屏事件,直到ListView将加载意图至为负值。
* 或操作者再次拉伸. 4.当不再有加载的意图,则有两种可能。一种是平常的触摸,不用作任何处理。
* 另一种是当时已经拉开了,用户又不想拉了,所以又向上推了回去 推回去后,加载意图被onScroll函数制为false。
* 在这种情况下,应该把第一行重新压缩回原状。
*/
// 手指滑动的距离
float gapValue_y = ev.getY() - lastActionDown_Y;
gapValue_y = gapValue_y * 2 / 3;// 减慢下拉速度,比真实的手指滑动速度略慢
if (isWantToExtrude) {
if (gapValue_y >= 0) {
isExtruded = true;
// 新的PaddingTop应该是滑动的距离减去总高,才会使加载行有慢慢拉宽的效果
int newPaddingTop = (int) (gapValue_y - topViewItemHeight);
topViewItem.setPadding(0, newPaddingTop, 0, 0);
// 如果新的PaddingTop大于等于20,说明已经正常显示的情况下还多拉了20像素,这就足够了,提示用户松开的话是可以更新的
if (newPaddingTop >= 20) {
setTopViewControl(2);
} else {
setTopViewControl(1);
}
// 屏蔽
return false;
}
} else {
// 用户此刻已经不想加载更多
// 可能顶部工具条已经消失不见,也可能拖到一半
if (isExtruded) {// 拖到一半,复原
isExtruded = false;
topViewItem.setPadding(0, 0 - topViewItemHeight, 0, 0);
}
}
} else if (action == MotionEvent.ACTION_UP) {
if (isExtruded) {
if (topViewItem.getPaddingTop() >= 20) {
if (this.onPDListListener != null) {
isLoading = true;
topViewItem.setPadding(0, 0, 0, 0);
setTopViewControl(3);
this.onPDListListener.onRefresh();
} else {
topViewItem.setPadding(0, 0 - topViewItemHeight, 0, 0);
isExtruded = false;
}
} else {
topViewItem.setPadding(0, 0 - topViewItemHeight, 0, 0);
isExtruded = false;
}
}
}
return super.onTouchEvent(ev);
}
// 提升性能,防止无意义的重复赋值
private int controlState = -1;
/**
* 用户是否正在往下拉
*
* @return
*/
public boolean isExtruded() {
return isExtruded;
}
/*
* ==================顶部工具条部分的私有函数=================
*/
/**
* 设置顶部工具条控件的显示内容
*
* @param state
* 0表示被用户重置,1表示下拉刷新,2表示松开刷新,3表示正在刷新
*/
private void setTopViewControl(int state) {
if (controlState != state) {
System.out.println("state=" + state);
controlState = state;
if (state == 0) {
imageView_top.setVisibility(View.VISIBLE);
progressBar_top.setVisibility(View.INVISIBLE);
} else if (state == 1) {
txtRefresh_top.setText("下拉更新");
imageView_top.setImageResource(imgRefreshDown);
} else if (state == 2) {
txtRefresh_top.setText("释放立即更新");
imageView_top.setImageResource(imgRefreshUp);
} else if (state == 3) {
imageView_top.setVisibility(View.INVISIBLE);
progressBar_top.setVisibility(View.VISIBLE);
txtRefresh_top.setText("正在更新...");
}
}
}
/*
* ==============底部工具条部分的私有函数=================
*/
// View的点击事件
private OnClickListener onClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (v.equals(bottomViewItem)) {
if (onPDListListener != null) {
onPDListListener.onloadMore();
loadingMoreView_State(1);
}
}
}
};
/**
* <p>
* 设置加载更多工具条的状态
* <p>
* -1=隐藏 0=显示并还原为待点击状态 1=处于加载中
* */
private void loadingMoreView_State(int state) {
if (state == -1) {
bottomViewItem.setVisibility(View.GONE);
} else if (state == 0) {
bottomViewItem.setVisibility(View.VISIBLE);
progressBar_bottom.setVisibility(View.INVISIBLE);
txtBottom.setText("点击加载更多");
bottomViewItem.setEnabled(true);
} else if (state == 2) {
bottomViewItem.setVisibility(View.VISIBLE);
progressBar_bottom.setVisibility(View.VISIBLE);
txtBottom.setText("正在努力加载...");
bottomViewItem.setEnabled(false);
}
}
/* =======================监听部分的公开函数====================== */
/** 设置刷新监听 */
public void setOnPDListen(OnPDListListener onPDListListener) {
this.onPDListListener = onPDListListener;
}
/** 移除刷新监听 */
public void removePDListen() {
this.onPDListListener = null;
}
/* ===============底部工具条部分的公开函数============= */
/** 是否显示加载更多 */
public void loadingMoreView_IsEnabled(boolean isEnabled) {
if (isEnabled) {
loadingMoreView_State(0);
} else {
loadingMoreView_State(-1);
}
}
/* ====================顶部工具条部分的公开函数==================== */
/** 弹出刷新条 并触发onRefresh接口 */
public void startRefresh() {
if (!isLoading && this.onPDListListener != null) {
isLoading = true;
topViewItem.setPadding(0, 0, 0, 0);
setTopViewControl(3);
setSelection(0);// 选中首行
this.onPDListListener.onRefresh();
}
}
/**
* 结束刷新
*
* @param isUpdateTime
* 是否更新时间,刷新失败可以不更新
*/
public void stopRefresh(boolean isUpdateTime) {
// 还原所有变量,首行重置回初始值,选中首行
if (isLoading) {
// 重置所有变量
isFirstItemShow = false;
isWantToExtrude = true;
isExtruded = false;
isLoading = false;
// 重置下拉行控件的内容
setTopViewControl(0);
// 更新时间
if (isUpdateTime) {
txtTime_top.setText("上一次更新时间:"
+ sdf.format(Calendar.getInstance().getTime()));
}
topViewItem.setPadding(0, 0 - topViewItemHeight, 0, 0);
setSelection(0);
}
}
/* ================测量长宽========================== */
/** 为View测量长宽 因为View在加入到界面之前,直接去获取长宽是无效的,跟在Activity的onCreat时无法获取控件长宽一个道理 */
private static void measureView(View child) {
ViewGroup.LayoutParams p = child.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_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);
}
private OnPDListListener onPDListListener = null;
public interface OnPDListListener {
/** 下拉刷新 */
public void onRefresh();
/** 加载更多 */
public void onloadMore();
}
}