本文主要的是介紹如何實現彈性的listview,以及上拉和下拉功能的實現,其實對一般的View也是適用的,稍微修改一下就可以啦。裏面涉及一些對事件分發的處理,有興趣的可以看一下這個鏈接,
1.使用介紹
(1)首先在xml中定義
<cn.appleye.flexiblelistview.FlexibleListView
android:id="@+id/flexible_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
(2)在代碼中實現回調就可以實現上拉和下拉功能
mFlexibleListView = (FlexibleListView) findViewById(R.id.flexible_list_view);
mFlexibleListView.setOnPullListener(new FlexibleListView.OnPullListener(){
@Override
public void onPullDown() {
//下拉刷新
}
@Override
public void onPullUp() {
//上拉加載更多
}
});
2.具體實現
拋開代碼細節,要實現彈性效果和上拉以及下拉功能需要了解以下幾點
(1)什麼是彈性效果?列表滑到底部或者頂部之後,還可以繼續滑動一定距離,然後再慢慢的恢復到底部或者頂部,恢復的過程有一個彈性的效果。
(2)什麼時候觸發?上面可以看到,滑到底部或者頂部之後開始觸發
(3)滑動多少距離開始恢復?定義好一個距離,合適就好
(4)恢復的過程的彈性效果怎麼實現?網上都有很多彈性公式
(5)什麼時候調用上拉或下拉回調?當上拉或下拉到一定距離手指離開開始調用
下面看一下具體代碼怎麼實現的。
/**
* 彈性ListView,實現了上拉和下拉功能
* @author newhope1106 2016-11-02
*/
public class FlexibleListView extends ListView implements OnTouchListener{
/**初始可拉動Y軸方向距離*/
private static final int MAX_Y_OVER_SCROLL_DISTANCE = 100;
private Context mContext;
/**實際可上下拉動Y軸上的距離*/
private int mMaxYOverScrollDistance;
private float mStartY = -1;
/**開始計算的時候,第一個或者最後一個item是否可見的*/
private boolean mCalcOnItemVisible = false;
/**是否開始計算*/
private boolean mStartCalc = false;
/**用戶自定義的OnTouchListener類*/
private OnTouchListener mTouchListener;
/**上拉和下拉監聽事件*/
private OnPullListener mPullListener;
private int mScrollY = 0;
private int mLastMotionY = 0;
private int mDeltaY = 0;
/**是否在進行動畫*/
private boolean mIsAnimationRunning = false;
/**手指是否離開屏幕*/
private boolean mIsActionUp = false;
public FlexibleListView(Context context){
super(context);
mContext = context;
super.setOnTouchListener(this);
initBounceListView();
}
public FlexibleListView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
super.setOnTouchListener(this);
initBounceListView();
}
public FlexibleListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = context;
initBounceListView();
}
private void initBounceListView(){
final DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
final float density = metrics.density;
mMaxYOverScrollDistance = (int) (density * MAX_Y_OVER_SCROLL_DISTANCE);
}
/**
* 覆蓋父類的方法,設置OnTouchListener監聽對象
* @param listener 用戶自定義的OnTouchListener監聽對象
* */
public void setOnTouchListener(OnTouchListener listener) {
mTouchListener = listener;
}
/**
* 設置上拉和下拉監聽對象
* @param listener 上拉和下拉監聽對象
* */
public void setOnPullListener(OnPullListener listener){
mPullListener = listener;
}
public void scrollTo(int x, int y) {
super.scrollTo(x, y);
mScrollY = y;
}
/**
* 在滑動的過程中onTouch的ACTION_DOWN事件可能丟失,在這裏進行初始值設置
* */
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
break;
default:
break;
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
/*用戶自定義的觸摸監聽對象消費了事件,則不執行下面的上拉和下拉功能*/
if(mTouchListener!=null && mTouchListener.onTouch(v, event)) {
return true;
}
/*在做動畫的時候禁止滑動列表*/
if(mIsAnimationRunning) {
return true;//需要消費掉事件,否者會出現連續很快下拉或上拉無法回到初始位置的情況
}
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:{
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
}
case MotionEvent.ACTION_MOVE:{
if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
mStartCalc = true;
mCalcOnItemVisible = false;
mStartY = event.getY();
}
final int y = (int) event.getY();
mDeltaY = mLastMotionY - y;
mLastMotionY = y;
if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
if(mDeltaY * mScrollY > 0) {
mDeltaY = 0;
}
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:{
mIsActionUp = true;
float distance = event.getY() - mStartY;
checkIfNeedRefresh(distance);
startBoundAnimate();
}
}
return false;
}
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
if(mDeltaY == 0 || mIsActionUp) {
return;
}
scrollBy(0, mDeltaY/2);
}
/**彈性動畫*/
private void startBoundAnimate() {
mIsAnimationRunning = true;
final int scrollY = mScrollY;
int time = Math.abs(500*scrollY/mMaxYOverScrollDistance);
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(time);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
float fraction = animator.getAnimatedFraction();
scrollTo(0, scrollY - (int) (scrollY * fraction));
if((int)fraction == 1) {
scrollTo(0, 0);
resetStatus();
animator.removeUpdateListener(this);
}
}
});
animator.start();
}
private void resetStatus() {
mIsAnimationRunning = false;
mStartCalc = false;
mCalcOnItemVisible = false;
}
/**
* 根據滑動的距離判斷是否需要回調上拉或者下拉事件
* @param distance 滑動的距離
* */
private void checkIfNeedRefresh(float distance) {
if(distance > 0 && getFirstVisiblePosition() == 0) { //下拉
View view = getChildAt(0);
if(view == null) {
return;
}
float realDistance = distance;
if(!mCalcOnItemVisible) {
realDistance = realDistance - view.getHeight();//第一個item的高度不計算在內容
}
if(realDistance > mMaxYOverScrollDistance) {
if(mPullListener != null){
mPullListener.onPullDown();
}
}
} else if(distance < 0 && getLastVisiblePosition() == getAdapter().getCount()-1) {//上拉
View view = getChildAt(getChildCount()-1);
if(view == null) {
return;
}
float realDistance = -distance;
if(!mCalcOnItemVisible) {
realDistance = realDistance - view.getHeight();//最後一個item的高度不計算在內容
}
if(realDistance > mMaxYOverScrollDistance) {
if(mPullListener != null){
mPullListener.onPullUp();
}
}
}
}
public interface OnPullListener{
/**
* 下拉
* */
void onPullDown();
/**
* 上拉
* */
void onPullUp();
}
}
代碼不長,只有200多行,比較簡單,也不涉及資源問題。
首先我們初始化一個最大距離:mMaxYOverScrollDistance,同時控件自己實現OnTouchListener的接口,所有的功能基本都是在onTouch實現的,我們先簡要的描述一下思路。
當手指按下屏幕的時候,檢查此時第一個或者最後一個item是否可見,如果不可見,當滑動手指的時候,檢查此時是否第一個或最後一個item是否可見,在滑動列表時,如果已經超過了listview頂部或底部的位置,通過改變其偏移量mScrollY,讓其可以再在原來的基礎上繼續滑動,但是當滑動到一定距離之後,禁止其改變偏移量,此時不能再繼續滑動了,當手指離開屏幕之後,再彈性回到頂部或底部位置,根據滑動的距離,來判斷是否需要進行下拉或上拉操作。爲什麼,ACTION_DOWN和ACTION_UP中都有這個檢測,主要是爲了在最後計算距離的時候判斷是否需要減去第一個item的高度,當然讀者也可以把它去掉,item高度不大的情況下,不會影響體驗。下面看代碼。
case MotionEvent.ACTION_DOWN:{
mIsActionUp = false;
resetStatus();
if(getFirstVisiblePosition() == 0 || (getLastVisiblePosition() == getAdapter().getCount()-1)) {
mStartY = event.getY();
mStartCalc = true;
mCalcOnItemVisible = true;
}else{
mStartCalc = false;
mCalcOnItemVisible = false;
}
mLastMotionY = (int)event.getY();
}
在ACTION_DOWN操作的時候,通過resetStatus(),初始化狀態,然後檢查第一個item或者最後一個item是否顯示,mStartCalc表示開始計算距離,mCalcOnItemVisible表示是否第一個item或者最後一個item可見的,如果是mStartCalc置爲true,mCalcOnItemVisible置爲true,同時開始記錄當前位置座標。
case MotionEvent.ACTION_MOVE:{
if(!mStartCalc && (getFirstVisiblePosition() == 0|| (getLastVisiblePosition() == getAdapter().getCount()-1))) {
mStartCalc = true;
mCalcOnItemVisible = false;
mStartY = event.getY();
}
final int y = (int) event.getY();
//獲取滑動的偏移量
mDeltaY = mLastMotionY - y;
mLastMotionY = y;
if(Math.abs(mScrollY) >= mMaxYOverScrollDistance) {
if(mDeltaY * mScrollY > 0) {
mDeltaY = 0;
}
}
break;
}
如果在ACTION_DOWN中沒有開始計算,那麼在ACTION_MOVE中判斷是否第一個或最後一個item可見,如果是,則將mStartCalc置爲true,mCalcOnItemVisible置爲false。將本次的位置和上次的y周位置進行比較,獲取偏移量。在滑動的過程中,都會調用onOverScrolled接口,然後調用scrollBy(實質上是調用scrollTo)接口,從而實現列表滑動。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX,
boolean clampedY) {
//滑動偏移量等於0或者手指離開屏幕都不在滑動列表
if(mDeltaY == 0 || mIsActionUp) {
return;
}
scrollBy(0, mDeltaY/2);
}
上述的ACTION_MOVE中會判斷當前listview的偏移量(mScrollY)是否超過最大距離,否則將滑動的偏移量(mDeltaY)置爲0,不讓其在onOverScrolled中滑動。上述的mDeltaY/2,作用是不讓其滑動太快,自然一些。
case MotionEvent.ACTION_UP:{
mIsActionUp = true;
float distance = event.getY() - mStartY;
checkIfNeedRefresh(distance);
startBoundAnimate();
}
當手指離開屏幕的時候,會調用ACTION_UP,此時將mIsActionUp置爲true,同時計算當前位置的座標和初始計算的位置座標,然後得出滑動的距離(往返滑動的情況不計算,只計算初始和終止位置),checkIfNeedRefresh用於判斷是否需要上拉或者下拉操作,根據distance的正負可以知道是上滑還是下滑,如果有必要,減去第一個或最後一個item的高度,得到listview實際滑動的距離,然後和最大距離進行比較,來判斷是否需要上拉加載更多,下拉刷新。
最後通過一個動畫startBoundAnimate實現彈性恢復的效果,動畫過程中不允許其滑動。
/*在做動畫的時候禁止滑動列表*/
if(mIsAnimationRunning) {
return true;//需要消費掉事件,否者會出現連續很快下拉或上拉無法回到初始位置的情況
}
一下有幾個注意點,onTouch一般情況下返回false,表示不消費事件,不能影響ListView的正常滑動。上拉或者下拉的時候,這裏並沒有做Loading效果,讀者可以自行添加一個footerView或者HeaderView來實現。
這裏都是在View的接口裏面實現的,因此實際上不限於ListView,其他的繼承自View的控件,都可以採用這種方法,如果只想用彈性效果,那麼也沒有必要實現上拉和下拉的效果,直接在xml中定義即可。
還有一點需要注意的是,有時滑動太快,會把ACTION_DOWN事件給忽略掉,因此需要在onInterceptTouchEvent做ACTION_DOWN事件的處理,可以把OnTouch方法中的ACTION_DOWN去掉。