二.自定义View之ListView下拉刷新,添加头脚布局,观察者模式

本章属于第三种自定义控件,继承已有控件,扩展其功能。


注意:

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无法滑动。)判断滑动距离来判断头布局的偏移量。


7753368-f1425e2ece31d681.png
分析

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&&currentState != 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);

}

效果图:
7753368-a89fe8edaf5ada21.gif
下拉刷新效果图



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中的某些数据作为参数传递到界面上。


7753368-c481a6be23405920.png

d.下拉刷新:一般情况 ,下拉都是重新加载一遍数据。在这里模拟加载一条数据,首先添加到list中,再通知adapter更新数据即可。刷新完成之后需要通知RefreshListView,把头布局收起来,因此在RefreshView中定义一个方法,completedRefresh(),在方法中更改当前状态,隐藏头布局,更新UI。


7753368-a3de46eb51134c19.png
执行下拉刷新


7753368-f8a857fb50f5a931.png
刷新完成之后更改布局

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(),用于加载更多数据。


7753368-864aaf4813965da9.png
添加onLoadMore方法

2.在RefreshListView中的脚布局出现时调用onLoadMore(),与下拉刷新相同,实际上onLoadMore()方法是在界面中使用接口时被调用。

7753368-1954ced41a6eb72d.png

在界面中进行加载更多的处理。


7753368-7078ca80655cf71a.png

3.加载完成同样调用completedRefresh()方法,在方法中处理。判断是下拉刷新还是上拉加载更多。


7753368-c94f9cd2057c3b38.png


7753368-20ce4d819a751889.gif
完成效果图



扩展内容:

1.自定义ProgressBar:

xml中添加该属性:indeterminateDrawable 无限循环的drawable

该属性的值为shape。


<rotate xmlns:android="http://schemas.android.com/apk/res/android"

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" 相对于父控件的中心位置-->

<shape

        android:shape="ring"

        android:innerRadius="@dimen/dp_20"

        android:thickness="@dimen/dp_5"

        android:useLevel="true"

        android:innerRadiusRatio="2.5"

        android:thicknessRatio="10"

      >

<gradient android:startColor="#88E93751"

            android:centerColor="#33E93751"

            android:endColor="#00000000"

            android:type="sweep"/>

   <!--在颜f色值前面添加00-f 00纯透明,ff不透明

         sweep:扫描-->

<!--

          内半径innerRadius

          厚度thickness

          内圆半径比 innerRadiusRatio="2.5" 内圆半径与容器宽高比

          圆环厚度比 thicknessRatio="10" 圆环厚度与容器宽高比

          -->

</shape>

</rotate>

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);

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值