這一次拆解的是今日頭條的關注頁面:點擊關注的頭像會彈出一個文章列表。在邊界拖拽會出現關閉提示。此次同時實現了Android端和IOS端的效果。git
效果以下圖: github
彈出來的頁面能夠左右切換,每一個頁面是單獨的列表,能上下滑動,因此這裏直接用viewPager+recycelrView實現。 當viewPager不能左右滑動的時候,移動整個viewPager,出現文字提示,當滑動距離超過閾值時,文字改變。 當手指鬆開時,若滑動距離未到達閾值,回彈;不然結束頁面。 一樣,當recyclerView在頂部不能滑動時,移動recyclerView,出現提示,後續跟viewPager一致故再也不贅述。canvas
這裏的回彈我自定義了一個回彈佈局,下面介紹一下回彈佈局的幾個重要方法: onInterceptTouchEvent()bash
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//記錄座標
break;
case MotionEvent.ACTION_MOVE:
int difX = (int) (ev.getX() - mDownX);
int difY = (int) (ev.getY() - mDownY);
if (orientation == LinearLayout.HORIZONTAL) {
.....
if (水平滑動) {
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右拉到邊界
return true;
}
if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到邊界
return true;
}
}
} else {
......
if (豎直滑動) {
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到邊界
return true;
}
if (!innerView.canScrollVertically(1) && difY < 0) {
//上拉到邊界
return true;
}
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
......重置變量
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
複製代碼
當控件方向爲橫向且滑動爲水平滑動時,檢測innerView可否在該方向上滑動;若不能,則攔截事件,交給自身處理(縱向同理)。 攔截事件後,在**onTouchEvent()**進行處理,實現移動和回彈。ide
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
if (orientation == LinearLayout.HORIZONTAL) {
int difX = (int) ((event.getX() - mDownX) / resistance);
boolean isRebound = false;
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右拉到邊界
isRebound = true;
} else if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到邊界
isRebound = true;
}
if (isRebound) {
//移動和回調
return true;
}
} else {
int difY = (int) ((event.getY() - mDownY) / resistance);
boolean isRebound = false;
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到邊界
isRebound = true;
} else if (!innerView.canScrollVertically(1) && difY < 0) {
//上拉到邊界
isRebound = true;
}
if (isRebound) {
//移動和回調
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (orientation == LinearLayout.HORIZONTAL) {
int difX = (int) innerView.getTranslationX();
if (difX != 0) {
if (Math.abs(difX) <= resetDistance || isNeedReset) {
innerView.animate().translationX(0).setDuration(mDuration).setInterpolator(mInterpolator);
}
//回調
}
} else {
int difY = (int) innerView.getTranslationY();
if (difY != 0) {
if (Math.abs(difY) <= resetDistance || isNeedReset) {
innerView.animate().translationY(0).setDuration(mDuration).setInterpolator(mInterpolator);
}
//回調
}
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
複製代碼
MOVE事件 利用**setTranslationX()和setTranslationY()**改變innerView的位置,同時將滑動距離和方向經過接口回調到外面。佈局
UP事件 判斷滑動距離是否小於閾值,小於則執行回彈動畫;同時回調到外面。 以上就是回彈佈局的簡單實現,主要是對滑動事件進行攔截處理,若是不清楚事件傳遞機制能夠到這裏查看。 佈局有3個自定義屬性動畫
<declare-styleable name="ReBoundLayout">
<attr name="reBoundOrientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<attr name="resistance" format="float" />
<attr name="reBoundDuration" format="integer" />
</declare-styleable>
複製代碼
分別是:回彈方向、阻力系數、回彈時間,剩餘屬性能夠調用**set()**方法修改。ui
好了,如今回彈實現了,接下來就是將文字提示加上,結束動畫加上。這裏有一點須要注意的是:demo中使用的是reBoundLayout+viewPager+fragment(reBoundLayout+recyclerView)的結構實現的。而文字是跟viewPager同一層級的,因此須要把fragment的回調回調到activity裏(也能夠getActivity()獲取對應的文字控件),詳見代碼。 如下是回調的僞代碼:spa
@Override
public void onDistanceChange(int distance, int direction) {
switch (direction) {
case DIRECTION_LEFT:
if (distance > showTipDistance) {
//文字改變,移動
} else {
rightTip.setVisibility(View.GONE);
}
break;
case DIRECTION_RIGHT:
if (distance > showTipDistance) {
//文字改變,移動
} else {
leftTip.setVisibility(View.GONE);
}
break;
case DIRECTION_UP:
break;
case DIRECTION_DOWN:
//fragment的回調會走到這裏
if (distance > showTipDistance) {
//文字改變,移動
} else {
topTip.setVisibility(View.GONE);
}
break;
default:
break;
}
}
@Override
public void onFingerUp(int distance, int direction) {
switch (direction) {
case DIRECTION_LEFT:
if (distance > mResetDistance) {
//結束頁面
} else {
//文字重置
}
break;
case DIRECTION_RIGHT:
if (distance > mResetDistance) {
//結束頁面
} else {
//文字重置
}
break;
case DIRECTION_DOWN:
if (distance > mResetDistance) {
//結束頁面
} else {
//文字重置
}
break;
default:
break;
}
}
複製代碼
效果以下: .net
PS:若是想圓心跟隨手指移動,須要增長如下計算:圓最大半徑、圓可移動距離與半徑變化關係
關鍵變量:
事件攔截跟ReBoundLayout一致,因此不贅述,主要看看滑動事件的處理
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
int difX = (int) ((event.getX() - mDownX) / resistance);
int difY = (int) ((event.getY() - mDownY) / resistance);
if (orientation == LinearLayout.HORIZONTAL) {
boolean needDrag = false;
if (!innerView.canScrollHorizontally(-1) && difX > 0) {
//右啦到邊界
needDrag = true;
} else if (!innerView.canScrollHorizontally(1) && difX < 0) {
//左拉到邊界
needDrag = true;
}
if (needDrag) {
//半徑計算
mTranslationX = difX;
mTranslationY = difY;
invalidate();
//回調
return true;
}
} else {
if (!innerView.canScrollVertically(-1) && difY > 0) {
//下拉到邊界
//回調
return true;
} else if (!innerView.canScrollVertically(1) && difY < 0) {
//上啦到邊界
innerView.setTranslationY(difY);
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (orientation == LinearLayout.HORIZONTAL) {
//水平
if (Math.abs(mTranslationX) >= resetDistance) {
//回調
} else {
//重置狀態
}
} else {
//豎直
if (innerView.getTranslationY() < 0) {
innerView.animate().setDuration(mDuration).translationY(0).setInterpolator(mInterpolator);
} else {
//回調
}
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
複製代碼
這裏跟ReBoundLayout有如下幾點區別:
PS:DragZoomLayout必定要設置背景,否則調用invalidate()會無效;上下滑動的mTranslationX、mTranslationY一直都是0(由於下滑咱們已經回調給最層的DragZoomLayout),因此在ACTION_UP、ACTION_CANCEL事件,豎直方向回調時是使用當前事件的x、y跟點擊的x、y相減的值去回調。
@Override
protected void onDraw(Canvas canvas) {
if (Math.abs(mTranslationX) > mLargeX) {
mTranslationX = mTranslationX > 0 ? mLargeX : -mLargeX;
}
if (Math.abs(mTranslationY) > mLargeY) {
mTranslationY = mTranslationY > 0 ? mLargeY : -mLargeY;
}
canvas.translate(mTranslationX, mTranslationY);
mPath.reset();
mPath.addCircle(mPoint.x, mPoint.y, mRadius, Path.Direction.CCW);
canvas.clipPath(mPath);
super.onDraw(canvas);
}
複製代碼
進行了一些位置和半徑的限制。 佈局完成,接下來處理頁面間的接口回調及結束動畫
動畫的計算有一點點麻煩,數學很差的同窗請多看幾遍,仍是不懂的趁着過年回高中找數學老師要回學費吧。
這是年前最後一篇博客了,今年立的flag好像都沒有實現,跟大佬的差距仍是那麼大,Bug仔仍需努力呀。