Android 自定義ScrollView 支援慣性滑動,慣性回彈效果。支援上拉載入更多
阿新 • • 發佈:2019-02-10
先講下原理:
ScrollView的子View 主要分為3部分:head頭部,滾動內容,fooder底部
我們實現慣性滑動,以及回彈,都是靠超過head或者fooder 就重新滾動到 ,內容的頂部或者底部。
之前看了Pulltorefresh 他是通過不斷改變 head或者 fooder的 pading 值來實現 上拉或者 下拉的效果。感覺有點不流暢,而且層次巢狀得比較多。當然他的好處是擴充套件性好。
因工作需求,需要層次巢狀少,對效能要求非常高。因此重新自定義了ViewGroup實現。
直接上程式碼:
package com.example.administrator.customscrollview; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.OverScroller; /** * 自定義 pulltorefresh Layout * TODO: ferris 2015年9月11日 18:52:40 */ public class PullTorefreshScrollView extends ViewGroup { private FoodeLayout fooder_layout;// top and buttom private View top_layout; private int desireWidth, desireHeight; private VelocityTracker velocityTracker; private int mPointerId; private float x, y; private OverScroller mScroller; private int maxFlingVelocity, minFlingVelocity; private int mTouchSlop; protected Boolean isMove = false; protected float downX = 0, downY = 0; private int top_hight = 0; private int scrollYButtom = 0; private int nScrollYButtom = 0; private int pullDownMin = 0; private Boolean isEnablePullDown = true; private Boolean isFirst=true; public void setEnablePullDown(Boolean isEnablePullDown) { this.isEnablePullDown = isEnablePullDown; } public PullTorefreshScrollView(Context context) { super(context); init(null, 0); } public PullTorefreshScrollView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs, 0); } public PullTorefreshScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(attrs, defStyle); } private void init(AttributeSet attrs, int defStyle) { // Load attributes // final TypedArray a = getContext().obtainStyledAttributes( // attrs, R.styleable.PullTorefreshScrollView, defStyle, 0); // // // a.recycle(); mScroller = new OverScroller(getContext()); maxFlingVelocity = ViewConfiguration.get(getContext()).getScaledMaximumFlingVelocity(); minFlingVelocity = ViewConfiguration.get(getContext()).getScaledMinimumFlingVelocity(); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } @Override protected void onFinishInflate() { super.onFinishInflate(); fooder_layout = (FoodeLayout) findViewById(R.id.fooder_layout); top_layout = findViewById(R.id.top_layout); if (isEnablePullDown) { fooder_layout.showFooderPull(); } else { fooder_layout.hideFooder(); } } public int getScrollYTop() { return top_hight; } public int getScrollYButtom() { return scrollYButtom; } public int getNScrollYTop() { return 0; } public int getNScrollYButtom() { return nScrollYButtom; } public int measureWidth(int widthMeasureSpec) { int result = 0; int measureMode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); switch (measureMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = width; break; default: break; } return result; } public int measureHeight(int heightMeasureSpec) { int result = 0; int measureMode = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); switch (measureMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = height; break; default: break; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 計算所有child view 要佔用的空間 int width = measureWidth(widthMeasureSpec); int height = measureHeight(heightMeasureSpec); desireWidth = 0; desireHeight = 0; int count = getChildCount(); for (int i = 0; i < count; ++i) { View v = getChildAt(i); if (v.getVisibility() != View.GONE) { LayoutParams lp = (LayoutParams) v.getLayoutParams(); measureChildWithMargins(v, widthMeasureSpec, 0, heightMeasureSpec, 0); //只是在這裡增加了垂直或者水平方向的判斷 if (v.getId() == R.id.top_layout) { top_hight = v.getMeasuredHeight(); } desireWidth = Math.max(desireWidth, v.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); desireHeight += v.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; } } // count with padding desireWidth += getPaddingLeft() + getPaddingRight(); desireHeight += getPaddingTop() + getPaddingBottom(); // see if the size is big enough desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth()); desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight()); //處理內容比較少的時候,就新增一定的高度 int scrollHight = height + top_hight * 2; if (scrollHight > desireWidth) { int offset = scrollHight - desireHeight; View view = new View(getContext()); view.setBackgroundResource(R.color.top_layout_color); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, offset); addView(view, getChildCount() - 1, lp); desireWidth = scrollHight; } setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec)); scrollYButtom = desireHeight - getMeasuredHeight() - top_hight; nScrollYButtom = desireHeight - getMeasuredHeight(); //如果上啦拖出一半的高度,就代表將要執行上啦 pullDownMin = nScrollYButtom - top_hight / 2; if(isFirst){ scrollTo(0, top_hight); isFirst=false; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { final int parentLeft = getPaddingLeft(); final int parentRight = r - l - getPaddingRight(); final int parentTop = getPaddingTop(); final int parentBottom = b - t - getPaddingBottom(); if (BuildConfig.DEBUG) Log.d("onlayout", "parentleft: " + parentLeft + " parenttop: " + parentTop + " parentright: " + parentRight + " parentbottom: " + parentBottom); int left = parentLeft; int top = parentTop; int count = getChildCount(); for (int i = 0; i < count; ++i) { View v = getChildAt(i); if (v.getVisibility() != View.GONE) { LayoutParams lp = (LayoutParams) v.getLayoutParams(); final int childWidth = v.getMeasuredWidth(); final int childHeight = v.getMeasuredHeight(); final int gravity = lp.gravity; final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; // layout vertical, and only consider horizontal gravity left = parentLeft; top += lp.topMargin; switch (horizontalGravity) { case Gravity.LEFT: break; case Gravity.CENTER_HORIZONTAL: left = parentLeft + (parentRight - parentLeft - childWidth) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: left = parentRight - childWidth - lp.rightMargin; break; } v.layout(left, top, left + childWidth, top + childHeight); top += childHeight + lp.bottomMargin; } } } @Override protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } @Override public android.view.ViewGroup.LayoutParams generateLayoutParams( AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected android.view.ViewGroup.LayoutParams generateLayoutParams( android.view.ViewGroup.LayoutParams p) { return new LayoutParams(p); } public static class LayoutParams extends MarginLayoutParams { public int gravity = -1; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.SlideGroup); gravity = ta.getInt(R.styleable.SlideGroup_layout_gravity, -1); ta.recycle(); } public LayoutParams(int width, int height) { this(width, height, -1); } public LayoutParams(int width, int height, int gravity) { super(width, height); this.gravity = gravity; } public LayoutParams(android.view.ViewGroup.LayoutParams source) { super(source); } public LayoutParams(MarginLayoutParams source) { super(source); } } /** * onInterceptTouchEvent()用來詢問是否要攔截處理。 onTouchEvent()是用來進行處理。 * <p/> * 例如:parentLayout----childLayout----childView 事件的分發流程: * parentLayout::onInterceptTouchEvent()---false?---> * childLayout::onInterceptTouchEvent()---false?---> * childView::onTouchEvent()---false?---> * childLayout::onTouchEvent()---false?---> parentLayout::onTouchEvent() * <p/> * <p/> * <p/> * 如果onInterceptTouchEvent()返回false,且分發的子View的onTouchEvent()中返回true, * 那麼onInterceptTouchEvent()將收到所有的後續事件。 * <p/> * 如果onInterceptTouchEvent()返回true,原本的target將收到ACTION_CANCEL,該事件 * 將會發送給我們自己的onTouchEvent()。 */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getActionMasked(); if (BuildConfig.DEBUG) Log.d("onInterceptTouchEvent", "action: " + action); if (action == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { // 該事件可能不是我們的 return false; } boolean isIntercept = false; switch (action) { case MotionEvent.ACTION_DOWN: // 如果動畫還未結束,則將此事件交給onTouchEvet()處理, // 否則,先分發給子View isIntercept = !mScroller.isFinished(); // 如果此時不攔截ACTION_DOWN時間,應該記錄下觸控地址及手指id,當我們決定攔截ACTION_MOVE的event時, // 將會需要這些初始資訊(因為我們的onTouchEvent將可能接收不到ACTION_DOWN事件) mPointerId = ev.getPointerId(0); // if (!isIntercept) { downX = x = ev.getX(); downY = y = ev.getY(); // } break; case MotionEvent.ACTION_MOVE: int pointerIndex = ev.findPointerIndex(mPointerId); if (BuildConfig.DEBUG) Log.d("onInterceptTouchEvent", "pointerIndex: " + pointerIndex + ", pointerId: " + mPointerId); float mx = ev.getX(pointerIndex); float my = ev.getY(pointerIndex); if (BuildConfig.DEBUG) Log.d("onInterceptTouchEvent", "action_move [touchSlop: " + mTouchSlop + ", deltaX: " + (x - mx) + ", deltaY: " + (y - my) + "]"); // 根據方向進行攔截,(其實這樣,如果我們的方向是水平的,裡面有一個ScrollView,那麼我們是支援巢狀的) if (Math.abs(y - my) >= mTouchSlop) { isIntercept = true; } //如果不攔截的話,我們不會更新位置,這樣可以通過累積小的移動距離來判斷是否達到可以認為是Move的閾值。 //這裡當產生攔截的話,會更新位置(這樣相當於損失了mTouchSlop的移動距離,如果不更新,可能會有一點點跳的感覺) if (isIntercept) { x = mx; y = my; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // 這是觸控的最後一個事件,無論如何都不會攔截 if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } break; case MotionEvent.ACTION_POINTER_UP: solvePointerUp(ev); break; } return isIntercept; } private void solvePointerUp(MotionEvent event) { // 獲取離開螢幕的手指的索引 int pointerIndexLeave = event.getActionIndex(); int pointerIdLeave = event.getPointerId(pointerIndexLeave); if (mPointerId == pointerIdLeave) { // 離開螢幕的正是目前的有效手指,此處需要重新調整,並且需要重置VelocityTracker int reIndex = pointerIndexLeave == 0 ? 1 : 0; mPointerId = event.getPointerId(reIndex); // 調整觸控位置,防止出現跳動 x = event.getX(reIndex); y = event.getY(reIndex); if (velocityTracker != null) velocityTracker.clear(); } } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); if (velocityTracker == null) { velocityTracker = VelocityTracker.obtain(); } velocityTracker.addMovement(event); switch (action) { case MotionEvent.ACTION_DOWN: // 獲取索引為0的手指id isMove = false; mPointerId = event.getPointerId(0); x = event.getX(); y = event.getY(); if (!mScroller.isFinished()) mScroller.abortAnimation(); break; case MotionEvent.ACTION_MOVE: isMove = true; // 獲取當前手指id所對應的索引,雖然在ACTION_DOWN的時候,我們預設選取索引為0 // 的手指,但當有第二個手指觸控,並且先前有效的手指up之後,我們會調整有效手指 // 螢幕上可能有多個手指,我們需要保證使用的是同一個手指的移動軌跡, // 因此此處不能使用event.getActionIndex()來獲得索引 final int pointerIndex = event.findPointerIndex(mPointerId); float mx = event.getX(pointerIndex); float my = event.getY(pointerIndex); moveBy((int) (x - mx), (int) (y - my)); x = mx; y = my; break; case MotionEvent.ACTION_UP: isMove = false; velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity); float velocityX = velocityTracker.getXVelocity(mPointerId); float velocityY = velocityTracker.getYVelocity(mPointerId); completeMove(-velocityX, -velocityY); if (velocityTracker != null) { velocityTracker.recycle(); velocityTracker = null; } break; case MotionEvent.ACTION_POINTER_UP: // 獲取離開螢幕的手指的索引 isMove = false; int pointerIndexLeave = event.getActionIndex(); int pointerIdLeave = event.getPointerId(pointerIndexLeave); if (mPointerId == pointerIdLeave) { // 離開螢幕的正是目前的有效手指,此處需要重新調整,並且需要重置VelocityTracker int reIndex = pointerIndexLeave == 0 ? 1 : 0; mPointerId = event.getPointerId(reIndex); // 調整觸控位置,防止出現跳動 x = event.getX(reIndex); y = event.getY(reIndex); if (velocityTracker != null) velocityTracker.clear(); } break; case MotionEvent.ACTION_CANCEL: isMove = false; break; } return true; } private Boolean isPull = false; //此處的moveBy是根據水平或是垂直排放的方向, //來選擇是水平移動還是垂直移動 public void moveBy(int deltaX, int deltaY) { if (BuildConfig.DEBUG) Log.d("moveBy", "deltaX: " + deltaX + " deltaY: " + deltaY); if (Math.abs(deltaY) >= Math.abs(deltaX)) { int mScrollY = getScrollY(); if (mScrollY <= 0) { scrollTo(0, 0); } else if (mScrollY >= getNScrollYButtom()) { scrollTo(0, getNScrollYButtom()); } else { scrollBy(0, deltaY); } if (isEnablePullDown && deltaY > 0 && mScrollY >= pullDownMin) { isPull = true; Log.d("onlayout", "isPull: true"); } } } private void completeMove(float velocityX, float velocityY) { int mScrollY = getScrollY(); int maxY = getScrollYButtom(); int minY = getScrollYTop(); if (mScrollY >= maxY) {//如果滾動,超過了 下邊界,就回彈到下邊界 if (isPull) {//滾動到最底部 mScroller.startScroll(0, mScrollY, 0, getNScrollYButtom() - mScrollY, 300); invalidate(); //顯示雷達 fooder_layout.showFooderRadar(); if (pullDownListem != null) { pullDownListem.onPullDown(); } Log.d("onlayout", "isPull: true 滾動到最底部,顯示出雷達"); } else { mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY); invalidate(); Log.d("onlayout", "isPull: true"); } } else if (mScrollY <= minY) {//如果滾動,超過了上邊界,就回彈到上邊界 // 超出了上邊界,彈回 mScroller.startScroll(0, mScrollY, 0, minY - mScrollY); invalidate(); } else if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {//大於1頁的時候 // mScroller.fling(0, mScrollY, 0, (int) (velocityY * 1.2f), 0, 0, minY, maxY); mScroller.fling(0, mScrollY, 0, (int) (velocityY * 2f), 0, 0, getNScrollYTop(), getNScrollYButtom()); invalidate(); } } public void ifNeedScrollBack() { int mScrollY = getScrollY(); int maxY = getScrollYButtom(); int minY = getScrollYTop(); if (mScrollY > maxY) { // 超出了下邊界,彈回 mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY); invalidate(); } else if (mScrollY < minY) { // 超出了上邊界,彈回 mScroller.startScroll(0, mScrollY, 0, minY - mScrollY); invalidate(); } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(0, mScroller.getCurrY()); postInvalidate(); } else { Log.d("onlayout", "computeScroll,isMove:"+isMove+",isPull:"+isPull); if (!isMove && !isPull) { ifNeedScrollBack(); } } } public void onPullSuccess() { soomToBack(); } public void soomToBack() { int mScrollY = getScrollY(); int maxY = getScrollYButtom(); Log.d("onlayout", "soomToBack: (maxY - mScrollY)="+(maxY - mScrollY)+",maxY="+maxY+",mScrollY="+mScrollY); // 超出了下邊界,彈回 mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY, 300); invalidate(); postDelayed(new Runnable() { @Override public void run() { fooder_layout.showFooderPull(); isPull = false; } }, 310); } private PullDownListem pullDownListem; public void setPullDownListem(PullDownListem pullDownListem) { this.pullDownListem = pullDownListem; } public interface PullDownListem { public void onPullDown(); } }