在前面三篇中,我为大家展示了使用ScrollView实现下拉回弹的效果。但如果ScrollView里如果嵌套使用ListView就可能会出现问题,因为两者都会有滑动监听。操作起来可能会起冲突,然后解决了冲突问题,到后面页面性能也会很差强人意。即然如此,那我们就直接使用listview来实现下拉回弹的效果就好了。
在这篇中,我先给大家展示一种比较容易出效果的方法——重写overScrollBy()函数。在下一篇中,我们将模仿PullScrollView中的实现方式自己对OnTouchEvent()进行监听、操作。
注意注意!!!!本篇文章讲述的OverScrollBy(),大家应该把最大注意力放在《4、用途:捕捉当前listview是否到底或到顶》部分,至于下拉回弹,大家看看就好,OverScrollBy()实现的下拉回弹,bug一堆,根本无法实际运用到实际项目中,大家看看就好,下篇将带着大家利用OnTouchEvent()实现下拉回弹的效果。
一、setOverScrollMode()与OverScrollBy()
1、OverScrollBy()
OverScrollBy()是Android 9 之后才新增的API. 用于设定listview滚出屏幕后的回弹效果。先看OverScrollBy()函数的定义:
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
- deltaX:当前X轴滑动的像素数
- deltaY:当前Y轴滑动的像素数
- scrollX:在加上deltaX值以前的X轴总的滑动量
- scrollY:在加上deltaY值以前的Y轴总滑动量
- scrollRangeX:
- scrollRangeY:这两个什么意思,我也没弄懂
- maxOverScrollX:X轴最大的OverScroll范围(最大可超过边界的像素)
- maxOverscrollY:Y轴最大的OverScroll范围(最大可超过边界的像素)
- isTouchEvent:当前overScrollBy函数的调用是否由Touch事件引起的
2、setOverScrollMode()
上面OverScrollBy()是当listview超过顶部或者底部的时候,会被调用。那定义listView能不能超过顶部或底部滑动,也就是说让不让OverScrollBy()调用,是通过setOverScrollMode()函数来定义的。
- public void setOverScrollMode(int mode)
- //一直允许超过顶部/底部下拉
- public static final int OVER_SCROLL_ALWAYS = 0;
- //只有Content足够大到能scroll的时候,才允许超过顶部/底部下拉(系统默认值)
- public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1;
- //不允许超过顶部/底部下拉
- public static final int OVER_SCROLL_NEVER = 2;
这里大家应该对OverScrollBy()有个初步的认识了,下面我们先看看怎么实现下拉回弹,然后再讲解,为什么要这么做。
二、简单示例
先看下效果:
注意,overScrollBy()实现的回弹效果是不能设置下拉方向的,即,不但顶部下拉会回弹,在底部下拉时也会回弹。但从效果图中也可以看到,顶部下拉会回弹,底部下拉是不会回弹的,这是因为在最终代码中做了顶部还是底部判断,当在底部时,就不让用户上拉回弹了,至于怎么做到的,来一起看代码吧。
下面我们就通过一个最简单的例子来看下OverScrollBy()的用法及效果。
1、实现OverScrollView
首先,新建一个类OverScrollView,重写ListView代码如下:
- class OverScrollList extends ListView {
- //定义最大滚动高度
- int mContentMaxMoveHeight = 300;
- public OverScrollList(Context context) {
- super(context);
- }
- public OverScrollList(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- public OverScrollList(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mContentMaxMoveHeight, isTouchEvent);
- }
- }
在这个类里面,只做了两件事:
第一:定义一下变量mContentMaxMoveHeight,来表示可滑动到最大高度
- int mContentMaxMoveHeight = 300;
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mContentMaxMoveHeight, isTouchEvent);
- }
2、ListView填充数据
在填充数据前,还是给大家看一下MainActivity的布局:(main.xml)
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- >
- <com.harvic.OverScrollDemo.OverScrollList
- android:id="@+id/listview"
- android:layout_width="match_parent"
- android:layout_height="match_parent"/>
- </LinearLayout>
先创建一个XML来做为Item的布局 : (item_layout.xml)
- <?xml version="1.0" encoding="utf-8"?>
- <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"/>
- 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 OverScrollList mListView;
- private ArrayAdapter<String> mAdapter;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mListView = (OverScrollList) findViewById(R.id.listview);
- //配置Adapter
- mListItems = new LinkedList<String>();
- mListItems.addAll(Arrays.asList(mStrings));
- mAdapter = new ArrayAdapter<String>(this,R.layout.item_layout, mListItems);
- mListView.setAdapter(mAdapter);
- }
- }
3、疑问
(1)、为什么在OverScrollList中重写了overScrollBy后,还要设定 super.overScrollBy()里的maxOverScrollY值呢?
答:通过打日志,大家可以看到,overScrollBy里的maxOverScrollY的值一直是0!!!
也就是说,google给我们实现了overScrollBy函数,但需要我们自己设定可overScroll的最大值。不然overScroll的最大值默认是0,即不会上下拉动!!!
(2)、overScrollBy()函数什么时候会调?
答:在listview正常滑动时,overScrollBy()函数是不会被调用的,只有在超出ListView滑动界限时才会被调用。
4、用途:捕捉当前listview是否到底或到顶
现在我们先总结一下:- listview具不具有下拉反弹的功能是靠setOverScrollMode()来设定的。
- 具有下拉反弹功能以后下拉多少是靠 super.overScrollBy()里的maxOverScrollY值确定的。
- overScrollBy()在listview到顶或到底时仍然下拉/上拉时才会被调用。
(1)、简单计算方法
先列出代码如下:
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- if (deltaY>0){
- System.out.println("滑动到底端");
- }else if (deltaY < 0){
- System.out.println("滑动到顶端");
- }
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
- }
1、首先,不改写super.overScrollBy()中的maxOverScrollY的值。因为maxOverScrollY默认是0,所以不会listview虽具有默认的滑动功能却不会滑动。
2、利用overScrollBy()只有在滑动到顶部、底部且超出滑动区域时,才会被调用的性质。当被overScrollBy调用时,肯定是到了顶部或者底部!!!所以,如果到了顶端,用户向下拉的时候overScrollBy才会被调用。此时手指向下滑动,deltaY必定小于0。同理,当listview到了底部时,只有用户向上拉的时候才会被调用,这时候手指肯定是向上移动的,所以deltaY肯定是大于0的。
deltaY的计算方法是用当前的触摸的手指位置减去上次捕捉到的手指位置。
(2)、系统给出的计算方法
看起来上面的方法好像是万无一失的,(其实我也没发现哪里会有问题),但系统研究了下源码,发现在View.java中的overScrollBy()的实现代码中有这么一段:
- protected boolean overScrollBy(int deltaX, int deltaY,
- int scrollX, int scrollY,
- int scrollRangeX, int scrollRangeY,
- int maxOverScrollX, int maxOverScrollY,
- boolean isTouchEvent) {
- …………
- int newScrollX = scrollX + deltaX;
- int newScrollY = scrollY + deltaY;
- // Clamp values if at the limits and record
- final int left = -maxOverScrollX;
- final int right = maxOverScrollX + scrollRangeX;
- final int top = -maxOverScrollY;
- final int bottom = maxOverScrollY + scrollRangeY;
- boolean clampedX = false;
- if (newScrollX > right) {
- newScrollX = right;
- clampedX = true;
- } else if (newScrollX < left) {
- newScrollX = left;
- clampedX = true;
- }
- boolean clampedY = false;
- if (newScrollY > bottom) {
- newScrollY = bottom;
- clampedY = true;
- } else if (newScrollY < top) {
- newScrollY = top;
- clampedY = true;
- }
- …………
- return clampedX || clampedY;
- }
- protected boolean overScrollBy(int deltaX, int deltaY,
- int scrollX, int scrollY,
- int scrollRangeX, int scrollRangeY,
- int maxOverScrollX, int maxOverScrollY,
- boolean isTouchEvent) {
- …………
- int newScrollY = scrollY + deltaY;
- final int top = -maxOverScrollY;
- final int bottom = maxOverScrollY + scrollRangeY;
- boolean clampedY = false;
- if (newScrollY > bottom) {
- newScrollY = bottom;
- clampedY = true;
- } else if (newScrollY < top) {
- newScrollY = top;
- clampedY = true;
- }
- …………
- return clampedX || clampedY;
- }
- protected boolean overScrollBy(int deltaX, int deltaY,
- int scrollX, int scrollY,
- int scrollRangeX, int scrollRangeY,
- int maxOverScrollX, int maxOverScrollY,
- boolean isTouchEvent) {
- int newScrollY = scrollY + deltaY;
- final int top = -maxOverScrollY;
- final int bottom = maxOverScrollY + scrollRangeY;
- if (newScrollY > bottom) {
- System.out.println("滑动到底端");
- } else if (newScrollY < top) {
- System.out.println("滑动到顶端");
- }
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
- }
三、带header的下拉回弹
这部分带着大家实现开篇的实现的效果。
1、listView添加透明header
(1)、概述
根据上面的效果图,我们直接来看看MainActivity的新布局代码:
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- 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.OverScrollDemo.OverScrollList
- android:id="@+id/listview"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:divider="@null"
- android:dividerPadding="0dp"
- android:dividerHeight="0dp"/>
- </FrameLayout>
不一样的是,这里的OverScrollList控件,是全屏显示的,我们要怎么样把底部的小狗显示出来呢?
可能大家最先想到把底部的小狗图片给空出来的方法,就是给listview添加margin或者padding,那添加margin或者padding到底行不行呢?我们为OverScrollList添加一个android:paddingTop="100dp"看一下效果:
从效果图中可以看到,在上滑时,ListView没办法滑动到屏幕顶部。所以,我们唯一的解决方案就是为OverScrollList添加一个透明header来占据空间。
(2)、header布局
下面就为listview添加一个透明的header,布局文件如下:
- <?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="100dp">
- </LinearLayout>
(3)、MainActivity中添加header
给ListView添加Header很简单,直接调用addHeaderView(View v)就可以了。代码如下 :
- public class MyActivity 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 OverScrollListView mListView;
- private ArrayAdapter<String> mAdapter;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mListView = (OverScrollListView) findViewById(R.id.listview);
- ImageView headerView = (ImageView)findViewById(R.id.background_img);
- mListView.setmHeaderView(headerView);
- 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);
- mListView.addHeaderView(view);
- mListView.setAdapter(mAdapter);
- }
- }
- LayoutInflater inflater = getLayoutInflater();
- View view = inflater.inflate(R.layout.headerview,mListView,false);
- mListView.addHeaderView(view);
2、实现底部图片下拉回弹
(1)、OverScrollList中的实现
们现在能够通过重新OverScrollBy()函数让系统自已给我们实现ListView的下拉回弹,但底部的小狗图片可还是要我们自己实现下拉和回弹的。
非常庆幸的是,在OverScrollBy()的一堆参数中:
overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)
有两个参数对我们来讲特别有用:
scrollY:在加上deltaY以前的滚动距离;(向上移动时为正值,向下移动时为负值)
deltaY:此次的手指移动距离(同样,向上移动时为正值,向下移动时为负值)
所以我们将scrollY与deltaY相加就可以得到当前的滚动距离了,由于在回弹时OverScrollBy()也会被调用,所以整个过程在下拉时newScrollY就会一直变到最大,而在回弹时,newScrollY会慢慢减为0;
所以我们根据这个特性,直接根据当前的滚动距离计算出当前小狗图片的位置即可
完整的代码如下:
- /**
- * 阻尼系数,越小阻力就越大.
- */
- public static final float SCROLL_RATIO = 0.25f;
- private Rect mHeadInitRect = new Rect();
- //设置topView
- private View mTopView;
- public void setTopView(View view) {
- mTopView = view;
- }
- //初始化TopView的原始位置
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mHeadInitRect.set(mTopView.getLeft(), mTopView.getTop(), mTopView.getRight(), mTopView.getBottom());
- }
- return super.onInterceptTouchEvent(ev);
- }
- //随下拉滚动
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- int headerMoveHeight = (int)Math.abs((scrollY + deltaY) * SCROLL_RATIO);
- int mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
- mTopView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, (int) (mHeadInitRect.bottom + headerMoveHeight));
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mContentMaxMoveHeight, isTouchEvent);
- }
第一:将topView设置进来:
- //设置topView
- private View mTopView;
- public void setTopView(View view) {
- mTopView = view;
- }
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- mHeadInitRect.set(mTopView.getLeft(), mTopView.getTop(), mTopView.getRight(), mTopView.getBottom());
- }
- return super.onInterceptTouchEvent(ev);
- }
第三:随下拉滚动
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- int headerMoveHeight = (int)Math.abs((scrollY + deltaY) * SCROLL_RATIO);
- int mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
- mTopView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, (int) (mHeadInitRect.bottom + headerMoveHeight));
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, mContentMaxMoveHeight, isTouchEvent);
- }
- 在向下移动时,scrollY和deltaY都是负值,所以要计算移动距离需要在外面加一层Math.abs()来取绝对值。
- 至于为什么在计算headerMoveHeight时要将移动距离乘以SCROLL_RATIO,在第二篇中也提到过,就是让小狗图片移动的慢一些,显得难拉一点,这比较符合正常思维
这里就非常简单了,就是调用OverScrollList的setTopView()将底部的小狗图片设置进去。
代码如下:(这段代码没什么难度,就不再细讲了)
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main);
- mListView = (OverScrollList) findViewById(R.id.listview);
- //设置topView
- ImageView topView = (ImageView)findViewById(R.id.background_img);
- mListView.setTopView(topView);
- //设置headerView
- LayoutInflater inflater = getLayoutInflater();
- View view = inflater.inflate(R.layout.headerview,mListView,false);
- mListView.addHeaderView(view);
- //初始化Adapter
- mListItems = new LinkedList<String>();
- mListItems.addAll(Arrays.asList(mStrings));
- mAdapter = new ArrayAdapter<String>(this,R.layout.item_layout, mListItems);
- mListView.setAdapter(mAdapter);
- }
3、问题改进
(1)、OverScrollList的透明headerView在点击时会变白
答:我们就让它不可点击就可以了。在添加headview时,调用public void addHeaderView(View v, Object data, boolean isSelectable),将isSelectable设置为false即可,代码如下:
- //设置headerview不可点击
- mListView.addHeaderView(view, null, false);
- <?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="100dp"
- android:clickable="false">
- <!--如果要让headview不可点击,在这里设置clickable="false"是没用的,只有通过ListView.addHeaderView(view,null,false);来设置-->
- </LinearLayout>
答:还记得我们上面有讲过,如何利用overScrollView监听到底到顶吗?这里就派上用场啦。我们可以监听是否已经到底,如果到底就将maxOverScrollY设为0,如果是顶部就设为mContentMaxMoveHeight;
- protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
- //监听是否到底,如果到底就将maxOverScrollY设为0
- int newScrollY = scrollY + deltaY;
- final int bottom = maxOverScrollY + scrollRangeY;
- final int top = -maxOverScrollY;
- if (newScrollY > bottom) {
- maxOverScrollY = 0;
- } else if (newScrollY < top) {
- maxOverScrollY = mContentMaxMoveHeight;
- }
- //在向下移动时,scrollY是负值,所以scrollY + deltaY应该是当前应当所在位置。而由于scrollY + deltaY是负值,所以外层要包一个Math.abs()来取绝对值
- int headerMoveHeight = (int)Math.abs((scrollY + deltaY) * SCROLL_RATIO);
- int mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight);
- mTopView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, (int) (mHeadInitRect.bottom + headerMoveHeight));
- return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
- }
四、OverScrollBy()存在问题
在实际应用中,发现有两个内部实现问题:1、手指滑出屏幕后,不会回弹。
2、如果在下拉到底以后,弹出一个Fragment把当前页面盖在后面,即使这个Fragment是透明的,也会导致OverScrollBy()卡住不会回弹,即使自己回弹,也不会在回弹时调用overScrollBy(),导致其它底部小狗图片回弹出错。
好啦,这里就不再啰嗦了,下篇给大家讲述怎么使用OnTouchEvent()来实现下拉回弹的效果,这才正宗。