這是山寨UC瀏覽器的下拉刷新效果的的結(jié)尾篇了观话,到這里,基本是實現(xiàn)了UC瀏覽器首頁的效果了越平!還沒有看之前的小伙伴記得出門左轉(zhuǎn)先看看喲(Android 自定義View UC下拉刷新效果(一)频蛔、Android 自定義View UC下拉刷新效果(二))。期間也有不小的改動秦叛,主要集中在那個小圓球拖拽時的繪制方式上晦溪,可以看到,最后的圓球效果比之前的順暢漂亮了很多U醢稀三圆!
經(jīng)過前面的兩篇文章,分別從小球動畫和下拉刷新兩個方面介紹了相關(guān)的內(nèi)容避咆,最后還剩首頁顯示過渡列表展示的內(nèi)容了舟肉!效果說明:
- 1、向上滑動牌借,背景和tab有個漸變效果
- 2度气、向下滑動,有一個放大和圓弧出現(xiàn)
功能拆分
- 1膨报、展開關(guān)閉top默認(rèn)值
因為這里有兩種狀態(tài)磷籍,一種是展開的,一種是首頁的關(guān)閉狀態(tài)现柠,展開的默認(rèn)top是TabLayout
的對應(yīng)高度加上自身的top值院领,而關(guān)閉時,默認(rèn)top值是上面的CurveView
的高度加上自身的top值够吩。 - 2比然、實現(xiàn)拖拽滑動效果
首先想到的就是ViewDragHelper
,使用它來控制相關(guān)的拖拽周循。 - 3强法、拖拽背景漸變效果
這個就是設(shè)置拖拽過程中相關(guān)的回調(diào)万俗。另外就是在首頁的狀態(tài),ViewPager是沒法左右滑動的饮怯。 - 4闰歪、繪制下拉的弧度
這個就得使用到drawPath()
繪制貝塞爾曲線了。
相關(guān)對象介紹
父布局是一個CurveLayout
,里面包含三個對象:
// child views & helpers
private View sheet;//target
private ViewDragHelper sheetDragHelper;
private ViewOffsetHelper sheetOffsetHelper;
sheet就是我們的拖拽目標(biāo)View
蓖墅,ViewDragHelper
拖拽輔助類库倘,寫好對應(yīng)的事件處理和Callback就可以實現(xiàn)拖拽功能了!這里不詳細(xì)介紹论矾。ViewOffsetHelper
,對于它的介紹教翩,可以看看下面的截圖:
因為我們這里只涉及上下的移動,所以介紹以下主要方法:
//構(gòu)造方法
public ViewOffsetHelper(View view) {
mView = view;
}
//onlayoutChange時調(diào)用
public void onViewLayout() {
// Grab the intended top & left
mLayoutTop = mView.getTop();
mLayoutLeft = mView.getLeft();
// And offset it as needed
updateOffsets();
}
//View位置改變時調(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()的方法中我們確定對應(yīng)的Target饱亿,然后為其設(shè)置一個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: 布局變化了A认路捧!" + sheet.getTop());
}
};
初始化sheetExpandedTop
,currentTop
等字段,并且調(diào)用上面提到的onViewLayout()
传黄,同步ViewOffsetHelper
的值。
拖拽滑動實現(xiàn)
ViewDragHelper
就不多說了队寇,Android自帶的輔助類膘掰,添加一個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);
}
這里獲取的這個currentX
是為了在下拉出現(xiàn)那個弧度的頂點识埋。在接下來的回調(diào)中會使用。
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()
的方法中會去調(diào)用resyncOffsets()
的方法同步ViewOffsetHelper
的對應(yīng)值窒舟。
在onViewReleased()
的方法中調(diào)用了animateSettle()
的方法,兩種情況诵盼,一種是展開惠豺,一種是關(guān)閉(首頁的狀態(tài)),所以這里有一個expand的變量來標(biāo)識风宁,如果展開洁墙,就展開到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();
}
這里有一個settleAnim
的屬性動畫,傳入的是ViewOffsetHelper
里面的OFFSET_Y
,在OFFSET_Y
的set()
方法中饮寞,調(diào)用setTopAndBottomOffset()
的方法去修改對應(yīng)的top值孝扛,從而實現(xiàn)了松手后展開或者關(guān)閉的動畫效果列吼。
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
用來處理對應(yīng)的回調(diào)冈欢,提供了三個方法:onSheetNarrowed()
,onSheetExpanded()
,onSheetPositionChanged()
,分別對應(yīng)的時候關(guān)閉了,展開了盈简,和改變了三種情況凑耻。
在onSheetPositionChanged(int sheetTop, float currentX, int dy, boolean userInteracted)
的方法中,有四個參數(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);
}
}
在具體是實現(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)改變瓢谢,那么就開始背景的移動或者背景的放大及畫出對應(yīng)的弧形畸写。另外在onSheetNarrowed()
或者onSheetExpanded()
中就是對View
做的一些初始化或者重置操作!
繪制下拉的弧度
當(dāng)是下拉的時候氓扛,需要繪制出弧形枯芬,這里使用到了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();
}
}
其實很簡單采郎,就是使用當(dāng)前的X值的坐標(biāo)和dy的值來進(jìn)行drawPath()
的操作千所。當(dāng)然這里有一個上限的限制。
到這里蒜埋,實現(xiàn)拖拽展開及關(guān)閉的邏輯就實現(xiàn)完成了淫痰,總結(jié)起來就是使用ViewDragHelper
來輔助實現(xiàn)拖拽功能,在松手的時候調(diào)用ViewOffsetHelper
來實現(xiàn)展開或者關(guān)閉的漸變動畫效果整份,期間調(diào)用Callbacks
回調(diào)對應(yīng)的狀態(tài)(展開了待错、關(guān)閉了、位置變化了)皂林。
圓球繪制邏輯改動
之前的第一篇文章中介紹的圓球拉伸繪制時采用的是drawArc()和drawPath結(jié)合的方法朗鸠,所以看著總覺得有點兒怪,然后查了相關(guān)的資料础倍,這里使用了新的方式烛占,請看圖:
意思就是一個圓形,可以理解為是采用了drawPath()
畫了四段弧。每段弧就是使用path.cubicTo()
繪制的貝塞爾曲線忆家。
根據(jù)網(wǎng)上的資料犹菇,這里的m的值就是半徑R*0.551915024494f。在豎直方向拖拽的過程中芽卿,其實就是改變這12個點的坐標(biāo)揭芍,從而繪制出想要的弧形。
項目下載:https://github.com/lovejjfg/UCPullRefresh
喜歡就請點個Start唄卸例。称杨。
參考資料
1、Plaid項目
2筷转、三次貝塞爾曲線練習(xí)之彈性的圓
---- Edit By Joe ----