前言:今天看到一篇文章,非常喜欢其中的一句话,送给大家——如果万事俱备,那还要你干嘛。
相关文章:
1、《PullScrollView详解(一)——自定义控件属性》
2、《PullScrollView详解(二)——Animation、Layout与下拉回弹》
3、《PullScrollView详解(三)——PullScrollView实现》
4、《PullScrollView详解(四)——完全使用listview实现下拉回弹(方法一)》
5、《PullScrollView详解(五)——完全使用listview实现下拉回弹(方法二)》
6、《PullScrollView详解(六)——延伸拓展(listview中getScrollY()一直等于0、ScrollView中的overScrollBy)》
在前面三篇中,我为大家展示了使用ScrollView实现下拉回弹的效果。但如果ScrollView里如果嵌套使用ListView就可能会出现问题,因为两者都会有滑动监听。操作起来可能会起冲突,然后解决了冲突问题,到后面页面性能也会很差强人意。即然如此,那我们就直接使用listview来实现下拉回弹的效果就好了。
在这篇中,我先给大家展示一种比较容易出效果的方法——重写overScrollBy()函数。在下一篇中,我们将模仿PullScrollView中的实现方式自己对OnTouchEvent()进行监听、操作。
注意注意!!!!本篇文章讲述的OverScrollBy(),大家应该把最大注意力放在《4、用途:捕捉当前listview是否到底或到顶》部分,至于下拉回弹,大家看看就好,OverScrollBy()实现的下拉回弹,bug一堆,根本无法实际运用到实际项目中,大家看看就好,下篇将带着大家利用OnTouchEvent()实现下拉回弹的效果。
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)它的参数多的有点让人模糊,下面给大家说下各个参数的意思。这些意思目前大家看不懂也没关系,先大致了解下,后面讲例子时还会再讲。
上面OverScrollBy()是当listview超过顶部或者底部的时候,会被调用。那定义listView能不能超过顶部或底部滑动,也就是说让不让OverScrollBy()调用,是通过setOverScrollMode()函数来定义的。
public void setOverScrollMode(int mode)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;当然,我们也可以不设置setOverScrollMode(int mode),系统默认的滚动属性为OVER_SCROLL_IF_CONTENT_SCROLLS;即只有listview的content足够大到可以滚动的情况下,才允许超过顶部/底部下拉
先看下效果:
注意,overScrollBy()实现的回弹效果是不能设置下拉方向的,即,不但顶部下拉会回弹,在底部下拉时也会回弹。但从效果图中也可以看到,顶部下拉会回弹,底部下拉是不会回弹的,这是因为在最终代码中做了顶部还是底部判断,当在底部时,就不让用户上拉回弹了,至于怎么做到的,来一起看代码吧。
下面我们就通过一个最简单的例子来看下OverScrollBy()的用法及效果。
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); } }
int mContentMaxMoveHeight = 300;第二:重写overScrollBy函数,把return语句中的maxOverScrollY替换成我们定义的mContentMaxMoveHeight;
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); }要实现可以overscroll的listview就需要做这么多,下面就是一如即往的往listview里填充数据的部分了。
<?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 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"/>然后就是在MainActivity中的填充数据的部分了,代码如下 :
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); } }这些都是有关ListView的最基本的部分了,没什么难度,也没什么讲解的必要。
(1)、为什么在OverScrollList中重写了overScrollBy后,还要设定 super.overScrollBy()里的maxOverScrollY值呢?
答:通过打日志,大家可以看到,overScrollBy里的maxOverScrollY的值一直是0!!!
也就是说,google给我们实现了overScrollBy函数,但需要我们自己设定可overScroll的最大值。不然overScroll的最大值默认是0,即不会上下拉动!!!
(2)、overScrollBy()函数什么时候会调?
答:在listview正常滑动时,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); }很简单的原理:
看起来上面的方法好像是万无一失的,(其实我也没发现哪里会有问题),但系统研究了下源码,发现在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; }所以上面就是在overScrollBy中判断是否到顶、到底的方法,在OverScrollList中仿照一下,应该是这样写的:
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); }这才是正宗的判断是否到底、到顶的方法,至于为什么这么写,我也懒得研究了,无外乎就是弄懂scrollRangeY的含义而已。大家可以看看源码。其实,我觉得overScrollBy()的最大用途就是判断是否到底、到顶,至于什么下拉回弹,bug一堆!!!根本无法实际用到项目中!这里也只是给大家讲下这种实现方法而已,下篇我们会通过拦截OnTouchEvent()自己来实现下拉回弹。
这部分带着大家实现开篇的实现的效果。
(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>与前几篇一样,同样为ImageView添加layout_marginTop="-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>在这里什么都没有定义,只定义了一个高度。这里也可以定义背景是透明的,但如果listview有背景的话,依然会被盖住,所以要确保listview是没有背景的。不过我们不添加背景的话,默认就是透明的了。
给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);没什么难度,就是根据layout获取到view,然后将其设置给list
(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 private View mTopView; public void setTopView(View view) { mTopView = view; }第二:在手指下按点击时,初始化mTopView的位置。
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); }在第二篇中,我们有讲过为什么mTopView的位置初始化操作要放在onInterceptTouchEvent中,这里再絮叨一遍:因为,要获取mTopView的位置获取要在onLayout()以后才可以得到,也就是要在整个控件绘制完以后才能获取它的显示位置。所以在OverScrollList的构造函数中,是获取不到它的位置的。大家还记得 《FlowLayout详解(一)——onMeasure()与onLayout()》 中说的那句吗:首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到.这里同样的道理。
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); }要注意:
这里就非常简单了,就是调用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); }好了,到这里基本上就结束了,下面还有两个问题需要改进,然后就完整给出大家源码。
(1)、OverScrollList的透明headerView在点击时会变白
答:我们就让它不可点击就可以了。在添加headview时,调用public void addHeaderView(View v, Object data, boolean isSelectable),将isSelectable设置为false即可,代码如下:
//设置headerview不可点击 mListView.addHeaderView(view, null, false);非常注意,在headview对应的xml中设置clickable="false"是没有作用的;也就是下面的代码是没有作用的:(headview.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="100dp" android:clickable="false"> <!--如果要让headview不可点击,在这里设置clickable="false"是没用的,只有通过ListView.addHeaderView(view,null,false);来设置--> </LinearLayout>(2)、到底部时,仍然可以向上拉,怎么实现只顶部下拉
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); }好了,所有的都讲完了
源码内容:
1、《二、简单示例》:对应框架部分代码
2、《最终源码》:最终代码
如果本文有帮到你,记得加关注哦
源码下载地址:http://download.csdn.net/detail/harvic880925/9052605
请大家尊重原创者版权,转载请标明出处:http://blog.csdn.net/harvic880925/article/details/48021931 谢谢