PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)

前言:当你感到不舒服的时候就是成长的时候。入职阿里时学长跟我说的这句话,一直记得。到死时,人们往往不会因为自己做过什么而后悔,而常常会因为没做什么而后悔。趁你还有激情,加油!

相关文章:

1、《PullScrollView详解(一)——自定义控件属性》
2、《PullScrollView详解(二)——Animation、Layout与下拉回弹》
3、《PullScrollView详解(三)——PullScrollView实现》
4、《PullScrollView详解(四)——完全使用listview实现下拉回弹(方法一)》
5、《PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)》
6、《PullScrollView详解(六)——延伸拓展(listview中getScrollY()一直等于0、ScrollView中的overScrollBy)》

言归正转,这篇就是终极篇了,一般来讲,终极篇总是最有难度的,已经折磨大家四篇文章,这篇文章也终于千呼万唤始出来了。在listview中实现下拉回弹是比较有难度的,因为在listview中有关scroll各方面的获取都需要自己来做,所以整体难度还是比较大。废话不多说,现在开整吧。
先来看看效果图:(与PullScrollView的效果是一样一样的)


一、搭框架

同前几篇一样,先出框架。这篇将是前几篇的集大成者,一些细小的部分,就不再细讲了,如果有疑问,我会在后面列出每个忽略未细讲的知识点所在的博客位置,大家可以先移步过去看看。
在这部分,我们要完成下面这个效果:

PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)_第1张图片

1、创建PullScrollListView 派生自ListView

public class PullScrollListView extends ListView {
    //用户定义的手指可移动的最大高度,在这里,手指移动距离是content移动距离的两倍,是header移动距离的四倍
    private int mContentMaxMoveHeight = 0;

    public PullScrollListView(Context context) {
        super(context);
    }

    public PullScrollListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public PullScrollListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        if (null != attrs) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView);
            if (ta != null) {
                mContentMaxMoveHeight = (int) ta.getDimension(R.styleable.PullScrollView_maxMoveHeight, -1);
                ta.recycle();

            }
        }
    }
}
这里可能难理解的部分,就是init()函数里的部分,这是通过declare-styleable来自定义控件属性的知识,在 《PullScrollView详解(一)——自定义控件属性》  中有详细讲述。

2、main.xml 主布局

这个布局每篇文章都会讲一遍,没什么难度

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:app="http://schemas.android.com/apk/res-auto"
             android:layout_width="fill_parent"
             android:layout_height="fill_parent">

    <ImageView
            android:id="@+id/background_img"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:layout_marginTop="-100dp"
            android:scaleType="fitXY"
            android:src="@drawable/pic3"/>

    <com.harvic.PullScrollListViewDemo.PullScrollListView
            android:id="@+id/pull_list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:divider="@null"
            android:dividerPadding="0dp"
            android:dividerHeight="0dp"
            app:maxMoveHeight="200dp"
            app:headerTopHeight = "100dp"
            app:headerVisibleHeight="150dp"/>
</FrameLayout>
这里可能难理解的部分也就app:XXXX这些自定义属性了,在上面的博客中都有的。
同样,注意ImageView要定义android:layout_marginTop="-100dp",即初始向上移100px,以便向下移动。

3、填充布局

(1)、Item布局(item_layout.xml)

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
          android:id="@android:id/text1"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:gravity="center_vertical"
          android:minHeight="40dp"
          android:background="#ffffff"/>
(2)、MainActivity填充数据
有关构造数据,填充数据的部分也不再细讲了,每篇文章都会讲,这是listview构造的基础知识,大家应该能看明白。

public class MainActivity extends Activity {
    private String[] mStrings = {"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
            "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre",
            "Allgauer Emmentaler", "Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
            "Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale", "Aisy Cendre",
            "Allgauer Emmentaler"};
    private LinkedList<String> mListItems;
    private PullScrollListView mListView;
    private ArrayAdapter<String> mAdapter;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mListView = (PullScrollListView) findViewById(R.id.pull_list_view);
        mListItems = new LinkedList<String>();
        mListItems.addAll(Arrays.asList(mStrings));
        mAdapter = new ArrayAdapter<String>(this,R.layout.item_layout, mListItems);

        mListView.setAdapter(mAdapter);
    }
}
到现在,我们数据填充已经完成了,但如何将底部的小狗图片显示出来呢?在上篇,我们讲过,不能用padding和marginTop,只能通过给ListView添加透明Header来实现。

4、添加Header

(1)、headerview的布局(headerview.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="300dp"
              android:clickable="false">
    <!--如果要让headview不可点击,在这里设置clickable="false"是没用的,只有通过ListView.addHeaderView(view,null,false);来设置-->

</LinearLayout>
(2)、在MainActivity中添加headerview
一定要注意的是,要通过listview的addHeaderView(View v, Object data, boolean isSelectable)函数来添加不可点击的headview,如果直接使用addHeaderView(View v)来添加,默认是可以点击的,当用户点击时,会变白。代码如下:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mListView = (PullScrollListView) findViewById(R.id.pull_list_view);
    mListItems = new LinkedList<String>();
    mListItems.addAll(Arrays.asList(mStrings));
    mAdapter = new ArrayAdapter<String>(this,R.layout.item_layout, mListItems);

    LayoutInflater inflater = getLayoutInflater();
    View view = inflater.inflate(R.layout.headerview,mListView,false);
    //设置headerview不可点击
    mListView.addHeaderView(view,null,false);

    mListView.setAdapter(mAdapter);
}
这样,整个架构就出来了
**源码在文章底部给出**

二、下拉回弹

1、添加topView

与前几篇一样,我们首先要讲小狗的图片对应的ImageView传给PullScrollListView,在它下拉的时候跟着一起下拉,在松开的时候,一起回弹。
PullScrollListView.java中:

//底部图片View
private View mTopView;
public void setmTopView(View view) {
    mTopView = view;
}

在MainActivity.java中:将图片ImageView设置进去

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mListView = (PullScrollListView) findViewById(R.id.pull_list_view);     
    ………………
    ImageView headerView = (ImageView)findViewById(R.id.background_img);
    mListView.setmTopView(headerView);
}

2、点击时保存初始化位置

我们这里尽量保持 《PullScrollView详解(三)——PullScrollView实现》   的逻辑,在用户点击时做变量的初始化操作,初始化操作包括
topView位置的初始化:用于在反弹时,回到初始化位置
contentView位置的初始化:同样用于在反弹时,回到初始化位置,但这里的contentVIew是listview本身,所以就是this变量。下面还会再讲。
点击位置的初始化:点击位置的初始化主要是用来计算当前用户移动了多少的。
这里还有其它变量的初始化,先列出完整代码,下面再细讲。

//底部View
private View mContentView = this;
//初始点击位置
private Point mTouchPoint = new Point();
//头部图片的初始化位置
private Rect mHeadInitRect = new Rect();
//ScrollView的contentView的初始化位置
private Rect mContentInitRect = new Rect();
//标识当前view是否移动
boolean mIsMoving = false;
//是否关闭ListView的滑动.
private boolean mEnableTouch = false;

public boolean onInterceptTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        //保存原始位置
        mTouchPoint.set((int) event.getX(), (int) event.getY());
        mHeadInitRect.set(mTopView.getLeft(), mTopView.getTop(), mTopView.getRight(), mTopView.getBottom());
        mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
        mIsMoving = false;
        mEnableTouch = false;
    }
    return super.onInterceptTouchEvent(event);
}
这里与博客三最大的不同在于,没有拦截ACTION_MOVE事件!为什么呢?因为listview本身就是一个控件,内部不会再有其它控件,顶部是不会有其它控件在OnTouchEvent中消费ACTION_MOVE事件的。所以不必拦截。因为在拦截之后,有关的ACTION_MOVE事件就不会再向上传递的,比如我们的HeaderView如果是一个viewPager呢?那它永远也不会滑动了,除非我们做到精确判断来拦截,即确定是在顶部下拉时才进行拦截,其它时候不拦截ACTION_MOVE消息。这样,只有在顶部下拉时才会被我们拦截起来,其它的滑动事件,比如左右滑,是会继续传递给子控件的。
这里就不再进行拦截,因为没有拦截的意义。在PullScrollView中对ScrollView的ACTION_MOVE事件进行了拦截,是因为ScrollView本身还是有子控件的。有可能在OnTouchEvent中把ACTION_MOVE事件消费掉而传不到ScrollView里。
(1)、变量定义

在定义的变量里,其实没什么难度,与博客三不相同的一点是:

private View mContentView = this;
mContentView的意义表示,向下拉动的内容部分,这里对应的是整个的listview,所以这里直接用this给它赋值。而不是像博客三一样从外部赋值。
(2)、初始化

上面也都说了,各个初始化的意义

mTouchPoint.set((int) event.getX(), (int) event.getY());
mHeadInitRect.set(mTopView.getLeft(), mTopView.getTop(), mTopView.getRight(), mTopView.getBottom());
mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom());
这三个变量mTouchPoint、mHeadInitRect、mContentInitRect分别表示手指点击位置、topView的初始化位置和listview的初始化位置。
另外还有两个变量:
mIsMoving = false;
mEnableTouch = false;
这两个变量现在讲出来,如果只看这篇文章可能有点难理解,有关他们的详细说明已经在 《PullScrollView详解(三)——PullScrollView实现》  详细说明,大家可以先看看这篇博客。这里我简单的说一下他们的意义,mIsMoving是用来标识当前View是不是正在向下移动,用来在ACTION_UP时,回弹View.如果不标识会怎样,那就会在ACTION_UP事件到来时,都会弹一下。比如当前你是在向上滚动,而不是在下拉,在你放开手指时,是不应该回弹的,这里给你回弹一下,你还能受得了,所以这个变量就是用来标识当手指放开时,要不要回弹的。
mEnableTouch这个变量的解释就有点难度了,它是用来标识是否禁止控件自身的移动的。这里估计也说不清楚,大家先知道就好,下面用到时会再讲。

3、获取listview滚动高度

在讲解下拉之前,我们得先考虑一个问题,即如何判断什么时候该下拉了,在PullScrollView中我们可以通过getScrollY()==0来判断当前View是不是在顶部,如果在顶部而且又是在下拉,那么就可以下拉滚动了。
那在ListView中要怎么判断当前是不是到顶部了呢?大家可以尝试在listview中,getScrollY()始终等于0. 有关《延伸:为什么PullScrollView中getScrollY()有值而ListView中的getScrollY()却一直为零》的原因,可以参考在下一篇的延伸部分。
我这里就直接讲在listview中要怎么获取滚动高度的方法。先看看下面的这幅图:
在这幅图中,listview的item分别是两个高度不等的headview和各个item。
在图片下面被黄色框框起来的部分,代表屏幕。那黄色框以上的部分,就表示已经滚出去的部分,那么它的高度就是我们要计算的scrollHeight;
所以就这个图片当前状态而言
scrollHeight = 所有的header高度 + 已经滚动过去的所有Item的高度 + 当前可见item的滚动部分。

PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)_第2张图片

在正式开始之前先给大家讲ListView的两个函数:

//获取当前可见item的索引
public int getFirstVisiblePosition()
//获取指定位置的item的视图
public View getChildAt(int index)

getFirstVisiblePosition()

这个函数用于获取当前可见item的索引,索引是从第一个headerview开始算的,第一个headerview索引是0,向下逐个加1.需要注意的是,只要当前item还能看见一丢丢,就算可见哦。比如,我上面的图中,利用getFirstVisiblePosition()得到的值会是3.即第四个item,虽然它向上滚动了一半,但它仍是可见的,所以getFirstVisiblePosition()获取到的是第四个item.

getChildAt(int index)

这个函数需要非常注意,它的index索引是从当前可见的位置开始的。当前第一个可见的Item的索引是0!!!!!不是从第一个headerview开始的!!!!就上面那个图而言,由于当前可见的item是第四个item.这时候调用getChildAt(0)获得是第四个item的View!!!!一定要非常注意。
但正是这个特性正好,我们可以使用getChildAt(0)来获得当前第一个可见item的视图,来得到它滚动的距离。

好了,介绍完计算方法和这两个函数,下面我们就开始看通过代码怎么计算滚动高度吧。

(1)、重写addHeaderView(View v)

因为我们会计算headview的高度,那我们需要得到所有headview的View。我们可以在重写ListView的addHeaderView(View v)方法,在用户利用addHeaderView(View v)添加headview时,我们顺便将它加入到我们的mHeadViews数组中。

private ArrayList<View> mHeadViews = new ArrayList<View>();

@Override
public void addHeaderView(View v) {
    super.addHeaderView(v);
    if (v != null) {
        mHeadViews.add(v);
    }
}

@Override
public void addHeaderView(View v, Object data, boolean isSelectable) {
    super.addHeaderView(v, data, isSelectable);
    if (v != null) {
        mHeadViews.add(v);
    }
}
在获得滚动高度时,因为每个headerview的高度一般都不一样,所以我们要分两种情况:第一种,没有完全滚出所有headview。第二种:完全滚出所有Headview
(2)没有完全滚出所有headview

在没有完全滚出所有Headview时,滚动高度就是已经滚动过去的所有headview的高度和加上当前可见的headview滚动过去的高度。代码如下:下面会细讲

public int getScrollHeight(ListView list, ArrayList<? extends View> headviews) {
    if (list == null || headviews == null){
        return -1;
    }

    int headCount = list.getHeaderViewsCount();
    int firstVisiblePos = list.getFirstVisiblePosition();
    int scrollHeight = 0;
    if (firstVisiblePos < headCount) {
        //如果还在header内部,说明只需要逐个计算header的高度就好了。
        if (headviews.size() == 0) {
            new Exception("内部含有headerView,请在函数入口处设置headview list");
            return -1;
        }
        for (int i = 0; i <= firstVisiblePos; i++) {
            View view = headviews.get(i);
            if (view != null && i == firstVisiblePos) {
                //注意,getTop()是负值,因为已经滚到不可见区域去了
                scrollHeight += (-view.getTop());
            } else if (i != firstVisiblePos) {
                scrollHeight += view.getHeight();
            }
        }
    } else {
	    //完全滚出headview
		…………
    }
    return scrollHeight;
}
这段代码分为两部分:
int headCount = list.getHeaderViewsCount();
int firstVisiblePos = list.getFirstVisiblePosition();
int scrollHeight = 0;
if (firstVisiblePos < headCount) {
//没有完全滚出Headview
}else{
//完全滚出Headview
}
在这段代码中,首先根据list.getHeaderViewsCount()获取到当前Headview的数量,其实使用headviews.size()也是一样的。然后利用list.getFirstVisiblePosition()得到当前可见Item的索引。然后判断当前可见的item是不是已经把headview全都给滚过去了。如果firstVisiblePos < headCount,即还没有完全滚过去,就开始if里的代码计算。这里要注意的是headCount是所有Headview的个数,而firstVisiblePos的值是从0开始的,所以要用小于号。当firstVisiblePos 等于 headCount的值的时候,其实已经过了Headview在第一个item项里了。
下面是计算滚动高度的代码了。
for (int i = 0; i <= firstVisiblePos; i++) {
    View view = headviews.get(i);
    if (view != null && i == firstVisiblePos) {
        //注意,getTop()是负值,因为已经滚到不可见区域去了
        scrollHeight += (-view.getTop());
    } else if (i != firstVisiblePos) {
        scrollHeight += view.getHeight();
    }
}
在上面的代码中,首先将所有已经滚过去的Headview的高度相加:
if (i != firstVisiblePos) {
   scrollHeight += view.getHeight();
}
当到当前可见的Item时,直接利用当前view.getTop()方法获取当前的顶点位置。它的位置在屏幕原点的位置是(0,0),现在在不可见区域,所以它的top值的绝对值就是当前已经滚出去的高度。其实这里说屏幕原点是不准确的,它的位置坐标系以是所在父控件的左上角为坐标系原点的。因为父控件是充满整个屏幕的,当然它的(0,0)原点就在屏幕左上角。
if (view != null && i == firstVisiblePos) {
   //注意,getTop()是负值,因为已经滚到不可见区域去了
   scrollHeight += (-view.getTop());
}
到这里就得到在没有滚出headview区域时的scrollHeight值了。
(3)、完全滚出所有headview

下面就来看看当滚出所有Headview时的代码计算方法:

public int getScrollHeight(ListView list, ArrayList<? extends View> headviews) {
   …………
   if (firstVisiblePos < headCount) {
       //如果还在header内部,说明只需要逐个计算header的高度就好了。
      
   } else {
       //先得到所有headview的高度和
       if (headviews != null) {
           for (View view : headviews) {
               scrollHeight += view.getHeight();
           }
       }
       //获取单个item的视图
       View itemView = list.getChildAt(0);
       if (itemView != null) {
           //这里计算的是从headview到当前可见的item之间已经被完全滚过去的item的总高度
           scrollHeight += (firstVisiblePos - headCount) * itemView.getHeight();
       }
       //最后加上当前可见的item,已经滚动的部分
       scrollHeight += (-itemView.getTop());
   }
   return scrollHeight;
}
这里分为三部分:
首先,得到所有Headview的高度和:
if (headviews != null) {
   for (View view : headviews) {
       scrollHeight += view.getHeight();
   }
}
然后得到所有滚过去的item的高度和:
View itemView = list.getChildAt(0);
if (itemView != null) {
    //这里计算的是从headview到当前可见的item之间已经被完全滚过去的item的总高度
    scrollHeight += (firstVisiblePos - headCount) * itemView.getHeight();
}
最后,再加上当前可见的item滚动过去的高度:
scrollHeight += (-itemView.getTop());
所以完整的获取滚动高度的代码如下:
public int getScrollHeight(ListView list, ArrayList<? extends View> headviews) {
    if (list == null || headviews == null){
        return -1;
    }
    //!!!注意!!!
    //这里使用list.getHeaderViewsCount();获取到的headview数量
    int headCount = list.getHeaderViewsCount();
    int firstVisiblePos = list.getFirstVisiblePosition();
    int scrollHeight = 0;
    if (firstVisiblePos < headCount) {
        //如果还在header内部,说明只需要逐个计算header的高度就好了。
        if (headviews.size() == 0) {
            new Exception("内部含有headerView,请在函数入口处设置headview list");
            return -1;
        }
        for (int i = 0; i <= firstVisiblePos; i++) {
            View view = headviews.get(i);
            if (view != null && i == firstVisiblePos) {
                //注意,getTop()是负值,因为已经滚到不可见区域去了
                scrollHeight += (-view.getTop());
            } else if (i != firstVisiblePos) {
                scrollHeight += view.getHeight();
            }
        }
    } else {
        //这里假设除了headview以外的所有的正常ListItem的高度都是一样的。如果你的不一样,需要改写这一部分的计算方式了
        //已经滚出所以headView,只需要将所以headview高度相加,然后再加上其它所有list的高度即可
        if (headviews != null) {
            for (View view : headviews) {
                scrollHeight += view.getHeight();
            }
        }
        //获取单个item的视图
        View itemView = list.getChildAt(0);
        //值得非常注意的是firstVisiblePos是从0开始算的,所以headCount正好对应listview的第一个item的索引
        if (itemView != null) {
            //这里计算的是从headview到当前可见的item之间已经被完全滚过去的item的总高度
            scrollHeight += (firstVisiblePos - headCount) * itemView.getHeight();
        }
        //最后加上当前可见的item,已经滚动的部分
        scrollHeight += (-itemView.getTop());
    }
    return scrollHeight;
}

4、下拉

在费了这么大劲讲解了getScrollHeight()的方法后,下面说说下拉的代码。
在onTouchEvent()中捕捉ACTION_MOVE事件,当满足条件时就使用layout(left,top,right,bottom)函数来移动当前布局。
代码如下,后面会细讲。
@Override
public boolean onTouchEvent(MotionEvent event) {
   switch (event.getAction()) {
       case MotionEvent.ACTION_MOVE: {
           int moveHeight = (int) event.getY() - mTouchPoint.y;
           int scrolledHeight = getScrollHeight(this, mHeadViews);;
           
           if (moveHeight > 0 && scrolledHeight == 0) {
               if (moveHeight > mContentMaxMoveHeight){
                   moveHeight = mContentMaxMoveHeight;
               }

               float headerMoveHeight = moveHeight * 0.5f * SCROLL_RATIO;
               float contentMoveHeight = moveHeight * SCROLL_RATIO;

               mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
               mContentTop = (int) (mContentInitRect.top + contentMoveHeight);

               mTopView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, (int) (mHeadInitRect.bottom + headerMoveHeight));
               mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, (int) (mContentInitRect.bottom + contentMoveHeight));


               mIsMoving = true;
               mEnableTouch = true;
           } else {
               mEnableTouch = false;
           }
       }
       break;
       …………
   return mEnableTouch || super.onTouchEvent(event);
}
这里代码比较好理解,首先是判断当前是不是在顶部滑动
int moveHeight = (int) event.getY() - mTouchPoint.y;
int scrolledHeight = getScrollHeight(this, mHeadViews);;
if (moveHeight > 0 && scrolledHeight == 0) {
}
其中moveHeight是手指的移动距离,只有当手指有移动,而且当前Listview在顶部,即滚动距离为0时,才开始向下拉动。
因为我们设置了最大移动距离,所以当移动距离超过最大移动距离时,就将其始终赋值为最大移动距离。
if (moveHeight > mContentMaxMoveHeight){
    moveHeight = mContentMaxMoveHeight;
}
然后根据手指的移动距离计算出topview和当前listview(也就是这里的contentview)的移动距离。因为我们要让topView(底部的小狗图片)移动的更慢些,所以在它的移动距离上额外乘以0.5.
float headerMoveHeight = moveHeight * 0.5f * SCROLL_RATIO;
float contentMoveHeight = moveHeight * SCROLL_RATIO;

mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
mContentTop = (int) (mContentInitRect.top + contentMoveHeight);
在计算出移动距离后,就是利用layout()函数将TopView和当前的listview(也就是这里的contentview)移动到指定的位置。
mTopView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, (int) (mHeadInitRect.bottom + headerMoveHeight));
mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, (int) (mContentInitRect.bottom + contentMoveHeight));
有关mEnableTouch的变量的作用,我不想再讲一遍了,这里的篇幅已经太长了,在 《 PullScrollView详解(三)——PullScrollView实现》  已经讲过了。大家可以去看看。其实建议大家先看这一篇,然后再回来看这篇,这样会理解的更透彻。

5、回弹

在下拉之后,当ACTION_UP时,要回弹的初始化位置。代码如下:

case MotionEvent.ACTION_UP: {
    //反弹
    if (mIsMoving) {
        mTopView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);
        TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0);
        headAnim.setDuration(200);
        mTopView.startAnimation(headAnim);
        mContentView.layout(mContentInitRect.left, mContentInitRect.top, mContentInitRect.right, mContentInitRect.bottom);

        TranslateAnimation contentAnim = new TranslateAnimation(0, 0, mContentTop - mContentInitRect.top, 0);
        contentAnim.setDuration(200);
        mContentView.startAnimation(contentAnim);
        mIsMoving = false;
    }
    mEnableTouch = false;
}
break;

这部分,涉及到layout()的应用,及与Animation的结合的方法。看起来这只有几行代码,但真正理解出来还是比较有难度的,我在第二篇博客中详细讲解了,大家可以去看《PullScrollView详解(二)——Animation、Layout与下拉回弹》  大家看了之后,这里就不会有什么问题了。就不再细讲了。
好了,到这里所有代码都讲完了

**源码在文章底部给出**

三、延伸拓展

1、ViewHelper.setTranslationY()——完美的滚动方案

我们博客中所有用到滚动的地方都用的layout()函数来实现的,但当布局层级比较复杂的时候,layout()会失效。这里向大家一个能够完美实现滚动的类:ViewHelper;它是nineoldandroids.jar包里的类。

NineOldAndroids的官网: http://nineoldandroids.com
NineOldAndroids源码地址: https://github.com/JakeWharton/NineOldAndroids

这个类能实现有关动画的很多功能,而且出错率很小,我们项目中也一直在用。
这里我们用到下面的函数,意义是将指定View在指定Y轴上移动指定距离。

public static void setTranslationY(View view, float translationY)
需要非常注意的是,这里的float translation的取值的意义。
比如我们先执行ViewHelper.setTranslationY(view,200);然后再执行ViewHelper.setTranslationY(view,0);那它的位置在哪呢?
比如我们原来的位置在A,然后执行setTranslationY(view,200)时View向下移动200像素到B,当执行.setTranslationY(view,0)时View会再回到A点!!!!
这说明,setTranslationY()的坐标原点始终是不会变的!!!!其实从函数名也比较好理解,Translation表示滚动。setTranslationY(view,200)表示沿Y轴向下滚动到200像素的位置。注意是滚动到XXX位置,而不是向下滚动XXX像素,他的意义是scrollTo()而不是scrollBy(),setTranslationY(view,translationY)中的translationY代表的是目的Y坐标!!!!
讲到这,大家应该都理解了,下面我们就将我们下拉和反弹部分替换成ViewHelper.setTranslationY(),代码如下:下面的代码中只是把layout()换成了setTranslationY(),其它都没变,这里就不再讲了。
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_MOVE: {
            int moveHeight = (int) event.getY() - mTouchPoint.y;
            int scrolledHeight = getScrollHeight(this, mHeadViews);
            if (moveHeight > 0 && scrolledHeight == 0) {
                if (moveHeight > mContentMaxMoveHeight){
                    moveHeight = mContentMaxMoveHeight;
                }

                float headerMoveHeight = moveHeight * 0.5f * SCROLL_RATIO;
                float contentMoveHeight = moveHeight * SCROLL_RATIO;
                mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
                mContentTop = (int) (mContentInitRect.top + contentMoveHeight);

                //viewHelper是导入的jar包,在有些情况下layout()函数实现的并不好使,源自NineOldAndroids开源项目
                ViewHelper.setTranslationY(mTopView, headerMoveHeight);
                ViewHelper.setTranslationY(mContentView, contentMoveHeight);

                mIsMoving = true;
                mEnableTouch = true;
            } else {
                mEnableTouch = false;
            }
        }
        break;
        case MotionEvent.ACTION_UP: {
            //反弹
            if (mIsMoving) {
                ViewHelper.setTranslationY(mTopView, 0);
                TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0);
                headAnim.setDuration(200);
                mTopView.startAnimation(headAnim);

                ViewHelper.setTranslationY(mContentView, 0);
                TranslateAnimation contentAnim = new TranslateAnimation(0, 0, mContentTop - mContentInitRect.top, 0);
                contentAnim.setDuration(200);
                mContentView.startAnimation(contentAnim);
                mIsMoving = false;
            }
            mEnableTouch = false;
        }
        break;
        case MotionEvent.ACTION_CANCEL: {
            mEnableTouch = false;
        }
        break;
    }
    // 禁止控件本身的滑动.
    //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event)
    //只有返回FALSE的时候,才会执行super.onTouchEvent(event)
    //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动
    //!!!!!这点对于listview控件尤为重要。因为在上滑时,如果不禁止控件本身的向上移动,
    // 就会乱套,因为你本不需要利用setTranslationY()上移的地方,他仍然会上移
    return mEnableTouch || super.onTouchEvent(event);
}

源码内容:
    1、基本框架:第一部分,搭框架对应源码
    2、最终代码:本文最终的源代码

如果本文有帮到你,记得加关注哦

源码下载地址:http://download.csdn.net/detail/harvic880925/9062013
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/48087649    谢谢


你可能感兴趣的:(PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二))