本章属于第三种自定义控件,继承已有控件,扩展其功能。
注意:
1.ListView的addHeaderView(view)/addFooterView(view)需要在ListView的setAdapter之前执行。
2.在onTouchEvent中,如果返回值为true,说明当前事件被消费,返回值false,说明不消费该事件。
步骤:
1.自定义RefreshListView继承ListView,重写其构造方法。在构造方法中initView()
2.给ListView添加头布局,并对头布局做相应的处理,根据下拉的状态不同而更改头布局的UI
1)使用:addHeaderView(view);
2)根据更改HeaderView的paddingTop为-height隐藏头布局。
mHeaderView = View.inflate(getContext(), R.layout.layout_header_view,null);
mHeaderView.measure(0,0);//传入0,表示按照Xml中设置的宽高进行测量
mHeaderViewHeight =mHeaderView.getMeasuredHeight();
mHeaderView.setPadding(0,-mHeaderViewHeight ,0,0);
addHeaderView(mHeaderView);
获取头布局的高度需要注意:
此时是无法获取到头布局的高度的,因为一进入页面在oncreate方法中findviewById找到控件,此时,自定义控件的构造函数就已经调用,initView方法即调用,而自定义控件的渲染是在onCreate()方法之后。此时使用mHeaderView.getMeasuredHeight()或者mHeaderView.getHeight()方法拿到的高度值为0.
解决方法:在获取高度之前手动测量一下控件的宽高。
mHeaderView.measure(0,0);//传入0,表示按照Xml中设置的宽高进行测量,(父View已经测量过子View之后,填0是指按照XML中设置的宽高进行测量,一般是传childView.getLayoutParams.width来进行测量)
测量之后,使用mHeaderView.getMeasuredHeight()可以取得高度值。(getHeight是获得mHeaderView真实显示在界面上的高度)
3)下拉时,通过不停的更改-paddingTop值来使头布局慢慢显示出来。
a.监听ListView的触摸事件。重写onToucheEvent(),(不能删除return的super.onTouchEvent(ev),源码中ListView做了很多的处理,如果删除,则ListView无法滑动。)判断滑动距离来判断头布局的偏移量。
b.两种情况不会显示头布局,第一种是disY<0说明屏幕在向上滑动,第二种,第一条可见的条目position不为0,此时不需要显示头布局,那么就不需要设置padding值。
case MotionEvent.ACTION_DOWN:
//按下时获取按下Y坐标
downY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
moveY = ev.getY();
disY =moveY -downY;
if(disY >0 && getFirstVisiblePosition() ==0){
mHeaderView.setPadding(0,(int)(-mHeaderViewHeight+disY),0,0);
}
break;
c.下拉结束后(即头布局完全显示),需要将箭头更改为向上,并且更改文字下拉刷新为松开刷新。当paddingTop >= 0时,更新UI。
定义几个int常量,记录头布局的状态:if paddingTop > = 0,说明头布局完全显示,那么此时头布局的状态应该为松开刷新,状态记为1:REFLEASE_REFRESH
如果paddingTop<0,说明头布局未全部显示,此时头布局为下拉刷新,状态记为0.还有一种状态是正在刷新,状态记为0:PULL_TO_REFRESH
正在刷新:REFRESHING = 2;
代码方面,为了避免时时检测paddingTop(因为手指在屏幕上每一次微小的移动都会调用ACTION_MOVE这个状态下的代码,添加判断,只会在状态改变时进入执行if中更改动画、文字等代码),节约性能,可以把更改文字和动画的动作之前添加一个判断
if(paddingTop>=0&¤tState != REFLEASE_REFRESH){
.....//此处为更改动画和文字的代码:松开刷新状态,当前状态不为松开刷新状态时,才会进入这里
}else if(paddingTop < 0 && currentSate != PULL_TO_REFRESH){
//同上,当前状态不为下来刷新状态时,才会进来这里,如果状态已经是下来刷新状态,即使paddingTop<0,也不会进来这里。}
此处应该注意的是,如果在MOVE中添加了自己的事件,在MOVE中break前,添加return true;表示当前事件被我们处理并消费。
d.手指松开时的监听处理:
1.当手指松开时,头布局未完全显示,即paddingTop<0,还没有转化成松开刷新状态,即当前状态为下拉刷新PULL_TO_REFRESH,此时松开ListView应该是弹回去,即头布局隐藏,把头布局的paddingTop设置为-measureHeight即可。
2.当手机松开时,头布局已经完全显示,即paddingTop>=0,已经转化成松开刷新状态,即当前状态是REALEASE_REFRESH,此时松开,头布局paddingTop设置为0,并且上面的文字改变为正在刷新,iv隐藏(隐藏之前要清除动画,否则无法隐藏),pb显示。
3.如果状态为正在刷新中,控制用户不能拖拽,在ACTION_MOVE事件中,添加判断,如果是正在刷新,则执行父类对touch事件的处理。
if(current == REFRESHING){
//正在刷新时,不能往下拖拽,执行父类的touch事件的处理方式,当头布局显示完全时,不能拖拽
return super.onTouchEvent(ev);
}
效果图:
3.监听回调,当头布局状态为正在刷新时,需要告知外界,这时我正在下拉刷新,外界需要调用相关方法进行下拉刷新。即观察者模式。
a.在RefreshListView中定义一个接口OnRefreshListener,接口中添加方法onRefresh();
public interface OnRefreshListener{
void onRefresh();
}
b.在RefreshListView中添加方法setOnRefreshListener(OnRefreshListener listener),便于外界使用该接口,
public void setOnRefreshListener(OnRefreshListener listener){
this.listener = listener;
}
外界使用方法:此处在MAinActivity中
refreshListView.setOnRefreshListener(new OnRreshListener(){
onRefresh(){
//当RefreshListView的状态为正在刷新时,这个地方的方法会被调用
}});
c.在自定义View中,在适当的位置调用onRefresh()方法,比如在这个案例中,当用户手指抬起并且状态为正在刷新时,调用该方法,在自定义View中调用onRefresh(),实际上外界的(这里是MainActivity中的onRefresh被调用),此时可以把自定义View中的某些数据作为参数传递到界面上。
d.下拉刷新:一般情况 ,下拉都是重新加载一遍数据。在这里模拟加载一条数据,首先添加到list中,再通知adapter更新数据即可。刷新完成之后需要通知RefreshListView,把头布局收起来,因此在RefreshView中定义一个方法,completedRefresh(),在方法中更改当前状态,隐藏头布局,更新UI。
4.添加脚布局,上拉加载更多。
a.添加脚布局并隐藏
mFooterView = View.inflate(getContext(), R.layout.layout_foot_view, null);
mFooterView.measure(0,0);
mFooterViewHeight =mFooterView.getMeasuredHeight();
mFooterView.setPadding(0,-mFooterViewHeight,0,0);
addFooterView(mFooterView);
b.添加onScrollListener,判断滑动状态,如果滑动状态为空闲状态,并且滑动到最后一个条目时,显示脚布局,跳到脚布局。
onScrollListener中的两个方法:onScrollStateChanged,当滑动状态改变时调用,滑动状态有三种分别是:
1.SCROLL_STATE_IDLE = 0 空闲状态,源码中的解释为:
The view is not scrolling. Note navigating the list using the trackball counts as being in the idle state since these transitions are not animated.
2.SCROLL_STATE_TOUCH_SCROLL = 1 用户在进行触摸滚动,源码中的解释为:
The user is scrolling using touch, and their finger is still on the screen
3.SCROLL_STATE_FLING = 2 滑翔状态 源码中的解释为:
The user had previously been scrolling using touch and had performed a fling. The animation is now coasting to a stop
用户滚动内容时,滚动状态变化的顺序为:0 --> 1--> 2 --> 0,即空闲-->用户开始滑动屏幕-->用户手指离开屏幕但屏幕仍在滑动-->滑翔结束回到空闲状态
此时需要在滚动状态重新回到空闲时判断是否滚动到最后一条,滚动到最后一条即显示脚布局:但是要注意,如果此时已经正在加载,用户往上拉的时候仍然会执行这几行代码,再一次进行加载更多的操作,为了避免这种情况发生,可以添加一个boolean类型的变量,进行标记和判断。
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
//当滚动状态改变时调用,当用户滑到最后一个并且滚动状态为空闲,getCount()得到的是adapter中的list中数据的总条数
if(scrollState == SCROLL_STATE_IDLE && getLastVisiblePosition() == getCount() - 1&&!isLoadingMore){
isLoadingMore= true;//标记为true,说明正在加载。
//说明滚到最后一条,显示脚布局
mFooterView.setPadding(0,0,0,0);
setSelection(getCount());//显示最后一条
}
c.接口回调
1.在OnRefreshListener中添加方法,onLoadMore(),用于加载更多数据。
2.在RefreshListView中的脚布局出现时调用onLoadMore(),与下拉刷新相同,实际上onLoadMore()方法是在界面中使用接口时被调用。
在界面中进行加载更多的处理。
3.加载完成同样调用completedRefresh()方法,在方法中处理。判断是下拉刷新还是上拉加载更多。
扩展内容:
1.自定义ProgressBar:
xml中添加该属性:indeterminateDrawable 无限循环的drawable
该属性的值为shape。
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
>
<!--旋转动画中可以包含shapeandroid:pivotX="50%" android:pivotY="50%" 相对于自己的中心位置android:pivotX="50%p" android:pivotY="50%p" 相对于父控件的中心位置-->
android:shape="ring"
android:innerRadius="@dimen/dp_20"
android:thickness="@dimen/dp_5"
android:useLevel="true"
android:innerRadiusRatio="2.5"
android:thicknessRatio="10"
>
android:centerColor="#33E93751"
android:endColor="#00000000"
android:type="sweep"/>
<!--
内半径innerRadius
厚度thickness
内圆半径比 innerRadiusRatio="2.5" 内圆半径与容器宽高比
圆环厚度比 thicknessRatio="10" 圆环厚度与容器宽高比
-->
2.旋转动画
头布局中箭头的旋转动画
//向下翻转动画
public static void RotateDown(View view){
RotateAnimation animation = new RotateAnimation(
180f,0f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(500);
animation.setFillAfter(true);
view.startAnimation(animation);
}
//向上翻转动画
public static void RotateUp(View view){
RotateAnimation animation = new RotateAnimation(
0f, 180f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
animation.setDuration(500);
animation.setFillAfter(true);
view.startAnimation(animation);
}