MyPullToRefreshView继承自LinearLayout,布局为vertical,该容器中包含三个子view,这三个view从上到下依次排列在LinearLayout中。
效果图如下:
下图中蓝色部分是充满屏幕的,HeaderView在ListView的上方,在代码中动态添加进来,使其底部Y轴坐标刚好为0,FooterView在ListView的下方,也在代码中动态添加进来,该View的TopMargin刚好为整个布局的高度。
首先看一下该控件的使用:
1.在xml中配置
<span style="font-size:14px;"><com.example.testpulltorefreshview.PullToRefreshView android:id="@+id/my_pull_to_refresh_view" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> </com.example.testpulltorefreshview.PullToRefreshView></span>
2.在代码中为ListView设置适配器,添加数据
<span style="font-size:14px;">ListView list=(ListView)findViewById(R.id.listView); list.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,new String[]{"sss"}));</span>
模块一:控件测绘
在代码中动态添加HeaderView时,需要首先测量HeaderView的高度h,因为我们要是其TopMargin设置为0-h,这样才能使得HeaderView的底部刚好Y轴坐标为0。同理,FooterView的topMargin应该设置为整个控件的高度getHeight。为了简化添加FooterView的处理,只需使ListView充满整个控件后再添加FooterView。
在构造函数中强制设置LinearLayout排列方法为vertical,然后在构造函数中添加HeaderView,因为此时ListView还未被添加进来。
<span style="font-size:14px;">public MyPullToRefreshView(Context context,AttributeSet attrs){ super(context,attrs); mContext=context; this.setOrientation(LinearLayout.VERTICAL);//强制设置控件的排列方向为vertical init(); }</span>
构造函数中通过init函数动态添加HeaderView,注意此时在xml文件中设置的ListView还未被添加到控件中。所以此时添加的HeaderView是LinearLayout容器中的第一个控件。
<span style="font-size:14px;">public void init(){ inflater=LayoutInflater.from(mContext); ... 此时创建动画资源,后面添加 ... addHeaderView(); }</span>
在addHeaderView函数中创建View,并测量其高度:
<span style="font-size:14px;">public void addHeaderView(){ mHeaderView=inflater.inflate(R.layout.refresh_header,this,false); ... 执行findViewById,初始化layout中的View实例 ... measureView(mHeaderView);//测量HeaderView的高度 mHeaderHeight=mHeaderView.getMeasuredHeight();//获得测量的HeaderView高度 LayoutParams params=new LayoutInflater(LayoutParams.MATCH_PARENT,mHeaderHeight);//创建HeaderView的布局参数LayoutParams params.topMargin=-mHeaderHeight;//设置HeaderView的topMargin为-mHeaderHeight,这样HeaderView的底部Y轴坐标为0。 addView(mHeaderView,params); }</span>
我们需要分析measureView函数,搞清楚如何可以测量HeaderView的高度。在此之前首先需要看一下R.layout.refresh_header代码。
<span style="font-size:14px;"><?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/rv_pull_to_refresh_header" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="8dp" android:paddingBottom="5dp">" <ImageView android:id="@+id/iv_arrow" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="60dp" android:contentDescription="@string/app_name" android:src="@drawable/arrow_up"/> <ProgressBar android:id="@+id/pull_to_refresh_progress" style="?android:attr/progressBarStyleInverse" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="60dp" android:indeterminate="true" android:visibility="gone"/> <TextView android:id="@+id/tv_state" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:layout_centerHorizontal="true" android:text="@string/pull_to_refresh" /> <TextView android:id="@+id/tv_date" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/tv_state" android:layout_below="@+id/tv_state" android:text="更新于。。。。" android:visibility="gone"/> </RelativeLayout></span>
在使用上可以设置HeaderView的高度为具体的值,如
<span style="font-size:14px;"><RelativeLayout android:layout_width=match_parent android:layout_height="20dp" ...> ... </RelativeLayout></span>
也可以设置HeaderView的高度刚好包裹内部的View,即wrap_content.
<span style="font-size:14px;"><RelativeLayout android:layout_width=match_parent android:layout_height="wrap_content" ...> ... </RelativeLayout></span>
所以在测量控件高度是要针对两种情况考虑。
<span style="font-size:14px;">public void measureView(View child){ ViewGroup.LayoutParams params=child.getLayoutParams(); if(params==null){ params=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT); } //如果params的值不是具体的dp值,那么等价于MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED),如果params为具体的dp值,那么等价于MeasureSpec.makeMeasureSpec(dp值,MesureSpec.EXACTLY) int widthMeasureSpec=getChildMeasureSpec(0,0+0,params.width); int heightMeasureSpec; if(params.height>0){//为具体的dp值 heightMeasureSpec=MeasureSpec.makeMeasureSpec(params.height,MeasureSpec.EXACTLY); } else{ heightMeasureSpec=MeasureSpec.makeMeasureSpec(0,MeasureSpec.UNSPECIFIED); } child.measure(widthMeasureSpec,heightMeasureSpec); }</span>
在添加完成HeaderView后该添加FooterView了。FooterView应该添加到整个控件的最后。Activity中执行setContentView来解析xml构建View
Tree。当解析完成当前的View后会回调View的onFinishInflate,在该函数中添加FooterView,就能保证FooterView被添加到了整个控件的尾部。
<span style="font-size:14px;">@Override protected void onFinishInflate(){ super.onFinishInflate(); addFooterView(); initContentAdapterView(); } public void addFooterView(){ mFooterView=inflater.inflate(R.layout.refresh_footer,this,false); ... 实例化view变量 ... measureView(mFooterView); mFooterHeight=mFooterView.getMeasuredHeight(); LayoutParams params=new LayoutParams(LayoutParams.MATCH_PARENT,mFooterHeight); addFooterView(mFooterView,params); }</span>
模块二:触摸分发
上面完成了控件中View的添加和布局,下面需要实现整个控件的触摸分发模块了。
下面先简单总结Android的触摸分发机制。
public boolean dispatchTouchEvent(MotionEvent ev) 分发触摸
public boolean onInterceptTouchEvent(MotionEvent ev) 拦截触摸
public boolean onTouchEvent(MotionEvent ev) 处理触摸
ViewGroup包含以上三个函数,Activity和View只包含dispatchTouchEvent和onTouchEvent两个函数。触摸事件的分发是从Activity开始的,再到ViewGroup,ViewGroup在向下传到View中。
<span style="font-size:14px;">//Activity.java public boolean dispatchTouchEvent(MotionEvent ev){ if(ev.getAction()==MotionEvent.ACTION_DOWN){ onUserInteraction();//该方法内部是空的,当每次传入触摸事件Action_down时,都会调用该方法,后续的action_move和action_up不会调用该方法。我们可以重载该方法 } if(getWindow().superDispatchTouchEvent(ev)){ return true; } return onTouchEvent(ev); }</span>
Activity将触摸分发给了DecorView(ViewGroup),DecorView在将触摸分发该ViewGroup或View,ViewGroup将触摸分发到onInterceptTouchEvent函数中,若该函数返回true,那么后续的触摸事件就直接由ViewGroup的onTouchEvent处理,不会传到ViewGroup中的View了,若返回的是false,那么进一步将触摸传到View中。在View中再执行dispatchTouchEvent。
<span style="font-size:14px;">//View.java public boolean dispatchTouchEvent(MotionEvent event){ ... if(li!=null && li.mOnTouchListener!=null &&...&& li.mOnTouchListener.onTouch(this,event)){ result=true; } if(!result && onTouchEvent(event)){ result=true; } ... }</span>
若View的onTouchListener不为空,执行onTouchListener的onTouch,否则执行View的onToucEvent函数。我们在MyPullToRefreshView中重载onInterceptTouchEvent函数。若onInterceptTouchEvent返回true,那么后续的触摸将在ViewGroup的onTouchEvent函数中处理了,不会分发给ViewGroup中的HeaderView,ListView和FooterView了。
我们现在需要先分析清楚在什么情况下需要拦截触摸,不让触摸传递子View中。
1.滑动位移过小,小于5,那么不会拦截触摸。
2.当ListView未被滑到最顶端或最底端的情况下,是不需要拦截触摸的,这时要让ListView可以自由滑动。
3.当ListView被滑动到最顶端,并且继续滑动的方向是向下的,那么就需要拦截触摸,然后在ViewGroup的onTouchEvent中使HeaderView向下移动。
4.当Listview被滑动到最底端,并且继续滑动的方向是向上的,那么就需要拦截触摸,然后在VrewGroup的onTouchEvent中使FooterView向上移动。
在这里我们要讨论一下填充控件的滑动视图ListView和ScrollView。
<span style="font-size:14px;">private AdapterView<?> mAdapterView; private ScrollView mSrollView;</span>
在onFinshInflate函数中实例化:
<span style="font-size:14px;">@Override protected void onFinishInflate(){ ... int childCount=this.getChildCount(); if(childCount<3){ //IllegalArgumentException继承自RuntimeException throw new IllegalArgumentException("this layout must contain 3 child views,and AdapterView or" + " ScrollView must in the second position! "); } //instanceof是一个二元操作符,作用是判断操作符左侧的对象是否是右侧的类的实例 if(this.getChildAt(1) instanceof AdapterView<?>){ mAdapterView=(AdapterView<?>) this.getChildAt(1); } else if(this.getChildAt(1) instanceof ScrollView){ mScrollView=(ScrollView) this.getChildAt(1); } if(mAdapterView==null && mScrollView==null){ throw new IllegalArgumentException("must contain a AdapterView or ScrollView in the layout"); } }</span>
在onInterceptTouchEvent中对ACTION_DOWN不拦截,我们需要拦截的是上述几种情况下的ACTION_MOVE,首先ACTION_DOWN会被传到View中处理,后续的不满足以上情况要求的部分ACTION_MOVE也会传到View中处理。一旦我们在onInterceptTouchEvent中拦截了ACTION_MOVE,那么之前处理触摸事件的view会接收到ACTION_CANCEL消息,之后所有的ACTION_MOVE全部直接被传到ViewGroup的onTouchEvent中,不会再到onInterceptTouchEvent中判读是否需要拦截了。
<span style="font-size:14px;">public boolean onInterceptTouchEvent(MotionEvent event){ int y=(int)e.getRawY(); //getRawX/getRawY是相对于屏幕的绝对距离,getX/getY是相对于View的相对距离 switch(e.getAction){ case MotionEvent.ACTION_DOWN: mLastActionDown=y; //mLastActionDown记录了上一次ACTION_DOWN的Y轴值 break; case MotionEvent.ACTION_MOVE: int delta=y-mLastActionDown; if(delta>=-5 && delta<=5){return false;} //滑动位移过小,不消费该触摸 if(isRefreshViewScroll(delta)){ //在isRefreshViewSrcoll函数中判断是否符合上述情况:ListView已滑到最顶端或最底端 return true; } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: break; } return false; }</span>
在下拉和上拉的过程中都有三个状态:
1.下拉时HeaderView未完全显示出来,此时释放不会导致刷新;
2.下拉时HeaderView已完全显示出来,此时释放会导致刷新;
3.释放,正在刷新。
在滑动方向上又分下拉刷新和上拉加载两种。
private static int PULL_DOWN_STATE=0; //两种方向
private static int PULL_UP_STATE=1;
pribate int mPullState;//当前滑动的方向
private static int PULL_TO_REFRESH=2;//滑动时状态
private static int RELEASE_TO_REFRESH=3;
private static int REFRESHING=4;
private int mHeaderState;//Headerview当前的状态
private int mFooterState;//FooterView当前的状态
<span style="font-size:14px;">boolean isRefreshViewScroll(int delta){ if(mHeaderState==REFRESHING || mFooterState==REFRESHING){ return false; //正在刷新时是否可以滑动控件识实际情况而定。 } if(delta>0){ mPullState=PULL_DOWN_STATE; }else{ mPullState=PULL_UP_STATE; } if(mAdapterView!=NULL){ if(delta>0){ View child=mAdapterView.getChildAt(0); if(child==null){ //listview中无数据,不拦截 return false; } if(child.getTop()==0 && mAdapterView.getFirstVisiblePosition()==0){ mPullState=PULL_DOWN_STATE; return ture; } //如果设置了ListView的padding或item的top,在此处添加对其的处理 } else if(delta<0){ View lastChild=mAdapterView.getChildAt(mAdapterView.getChildCount()-1); if(lastChild==null){ return false; } if(lastChild.getBottom()<=getHeight() mAdapterView.getLastVisiblePosition()==mAdapterView.getChildCount()-1){ mPullState=PULL_UP_STATE; return true; } //如果设置了ListView的padding或item的bottom,在此处添加对其的处理 } } if(mScrollView!=null){ if(delta>0 && mScrollView.getScrollY()==0){ mPullState=PULL_UP_STATE; return true; } else if(delta<0 && mScrollView.getScrollY<=getHeight()-mScrollView.getChildAt(0).getHeight()){ mPullState=PULL_DOWN_STATE; return true; } } return false; }</span>
当isRefreshViewScroll返回true,那么后续的ACTION_MOVE就直接分发到ViewGroup的onTouchEvent中处理。要注意的是此时的Math.abs(ACTION_MOVE-
mLastActionDown)是大于5的。
<span style="font-size:14px;">@Override public boolean onTouchEvent(MotionEvent event){ int y=event.getRawY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //在onInterceptTouchEvent中已经记录 break; case MotionEvent.ACTION_MOVE: int delta=y-mLastActionDown; if(mPullState==PULL_DOWN_STATE){ headerPrepareToRefresh(delta); } else if(mPullState==PULL_UP_STATE){ footerPrepareToRefresh(delta) } mLastActionDown=y; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: ..... break; } }</span>
在headerPrepareToRefresh或footerPrepareToRefresh中改变HeaderView或FooterView的位置,以及在HeaderView或FooterView中显示提示信息。
首先来看一下如何改变HeaderView或FooterView的位置。
<span style="font-size:14px;">public int changingHeaderViewTopMargin(int delta){ LayoutParams params=(LayoutParams)mHeaderView.getLayoutParams(); int newTopMargin=(int) (params.topMargin+deltaY*0.6); params.topMargin=newTopMargin; mHeaderView.setLayoutParams(params); invalidate(); return newTopMargin; }</span>
很简单,直接改变HeaderView的TopMargin就可以,然后调用invalidate来进行重绘。
当HeaderView完全显示出来后要将Headerview中的箭头旋转向上,此时通过旋转动画RotateAnimation来实现。
我们在构造函数中调用init来初始化动画资源。
<span style="font-size:14px;">public void init(){ inflater=LayoutInflater.from(mContext); mFlipAnimation=new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); mFlipAnimation.setInterpolator(new LinearInterpolator()); mFlipAnimation.setDuration(250); mFlipAnimation.setFillAfter(true);//这样动画播放完会停留在最后一帧 mReverseFlipAnimation=new RotateAnimation(180,0,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f); mReverseFlipAnimation.setInterpolator(new LinearInterpolator()); mReverseFlipAnimation.setDuration(250); mReverseFlipAnimation.setFillAfter(true); addHeaderView();//在构造函数中addView,可以保证是第一个添加到LinearLayout中的,此时xml中设置的子View还未被添加到LinearLayout中 } public void headerPrepareToRefresh(int delta){ int newTopMargin=changingHeaderViewTopMargin(delta); //根据newTopMargin的值判断是否播放动画 if(newTopMargin>=0 && mHeaderState!=RELEASE_TO_REFRESH){ mHeaderText.setText("释放完成刷新"); mHeaderImage.clearAnimation(); mHeaderImage.startAnimation(mFlipAnimation); mHeaderState=RELEASE_TO_REFRESH; }else if(newTopMargin<0 && mHeaderState!=PULL_TO_REFRESH){ mHeaderText.setText("下拉刷新"); mHeaderImage.clearAnimation(); mHeaderImage.startAnimation(mReverseFlipAnimation); mHeaderState=PULL_TO_REFRESH; } } public void footerPrepareToRefresh(int delta){ int newTopMargin=changingHeaderViewTopMargin(delta); //根据newTopMargin的值判断是否播放动画 if(Math.abs(newTopMargin)>=(mHeaderHeight+mFooterHeight) && mFooterState!=RELEASE_TO_REFRESH){ mFooterText.setText("释放完成刷新"); mFooterState=RELEASE_TO_REFRESH; } else if(Math.abs(newTopMargin)<(mHeaderHeight+mFooterHeight) && mFooterState!=PULL_TO_REFRESH){ mFooterText.setText("上拉刷新"); mFooterState=PULL_TO_REFRESH; } }</span>
当滑动释放时,会分发ACTION_UP到ViewGroup的onTouchEvent中,此时我们就要进入更新状态了。
<span style="font-size:14px;">@Override public boolean onTouchEvent(MotionEvent event){ int y=event.getRawY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //在onInterceptTouchEvent中已经记录 break; case MotionEvent.ACTION_MOVE: .... break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: //首先要通过TopMargin来查看HeaderView或FooterView是否显示完全了,否则不刷新 int topMargin=mHeaderView.getTopMargin(); if(topMargin>0 && mPullState==PULL_DOWN_STATE){ onHeaderRefreshing(); } else if(topMargin<-mHeaderHeight-mFooterHeight && mPullState==PULL_UP_STATE){ onFooterRefreshing(); }else{ mHeaderView.setTopMargin(-mHeaderHeight);//不刷新 } break; } }</span>
在onHeaderRefreshing/onFooterRefreshing中要完成显示加载进度,然后调用接口中的函数去执行一些耗时任务。
<span style="font-size:14px;">public void onHeaderRefreshing(){ mHeaderState=REFRESHING; setHeaderTopMargin(0); mHeaderImage.setVisibility(View.GONE); mHeaderImage.clearAnimation(); mHeaderProgress.setVisibility(View.VISIBLE); mHeaderText.setText("正在刷新..."); if(mOnHeaderViewListener!=null){ mOnHeaderViewListener.onHeaderRefreshing(this); } } public void onFooterRefreshing(){ mFooterState=REFRESHING; setHeaderTopMargin(-mHeaderHeight-mFooterHeight); mFooterImage.setVisibility(View.GONE); mFooterImage.clearAnimation(); mFooterProgress.setVisibility(View.VISIBLE); mFooterText.setText("正在加载中..."); if(mOnFooterViewListener!=null){ mOnFooterViewListener.onFooterRefreshing(this); } }</span>
模块三:接口设计
我们的下拉刷新控件是观察者模式中的主题,当在下拉和上拉释放时会通知观察者。观察者会进行一些耗时处理,然后回调主题,通知其完成刷新。
1.创建接口类(该接口类可以作为控件的内部类)
<span style="font-size:14px;">interface OnHeaderViewListener{ public void onHeaderRefreshing(MyPullToRefreshView v); } interface OnFooterViewListener{ public void onFooterRefreshing(MyPullToRefreshView v); }</span>
2.在控件内声明用于外界添加监听的函数
<span style="font-size:14px;"> //首先创建保存监听器(观察者)的变量 private OnHeaderViewListener mOnHeaderViewListener; private OnFooterViewListener mOnFooterViewListener; //外界可通过该函数来添加观察者 public void setOnHeaderViewListenr(OnHeaderViewListener listener){ mOnHeaderViewListener=listener; } public void setOnFooterViewListener(OnFooterViewListener listener){ mOnFooterViewListener=listener; }</span>
3.控件在刷新时通知外界执行一些耗时任务
<span style="font-size:14px;">if(mOnHeaderViewListener!=null){ mOnHeaderViewListener.onHeaderRefreshing(this);//将事件源封装在参数中,传给外界,让外界完成耗时任务后回调MyPullToRefreshView.onRefreshComplete结束刷新。 } if(mOnFooterViewListener!=null){ mOnFooterViewListener.OnFooterViewListener(this); }</span>
当耗时任务完成后,需要回调MyPullToRefreshView的onHeaderRefreshComplete/onFooterRefreshComplete函数来完成刷新。
<span style="font-size:14px;">public void onHeaderRefreshComplete() { // 完成刷新 setHeaderTopMargin(-mHeaderViewHeight); mHeaderImage.setVisibility(View.VISIBLE); mHeaderImage.setImageResource(R.drawable.ic_pulltorefresh_arrow); mHeaderText.setText(R.string.pull_to_refresh_pull_label); mHeaderProgressBar.setVisibility(View.GONE); mHeaderState = PULL_TO_REFRESH; if (mScrollView != null) { mScrollView.smoothScrollTo(0, 0); } } public void onFooterRefreshComplete() { setHeaderTopMargin(-mHeaderViewHeight); mFooterImage.setVisibility(View.VISIBLE); mFooterImage.setImageResource(R.drawable.icon_host_pull); mFooterText.setText(R.string.pull_to_refresh_footer_pull_label); mFooterProgressBar.setVisibility(View.GONE); mFooterState = PULL_TO_REFRESH; }</span>
最后来看一下在Activity中的使用。
<span style="font-size:14px;">public class MainActivity extends Activity implements OnFooterViewListener,OnHeaderViewListener { private MyPullToRefreshView mPullToRefreshView; private ListView mListView; private ArrayAdapter<String> arrayAdapter; private String[] strs={"1111","2222","3333"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } public void init(){ mPullToRefreshView=(MyPullToRefreshView)findViewById(R.id.my_pull_to_refresh_view); mListView=(ListView)findViewById(R.id.listView); arrayAdapter=new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,strs); mListView.setAdapter(arrayAdapter); mPullToRefreshView.setOnHeaderViewListener(this); mPullToRefreshView.setOnFooterViewListener(this); } @Override public void onFooterRefreshing(final MyPullToRefreshView v) { //向UI的消息队列中投递一个runnable,投递成功返回true,并不代表runnable成功执行,looper可能退出导致runnable被丢弃 v.postDelayed(new Runnable(){ @Override public void run() { try { Thread.sleep(3000); v.onFooterRefreshComplete(); } catch (InterruptedException e) { e.printStackTrace(); } } }, 0); } @Override public void onHeaderRefreshing(final MyPullToRefreshView v) { v.postDelayed(new Runnable(){ @Override public void run(){ try { Thread.sleep(3000); v.onHeaderRefreshComplete(); } catch (InterruptedException e) { e.printStackTrace(); } } },0); } }</span>