自定义View之实现ListView的下拉刷新
来源:互联网 发布:足球外文网站 知乎 编辑:程序博客网 时间:2024/05/16 03:05
自己花了两个礼拜基本掌握了自定义View,无论是继承现有的TextView,LinearLayout等还是继承View,ViewGroup,我都实现了一遍,收获了许多。自定义View的基本流程是:自定义属性一>测量onMeasure一>布局onLayout一>绘制onDraw一>处理onTouchEvent等。实际使用时,只要处理其中几个环节就行。
像我自己绘制的天气中的折线图以及空调遥控器,关键就是onDraw方法,灵活运用paint,canvas的绘图技巧,加上精确计算。而ListView的下拉刷新就是onLayout和LayoutParams的使用。
这里参考了郭婶的一篇博客,主要部分加入了我自己的想法,附上链接郭婶的博客
原理:(引用郭婶的)先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头(header)和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏 。
header布局:一个下拉箭头,一个文字描述,一个刷新时的图片wait(这里简单就用ic_launcher),当正在刷新时,使得下拉箭头隐藏,wait图片可见。
</span><pre class="html" name="code"><?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/layout_head" android:layout_width="match_parent" android:layout_height="100dp" android:orientation="horizontal" android:paddingBottom="30dp"> <RelativeLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2"> <ImageView android:id="@+id/arrow" android:layout_width="40dp" android:layout_height="40dp" android:src="@drawable/arrow" android:layout_alignParentEnd="true" android:layout_centerVertical="true"/> <ImageView android:id="@+id/wait" android:layout_width="40dp" android:layout_height="40dp" android:visibility="gone" android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:src="@mipmap/ic_launcher"/> </RelativeLayout> <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="3" android:layout_marginLeft="10dp"> <TextView android:id="@+id/description" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" android:text="下拉可刷新" /> </LinearLayout></LinearLayout>
1.新建一个类继承LinearLayout,先看看声明了哪些变量和常量:
public class MyRefreshView extends LinearLayout implements View.OnTouchListener { private static final String TAG = "MyRefreshView"; 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;//刷新完成或未刷新状态 //header下拉的最大的topMargin,效果是下拉到一定程度就下拉不了 private static final int MAX_TOP_MARGIN=80; private PullToRefreshListener mListener;//回调接口 private View header;//下拉头的View private ListView listView; private ImageView arrow; private ImageView wait; private TextView description;//文字描述 private MarginLayoutParams headerLayoutParams;//下拉头的布局参数 private int hideHeaderHeight;//下拉头的高度 //当前状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH, //STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED private int currentStatus = STATUS_REFRESH_FINISHED; private float yDown;//手指按下时的屏幕纵坐标 private float yMove;//手指移动时的屏幕纵坐标 private int touchSlop;//系统所能识别的被认为是滑动的最小距离 private int top_Margin;//记录header的headerLayoutParams.topMargin
2.重写他的onLayout方法:刚开始时,让下拉头隐藏在屏幕上方
public MyRefreshView(Context context) { super(context); init(context); } public MyRefreshView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); hideHeaderHeight = header.getHeight();//得到下拉头View的高度 headerLayoutParams = (MarginLayoutParams) header.getLayoutParams(); //让其LayoutParams.topMargin为负的下拉头高度,这样刚开始时,下拉头就会隐藏在屏幕上方 headerLayoutParams.topMargin = -hideHeaderHeight; listView = (ListView) getChildAt(1);//得到ListView listView.setOnTouchListener(this);//设置onTouch监听,来处理下拉的具体逻辑 } private void init(Context context) { header = LayoutInflater.from(context).inflate(R.layout.layout_myhead, null, true); wait = (ImageView) header.findViewById(R.id.wait); arrow = (ImageView) header.findViewById(R.id.arrow); description = (TextView) header.findViewById(R.id.description); //系统所能识别的被认为是滑动的最小距离 touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); setOrientation(VERTICAL); addView(header, 0); }
3.只有在listView滑到顶部的前提下,才再考虑若手指向下滑动让下拉头显示的逻辑。这个方法就是判断listView是否滑到顶部:
private boolean IsAbleToPull() { View firstChild = listView.getChildAt(0);//得到listView的第一个item if (firstChild != null) { int firstVisiblePos = listView.getFirstVisiblePosition(); if (firstVisiblePos == 0 && firstChild.getTop() == 0) { //如果可视的第一个Item的position为0,说明当前的第一个Item为整个listView的第一个Item,并且 // 第一个Item的上边缘距离父布局值为0,两者同时满足说明ListView滚动到了最顶部,此时允许下拉刷新 return true; } else { if (headerLayoutParams.topMargin != -hideHeaderHeight) { headerLayoutParams.topMargin =- hideHeaderHeight; header.setLayoutParams(headerLayoutParams); } return false; } } else { return true;// 如果ListView中没有元素,默认允许下拉刷新 } }
4.具体分析下拉情况:刚开始时,手指向下滑动,下拉头慢慢下移,下拉头内容(箭头下指,文字描述为下拉可刷新);当下拉头刚好完全显示时,下拉头内容变为(箭头上指,文字描述为释放立即刷新);释放时,下拉头内容(箭头隐藏,wait图片可见,文字描述为正在刷新...);完成刷新后,下拉头隐藏。我把这些写在了一个方法里,根据传入的参数具体变化:
public void headerInfoChange(int i) { ObjectAnimator anim; switch (i) { case STATUS_PULL_TO_REFRESH: description.setText("下拉可刷新"); wait.setVisibility(GONE); arrow.setVisibility(VISIBLE); anim = ObjectAnimator.ofFloat(arrow, "rotation", 180, 0); anim.setDuration(300).start(); break; case STATUS_RELEASE_TO_REFRESH: description.setText("释放立即刷新"); wait.setVisibility(GONE); arrow.setVisibility(VISIBLE); anim = ObjectAnimator.ofFloat(arrow, "rotation", 0, 180); anim.setDuration(300).start(); break; case STATUS_REFRESHING: wait.setVisibility(VISIBLE); description.setText("正在刷新"); arrow.setVisibility(INVISIBLE); break; } }
5.关键:重写listView的onTouch方法,根据手指滑动,处理相应逻辑:
public boolean onTouch(View view, MotionEvent event) { if (IsAbleToPull()) {//判断listView滑到顶部 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: yDown = event.getRawY(); break; case MotionEvent.ACTION_MOVE: yMove = event.getRawY(); int distance = (int) (yMove - yDown); //distance>0说明手指是向下滑动(下拉),distance>touchSlop说明这次为有效下拉操作 if (distance > touchSlop) { if (currentStatus != STATUS_REFRESHING) { // 通过偏移下拉头的topMargin值,来实现下拉效果 //distance/2是为了有更好的下拉体验,你得向下滑动header高度的两倍,才能使header刚好全部显示 headerLayoutParams.topMargin = (distance / 2) - hideHeaderHeight; if (headerLayoutParams.topMargin > MAX_TOP_MARGIN) { headerLayoutParams.topMargin = MAX_TOP_MARGIN;//下拉到一定程度就下拉不了了 } header.setLayoutParams(headerLayoutParams); if (headerLayoutParams.topMargin > 0) {//当header全部显示出来时,箭头上指,释放立即刷新 if (currentStatus != STATUS_RELEASE_TO_REFRESH) {//加个判断是为了当headerLayoutParams.topMargin>0 headerInfoChange(STATUS_RELEASE_TO_REFRESH);//的所有下拉过程中只执行一次 } currentStatus = STATUS_RELEASE_TO_REFRESH; } else {//当header没有全部显示时,箭头下指,下拉可刷新 if (currentStatus != STATUS_PULL_TO_REFRESH) {//加个判断是为了当headerLayoutParams.topMargin<0 headerInfoChange(STATUS_PULL_TO_REFRESH);//的所有下拉过程中只执行一次 } currentStatus = STATUS_PULL_TO_REFRESH; } } } break; case MotionEvent.ACTION_UP: default: if (currentStatus == STATUS_RELEASE_TO_REFRESH) { headerInfoChange(STATUS_REFRESHING);//arrow隐藏,wait可见,正在刷新 new RefreshingTask().execute();// 松手时如果是释放立即刷新状态,就调用正在刷新的任务 } else if (currentStatus == STATUS_PULL_TO_REFRESH) { hideHeader();// 松手时如果是下拉状态,就隐藏下拉头 } break; } if (currentStatus == STATUS_PULL_TO_REFRESH || currentStatus == STATUS_RELEASE_TO_REFRESH) { // 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态 listView.setPressed(false); //Set whether this view can receive the focus. Setting this to false will also ensure that this view is not focusable in touch mode listView.setFocusable(false); listView.setFocusableInTouchMode(false); // 当前正处于下拉或释放状态,通过返回true屏蔽掉ListView的滚动事件 return true; } } return false; }
6.隐藏下拉头方法:
public void hideHeader() { top_Margin = headerLayoutParams.topMargin;//先记录下header上移的初始位置 final int height =top_Margin + hideHeaderHeight;//header从开始上移到结束上移的总高度 ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 1f); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { //valueAnimator.getAnimatedValue()从0一>1变化,当为0时,表示开始上移,这时headerLayoutParams.topMargin //应该为之前保存的top_Margin,当为1时,表示结束上移,这时headerLayoutParams.topMargin应该为负的hideHeaderHeight headerLayoutParams.topMargin = top_Margin - (int) ((float) valueAnimator.getAnimatedValue() * height); header.setLayoutParams(headerLayoutParams); } }); valueAnimator.setDuration(1000); valueAnimator.start(); currentStatus=STATUS_REFRESH_FINISHED; }
7.正在刷新时,调用回调接口。这里用AsyncTask实现:
class RefreshingTask extends AsyncTask<Void, Integer, Void> { @Override protected Void doInBackground(Void... params) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } currentStatus = STATUS_REFRESHING; if (mListener != null) { mListener.onRefresh(); } return null; } @Override protected void onPostExecute(Void aVoid) { hideHeader(); currentStatus = STATUS_REFRESH_FINISHED; } }
8.最后是接口定义,给出设置接口的公共方法:
public interface PullToRefreshListener { /** * 刷新时会去回调此方法,在方法内编写具体的刷新逻辑。注意此方法是在子线程中调用的, 你可以不必另开线程来进行耗时操作。 */ void onRefresh(); } public void setOnRefreshListener(PullToRefreshListener listener) { mListener = listener; }
至此,自定义View完成。这里有个问题:当刷新完成后,执行AsyncTask子类的onPostExecute方法里的hideHeader方法,应该有动画(下拉头慢慢移上去),但实际效果是下拉头瞬间隐藏。用Log输出hideHeader方法里的headerLayoutParams.topMargin值,发现当刷新完成后值为-hideHeaderHeight的,这本该是动画结束后的值。
后来我怀疑是多线程AsyncTask的原因,所以我在AsyncTask的doInBackgroud方法中获取到headerLayoutParams.topMargin值,在onProgressUpdate中再将值赋给headerLayoutParams.topMargin。问题解决了,但还是不清楚为什么会出现这个问题。希望以后能找到答案。附上修改后的代码:
class RefreshingTask extends AsyncTask<Void, Integer, Void> { @Override protected Void doInBackground(Void... params) { int topMargin = headerLayoutParams.topMargin; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } currentStatus = STATUS_REFRESHING; if (mListener != null) { mListener.onRefresh(); } publishProgress(topMargin); return null; } @Override protected void onPostExecute(Void aVoid) { hideHeader(); currentStatus = STATUS_REFRESH_FINISHED; } @Override protected void onProgressUpdate(Integer... values) { headerLayoutParams.topMargin = values[0]; } }
最后给出源码:点击打开链接
- 自定义View之实现ListView的下拉刷新
- 自定义listview实现下拉刷新的效果
- Android自定义View之快速实现下拉刷新, 点击加载更多ListView
- 自定义ListView实现下拉刷新
- 自定义ListView实现下拉刷新
- View系列(1)--自定义一个ListView的下拉刷新
- android之Listview的下拉刷新实现
- 自定义下拉刷新的listview
- Android开发之自定义控件--ListView的下拉刷新功能
- ListView下拉刷新的实现
- ListView下拉刷新的实现
- Android 自定义view:实现ListView下拉的视差特效
- 自定义ListView实现下拉刷新功能
- android 自定义listview实现下拉刷新(一)
- android 自定义ListView实现下拉刷新
- Android自定义View实现下拉刷新控件
- 自定义控件之ListView下拉刷新
- Andriod自定义View之(下拉刷新)
- 队列的链式存储结构
- GIS基本算法基础
- poj1321棋盘问题(dfs)
- 数据库总结
- jqchart 去掉右下角的水印的方法
- 自定义View之实现ListView的下拉刷新
- iOS封装常用的方法
- 关于下载使用Genymotion的问题
- Php的运行模式
- clion undefined reference to `boost::system::generic_category()'
- 【贪心算法】区间调度问题总结
- Spring Data Jpa 自定义方法实现问题
- 51nod1006 最长公共子序列Lcs
- ARC机制的基本规则及强制规定