這是山寨UC瀏覽器的下拉刷新效果的的結(jié)尾篇了,到這里,基本是實(shí)現(xiàn)了UC瀏覽器首頁的效果了!還沒有看之前的小伙伴記得出門左轉(zhuǎn)先看看喲(Android 自定義View UC下拉刷新效果(一)、Android 自定義View UC下拉刷新效果(二))。期間也有不小的改動(dòng),主要集中在那個(gè)小圓球拖拽時(shí)的繪制方式上,可以看到,最后的圓球效果比之前的順暢漂亮了很多?。?/p>




經(jīng)過前面的兩篇文章,分別從小球動(dòng)畫和下拉刷新兩個(gè)方面介紹了相關(guān)的內(nèi)容,最后還剩首頁顯示過渡列表展示的內(nèi)容了!效果說明:
- 1、向上滑動(dòng),背景和tab有個(gè)漸變效果
- 2、向下滑動(dòng),有一個(gè)放大和圓弧出現(xiàn)
功能拆分
- 1、展開關(guān)閉top默認(rèn)值
因?yàn)檫@里有兩種狀態(tài),一種是展開的,一種是首頁的關(guān)閉狀態(tài),展開的默認(rèn)top是TabLayout的對(duì)應(yīng)高度加上自身的top值,而關(guān)閉時(shí),默認(rèn)top值是上面的CurveView的高度加上自身的top值。 - 2、實(shí)現(xiàn)拖拽滑動(dòng)效果
首先想到的就是ViewDragHelper,使用它來控制相關(guān)的拖拽。 - 3、拖拽背景漸變效果
這個(gè)就是設(shè)置拖拽過程中相關(guān)的回調(diào)。另外就是在首頁的狀態(tài),ViewPager是沒法左右滑動(dòng)的。 - 4、繪制下拉的弧度
這個(gè)就得使用到drawPath()繪制貝塞爾曲線了。
相關(guān)對(duì)象介紹
父布局是一個(gè)CurveLayout,里面包含三個(gè)對(duì)象:
// child views & helpers
private View sheet;//target
private ViewDragHelper sheetDragHelper;
private ViewOffsetHelper sheetOffsetHelper;
sheet就是我們的拖拽目標(biāo)View,ViewDragHelper拖拽輔助類,寫好對(duì)應(yīng)的事件處理和Callback就可以實(shí)現(xiàn)拖拽功能了!這里不詳細(xì)介紹。ViewOffsetHelper,對(duì)于它的介紹,可以看看下面的截圖:

因?yàn)槲覀冞@里只涉及上下的移動(dòng),所以介紹以下主要方法:
//構(gòu)造方法
public ViewOffsetHelper(View view) {
mView = view;
}
//onlayoutChange時(shí)調(diào)用
public void onViewLayout() {
// Grab the intended top & left
mLayoutTop = mView.getTop();
mLayoutLeft = mView.getLeft();
// And offset it as needed
updateOffsets();
}
//View位置改變時(shí)調(diào)用該方法
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));
}
展開、關(guān)閉的默認(rèn)top值
@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()的方法中我們確定對(duì)應(yīng)的Target,然后為其設(shè)置一個(gè)OnLayoutChangeListener。
//設(shè)置默認(rèn)的dismissTop值
public void setDismissOffset(int dismissOffset) {
this.dismissOffset = currentTop + dismissOffset;
}
//設(shè)置默認(rèn)的expandTop值
public void setExpandTopOffset(int tabOffset) {
if (this.expandTopOffset != tabOffset) {
this.expandTopOffset = tabOffset;
sheetExpandedTop = currentTop + expandTopOffset;
}
}
接下來看看OnLayoutChangeListener里面的相關(guān)邏輯:
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等字段,并且調(diào)用上面提到的onViewLayout(),同步ViewOffsetHelper的值。
拖拽滑動(dòng)實(shí)現(xiàn)
ViewDragHelper就不多說了,Android自帶的輔助類,添加一個(gè)Callback,然后處理相關(guān)回調(diào)方法就可以了!
判斷是否攔截處理事件:
@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);
}
這里獲取的這個(gè)currentX是為了在下拉出現(xiàn)那個(gè)弧度的頂點(diǎn)。在接下來的回調(diào)中會(huì)使用。
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()的方法中會(huì)去調(diào)用resyncOffsets()的方法同步ViewOffsetHelper的對(duì)應(yīng)值。
在onViewReleased()的方法中調(diào)用了animateSettle()的方法,兩種情況,一種是展開,一種是關(guān)閉(首頁的狀態(tài)),所以這里有一個(gè)expand的變量來標(biāo)識(shí),如果展開,就展開到sheetExpandedTop的高度,關(guān)閉的話,那么就是到dismissOffset的高度。
animateSettle()方法最終執(zhí)行以下方法邏輯:
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();
}
這里有一個(gè)settleAnim的屬性動(dòng)畫,傳入的是ViewOffsetHelper里面的OFFSET_Y,在OFFSET_Y的set()方法中,調(diào)用setTopAndBottomOffset()的方法去修改對(duì)應(yīng)的top值,從而實(shí)現(xiàn)了松手后展開或者關(guān)閉的動(dòng)畫效果。
final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper,
ViewOffsetHelper.OFFSET_Y,
initialOffset,
targetOffset);
public static final Property<ViewOffsetHelper, Integer> OFFSET_Y =
AnimUtils.createIntProperty(
new AnimUtils.IntProp<ViewOffsetHelper>("topAndBottomOffset") {
@Override
public void set(ViewOffsetHelper viewOffsetHelper, int offset) {
viewOffsetHelper.setTopAndBottomOffset(offset);
}
@Override
public int get(ViewOffsetHelper viewOffsetHelper) {
return viewOffsetHelper.getTopAndBottomOffset();
}
});
拖拽背景漸變效果
說到背景的漸變效果,那么肯定就是要講相關(guān)的回調(diào)了!Callbacks用來處理對(duì)應(yīng)的回調(diào),提供了三個(gè)方法:onSheetNarrowed(),onSheetExpanded(),onSheetPositionChanged(),分別對(duì)應(yīng)的時(shí)候關(guān)閉了,展開了,和改變了三種情況。
在onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted)的方法中,有四個(gè)參數(shù),分別是當(dāng)前的top值,當(dāng)前touch的x值,豎直方向的改變值,以及是否是由開到關(guān)或者由關(guān)到開的情況。
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);
}
}
在具體是實(shí)現(xiàn)中是這樣的:
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 < 0 && !mBoottom.isExpanded()) {//向下拉
mTab.setVisibility(View.GONE);
mCurveView.onDispatch(currentX, dy);
mCurveView.setScaleX(1 - fraction * 0.5f);
mCurveView.setScaleY(1 - fraction * 0.5f);
}
}
}
});
可以看到,在onSheetPositionChanged()的方法中,首先是進(jìn)行了一些值的初始化,然后根據(jù)reverse來判斷,如果不是由開到關(guān)或者由關(guān)到開的狀態(tài)改變,那么就開始背景的移動(dòng)或者背景的放大及畫出對(duì)應(yīng)的弧形。另外在onSheetNarrowed()或者onSheetExpanded()中就是對(duì)View做的一些初始化或者重置操作!
繪制下拉的弧度
當(dāng)是下拉的時(shí)候,需要繪制出弧形,這里使用到了CurveView以及它的onDispatch()方法!
@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();
}
}
其實(shí)很簡單,就是使用當(dāng)前的X值的坐標(biāo)和dy的值來進(jìn)行drawPath()的操作。當(dāng)然這里有一個(gè)上限的限制。
到這里,實(shí)現(xiàn)拖拽展開及關(guān)閉的邏輯就實(shí)現(xiàn)完成了,總結(jié)起來就是使用ViewDragHelper來輔助實(shí)現(xiàn)拖拽功能,在松手的時(shí)候調(diào)用ViewOffsetHelper來實(shí)現(xiàn)展開或者關(guān)閉的漸變動(dòng)畫效果,期間調(diào)用Callbacks回調(diào)對(duì)應(yīng)的狀態(tài)(展開了、關(guān)閉了、位置變化了)。
圓球繪制邏輯改動(dòng)
之前的第一篇文章中介紹的圓球拉伸繪制時(shí)采用的是drawArc()和drawPath結(jié)合的方法,所以看著總覺得有點(diǎn)兒怪,然后查了相關(guān)的資料,這里使用了新的方式,請(qǐng)看圖:

意思就是一個(gè)圓形,可以理解為是采用了drawPath()畫了四段弧。每段弧就是使用path.cubicTo()繪制的貝塞爾曲線。
根據(jù)網(wǎng)上的資料,這里的m的值就是半徑R*0.551915024494f。在豎直方向拖拽的過程中,其實(shí)就是改變這12個(gè)點(diǎn)的坐標(biāo),從而繪制出想要的弧形。
項(xiàng)目下載:https://github.com/lovejjfg/UCPullRefresh
喜歡就請(qǐng)點(diǎn)個(gè)Start唄。。
參考資料
1、Plaid項(xiàng)目
2、三次貝塞爾曲線練習(xí)之彈性的圓
---- Edit By Joe ----