标签:
PullScrollView是Github上面的一个开源项目,主要用于实现下拉时头部伸缩的效果,项目地址见https://github.com/MarkMjw/PullScrollView
大家先来看一下实现的效果图(图侵删)
但是由于这个项目的代码比较复杂,而实际上不这样做也可实现效果,我在网上找了另外一篇介绍pullScrollView的文章,写得非常好,本文也只是基于文章http://blog.csdn.net/harvic880925/article/details/46728247 谈谈自己的理解和收获,大家如果想知道更详细,可以拜访这篇文章。
下面我先来说明一下实现的思路,整体的布局是一个ScrollView,意味着我们要继承ScrollView来实现自己的控件pullScrollView,对于下拉时会有拉升效果的图片,其实是ScrollView以外的一个布局,pullScrollView提供一个setHeader()方法来为自己设置下拉头部。
我们可以为任意一个View设置为pullScrollView的头部,只要pullScrollView被拖动的时候,改变这个View的布局就可以了,我们称这个View为HeaderView
那么具体是怎么改变的呢?通过layout()方法来改变HeaderView的位置,拖动的时候,不断修改HeaderView位置使之下移,但是这样只是实现了拖动的效果,要有类似拉伸的效果的话,需要我们将HeaderView的marginTop改成负数,这样位置移动的时候,感觉HeaderView就会被拉长。
更好的效果是,底部也会拉长,这样看起来才像是两边一起拉造成的。在图片中我们看到HeaderView下面的主体布局,我们称为ContentView,我们让ContentView遮蔽HeaderView的一部分,然后让ContentView和HeaderView的下拉增长速度不一样,这样就可以造成底部也拉长的效果了。
现在来思考怎么使用layout()?显然我们要通过监听手指的down和move事件,down的时候记录下位置,move的时候用当前位置减去down的位置,这个差值就是HeaderView位置的增长距离,至于ContentView我们可以将之乘以一个小数使之小一些。
理论上我们实现了下拉的效果,但是有很多的缺陷,现在我们先看源码。
首先是属性和构造方法
public class PullScrollView extends ScrollView { //底部图片View private View mHeaderView; //头部图片的初始化位置 private Rect mHeadInitRect = new Rect(); //底部View private View mContentView; //ScrollView的contentView的初始化位置 private Rect mContentInitRect = new Rect(); //初始点击位置 private Point mTouchPoint = new Point(); //标识当前view是否移动 boolean mIsMoving = false; //是否禁止控件本身的的移动 boolean mEnableMoving = false; //是否使用layout函数移动布局 boolean mIsLayout = false; /** * 阻尼系数,越小阻力就越大. */ private static final float SCROLL_RATIO = 0.5f; private int mContentTop, mContentBottom; private int mHeaderCurTop, mHeaderCurBottom; //用户定义的headview高度 private int mHeaderHeight = 0; public PullScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public PullScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { // set scroll mode setOverScrollMode(OVER_SCROLL_NEVER); if (null != attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView); if (ta != null) { mHeaderHeight = (int) ta.getDimension(R.styleable.PullScrollView_headerHeight, -1); ta.recycle(); } } }对于成员属性大家可以看注释,我们初始化了两个Rect,和一个Point。对于init()方法,其实就是获取了两个我们自定义的属性。
注意,在这里ScrollView的位置使一直不变化的,变化的是ScrollView里面的contentView,所以我们使用setOverScrollMode(OVER_SCROLL_NEVER)来限制它的移动。
然后根据上面所说,我们要有一个setHeader()方法
public void setmHeaderView(View view) { mHeaderView = view; }
@Override protected void onFinishInflate() { if (getChildCount() > 0) { mContentView = getChildAt(0); } super.onFinishInflate(); }
@Override public boolean onInterceptTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //保存原始位置 mTouchPoint.set((int) event.getX(), (int) event.getY()); mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom()); mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom()); mIsMoving = false; //如果当前不是从初始化位置开始滚动的话,就不让用户拖拽 if (getScrollY() == 0){ mIsLayout = true; } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { //如果当前的事件是我们要处理的事件时,比如现在的下拉,这时候,我们就不能让子控件来处理这个事件 //这里就需要把它截获,不传给子控件,更不能让子控件消费这个事件 //不然子控件的行为就可能与我们的相冲突 int deltaY = (int) event.getY() - mTouchPoint.y; deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY); if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) { onTouchEvent(event); return true; } } return super.onInterceptTouchEvent(event); }有人会奇怪,为什么是在onInterceptTouchEvent(MotionEvent event)方法里面监听,而不是onTouchEvent(MotionEvent event)里面呢?
这里其实涉及一个事件拦截和传递机制的问题,首先我们要明白,对于ScrollView这个GroupView,当我们手指按下的时候,首先调用的是dispatchTouchEvent()方法,这方法用于分发Touch事件,接下来默认会调用onInterceptTouchEvent(MotionEvent event)。这个方法是用于事件拦截的,如果返回true,则事件不再向下传递,也就是说不再分发给子控件。
如果返回false,则会分发给子控件,由子控件的onTouchEvent()方法去处理,如果子控件的onTouchEvent()方法返回false,则再将事件分发给ScrollView的onTouchEvent()方法处理,返回true则不分发给ScrollView的onTouchEvent()方法。
也就说Touch事件分发顺序如下:
父控件的onInterceptTouchEvent(MotionEvent event) ---》子控件的onTouchEvent() --》父控件的nTouchEvent()
在上面的传递过程中,有任意一个返回true,则不向下继续分发。
OK,现在我们去理解上面的写法,我们在onInterceptTouchEvent(MotionEvent event) 方法里面获得down的位置,是为了避免子控件消费掉事件(作为pullScrollView的作者,我们不能保证使用者的子控件会不会消费掉)。
另外,在down的时候,我们记录下了手指位置,这个使用mTouchPoint来记录
记录下了headerView的初始位置,这个使用mHeadInitRect来记录
记录下了contentView的初始位置,这个使用mContentInitRect来记录
其他的设置,我们暂时不管。
接下来我们看move的时候,
int deltaY = (int) event.getY() - mTouchPoint.y; deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY);这里注意,我们对于移动的距离进行了处理,我们保证deltaY的最小值为0,最大值为一个mHeaderHeight,我们自定义的一个属性。
这样是为了限制下拉的距离,也就是说我们不能无限下拉,下拉到一定距离就不能继续拉了。
另外大家可能会想到上滑的情况,上滑有两种,一种是在初始位置就上滑。
这时headerView不应该动,而是交由ScrollView去处理这个事件(因为这个是ScrollView本身的滑动效果啊!)
所以我们要用getScrollY()==0来判断是不是在初始位置就开始上滑,如果是,则不实现拖拽
所以我们可以看到在down里面的判断,使用mIsLayout来做这个标记
//如果当前不是从初始化位置开始滚动的话,就不让用户拖拽 if (getScrollY() == 0){ mIsLayout = true; }
另外还有,就是下拉一段距离,再上滑,这时跟下拉的处理是一样的,只要调用layout修改位置就好了
可以看到,如果
if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) { onTouchEvent(event); return true; }控件都在初始位置,并且下拉了,我们就直接调用ScrollView的onTouchEvent()方法,并且拦截事件
我再来看onTouchEvent()方法做了什么,先看move的情况
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { int deltaY = (int) event.getY() - mTouchPoint.y; deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY); if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) { float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO; mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight); mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight); float contentMoveHeight = deltaY * SCROLL_RATIO; mContentTop = (int) (mContentInitRect.top + contentMoveHeight); mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight); if (mContentTop <= mHeaderCurBottom) { mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom); mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom); mIsMoving = true; mEnableMoving = true; } } } break;
这里有一个SCROLL_RATIO我们称之为阻尼系数,目的是提供下拉困难的效果(也就是说不能我们下拉多少就移动多少)
另外还要一个判断
if (mContentTop <= mHeaderCurBottom) {<span style="font-family:Arial, Helvetica, sans-serif;">...}</span>这就是说contentView的顶部,不能超过headerView的底部,否则两者就分离了!因为contentView移动得比较慢,所以headerView不动以后,contentView还是可以继续下拉,所以这时就不能超过headerView的底部。
Ok,到此为止,下拉效果基本实现。
下拉来看回弹效果,这显然要在up事件中处理
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { ...... } break; case MotionEvent.ACTION_UP: { //反弹 if (mIsMoving) { mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom); TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0); headAnim.setDuration(200); mHeaderView.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; } mEnableMoving = false; mIsLayout = false; } break; } // 禁止控件本身的滑动. //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event) //只有返回FALSE的时候,才会执行super.onTouchEvent(event) //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动 return mEnableMoving || super.onTouchEvent(event); }
没错,回弹是使用动画TranslateAnimation来实现的
比较让人疑惑的是,
mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom);这句目的是什么。我将headerView还原到了初始位置,然后再调用动画。
对于动画的坐标计算,我们要知道,原点是View当前的位置,也就是说即使View被下拉了,使用动画时,也是按照当前位置为起点计算的。
现在我们先把View的位置复原,那么我们在TranslateAnimation设置目的Y坐标就是0,其实Y坐标,就是当前位置减去初始位置。
这样才能合理地计算动画的移动距离。
有人会说,这样不会闪屏吗?答案是不会的,因为onlayout是在一瞬间完成的,所以人眼是感觉不出来这个变化的。
所谓回弹,就是让在一定时间内让控件不断地向初始位置复原。
最后我们来看一下onTouchEvent(MotionEvent event)的返回值,
// 禁止控件本身的滑动. //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event) //只有返回FALSE的时候,才会执行super.onTouchEvent(event) //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动 return mEnableMoving || super.onTouchEvent(event);注释说明得很清楚了,就是用于禁止ScrollView本身的滑动,在拖拽效果时,将mEnableMoving设置为true,这样super.onTouchEvent(event);就不会被调用,也就是ScrollView的效果被禁止。
另外在Action_up我们可以看到,要将mEnableMoving设置为false,恢复其滑动功能。
最后,希望大家看懂了我的讲解,如果不明白,大家可以看原作者的博客。
至于github的pullScrollView的具体实现,大家可以看一下我写的注释,其实思路是一样的,但是实现起来更为繁杂。
资源地址是:http://download.csdn.net/detail/kangaroo835127729/8945515
最后贴出本文pullScrollView的完整源代码
/** * Created by harvic on 2015/6/17 * @adress blog.csdn.net/harvic880925 */ public class PullScrollView extends ScrollView { //底部图片View private View mHeaderView; //头部图片的初始化位置 private Rect mHeadInitRect = new Rect(); //底部View private View mContentView; //ScrollView的contentView的初始化位置 private Rect mContentInitRect = new Rect(); //初始点击位置 private Point mTouchPoint = new Point(); //标识当前view是否移动 boolean mIsMoving = false; //是否禁止控件本身的的移动 boolean mEnableMoving = false; //是否使用layout函数移动布局 boolean mIsLayout = false; /** * 阻尼系数,越小阻力就越大. */ private static final float SCROLL_RATIO = 0.5f; private int mContentTop, mContentBottom; private int mHeaderCurTop, mHeaderCurBottom; //用户定义的headview高度 private int mHeaderHeight = 0; public PullScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public PullScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } private void init(Context context, AttributeSet attrs) { // set scroll mode setOverScrollMode(OVER_SCROLL_NEVER); if (null != attrs) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PullScrollView); if (ta != null) { mHeaderHeight = (int) ta.getDimension(R.styleable.PullScrollView_headerHeight, -1); ta.recycle(); } } } public void setmHeaderView(View view) { mHeaderView = view; } @Override protected void onFinishInflate() { if (getChildCount() > 0) { mContentView = getChildAt(0); } super.onFinishInflate(); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_MOVE: { int deltaY = (int) event.getY() - mTouchPoint.y; // deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY); deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY); if (deltaY > 0 && deltaY >= getScrollY() && mIsLayout) { float headerMoveHeight = deltaY * 0.5f * SCROLL_RATIO; mHeaderCurTop = (int) (mHeadInitRect.top + headerMoveHeight); mHeaderCurBottom = (int) (mHeadInitRect.bottom + headerMoveHeight); float contentMoveHeight = deltaY * SCROLL_RATIO; mContentTop = (int) (mContentInitRect.top + contentMoveHeight); mContentBottom = (int) (mContentInitRect.bottom + contentMoveHeight); if (mContentTop <= mHeaderCurBottom) { mHeaderView.layout(mHeadInitRect.left, mHeaderCurTop, mHeadInitRect.right, mHeaderCurBottom); mContentView.layout(mContentInitRect.left, mContentTop, mContentInitRect.right, mContentBottom); mIsMoving = true; mEnableMoving = true; } } } break; case MotionEvent.ACTION_UP: { //反弹 if (mIsMoving) { mHeaderView.layout(mHeadInitRect.left, mHeadInitRect.top, mHeadInitRect.right, mHeadInitRect.bottom); TranslateAnimation headAnim = new TranslateAnimation(0, 0, mHeaderCurTop - mHeadInitRect.top, 0); headAnim.setDuration(200); mHeaderView.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; } mEnableMoving = false; mIsLayout = false; } break; } // 禁止控件本身的滑动. //这句厉害,如果mEnableMoving返回TRUE,那么就不会执行super.onTouchEvent(event) //只有返回FALSE的时候,才会执行super.onTouchEvent(event) //禁止控件本身的滑动,就会让它,本来应有的滑动就不会滑动了,比如向上滚动 return mEnableMoving || super.onTouchEvent(event); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { //保存原始位置 mTouchPoint.set((int) event.getX(), (int) event.getY()); mHeadInitRect.set(mHeaderView.getLeft(), mHeaderView.getTop(), mHeaderView.getRight(), mHeaderView.getBottom()); mContentInitRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom()); mIsMoving = false; //如果当前不是从初始化位置开始滚动的话,就不让用户拖拽 if (getScrollY() == 0){ mIsLayout = true; } } else if (event.getAction() == MotionEvent.ACTION_MOVE) { //如果当前的事件是我们要处理的事件时,比如现在的下拉,这时候,我们就不能让子控件来处理这个事件 //这里就需要把它截获,不传给子控件,更不能让子控件消费这个事件 //不然子控件的行为就可能与我们的相冲突 int deltaY = (int) event.getY() - mTouchPoint.y; // deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderView.getHeight() ? mHeaderView.getHeight() : deltaY); deltaY = deltaY < 0 ? 0 : (deltaY > mHeaderHeight ? mHeaderHeight : deltaY); if (deltaY > 0 && deltaY >= getScrollY()&& getScrollY() == 0) { onTouchEvent(event); return true; } } return super.onInterceptTouchEvent(event); } }
版权声明:本文为博主原创文章,未经博主允许不得转载。
标签:
原文地址:http://blog.csdn.net/crazy__chen/article/details/47143995