啦啦啦,這是山寨UC瀏覽器的下拉重新整理效果的第二篇,第一篇請移步Android 自定義View UC下拉重新整理效果(一)
我們看圖說話:
主要工作
1.下拉重新整理的圓形向回首頁的圓形的過度以及返回的效果。
2.View的事件分發等等。
3.相關介面回撥。
對於第一塊,就是這個切換是的效果,其實在Android drawPath實現QQ拖拽泡泡我的第一篇文章中就講了,主要就是使用貝塞爾曲線來實現的。
只是這裡我試著使用了四階的貝塞爾曲線,因為控制點如果就一個的話,看起來有時候會覺得那個弧度拉得特別的尖,一點都不好看,而且我山寨的這個效果也沒有UC的那個那麼帥氣,可能還需要做相關的改進,如果你有好的點子請記得給我留言,一起完善嘛!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private void drawSecPath(Canvas canvas) { path.reset(); path.moveTo((float) (secondRectf.centerX() + Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top))); path.cubicTo(secondRectf.centerX() - 10*density, secondRectf.centerY() - backpaths, secondRectf.centerX() + 10*density, secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top))); //path.quadTo(secondRectf.centerX(), secondRectf.centerY() - backpaths, (float) (secondRectf.centerX() - Math.cos(180 / Math.PI * 30) * (secondRectf.centerX() - secondRectf.left)), (float) (secondRectf.centerY() + Math.sin(180 / Math.PI * 30) * (secondRectf.centerY() - secondRectf.top))); canvas.drawArc(secondRectf, 0, 360, true, secPaint); canvas.drawPath(path, secPaint); //drawArc(canvas); } private void drawFirstPath(Canvas canvas) { path.reset(); path.moveTo((float) (outRectF.centerX() - Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top))); //path.quadTo(outRectF.centerX(), outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top))); path.cubicTo(outRectF.centerX() + 10 * density, outRectF.centerY() + paths, outRectF.centerX() - 10 * density, outRectF.centerY() + paths, (float) (outRectF.centerX() + Math.cos(180 / Math.PI * 30) * (outRectF.centerX() - outRectF.left)), (float) (outRectF.centerY() - Math.sin(180 / Math.PI * 30) * (outRectF.centerY() - outRectF.top))); canvas.drawArc(outRectF, 0, 360, true, paint); canvas.drawPath(path, paint); drawArc(canvas); } |
這裡兩個控制點的偏移量是寫死的,而不是根據圓形的size的百分比計算出來的,所以如果你修改了圓形的半徑,那麼這裡可能會出現小小的問題,需要手動完善下!
下拉重新整理
其實現在的下拉重新整理也是爛大街的,就我現在理解的下拉重新整理其實有兩種模式了,一種是之前的寫好一個頭佈局在那個HeaderLayout
中,然後margin將其隱藏掉,然後在下拉的時候攔截相關事件,決定是否應該讓Header顯示出來。攔截的條件就是子View(ListView ScrollView RecycleView等等是否在頂部了而且手勢是向下拉(dy
今天我們不說這種下拉,而是介紹Google在Android5.0(希望我沒有記錯 )提供的巢狀滑動的新機制
向下相容的問題
從API21(就是5.0開始),ViewParent
的介面裡面多了onStartNestedScroll()
、onStopNestedScroll()
等等的方法!當然,對應的ViewGroup
中也有了這些方法,目測是空實現,因為它實現了這個介面嘛。那麼問題來了,如果你要向下相容腫麼辦呢?!
這裡有supportV4包來提供向下相容,不會寫不懂這玩意兒不著急,想想Android新的控制元件(RecycleView SwipeRefreshLayout NestedScrollView)這些都是支援巢狀滑動滴。。
相關介面方法
NestedScrollingParent
和NestedScrollingChild
這兩個介面就是用來實現相關的向下相容的方法滴。。
This interface should be implemented by
ViewGroup
subclasses that wish to support scrolling operations delegated by a nested child view.
Classes implementing this interface should create a final instance of aNestedScrollingParentHelper
as a field and delegate any View orViewGroup
methods to theNestedScrollingParentHelper
methods of the same signature.
Views invoking nested scrolling functionality should always do so from the relevantViewCompat
,ViewGroupCompat
orViewParentCompat
compatibility shim static methods. This ensures interoperability with nested scrolling views on Android 5.0 Lollipop and newer.
這個是NestedScrollingParent
自己的一番解釋,可以明確知道,在5.0或者更新的,什麼ViewCompat
等就提供了相關支援了(這個就是前面我說的那個嘛!),然後相容的話,就要用這個,而且還要使用一個叫NestedScrollingParentHelper
的輔助類來統一處理一些東西。
然後是不是感覺要嗶了狗了,這麼多方法要實現?!其實我也是醉醉的,然後打算抄抄別人的就好了!
1 2 3 4 5 6 7 |
private final NestedScrollingParentHelper mNestedScrollingParentHelper; private final NestedScrollingChildHelper mNestedScrollingChildHelper; //初始化兩個helper mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); setNestedScrollingEnabled(true); |
然後各種實現的方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@Override public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { return isEnabled() && canChildScrollUp() && !mReturningToStart && !mRefreshing && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedScrollAccepted(View child, View target, int axes) { // Reset the counter of how much leftover scroll needs to be consumed. mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); // Dispatch up to the nested parent startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); mTotalUnconsumed = 0; mNestedScrollInProgress = true; } @Override public boolean hasNestedScrollingParent() { return mNestedScrollingChildHelper.hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return mNestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean onNestedPreFling(View target, float velocityX, float velocityY) { return dispatchNestedPreFling(velocityX, velocityY); } @Override public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { return dispatchNestedFling(velocityX, velocityY, consumed); } ....... |
新的巢狀滑動的分發機制:
1 2 3 4 5 |
子View parent startNestedScroll ---> onStartNestedScroll、onNestedScrollAccepted dispatchNestedPreScroll ---> onNestedPreScroll dispatchNestedScroll ---> onNestedScroll stopNestedScroll ---> onStopNestedScroll |
所以說並不是很複雜,其實就是在以前的事件分發的基礎上給父View提供了一個消費事件的機會,以前的話,誰接受了DOWN事件,那麼之後所有的事件都會交給它處理,直到它不處理的時候才會又依次返回給父View,或者直到新的DOWN事件開始分發。
巢狀滑動的意思就是在子View處理相關事件的時候,可以根據情況反饋給父View,然後根據父View處理的結果再進行下一步的處理!
RecycleView
實現了NestedScrollingChild
,在TouchEvet()中有以下邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
switch (action) { case MotionEvent.ACTION_DOWN: { ..... startNestedScroll(nestedScrollAxis); } break; case MotionEvent.ACTION_MOVE: { if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) { dx -= mScrollConsumed[0]; dy -= mScrollConsumed[1]; vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); // Updated the nested offsets mNestedOffsets[0] += mScrollOffset[0]; mNestedOffsets[1] += mScrollOffset[1]; } ..... } break; case MotionEvent.ACTION_UP: { resetTouch(); } break; ...... } private void resetTouch() { if (mVelocityTracker != null) { mVelocityTracker.clear(); } stopNestedScroll(); releaseGlows(); } |
根據上面的程式碼可以看出onNestedPreScroll()
,這個就是在子View還沒有滑動之前會先走的,如果父View有相關消費,那麼子View會計算出父View消費的偏移量,繼續消費剩餘的偏移量。而在子View的消費的過程中,它會計算出過程中並沒有消費的偏移量。
1 2 3 4 |
if (y != 0) { consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); unconsumedY = y - consumedY; } |
然後回撥dispatchNestedScroll
,父View就可以在onNestedScroll()
中進行處理了!
最後在UP或者CANCLE事件中,子View會stopNestedScroll()
,然後父View就走到了onStopNestedScroll()
。整個巢狀滑動到此結束!
具體實現
1.下拉的時候展現頭佈局
這裡其實就是走onNestedScroll()
,因為這個時候子View已經在頂部了,向下拉的dy偏移量它肯定消費不了,所以在onNestedScroll()
中unconsumedY就是父View需要消費的。
2.下拉的過程中又開始向上滑動
這裡就需要注意了,這個時候,父View和子View都可以響應和消費對應的事件的,因為他們現在都是可以向上滑動的,但是這裡必須要父View優先消費事件,所以這裡就要在onNestedPreScroll()中做相關的處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { // if we're in a drag gesture and the user reverses up the we should take those events if (!header.ismRunning() && dy > 0 && totalDrag > defaulTranslationY) { Log.e(TAG, "onNestedPreScroll:消費 " + dy); updateOffset(dy); consumed[1] = dy;//通知子View我已經消費的偏移量 } } @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (!header.ismRunning() && dyUnconsumed |
OK,到這裡,巢狀滑動就基本好了!接下來就是控制頭佈局的展現了!這裡就是直接讓子View向下移動,頭佈局自然就出現了!然後將相關偏移量傳到之前的TouchCircleView
中,完成相關動畫!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private void updateOffset(int dyUnconsumed) { totalDrag -= dyUnconsumed * 0.5; Log.i(TAG, "updateOffset: " + totalDrag); if (totalDrag header.getHeight() * 1.5) { totalDrag = header.getHeight() * 1.5f; } if (targetView != null) { targetView.setTranslationY(totalDrag); } if (!header.ismRunning()) { header.handleOffset((int) (totalDrag)); } } |
相關方法及回撥
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//設定為重新整理的loading狀態 public void setRefresh(boolean refresh) { if (mRefresh == refresh) { return; } mRefresh = refresh; header.setRefresh(mRefresh); } //重新整理失敗狀態 public void setRefreshError() { header.setRefreshError(); } //重新整理成功狀態 public void setRefreshSuccess() { header.setRefreshSuccess(); } mHeader.addLoadingListener(new TouchCircleView.OnLoadingListener() { @Override public void onProgressStateChange(int state, boolean hide) { //狀態改變 } @Override public void onProgressLoading() { //正在loading 載入相關資料! } }); |
相關Demo請移步我的github。。。!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式