這是山寨UC瀏覽器的下拉重新整理效果的的結尾篇了,到這裡,基本是實現了UC瀏覽器首頁的效果了!還沒有看之前的小夥伴記得出門左轉先看看喲(Android 自定義View UC下拉重新整理效果(一)、Android 自定義View UC下拉重新整理效果(二))。期間也有不小的改動,主要集中在那個小圓球拖拽時的繪製方式上,可以看到,最後的圓球效果比之前的順暢漂亮了很多!!
經過前面的兩篇文章,分別從小球動畫和下拉重新整理兩個方面介紹了相關的內容,最後還剩首頁顯示過渡列表展示的內容了!效果說明:
- 1、向上滑動,背景和tab有個漸變效果
- 2、向下滑動,有一個放大和圓弧出現
功能拆分
- 1、展開關閉top預設值
因為這裡有兩種狀態,一種是展開的,一種是首頁的關閉狀態,展開的預設top是TabLayout
的對應高度加上自身的top值,而關閉時,預設top值是上面的CurveView
的高度加上自身的top值。 - 2、實現拖拽滑動效果
首先想到的就是ViewDragHelper
,使用它來控制相關的拖拽。 - 3、拖拽背景漸變效果
這個就是設定拖拽過程中相關的回撥。另外就是在首頁的狀態,ViewPager是沒法左右滑動的。 - 4、繪製下拉的弧度
這個就得使用到drawPath()
繪製貝塞爾曲線了。
相關物件介紹
父佈局是一個CurveLayout
,裡面包含三個物件:
1 2 3 4 |
// child views & helpers private View sheet;//target private ViewDragHelper sheetDragHelper; private ViewOffsetHelper sheetOffsetHelper; |
sheet就是我們的拖拽目標View
,ViewDragHelper
拖拽輔助類,寫好對應的事件處理和Callback就可以實現拖拽功能了!這裡不詳細介紹。ViewOffsetHelper
,對於它的介紹,可以看看下面的截圖:
因為我們這裡只涉及上下的移動,所以介紹以下主要方法:
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 |
//構造方法 public ViewOffsetHelper(View view) { mView = view; } //onlayoutChange時呼叫 public void onViewLayout() { // Grab the intended top & left mLayoutTop = mView.getTop(); mLayoutLeft = mView.getLeft(); // And offset it as needed updateOffsets(); } //View位置改變時呼叫該方法 public boolean setTopAndBottomOffset(int absoluteOffset) { if (mOffsetTop != absoluteOffset) { mOffsetTop = absoluteOffset; updateOffsets(); return true; } return false; } //同步 public void resyncOffsets() { mOffsetTop = mView.getTop() - mLayoutTop; mOffsetLeft = mView.getLeft() - mLayoutLeft; } //更新值 private void updateOffsets() { ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop)); ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft)); } |
展開、關閉的預設top值
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override public void addView(View child, int index, ViewGroup.LayoutParams params) { if (sheet != null) { throw new UnsupportedOperationException("CurveLayout must only have 1 child view"); } sheet = child; sheetOffsetHelper = new ViewOffsetHelper(sheet); sheet.addOnLayoutChangeListener(sheetLayout); // force the sheet contents to be gravity bottom. This ain't a top sheet. ((LayoutParams) params).gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; super.addView(child, index, params); } |
在addView()的方法中我們確定對應的Target,然後為其設定一個OnLayoutChangeListener
。
1 2 3 4 5 6 7 8 9 10 11 |
//設定預設的dismissTop值 public void setDismissOffset(int dismissOffset) { this.dismissOffset = currentTop + dismissOffset; } //設定預設的expandTop值 public void setExpandTopOffset(int tabOffset) { if (this.expandTopOffset != tabOffset) { this.expandTopOffset = tabOffset; sheetExpandedTop = currentTop + expandTopOffset; } } |
接下來看看OnLayoutChangeListener
裡面的相關邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private final OnLayoutChangeListener sheetLayout = new OnLayoutChangeListener() { @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { sheetExpandedTop = top + expandTopOffset; sheetBottom = bottom; currentTop = top; sheetOffsetHelper.onViewLayout(); // modal bottom sheet content should not initially be taller than the 16:9 keyline if (!initialHeightChecked) { applySheetInitialHeightOffset(false, -1); initialHeightChecked = true; } else if (!hasInteractedWithSheet && (oldBottom - oldTop) != (bottom - top)) { /* sheet height changed */ /* if the sheet content's height changes before the user has interacted with it then consider this still in the 'initial' state and apply the height constraint, but in this case, animate to it */ applySheetInitialHeightOffset(true, oldTop - sheetExpandedTop); } Log.e(TAG, "onLayoutChange: 佈局變化了!!" + sheet.getTop()); } }; |
初始化sheetExpandedTop
,currentTop
等欄位,並且呼叫上面提到的onViewLayout()
,同步ViewOffsetHelper
的值。
拖拽滑動實現
ViewDragHelper
就不多說了,Android自帶的輔助類,新增一個Callback,然後處理相關回撥方法就可以了!
判斷是否攔截處理事件:
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 |
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { currentX = ev.getRawX(); Log.e(TAG, "BottomSheet onInterceptTouchEvent: " + currentX); if (isExpanded()) { sheetDragHelper.cancel(); return false; } hasInteractedWithSheet = true; final int action = MotionEventCompat.getActionMasked(ev); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { sheetDragHelper.cancel(); return false; } return isDraggableViewUnder((int) ev.getX(), (int) ev.getY()) && (sheetDragHelper.shouldInterceptTouchEvent(ev)); } @Override public boolean onTouchEvent(MotionEvent ev) { currentX = ev.getRawX(); sheetDragHelper.processTouchEvent(ev); return sheetDragHelper.getCapturedView() != null || super.onTouchEvent(ev); } |
這裡獲取的這個currentX
是為了在下拉出現那個弧度的頂點。在接下來的回撥中會使用。
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 |
private final ViewDragHelper.Callback dragHelperCallbacks = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return child == sheet && !isExpanded();//是否可以拖拽 } @Override public int clampViewPositionVertical(View child, int top, int dy) { //豎直方向的值 return Math.min(Math.max(top, sheetExpandedTop), sheetBottom); } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return sheet.getLeft(); } @Override public int getViewVerticalDragRange(View child) { //豎直方向的拖拽範圍 return sheetBottom - sheetExpandedTop; } @Override public void onViewPositionChanged(View child, int left, int top, int dx, int dy) { // view的拖拽過程中 reverse = false; //change的過程中通知同步改變 sheetOffsetHelper.resyncOffsets(); dispatchPositionChangedCallback(); canUp = Math.abs(top - dismissOffset) > MIN_DRAG_DISTANCE; } @Override public void onViewReleased(View releasedChild, float velocityX, float velocityY) { //鬆手後 boolean expand = canUp || Math.abs(velocityY) > MIN_FLING_VELOCITY; reverse = false; animateSettle(expand ? sheetExpandedTop: dismissOffset, velocityY); } }; |
可以看到,在onViewPositionChanged()
的方法中會去呼叫resyncOffsets()
的方法同步ViewOffsetHelper
的對應值。
在onViewReleased()
的方法中呼叫了animateSettle()
的方法,兩種情況,一種是展開,一種是關閉(首頁的狀態),所以這裡有一個expand的變數來標識,如果展開,就展開到sheetExpandedTop
的高度,關閉的話,那麼就是到dismissOffset
的高度。
animateSettle()
方法最終執行以下方法邏輯:
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 |
private void animateSettle(int initialOffset, final int targetOffset, float initialVelocity) { if (settling) return; Log.e(TAG, "animateSettle:TopAndBottom :::" + sheetOffsetHelper.getTopAndBottomOffset()); if (sheetOffsetHelper.getTopAndBottomOffset() == targetOffset) { if (targetOffset >= dismissOffset) { dispatchDismissCallback(); } return; } settling = true; final boolean dismissing = targetOffset == dismissOffset; final long duration = computeSettleDuration(initialVelocity, dismissing); final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper, ViewOffsetHelper.OFFSET_Y, initialOffset, targetOffset); settleAnim.setDuration(duration); settleAnim.setInterpolator(getSettleInterpolator(dismissing, initialVelocity)); settleAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { dispatchPositionChangedCallback(); if (dismissing) { dispatchDismissCallback(); } settling = false; } }); if (callbacks != null && !callbacks.isEmpty()) { settleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (animation.getAnimatedFraction() > 0f) { dispatchPositionChangedCallback(); } } }); } settleAnim.start(); } |
這裡有一個settleAnim
的屬性動畫,傳入的是ViewOffsetHelper
裡面的OFFSET_Y
,在OFFSET_Y
的set()
方法中,呼叫setTopAndBottomOffset()
的方法去修改對應的top值,從而實現了鬆手後展開或者關閉的動畫效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper, ViewOffsetHelper.OFFSET_Y, initialOffset, targetOffset); public static final Property OFFSET_Y = AnimUtils.createIntProperty( new AnimUtils.IntProp("topAndBottomOffset") { @Override public void set(ViewOffsetHelper viewOffsetHelper, int offset) { viewOffsetHelper.setTopAndBottomOffset(offset); } @Override public int get(ViewOffsetHelper viewOffsetHelper) { return viewOffsetHelper.getTopAndBottomOffset(); } }); |
拖拽背景漸變效果
說到背景的漸變效果,那麼肯定就是要講相關的回撥了!Callbacks
用來處理對應的回撥,提供了三個方法:onSheetNarrowed()
,onSheetExpanded()
,onSheetPositionChanged()
,分別對應的時候關閉了,展開了,和改變了三種情況。
在onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted)
的方法中,有四個引數,分別是當前的top值,當前touch的x值,豎直方向的改變值,以及是否是由開到關或者由關到開的情況。
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 |
public static abstract class Callbacks { public void onSheetNarrowed() { } public void onSheetExpanded() { } public void onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted) { } } public void registerCallback(Callbacks callback) { if (callbacks == null) { callbacks = new CopyOnWriteArrayList(); } callbacks.add(callback); } public void unregisterCallback(Callbacks callback) { if (callbacks != null && !callbacks.isEmpty()) { callbacks.remove(callback); } } |
在具體是實現中是這樣的:
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 46 47 |
mBoottom.registerCallback(new CurveLayout.Callbacks() { private int dy; @Override public void onSheetExpanded() { Log.e(TAG, "onSheetExpanded: "); mCurveView.onDispatchUp(); mCurveView.setTranslationY(0); mCurveView.setVisibility(View.GONE); mTab.setTranslationY(-mCurveView.getHeight()); mTab.setVisibility(View.VISIBLE); mCurveView.setScaleX(1.f); mCurveView.setScaleY(1.f); mViewPager.setScrollable(true); dy = 0; } @Override public void onSheetNarrowed() { Log.e(TAG, "onSheetNarrowed: "); mCurveView.onDispatchUp(); mCurveView.setTranslationY(0); mCurveView.setScaleX(1.f); mCurveView.setScaleY(1.f); mTab.setVisibility(View.GONE); mViewPager.setScrollable(false); mCurveView.setVisibility(View.VISIBLE); dy = 0; } @Override public void onSheetPositionChanged(int sheetTop, float currentX, int ddy, boolean reverse) { if (mCurveViewHeight == 0) { mCurveViewHeight = mCurveView.getHeight(); mBoottom.setDismissOffset(mCurveViewHeight); } this.dy += ddy; float fraction = 1 - sheetTop * 1.0f / mCurveViewHeight; if (!reverse) { if (fraction >= 0 && !mBoottom.isExpanded()) {//向上拉 mTab.setVisibility(View.VISIBLE); mBoottom.setExpandTopOffset(mTab.getHeight()); mCurveView.setTranslationY(dy * 0.2f); mTab.setTranslationY(-fraction * (mCurveView.getHeight() + mTab.getHeight())); } else if (fraction |
可以看到,在onSheetPositionChanged()
的方法中,首先是進行了一些值的初始化,然後根據reverse來判斷,如果不是由開到關或者由關到開的狀態改變,那麼就開始背景的移動或者背景的放大及畫出對應的弧形。另外在onSheetNarrowed()
或者onSheetExpanded()
中就是對View
做的一些初始化或者重置操作!
繪製下拉的弧度
當是下拉的時候,需要繪製出弧形,這裡使用到了CurveView
以及它的onDispatch()
方法!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override protected void onDraw(Canvas canvas) { path.reset(); path.moveTo(0, getMeasuredHeight()); path.quadTo(currentX, currentY + getMeasuredHeight(), getWidth(), getMeasuredHeight()); canvas.drawPath(path, paint); } public void onDispatch(float dx, float dy) { currentY = dy > MAX_DRAG ? MAX_DRAG : dy; currentX = dx; if (dy > 0) { invalidate(); } } |
其實很簡單,就是使用當前的X值的座標和dy的值來進行drawPath()
的操作。當然這裡有一個上限的限制。
到這裡,實現拖拽展開及關閉的邏輯就實現完成了,總結起來就是使用ViewDragHelper
來輔助實現拖拽功能,在鬆手的時候呼叫ViewOffsetHelper
來實現展開或者關閉的漸變動畫效果,期間呼叫Callbacks
回撥對應的狀態(展開了、關閉了、位置變化了)。
圓球繪製邏輯改動
之前的第一篇文章中介紹的圓球拉伸繪製時採用的是drawArc()和drawPath結合的方法,所以看著總覺得有點兒怪,然後查了相關的資料,這裡使用了新的方式,請看圖:
意思就是一個圓形,可以理解為是採用了drawPath()
畫了四段弧。每段弧就是使用path.cubicTo()
繪製的貝塞爾曲線。
根據網上的資料,這裡的m的值就是半徑R*0.551915024494f。在豎直方向拖拽的過程中,其實就是改變這12個點的座標,從而繪製出想要的弧形。
專案下載:https://github.com/lovejjfg/UCPullRefresh
喜歡就請點個Start唄。。
參考資料
—- Edit By Joe —-
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式