彈性效果包括過度拉伸效果和反彈效果。
實作思路請看這篇文章,我基于他的實作進行了一些優化。
一、實作思路
這個問題的本質是控制ScrollView裝載的View(不妨叫它innerView)的顯示位置。而innerView顯示位置的改變可以通過兩種方式實作。
- 改變ScrollView的mScrollY
- 調用innerView的layout(l, t, r, b)函數
前者就是ScrollView實作正常滾動的方式。而後者就是我們拿來實作反彈效果的。也就是說,在ScrollView正常滾動達到極限的時候,我們調用innerView的layout(l, t, r, b)函數繼續"滾動"innerView。手指擡起時,意味着一次滑動的結束,這個時候,我們要判斷是否要"反彈"。是以這裡面有兩個關鍵問題。
- 如何判斷ScrollView的滾動已經達到極限
- 如何判斷是否需要反彈
對于問題一,參照以下代碼。
public boolean needOverScroll(float deltaY) {
final int offset = innerView.getMeasuredHeight() - getHeight();
final float scrollY = getScrollY();
return (scrollY == 0 && deltaY > 0)|| (scrollY == offset && deltaY < 0);
}
ScrollView的滾動已經達到極限分兩種情況。
第一種情況是手指往下滑動無法移動innerView了。我們知道,手指往下滑動,ScrollView的mScrollY減小,直至為0。ScrollView的mScrollY減為0之後(scrollY == 0),再向下滑動(deltaY > 0),mScrollY就不再變化了(不會變為負數),innerView也就不會移動了。這個時候,就需要我們調用innerView的layout(l, t, r, b)函數繼續"滾動"innerView。
第二種情況是手指往上滑動無法移動innerView了。我們知道,手指往上滑動,ScrollView的mScrollY增大,直至為innerView.getMeasuredHeight() - getHeight()(即mScrollY最大值為ScrollView裝載的View的測量高度與ScrollView提供給其顯示的高度的內插補點)。ScrollView的mScrollY增大為innerView.getMeasuredHeight() - getHeight()之後(scrollY == offset),再向上滑動(deltaY < 0),mScrollY就不再變化了,innerView也就不會移動了。這個時候,也需要我們調用innerView的layout(l, t, r, b)函數繼續"滾動"innerView。
這裡相對于我參考博文的那種實作,增加了對deltaY的考慮。這麼做,可以避免本可以利用ScrollView的本身滾動的時候,使用layout(l, t, r, b)進行滾動。是對初始時就向上滑動的情況的優化。如果不考慮deltaY的話,一開始向上滑動也會調用layout(l, t, r, b)移動布局。雖然根據列印的日志,隻移動了幾個像素(之後因為mScrollY不再為0就不再調用layout(l, t, r, b)),但是既然能避免還是應該避免。
對于問題二,參照以下代碼。
public boolean needRebound() {
return getInnerViewActualTop() > 0 || getInnerViewActualBottom() < getHeight();
}
public int getInnerViewActualTop(){
return innerView.getTop() - getScrollY();
}
public int getInnerViewActualBottom(){
return innerView.getBottom() - getScrollY();
}
是否需要反彈也分兩種情況
第一種情況是需要向上反彈,即getInnerViewActualTop() > 0。
第二種情況是需要向下反彈,即getInnerViewActualBottom() < getHeight()。
說明一下getInnerViewActualTop()函數。我在調試的時候發現,使用原生的ScrollView時,無論你如何滾動,innerView.getTop()都是0。而視覺上,以ScrollView為參考系,innerView的上邊界是不斷變化的。(我推想,應該是在onDraw()函數中,getTop()-getScrollY()來确定上邊界,是以getTop()可以一直不變,改變mScrollY就行了)是以,我寫了getInnerViewActualTop()函數,來獲得innerView相對于ScrollView視覺上的上邊界。
這樣實作有一個好處,就是當你過度拉伸,不松手,再往回滑動,讓innerView重新顯示到合理的位置上(沒有過度拉伸,沒有空白的區域),擡手,innerView不會反彈。而我參考博文的那種實作,隻要你過度拉伸過,擡手就會反彈,體驗不是太好。
二、源碼
package com.example.ligang.demo_autopullrefreshlistview;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
public class OverScrollView extends ScrollView {
private static final float OVER_SCROLL_RATIO = 0.5f;
private static final int REBOUND_DURATION = 200;
private View innerView;
private float lastY = -1;
private Rect originalRect = new Rect();
public OverScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
if (getChildCount() > 0) {
innerView = getChildAt(0);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (innerView != null) {
handleTouchEvent(ev);
}
return super.onTouchEvent(ev);
}
public void handleTouchEvent(MotionEvent ev) {
final float currY = ev.getY();
if (lastY == -1) {
lastY = currY;
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = currY;
break;
case MotionEvent.ACTION_MOVE:
final float deltaY = (currY - lastY) * OVER_SCROLL_RATIO;
lastY = currY;
if (needOverScroll(deltaY)) {
if (originalRect.isEmpty()) {
originalRect.set(innerView.getLeft(), innerView.getTop(), innerView.getRight(), innerView.getBottom());
}
if (deltaY > 0) {
innerView.layout(innerView.getLeft(), innerView.getTop() + (int) (Math.ceil(deltaY)), innerView.getRight(), innerView.getBottom() + (int) (Math.ceil(deltaY)));
} else {
innerView.layout(innerView.getLeft(), innerView.getTop() + (int) (Math.floor(deltaY)), innerView.getRight(), innerView.getBottom() + (int) (Math.floor(deltaY)));
}
}
break;
case MotionEvent.ACTION_UP:
if (needRebound()) {
rebound();
}
reset();
break;
default:
reset();
break;
}
}
private void reset() {
lastY = -1;
}
public void rebound() {
TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, innerView.getTop(), originalRect.top);
translateAnimation.setDuration(REBOUND_DURATION);
innerView.startAnimation(translateAnimation);
innerView.layout(originalRect.left, originalRect.top, originalRect.right, originalRect.bottom);
originalRect.setEmpty();
}
public boolean needRebound() {
return getInnerViewActualTop() > 0 || getInnerViewActualBottom() < getHeight();
}
public int getInnerViewActualTop(){
return innerView.getTop() - getScrollY();
}
public int getInnerViewActualBottom(){
return innerView.getBottom() - getScrollY();
}
public boolean needOverScroll(float deltaY) {
final int offset = innerView.getMeasuredHeight() - getHeight();
final float scrollY = getScrollY();
return (scrollY == 0 && deltaY > 0)|| (scrollY == offset && deltaY < 0);
}
}