2019-12-20
关键字:自定义上下拉ListView
在 APK 开发中,一个具备在列表顶部下拉刷新、在列表尾部上拉加载功能的 ListView 的需求还是比较多的。
具备这种功能的优秀开源代码同样也有很多。
但今天,笔者就非要自己实现一个这样的控件不可。
以下是成品效果图:
这个控件的结构很简单:
1、一个LinearLayout容器打底;
2、一个ListView置于中间;
3、一个用于标识头部“下拉刷新”标语的控件;
4、一个用于标识尾部“上拉加载”标语的控件;
仅此而已。
所以,笔者这个上下拉列表控件其实是需要自定义一个LinearLayout容器控件。然后在这个容器控件里根据规则来处理触摸事件、点击事件并通知上下拉事件等。
public class PullingListView extends LinearLayout
这里有几个难点:
1、如何监听列表滚动到头部还是尾部亦或正处于中间?
2、上在列表上的上、下滑事件应如何响应成滑出对应的提示标语?
3、首尾提示标语应如何随手势滑出来?
关于第 1 点,直接通过监听 ListView 的 onScrollListener 即可勉强达到目的。
listview.setOnScrollListener(this);
为什么说是勉强呢?因为这个监听会在ListView滚动时回调,虽然它会告诉我们当前ListView中第 1 个可见Item的标号与最后一个可见Item的标号以及总Item数量。但它会在Item刚一加载时就通知,而不是在Item真正展示出来或者真正展示完全以后才通知。这就会存在一个“超前通知”的问题。就是实际上我们还没有看到第 1 个Item,但你却在回调方法中告诉我它已经展示出来了。这会让我们误判。关于这个问题,笔者目前还没有找到解决办法。
而关于第 2 点,则是通过监听ListView的触摸事件,并根据前面 onScrollListener 中得到的当前列表位置,再根据手势方向来决定是该滑出提示语还是让其滚动ListView。
listview.setOnTouchListener(this);
第 3 点其实也不难,只需要在 onTouch 中判断出当前是要滑出头提示还是尾提示,然后再根据手势滑动的垂直距离来实时改变头尾控件的高度,再调用容器中的更新子布局方法即可。
head.setLayoutHeight((int) distanceVertical);
requestLayout();
整个控件的核心就这些东西。整体代码量不多,能实现上面效果图中的功能,但同样也存在一些问题。具体问题就是在列表中数量超过一屏幕容量时,上、下滑动未及端点即开始响应滑出提示语的现象。这个现象的原因笔者在上面已经分析过了。
以下贴出完整源码:
/**
* 一个具备上拉刷新与下拉加载功能的ListView
* */
public class PullingListView extends LinearLayout implements View.OnTouchListener, AdapterView.OnItemClickListener, AbsListView.OnScrollListener {
private static final String TAG = "PullingListView";
private static final int LISTVIEW_SCROLL_STATUS_IN_HEAD = 0;
private static final int LISTVIEW_SCROLL_STATUS_IN_MIDDLE = 1;
private static final int LISTVIEW_SCROLL_STATUS_IN_TAIL = 2;
private float y0;
private float lastDisHeight; //上次垂直移动的高度。
private int listViewPos;
private ListView listview;
private Header head;
private Header foot;
private OnPullingListViewListener listener;
private ListAdapter adapter;
public PullingListView(Context context){
super(context);
init();
}
private void init(){
Logger.v(TAG, "init()");
setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
setOrientation(VERTICAL);
listview = new ListView(getContext());
LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
llp.weight = 1;
listview.setLayoutParams(llp);
head = new Header(true);
foot = new Header(false);
listview.setOnTouchListener(this);
listview.setOnItemClickListener(this);
listview.setOnScrollListener(this);
addView(head.getView());
addView(listview);
addView(foot.getView());
}
@Override
public boolean onTouch(View v, MotionEvent event){
// Logger.v(TAG, "onTouch,action:" + event.getAction() + ",listViewPos:" + listViewPos);
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
y0 = event.getY();
lastDisHeight = 0;
listview.scrollTo(0, 0);
head.setLayoutHeight(0);
foot.setLayoutHeight(0);
requestLayout();
}break;
case MotionEvent.ACTION_MOVE:{
float distanceVertical = (event.getY() - y0) / 2.0f; //为了避免响应过于灵敏,垂直滑动距离应延缓5倍。
switch(listViewPos){
case LISTVIEW_SCROLL_STATUS_IN_MIDDLE:{
Logger.d(TAG, "MIDDLE");
return false;
}
case LISTVIEW_SCROLL_STATUS_IN_HEAD:{
Logger.d(TAG, "HEAD");
if(distanceVertical > 0){
//往下滑动。
head.setLayoutHeight((int) distanceVertical);
requestLayout();
}else{
//往上滑动,要看有没有填满。
if(adapter.getCount() > 0){
int shownHeight = listview.getChildCount() * (listview.getChildAt(0).getHeight() + listview.getDividerHeight());
if(shownHeight <= listview.getHeight()){
// Logger.d(TAG, "None fill out.");
//没填满.
foot.setLayoutHeight((int) distanceVertical);
requestLayout();
if(foot.getView().getLayoutParams().height >= foot.HEAD_LAYOUT_HEIGHT_MAX){
lastDisHeight = distanceVertical;
listview.scrollTo(0, foot.getView().getLayoutParams().height);
}else{
listview.scrollBy(0, (int) (distanceVertical - lastDisHeight) * -1);
}
lastDisHeight = distanceVertical;
}else{
// Logger.d(TAG, "filled out.");
//填满了,要滑动item。
return false;
}
}else{
// Logger.d(TAG, "No records");
//没有数据,则忽略掉滑动事件。
return true;
}
}
}break;
case LISTVIEW_SCROLL_STATUS_IN_TAIL:{
Logger.d(TAG, "TAIL");
if(distanceVertical < 0){
//往上滑动,加载。
foot.setLayoutHeight((int) distanceVertical);
requestLayout();
listview.scrollTo(0, 0);
}else{
//往下滑动
return false;
}
}break;
}
}break;
case MotionEvent.ACTION_UP:{
if(head.canLoad()){
head.load();
if(listener != null) {
listener.onRefresh();
}
}else if(foot.canLoad()){
foot.load();
if(listener != null) {
listener.onLoad();
}
}else{
foot.setLayoutHeight(0);
head.setLayoutHeight(0);
requestLayout();
listview.scrollTo(0, 0);
}
}break;
}//switch(event.getAction()) -- end
return false;
}//onTouch() -- end
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if(listener != null) {
listener.onItemClick(parent, view, position, id);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// do nothing.
Logger.d(TAG, "onScrollStateChanged,scrollState:" + scrollState);
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
Logger.d(TAG, "onScroll,firstVisibleItem:" + firstVisibleItem + ",visibleItemCount:" + visibleItemCount + ",totalItemCount:" + totalItemCount);
if (firstVisibleItem == 0) {
listViewPos = LISTVIEW_SCROLL_STATUS_IN_HEAD;
}else if (visibleItemCount + firstVisibleItem == totalItemCount) {
listViewPos = LISTVIEW_SCROLL_STATUS_IN_TAIL;
}else{
listViewPos = LISTVIEW_SCROLL_STATUS_IN_MIDDLE;
}
}
public void setDivider(Drawable divider){
listview.setDivider(divider);
}
public void setDividerHeight(int height){
listview.setDividerHeight(height);
}
public void setAdapter(ListAdapter adapter){
this.adapter = adapter;
listview.setAdapter(adapter);
}
public void setOnPullingListViewListener(OnPullingListViewListener listener){
this.listener = listener;
}
public void refreshed(){
Logger.v(TAG, "refreshed");
listview.post(new Runnable() {
@Override
public void run() {
foot.loadFinished();
head.loadFinished();
requestLayout();
listview.scrollTo(0, 0);
}
});
}
public void setSelction(int selection){
Logger.v(TAG, "setSelction:" + selection);
listview.setSelection(selection);
}
/**
* 上下两个页眉的布局管理。
* */
private class Header extends BaseLayoutManager {
private final int HEAD_LAYOUT_HEIGHT_MAX = UnitManager.px2dp(60);
private final int STATUS_NORMAL = 0;
private final int STATUS_TIP = 1;
private final int STATUS_LOADING = 2;
private int status;
private boolean isTop;
private TextView tv;
private Header(boolean isTop){
super(null);
this.isTop = isTop;
LinearLayout linearLayout = new LinearLayout(PullingListView.this.getContext());
LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(-1, 0);
linearLayout.setLayoutParams(llp);
linearLayout.setBackgroundColor(ResourcesManager.getColor(R.color.activity_base_bg));
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
linearLayout.setGravity(Gravity.CENTER);
view = linearLayout;
ProgressBar pb = new ProgressBar(getContext());
llp = new LinearLayout.LayoutParams(UnitManager.px2dp(35), UnitManager.px2dp(35));
pb.setLayoutParams(llp);
tv = new TextView(getContext());
if(isTop) {
tv.setText("刷新列表");
}else {
tv.setText("加载更多");
}
tv.setGravity(Gravity.CENTER_VERTICAL);
llp = new LinearLayout.LayoutParams(-2, -1);
llp.leftMargin = UnitManager.px2dp(15);
tv.setLayoutParams(llp);
linearLayout.addView(pb);
linearLayout.addView(tv);
}
private void setLayoutHeight(int height){
height = Math.abs(height);
if(height < HEAD_LAYOUT_HEIGHT_MAX){
view.getLayoutParams().height = height;
if(height > (HEAD_LAYOUT_HEIGHT_MAX * 0.7)){
if(status != STATUS_TIP){
status = STATUS_TIP;
if(isTop) {
tv.setText("松开以刷新");
}else{
tv.setText("松开以加载");
}
}
}else {
if(status != STATUS_NORMAL){
status = STATUS_NORMAL;
if(isTop) {
tv.setText("刷新列表");
}else{
tv.setText("加载更多");
}
}
}
}else{
view.getLayoutParams().height = HEAD_LAYOUT_HEIGHT_MAX;
}
}
private boolean canLoad(){
return status == STATUS_TIP;
}
private void load(){
status = STATUS_LOADING;
if(isTop) {
tv.setText("刷新中,请稍候");
}else{
tv.setText("加载中,请稍候");
}
}
private void loadFinished(){
status = STATUS_NORMAL;
view.getLayoutParams().height = 0;
if(isTop) {
tv.setText("刷新列表");
}else {
tv.setText("加载更多");
}
}
}// class Header -- end
public interface OnPullingListViewListener {
void onRefresh();
void onLoad();
void onItemClick(AdapterView<?> parent, View view, int position, long id);
}
}
一种具备上下拉刷新功能的ListView