第五周时候解决的问题。
就拿自己做的那个APP项目来说吧。由于项目需求,清明前花了一个下午时间来实现一个下拉刷新的ListView。上网看了第三方的库,发现不是很适合自己用。于是自己尝试的去实现了个一个下拉刷新的ListVIew。
项目地址: https://github.com/wukunguang/GongGong
首先,大概描述下用户使用整个下拉刷新的过程。
触摸-> 按住 -> 向下拖动 -> 松开
那么程序内部实现的操作大概可分解为:
捕获触摸动作 -> 捕获向下拖动 -> 向用户展示提示信息 -> 开始执行刷新操作 -> 执行刷新后的结果。
那么我们就可以开始做了。
首先,我们创建一个 RefreshListView 的类,继承于 ListView类。
因为,我们这个情况需要监听滚动情况。于是我又实现了OnScrollListener 接口。
那么即需要实现这两个抽象方法。
@Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { }
那么,我们要判断用户执行下拉刷新操作,那么我们必须要判定当前列表视图从上往下数的第一个元素是否为其原始列表内容的第一个元素。那么
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { }
这个方法很好地抽象了滑动过程中的内容和视图的交替变化。即传入参数 firstVisibleItem 即为第一个可见元素它的下标。
所以我们定义一个成员变量用于获得它,用来判断用户当前是否滚动到列表的顶部。
即
private int firstVisibleItem; //当前可见第一个的元素位置。
@Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { this.firstVisibleItem = firstVisibleItem; }
然后获取到了当前滚动可视的第一个元素的下标。我们还需要做个标记,确保当前的可视的第一个元素为List中第一个元素。
那么我们定义 一个成员变量:
private boolean isInFirstDown; //表示在最顶端按下。
判定当前按下时候,如果可视的第一个元素为List中第一个元素。则为true 否则为 false。
那么。除此之外,我们还需要给一个用户良好的体验吧。我们需要提醒用户什么时候下拉松开,什么时候刷新,什么时候刷新结束。
所以我们定义一个VIew 用户显示在 ListVIew的顶部。高度,宽度呢根据其容器内容自行定义。
创建一个布局文件:
fresh_listview_header.xml
其定义内容为:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/white" > <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="@dimen/swzl_item_thing_padding" android:paddingBottom="@dimen/swzl_item_thing_padding" > <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:layout_centerInParent="true" android:id="@+id/fresh_listview_layout" android:gravity="center"> <com.gc.materialdesign.views.ProgressBarCircularIndeterminate android:layout_width="32dp" android:layout_height="32dp" android:background="#1E88E5" android:id="@+id/refresh_listView_ProgressBar"> </com.gc.materialdesign.views.ProgressBarCircularIndeterminate> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/refresh_tip" android:text="@string/fresh_listView_pullTip" android:textSize="@dimen/swzl_attention_size" /> </LinearLayout> </RelativeLayout> </LinearLayout>
这里就不必多讲了。相信写过Android的人都知道这个只是一个简单的布局。
内容大概就是一个转圈圈的ProgressBar和一个文字提示信息。
好了,那么我们把它添加进入 ListView。那么我们发现,ListVIew实现了一个方法。叫做addHeaderView(View view);
那么我们可以写一个方法。
/** * 初始化界面,添加顶部布局View * @param ctx */ private void initView(Context ctx){ LayoutInflater inflater = LayoutInflater.from(ctx); headerView = inflater.inflate(R.layout.fresh_listview_header, null); //获得资源。 resources = ctx.getResources(); measureView(headerView); headerHeight = headerView.getMeasuredHeight(); Log.i("refreshListView",headerHeight+">>>>>>"); setTopHeaderViewHeight(-headerHeight); this.addHeaderView(headerView); this.setOnScrollListener(this); }
传入参数Context类型。用户获取Resource和设定View Layout的解析器,这个后面讲。我们需要理解的是,我们通过解析器解析返回的View就能完成视图和View的绑定。
那么我们只需要执行addHeaderView(View view);方法就行了。
但是执行之前,我们必须设置提示内容需要隐藏进来。为什么呢,因为平时不下拉时候,我们应该是什么提示信息都看不到的吧?
那么我们创建一个方法。叫做measureView(View view);
代码如下:
/** * 通知父类布局占用其内容大小 * @param view //顶部View */ private void measureView(View view){ ViewGroup.LayoutParams p = view.getLayoutParams(); //获取Layout参数。 if (p == null){ p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); //如果参数为空。那么就重新实例化一个。 } int width = ViewGroup.getChildMeasureSpec(0,0,p.width); int height; int tempHeight = p.height; if(tempHeight > 0){ height = MeasureSpec.makeMeasureSpec(tempHeight,MeasureSpec.EXACTLY); } else { height = MeasureSpec.makeMeasureSpec( 0 ,MeasureSpec.UNSPECIFIED); } view.measure(width, height); //重新确定宽高。 }
因为我们光设置它的高度为0是不可能的,因为这样没法立即刷新整个视图。我们必须在设置时候,通知它的父类容器对整个视图进行刷新并获取。
好了。那么我们添加顶部View完毕。
那么我们开始监听事件。
我们都知道 继承与View的东西都会实现一个 叫做 onTouchEvent(MotionEven ev);的方法。此时我们要设置手势监听。必须重写此方法。
那么这几个事件的触发我也不在注释多写了。写android基本都知道代表什么手势。
代码如下。
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: setRefreshViewByState(); //及时刷新视图。 if (firstVisibleItem == 0){ //代表当前最顶端 isInFirstDown = true; startY = (int) ev.getY(); } break; case MotionEvent.ACTION_MOVE: setRefreshViewByState(); onMove(ev); break; case MotionEvent.ACTION_UP: if (RELESE == currentState){ //设置刷新。 currentState = REFRESHING; setRefreshViewByState(); }else if (currentState == PULL){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); } break; } return super.onTouchEvent(ev); }
最主要的就是在MotionEvent.ACTION_MOVE 这个事件。即表示你在执行拖动时候触发的。
onMove(ev);方法如下:
同时,我们需要判定刷新前后的几个状态。定义静态成员常量:
private int currentState;//定义当前状态位 private final int NONE = 0;//无状态,即什么都没有 private final int PULL = 1;//当前下拉状态 private final int RELESE = 2;//提示可释放状态 private final int REFRESHING = 3;//正在刷新状态
/** * 移动时候执行的操作。 * @param ev 事件 */ private void onMove(MotionEvent ev){ if (!isInFirstDown){ return; } int tempY = (int) ev.getY(); //获取当前滑动时候的Y的坐标。 int moveSpace = tempY - startY; //获取坐标差。 int topPadding = moveSpace - headerHeight; //获取需要显示出来的高度 Log.d("freshListView","state:"+currentState+"----"); //Log.d("frechListView","headerHeight"+headerHeight+"---"); switch (currentState){ case NONE: if (moveSpace > 0){ currentState = PULL; setRefreshViewByState(); } break; case PULL: //设置显示高度 setTopHeaderViewHeight(topPadding); //下拉高度差大于30且当前正在下拉时候 if (moveSpace > headerHeight+30 && scrollState ==SCROLL_STATE_TOUCH_SCROLL){ Log.i("refresh_msg","hahahha--"); currentState = RELESE; setRefreshViewByState(); } break; case RELESE: //设置显示高度 setTopHeaderViewHeight(topPadding); if (moveSpace < headerHeight +30 ){ currentState = PULL; setRefreshViewByState(); }else if (moveSpace <= 0){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); } break; case REFRESHING: break; } }
好了 ,大概就是这些了。我们首先判断它下滑到多少高度差时候作出可刷新判断。我这里设置预设的值是30。那么当前刷新状态即可改变为可松开。
当松开时候,那么状态转化为松开状态。这时候当收回高度小于30时候,当前就执行刷新操作:
runnable.run();
我这里写了个回调的run方法。被使用者需要在自身实现Runnable接口,同时把自身传入到该类。并在run方法内执行相关刷新操作即可。
下面附上视图刷新过程方法:
/** * 根据当前状态改变headerView显示内容。 */ private void setRefreshViewByState(){ TextView textView = (TextView) headerView.findViewById(R.id.refresh_tip); ProgressBarCircularIndeterminate progressBar = (ProgressBarCircularIndeterminate) headerView.findViewById(R.id.refresh_listView_ProgressBar); switch (currentState){ case NONE: setTopHeaderViewHeight(-headerHeight); break; case PULL: progressBar.setVisibility(GONE); textView.setText(resources.getString(R.string.fresh_listView_pullTip)); break; case RELESE: progressBar.setVisibility(GONE); textView.setText(resources.getString(R.string.fresh_listView_releaseTip)); break; case REFRESHING: runnable.run(); setTopHeaderViewHeight(headerHeight); progressBar.setVisibility(VISIBLE); textView.setText(resources.getString(R.string.fresh_listView_freshNow)); Log.d("refresh_msg", "Refreshing!"); break; } }
这些内容简单易懂。基本都是最简单的android内容。所以没必要写注释了。就是视图的改变而已。
好了,那么执行刷新完了后改干什么。那么肯定就是关闭当前显示正在刷新的视图啦~。
public void dismissHeaderView(){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); //TextView tx = (TextView) headerView.findViewById(R.id.refresh_tip); }
整个方法写下来也就200多行。很简单~~个人认为比较坑的就是顶部视图隐藏部分。其他都可以自己想出来。
下面附上所有代码。
package com.sky31.gonggong.widget; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.ListView; import android.widget.TextView; import com.gc.materialdesign.views.ProgressBarCircularIndeterminate; import com.sky31.gonggong.R; import com.sky31.gonggong.util.Debug; import java.util.Timer; import java.util.TimerTask; /** * Created by wukunguang on 16-3-30. */ public class RefreshListView extends ListView implements AbsListView.OnScrollListener{ View headerView; //顶部View private int headerHeight; //顶部布局文件高度 private int firstVisibleItem; //当前可见第一个的元素位置。 private boolean isInFirstDown; //表示在最顶端按下。 private int startY; // 按下时候开始的Y数值 private Runnable runnable; private int currentState;//定义当前状态位 private final int NONE = 0;//无状态,即什么都没有 private final int PULL = 1;//当前下拉状态 private final int RELESE = 2;//提示可释放状态 private final int REFRESHING = 3;//正在刷新状态 private Resources resources; private int scrollState;//当前滚动状态 /** * 初始化界面,添加顶部布局View * @param ctx */ private void initView(Context ctx){ LayoutInflater inflater = LayoutInflater.from(ctx); headerView = inflater.inflate(R.layout.fresh_listview_header, null); //获得资源。 resources = ctx.getResources(); measureView(headerView); headerHeight = headerView.getMeasuredHeight(); Log.i("refreshListView",headerHeight+">>>>>>"); setTopHeaderViewHeight(-headerHeight); this.addHeaderView(headerView); this.setOnScrollListener(this); } /** * 通知父类布局占用其内容大小 * @param view //顶部View */ private void measureView(View view){ ViewGroup.LayoutParams p = view.getLayoutParams(); if (p == null){ p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } int width = ViewGroup.getChildMeasureSpec(0,0,p.width); int height; int tempHeight = p.height; if(tempHeight > 0){ height = MeasureSpec.makeMeasureSpec(tempHeight,MeasureSpec.EXACTLY); } else { height = MeasureSpec.makeMeasureSpec( 0 ,MeasureSpec.UNSPECIFIED); } view.measure(width, height); } public RefreshListView(Context context) { super(context); initView(context); } public RefreshListView(Context context, AttributeSet attrs) { super(context, attrs); initView(context); } public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } /** * 设置它的边距。用于下拉显示和默认隐藏 * @param topPadding //顶部边距 */ private void setTopHeaderViewHeight(int topPadding){ headerView.setPadding(headerView.getPaddingLeft(), topPadding, headerView.getPaddingRight(), headerView.getPaddingBottom()); headerView.invalidate(); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { this.scrollState = scrollState; Log.d("freshListView", "scrollState:" + scrollState + ""); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { this.firstVisibleItem = firstVisibleItem; } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: setRefreshViewByState(); if (firstVisibleItem == 0){ //代表当前最顶端 isInFirstDown = true; startY = (int) ev.getY(); } break; case MotionEvent.ACTION_MOVE: setRefreshViewByState(); onMove(ev); break; case MotionEvent.ACTION_UP: if (RELESE == currentState){ //设置刷新。 currentState = REFRESHING; setRefreshViewByState(); }else if (currentState == PULL){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); } break; } return super.onTouchEvent(ev); } /** * 移动时候执行的操作。 * @param ev 事件 */ private void onMove(MotionEvent ev){ if (!isInFirstDown){ return; } int tempY = (int) ev.getY(); //获取当前滑动时候的Y的坐标。 int moveSpace = tempY - startY; //获取坐标差。 int topPadding = moveSpace - headerHeight; //获取需要显示出来的高度 Log.d("freshListView","state:"+currentState+"----"); //Log.d("frechListView","headerHeight"+headerHeight+"---"); switch (currentState){ case NONE: if (moveSpace > 0){ currentState = PULL; setRefreshViewByState(); } break; case PULL: //设置显示高度 setTopHeaderViewHeight(topPadding); //下拉高度差大于30且当前正在下拉时候 if (moveSpace > headerHeight+30 && scrollState ==SCROLL_STATE_TOUCH_SCROLL){ Log.i("refresh_msg","hahahha--"); currentState = RELESE; setRefreshViewByState(); } break; case RELESE: //设置显示高度 setTopHeaderViewHeight(topPadding); if (moveSpace < headerHeight +30 ){ currentState = PULL; setRefreshViewByState(); }else if (moveSpace <= 0){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); } break; case REFRESHING: break; } } public void initRunable(Runnable runnable){ this.runnable = runnable; } /** * 根据当前状态改变headerView显示内容。 */ private void setRefreshViewByState(){ TextView textView = (TextView) headerView.findViewById(R.id.refresh_tip); ProgressBarCircularIndeterminate progressBar = (ProgressBarCircularIndeterminate) headerView.findViewById(R.id.refresh_listView_ProgressBar); switch (currentState){ case NONE: setTopHeaderViewHeight(-headerHeight); break; case PULL: progressBar.setVisibility(GONE); textView.setText(resources.getString(R.string.fresh_listView_pullTip)); break; case RELESE: progressBar.setVisibility(GONE); textView.setText(resources.getString(R.string.fresh_listView_releaseTip)); break; case REFRESHING: runnable.run(); setTopHeaderViewHeight(headerHeight); progressBar.setVisibility(VISIBLE); textView.setText(resources.getString(R.string.fresh_listView_freshNow)); Log.d("refresh_msg", "Refreshing!"); break; } } public void dismissHeaderView(){ currentState = NONE; isInFirstDown = false; setRefreshViewByState(); //TextView tx = (TextView) headerView.findViewById(R.id.refresh_tip); } }
完结,撒花。