前言
Android 事件分發(fā)實例之右滑結(jié)束Activity(一)
Android 事件分發(fā)實例之右滑結(jié)束Activity(二)
前兩篇主要是介紹通過處理滑動事件實現(xiàn)右滑結(jié)束Activity,功能簡單,并且存在諸多不足之處。在思考之后,遂想實現(xiàn)在不同方向上的滑動,并且加入背景漸變、背景縮放、組合方向上滑動、支持多觸點、沉浸式系統(tǒng)狀態(tài)欄等。通過自定義屬性,設(shè)置不同的方向組合、背景屬性等實現(xiàn)不同方向組合的滑動,通過組合總有15種類型的組合滑動方式,可通過在XML中設(shè)置屬性或者動態(tài)設(shè)置屬性的方式,組合不同的屬性,實現(xiàn)不同的效果。遂起名為SuperSlideLayout,其內(nèi)部依然是通過Scroller實現(xiàn)內(nèi)容滑動,通過判斷滑動條件以及滑動狀態(tài),重寫攔截和消費事件以及解決事件沖突問題,處理滑動事件,實現(xiàn)滑動效果。所有特效都是使用APP過程發(fā)現(xiàn)的,SuperSlideLayout還可實現(xiàn)的更多的效果,下列的特效均為市面APP常見效果,之后還會陸續(xù)實現(xiàn)更多的特效。
事件分發(fā)實例之SuperSlideLayout
實現(xiàn)效果
側(cè)滑結(jié)束Activity
可自由組合實現(xiàn)多種側(cè)滑結(jié)束Activity的效果,適配系統(tǒng)多種ViewGroup

上下滑動圖集
此效果模仿今日頭條的圖集瀏覽功能,剛工作那會就感覺今日頭條的圖集瀏覽功能真贊。右滑過程中,window背景漸變?yōu)橥该?,?dāng)左右上滑過程,window背景設(shè)置為透明,Activity中除了Viewpager部分內(nèi)容(不是背景)漸變透明。效果如下:


底部彈出框
很多應(yīng)用都會有底部可拖拽彈出框,基本都是采用BottomSheetDialog來實現(xiàn)的,效果極佳,但是也有不足之處,就是默認(rèn)情況下如果子視圖過多可滾到頂部,有時候可能并不需要滾到頂部,但是系統(tǒng)并沒有直接設(shè)置的方法,不過已經(jīng)總結(jié)解決方案,具體解決請參考上一篇。Android 修改BottomSheetDialog不滾動到頂部。因此本文也會介紹使用封裝的SuperSlideLayout實現(xiàn)如同BottomSheetDialog的效果。另外還可以通過實現(xiàn)設(shè)置滑動邊緣,實現(xiàn)今日頭條評論列表彈出框效果,具體請參考demo,如需要實現(xiàn)特效或者其他方向彈出等,需自己修改個別屬性。

全屏評論框
模仿今日頭條全屏評論框,看名稱肯定會覺得實現(xiàn)是使用上一步中的底部彈出框,其實不然,兩者沒有任何關(guān)系,除了都使用SuperSlideLayout之外。
主要是通過設(shè)置兩個SuperSlideLayout,設(shè)置不同的方向上滑動的屬性,并且外層的添加系統(tǒng)欄顏色,加以區(qū)分,里層添加列表數(shù)據(jù),效果上如同彈出框。里層下拉過程中,需要外層透明并且系統(tǒng)欄漸變透明,具體參考demo。


可拖拽共享圖集
Android5.0之后出了過渡動畫,效果也是非常好,特別是共享元素,使Activity的跳轉(zhuǎn)更平滑。在圖集的基礎(chǔ)上添加可拖拽結(jié)束,并且加入共享元素功能。效果如下:



可拖拽視頻窗口
此效果模仿皮皮蝦APP的視頻詳情頁效果,下方評論列表可滑動,滑動頂部,向下拖拽可使背景漸變透明,視頻窗口大小不改變(可通過參數(shù)改變大小),視頻窗口隨著手勢改變,并且已經(jīng)實現(xiàn)共享元素,超過閥值,視頻窗口會自動回歸到主頁列表。


屬性介紹
自定義屬性
boolean mSlideEnable:是否支持滾動
int mSlideEdge:從哪個邊緣可滑動(是支持全屏,準(zhǔn)確點是方向)
float mSlideThresholdRate:閾值比率
boolean mCheckThreshold:是否需要判斷閾值
boolean mAlphaEnable:是否支持背景透明度變化
float mAlphaRate:透明度變化比率
float mMinAlpha:最小背景透明度
boolean mScaleEnable:是否支持縮放
float mScaleRate:縮放比率
float mMinScale:最小縮放比例
boolean mOverflowParent:滑動是否可越過父類邊界
boolean mSingleDirection:滑動是否是單一方向
boolean mMultiPointerEnable:是否支持多點觸摸
int mScrollTime:總滑動時間
默認(rèn)屬性
- 支持四個方向組合方式的滑動,組合方式總有15種情況,通過 “ | ” 組合。
public static final int EDGE_LEFT = 1 << 0;
public static final int EDGE_RIGHT = 1 << 1;
public static final int EDGE_TOP = 1 << 2;
public static final int EDGE_BOTTOM = 1 << 3;
public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
- 滑動方向分為水平、垂直、以及水平加垂直,三種方式
public static final int DIRECTION_HORIZONTAL = 1 << 0;
public static final int DIRECTION_VERTICAL = 1 << 1;
public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
- 根據(jù)滑動過程變化分為三種滑動狀態(tài),閑置、拖拽、釋放
public static final int STATE_IDLE = 1 << 0;
public static final int STATE_DRAG = 1 << 1;
public static final int STATE_RELEASE = 1 << 2;
public int mCurrentState = STATE_IDLE;
成員變量介紹
Scroller mScroller:實現(xiàn)滑動
Activity mActivity:需要關(guān)閉的activity
boolean mOverThreshold:是否超越閥值
boolean mIsBeingDragged:是否攔截或者可拖拽
int mDirection:方向(指滑動水平、垂直或者組合方式)
float mDownX, mDownY:觸點位置(不一定是按下位置)
boolean mPositiveX, mPositiveY:X軸、Y軸正方向向量
int mMeasuredWidth, mMeasuredHeight:測量的寬高
View mChildRootView:直屬子視圖(注:只能有一個直屬子視圖,同ScrollView)
Drawable mBackground:背景(滑動直屬子視圖后面的背景)
Drawable mForeground:前景(直屬子視圖的背景)
OnSlideListener mOnSlideListener:滑動監(jiān)聽器
//下面是觸點
final int INVALID_POINTER = -1;
int mActivePointerId = INVALID_POINTER;
boolean mCheckTouchInChild;//觸點是否在子類中
//系統(tǒng)狀態(tài)欄
WindowInsetsCompat mLastInsets
boolean mDrawStatusBarBackground
Drawable mStatusBarBackground
處理滑動事件
本文主要采用Scroller實現(xiàn)其內(nèi)部子視圖滑動,滑動的核心內(nèi)容還是重寫ViewGroup的onInterceptTouchEvent和onTouchEvent方法。
攔截事件
分析攔截條件:
- 由于采用Scroller滑動,因此必須需要其內(nèi)部包含至少一個子類
- 觸點必須落在其子視圖中才能攔截
- 觸點所在的子視圖無法實現(xiàn)自身的滑動
- 必須在指定的mSlideEdge滑動
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
int pointerIndex;
//第一步:監(jiān)測是否含子類
boolean checkNullChild = checkNullChild();
if (!mSlideEnable || checkNullChild) {
return super.onInterceptTouchEvent(event);
}
switch (action) {
case MotionEvent.ACTION_DOWN:
//判斷觸點是否在子view中
mDownX = event.getX();
mDownY = event.getY();
mActivePointerId = event.getPointerId(0);
mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
//判斷是否觸點是否在子類外部
if (!mCheckTouchInChild) {
if (mOnSlideListener != null) {
mOnSlideListener.onTouchOutside(this, mCheckTouchInChild);
}
return super.onInterceptTouchEvent(event);
}
mScroller.computeScrollOffset();
if (mCurrentState != STATE_IDLE
&& Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
mScroller.abortAnimation();
mIsBeingDragged = true;
disallowInterceptTouchEvent();
} else {
mIsBeingDragged = false;
}
break;
case MotionEvent.ACTION_MOVE:
//計算移動距離 判定是否滑動
pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER) {
break;
}
float dx = event.getX(pointerIndex) - mDownX;
float dy = event.getY(pointerIndex) - mDownY;
mIsBeingDragged = chechkCanDrag(dx, dy);
if (mIsBeingDragged) {
performDrag(event, dx, dy, pointerIndex);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//如果在可拖拽情況下復(fù)位
if (mIsBeingDragged) {
revertOriginalState(getScrollY(), getScrollY(), false);
}
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
break;
}
return mIsBeingDragged;
}
說明:使用event.getActionMasked()判斷事件的類型,主要是為了多觸點,這個知識點可自行查知,本文不做特殊介紹。
解決分析第一點:
//第一步:監(jiān)測是否含子類
boolean checkNullChild = checkNullChild();
if (!mSlideEnable || checkNullChild) {
return super.onInterceptTouchEvent(event);
}
/**
* 監(jiān)測是否有子類
* 無子視圖禁止拖動
*
* @return
*/
private boolean checkNullChild() {
mChildRootView = getChildAt(0);
return getChildCount() == 0;
}
解決分析第二點:
/**
* 檢測觸點是否在當(dāng)前view中
*/
private boolean checkTouchInChild(View childView, float x, float y) {
if (childView != null) {
int scrollX = getScrollX();
int scrollY = childView.getScrollY();
//需要加上已經(jīng)滑動的距離
float left = childView.getLeft() - scrollX;
float right = childView.getRight() - scrollX;
float top = childView.getTop() - scrollY;
float bottom = childView.getBottom() - scrollY;
if (y >= top && y <= bottom && x >= left
&& x <= right) {
return true;
}
}
return false;
}
說明:
1、使用getScrollX()而不是childView.getScrollX()的原因,需要了解Scroller,其實現(xiàn)內(nèi)部內(nèi)容滑動,因此應(yīng)該獲取SuperSlideLayout的值,而不是其子視圖的getScrollX()值
2、計算觸點為什么要加上getScrollX()的值,目的是滑動過程中多觸點判斷,滑動過程中,子視圖的坐標(biāo)點位置不會改變,子視圖內(nèi)容改變,為了做到視覺上觸點的位置是否在子視圖中,因此需要加上getScrollX()的值。舉個例子:如果從左向右滑動過程中,滑動了100px,getScrollX()的值是負(fù)數(shù)(-100px),如果直屬子視圖寬度是等于父類的話,那么getLeft()的值是不會隨著滑動改變的,因此一直是0,此時直屬子視圖的左邊距應(yīng)當(dāng)判定為100px,其他的計算同理。
解決分析后兩點:
最后兩點包括,監(jiān)測邊緣和方向上是否可滑動,還有一點就是外加的,關(guān)于方向上優(yōu)先級處理
/**
* 檢測是否可以拖拽
*
* @param dx
* @param dy
* @return
*/
private boolean chechkCanDrag(float dx, float dy) {
boolean mMinTouchSlop = checkEdgeAndTouchSlop(dx, dy);
boolean chcekScrollPriority = chcekScrollPriority(dx, dy);
boolean checkCanScrolly = checkCanScrolly(dx, dy);
return mMinTouchSlop && chcekScrollPriority && !checkCanScrolly;
}
下面這個方法主要是根據(jù)設(shè)置的滑動邊緣判斷方向
/**
* 邊緣滾動
*
* @param dx
* @param dy
* @return
*/
private boolean checkEdgeAndTouchSlop(float dx, float dy) {
boolean mMinTouch = false;
if (mSlideEdge == EDGE_LEFT) {
mDirection = DIRECTION_HORIZONTAL;
mPositiveX = dx > 0;
mMinTouch = mPositiveX;
} else if (mSlideEdge == EDGE_RIGHT) {
mDirection = DIRECTION_HORIZONTAL;
mPositiveX = dx > 0;
mMinTouch = -dx > 0;
} else if (mSlideEdge == EDGE_TOP) {
mDirection = DIRECTION_VERTICAL;
mPositiveY = dy > 0;
mMinTouch = mPositiveY;
} else if (mSlideEdge == EDGE_BOTTOM) {
mDirection = DIRECTION_VERTICAL;
mPositiveY = dy > 0;
mMinTouch = -dy > 0;
} else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT)) {
mDirection = DIRECTION_HORIZONTAL;
mPositiveX = dx > 0;
mMinTouch = Math.abs(dx) > 0;
} else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM)) {
mDirection = DIRECTION_VERTICAL;
mPositiveY = dy > 0;//正方向
mMinTouch = Math.abs(dy) > 0;
} else if (mSlideEdge == (EDGE_LEFT | EDGE_TOP)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? dx > 0 : dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = mPositiveX && mPositiveY;
}
} else if (mSlideEdge == (EDGE_LEFT | EDGE_BOTTOM)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? dx > 0 : -dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = mPositiveX && !mPositiveY;
}
} else if (mSlideEdge == (EDGE_RIGHT | EDGE_TOP)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? -dx > 0 : dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = !mPositiveX && mPositiveY;
}
} else if (mSlideEdge == (EDGE_RIGHT | EDGE_BOTTOM)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? -dx > 0 : -dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = !mPositiveX && !mPositiveY;
}
} else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT | EDGE_TOP)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? Math.abs(dx) > 0 : dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = mPositiveY;
}
} else if (mSlideEdge == (EDGE_LEFT | EDGE_RIGHT | EDGE_BOTTOM)) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
if (mSingleDirection) {
//必須只有一種情況的下
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? Math.abs(dx) > 0 : -dy > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx >= 0;
mPositiveY = dy >= 0;
mMinTouch = !mPositiveY;
}
} else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM | EDGE_LEFT)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? dx > 0 : Math.abs(dy) > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = mPositiveX;
}
} else if (mSlideEdge == (EDGE_TOP | EDGE_BOTTOM | EDGE_RIGHT)) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) > Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? -dx > 0 : Math.abs(dy) > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx >= 0;
mPositiveY = dy >= 0;
mMinTouch = !mPositiveX;
}
} else if (mSlideEdge == EDGE_ALL) {
if (mSingleDirection) {
boolean slideX = Math.abs(dx) >= Math.abs(dy);
mDirection = slideX ? DIRECTION_HORIZONTAL : DIRECTION_VERTICAL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mMinTouch = slideX ? Math.abs(dx) > 0 : Math.abs(dy) > 0;
} else {
mDirection = DIRECTION_ALL;
mPositiveX = dx > 0;
mPositiveY = dy > 0;
mOverflowParent = true;
mMinTouch = true;
}
}
return mMinTouch;
}
/**
* 優(yōu)先在某個方向上滾動
*/
private boolean chcekScrollPriority(float dx, float dy) {
if (mDirection == DIRECTION_HORIZONTAL) {
return Math.abs(dx) - Math.abs(dy) > 0;
} else if (mDirection == DIRECTION_VERTICAL) {
return Math.abs(dy) - Math.abs(dx) > 0;
} else {
//互斥方向的話無優(yōu)先級
return true;
}
}
/**
* 檢測是否可以滾動
*
* @return
*/
private boolean checkCanScrolly(float dx, float dy) {
//如果優(yōu)先處理子類View的滾動事件的話,需要先處理子類的,然后才交給自己
if (mDirection == DIRECTION_HORIZONTAL) {
return canScrollHorizontally(this, false, (int) dx, (int) mDownX, (int) mDownY);
} else if (mDirection == DIRECTION_VERTICAL) {
return canScrollVertically(this, false, (int) dy, (int) mDownX, (int) mDownY);
} else if (mDirection == DIRECTION_ALL) {
boolean canScrollH2 = canScrollHorizontally(this, false, (int) dx, (int) mDownX, (int) mDownY);
boolean canScrollV2 = canScrollVertically(this, false, (int) dy, (int) mDownX, (int) mDownY);
return canScrollH2 || canScrollV2;
}
return false;
}
關(guān)于監(jiān)測視圖水平或者垂直方向上是否可滑動,下面只以監(jiān)測水平方向為例子說明,垂直方向的可參考代碼。
/**
* 當(dāng)前觸點所在iew
* 垂直方向上是否
* 可以滾動
*
* @param v
* @param dy
* @param x
* @param y
* @return
*/
private boolean canScrollVertically(View v, boolean checkV, int dy, int x, int y) {
if (v instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) v;
final int scrollX = getScrollX();
final int scrollY = getScrollY();
final int count = group.getChildCount();
for (int i = count - 1; i >= 0; i--) {
final View child = group.getChildAt(i);
boolean touchInChild = checkTouchInChild(child, x, y);
//只有當(dāng)觸點在view之內(nèi)才判斷
if (touchInChild && canScrollVertically(child, true, dy,
x + scrollX - child.getLeft(),
y + scrollY - child.getTop()))
return true;
}
}
return checkV && v.canScrollVertically(-dy);
}
說明:
1、如果視圖是ViewGroup類型,通過遞歸判斷其子類是否可水平滑動
2、判斷當(dāng)前觸點所在View水平方向上是否可滑動
3、checkV:false代表不檢測自己,true代表檢測子視圖
DOWN事件:
主要是記錄觸點已經(jīng)判斷觸點是否在直屬子視圖中,還有對于scroller滑動狀態(tài)的判斷,當(dāng)處于滑動未結(jié)束的情況下,需要禁止父類攔截
/**
* 不讓父類攔截事件
*/
private void disallowInterceptTouchEvent() {
final ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(true);
}
UP和CANCLE事件
如果處于可拖拽狀態(tài),需要恢復(fù)默認(rèn)位置
/**
* 恢復(fù)初始狀態(tài)
*
* @param scrollX
* @param scrollY
*/
private void revertOriginalState(int scrollX, int scrollY, boolean overThreshold) {
//恢復(fù)真正的狀態(tài)
smoothllyScroll(scrollX, scrollY, -scrollX, -scrollY, mScrollTime);
//監(jiān)聽
if (mOnSlideListener != null)
mOnSlideListener.onSlideRecover(this, overThreshold);
}
/**
* 平滑滑動
*
* @param startX
* @param startY
* @param endX
* @param endY
* @param computeTime 計算滑動時間
* @param mScrollTime
*/
public void smoothllyScroll(int startX, int startY, int endX, int endY, boolean computeTime, int mScrollTime) {
mCurrentState = STATE_RELEASE;
int duration;
if (computeTime) {
//計算百分比時間
float offsetXPercent = Math.abs(endX) * 1f / mMeasuredWidth;
float offsetYPercent = Math.abs(endY) * 1f / mMeasuredHeight;
duration = (int) (Math.max(offsetXPercent, offsetYPercent) * mScrollTime);
} else {
duration = mScrollTime;
}
mScroller.startScroll(startX, startY, endX, endY, duration);
ViewCompat.postInvalidateOnAnimation(this);
}
POINTER_UP事件
手勢第二次觸點抬起的動作,恢復(fù)觸點位置即可
/**
* 釋放第二次觸點
*
* @param ev
*/
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mDownX = ev.getX(newPointerIndex);
mDownY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
消費事件
消費事件有三處來源,一是通過攔截攔截,二是不攔截,但是其子視圖不消費,自身消費事件,三是手指二次按下,由于第一次已經(jīng)消費,因此此次按下當(dāng)由自身消費。消費的條件與攔截條件幾乎一致,只是對事件做了不同的處理條件,具體如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
int pointerIndex;
//第一步:監(jiān)測是否含子類
boolean checkNullChild = checkNullChild();
if (checkNullChild || !mSlideEnable) {
return super.onTouchEvent(event);
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownX = event.getX();
mDownY = event.getY();
mActivePointerId = event.getPointerId(0);
mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
if (mIsBeingDragged) {
disallowInterceptTouchEvent();
}
break;
case MotionEvent.ACTION_MOVE:
//如果觸點不在子類中直接返回
if (!mCheckTouchInChild) {
break;
}
//檢測觸點
pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER) {
break;
}
float dx = event.getX(pointerIndex) - mDownX;
float dy = event.getY(pointerIndex) - mDownY;
performDrag(event, dx, dy, pointerIndex);
break;
case MotionEvent.ACTION_UP:
// 根據(jù)手指釋放時的位置決定回彈還是關(guān)閉,只要有一方超越就結(jié)束
pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER) {
break;
}
performRelease();
mActivePointerId = INVALID_POINTER;
break;
case MotionEvent.ACTION_POINTER_DOWN:
//第二步:監(jiān)測觸點范圍(必須有子類才去監(jiān)測觸點范圍)
if (mMultiPointerEnable) {
pointerIndex = event.getActionIndex();
mDownX = (int) event.getX(pointerIndex);
mDownY = (int) event.getY(pointerIndex);
mActivePointerId = event.getPointerId(pointerIndex);
mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
if (mIsBeingDragged) {
disallowInterceptTouchEvent();
}
}
break;
case MotionEvent.ACTION_POINTER_UP:
//也可以做邊緣釋放,后期可以添加
if (mMultiPointerEnable) {
onSecondaryPointerUp(event);
mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
}
break;
case MotionEvent.ACTION_CANCEL:
if (mIsBeingDragged) {
revertOriginalState(getScrollY(), getScrollY(), false);
}
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
return false;
}
return true;
}
Move事件:
由于來源有三處,其中有通過攔截獲取,因此首先判斷當(dāng)前觸點是否在直屬子視圖中。
/**
* 拖拽操作
*
* @param event
* @param dx
* @param dy
* @param pointerIndex
*/
private void performDrag(MotionEvent event, float dx, float dy, int pointerIndex) {
if (mIsBeingDragged) {
disallowInterceptTouchEvent();
//觸發(fā)監(jiān)聽 UP的時候取消監(jiān)聽
if (mOnSlideListener != null && mCurrentState != STATE_DRAG) {
mOnSlideListener.onSlideStart(this);
}
mCurrentState = STATE_DRAG;
int scrollX = getScrollX();
int scrollY = getScrollY();
if (mDirection == DIRECTION_HORIZONTAL) {
boolean slideWelt = mPositiveX ? scrollX >= dx : scrollX <= dx;
if (slideWelt && !mOverflowParent) {
scrollTo(0, 0);
} else {
scrollBy((int) -dx, 0);
}
} else if (mDirection == DIRECTION_VERTICAL) {
boolean slideWelt = mPositiveY ? scrollY >= dy : scrollY <= dy;
if (slideWelt && !mOverflowParent) {
scrollTo(0, 0);
} else {
scrollBy(0, (int) -dy);
}
} else if (mDirection == DIRECTION_ALL) {
boolean limitX = mPositiveX ? scrollX >= dx : scrollX <= dx;
boolean limitY = mPositiveY ? scrollY >= dy : scrollY <= dy;
int realDx = limitX ? mOverflowParent ? (int) -dx : 0 : (int) -dx;
int realDy = limitY ? mOverflowParent ? (int) -dy : 0 : (int) -dy;
scrollBy(realDx, realDy);
}
//繪制背景
invalidateBackground(scrollX, scrollY);
mDownX = event.getX(pointerIndex);
mDownY = event.getY(pointerIndex);
} else {
mIsBeingDragged = chechkCanDrag(dx, dy);
}
}
/**
* 繪制背景
* 縮放和背景顏色漸變
*
* @param scrollX
* @param scrollY
*/
private void invalidateBackground(int scrollX, int scrollY) {
//計算滑動比例
float mPercentSlideX = (scrollX * 1.0f) / mMeasuredWidth;
float mPercentSlideY = (scrollY * 1.0f) / mMeasuredHeight;
float maxPercent = Math.max(Math.abs(mPercentSlideX), Math.abs(mPercentSlideY));
float mMaxScal = 0, mMaxAlpha = 0;
//設(shè)置縮放
if (mScaleEnable && mChildRootView != null) {
//限制縮放最小值
mMaxScal = maxPercent / mScaleRate;
float limitScal = mMaxScal > 1 - mMinScale ? 1 - mMinScale : mMaxScal;
mMaxScal = 1 - limitScal;
mChildRootView.setScaleX(mMaxScal);
mChildRootView.setScaleY(mMaxScal);
}
//設(shè)置背景
if (mAlphaEnable) {
float maxAlpha = maxPercent / mAlphaRate;
float limitAlpha = maxAlpha > 1 - mMinAlpha ? 1 - mMinAlpha : maxAlpha;
mMaxAlpha = 1 - limitAlpha;
if (mBackground != null && mAlphaEnable) {
mBackground.mutate().setAlpha((int) ((mMaxAlpha) * 255));
}
}
//相對于屏幕的比例
if (mOnSlideListener != null)
mOnSlideListener.onSlideChange(this,
mPercentSlideX, mPercentSlideY,
mMaxScal, mMaxAlpha);
}
說明:
1、拖拽條件判斷與攔截一致
2、當(dāng)執(zhí)行拖拽的時,禁止父類攔截事件
Up事件:
up事件主要是松手之后執(zhí)行自動滑動,通過判斷拖拽位置是否超越閾值來設(shè)置最后的狀態(tài)為原始狀態(tài)還是關(guān)閉狀態(tài)
/**
* 釋放手勢
*/
private void performRelease() {
if (mIsBeingDragged) {
int scrollX = getScrollX();
int scrollY = getScrollY();
mOverThreshold = checkThreshold(scrollX, scrollY);
if (mCheckThreshold && mOverThreshold) {
int endScrollX = scrollX < 0 ? -scrollX - mMeasuredWidth : mMeasuredWidth - scrollX;
int endScrollY = scrollY < 0 ? -scrollY - mMeasuredHeight : mMeasuredHeight - scrollY;
endScrollX = mDirection == DIRECTION_VERTICAL ? 0 : endScrollX;
endScrollY = mDirection == DIRECTION_HORIZONTAL ? 0 : endScrollY;
smoothllyScroll(scrollX, scrollY, endScrollX, endScrollY, mScrollTime);
} else {
revertOriginalState(scrollX, scrollY, mOverThreshold);
}
}
}
/**
* 檢測閾值
*
* @return
*/
private boolean checkThreshold(int scrollX, int scrollY) {
if (mDirection == DIRECTION_HORIZONTAL) {
return Math.abs(scrollX) > mMeasuredWidth * mSlideThresholdRate;
} else if (mDirection == DIRECTION_VERTICAL) {
return Math.abs(scrollY) > mMeasuredHeight * mSlideThresholdRate;
} else {
boolean xThreshold = Math.abs(scrollX) > mMeasuredWidth * mSlideThresholdRate;
boolean yThreshold = Math.abs(scrollY) > mMeasuredHeight * mSlideThresholdRate;
return xThreshold || yThreshold;
}
}
說明:重寫computeScroll可監(jiān)測滑動是否結(jié)束,在釋放手勢的時候回調(diào),并且需要繪制背景
/**
* 平滑的滾動到最終位置
*/
@Override
public void computeScroll() {
int oldX = getScrollX();
int oldY = getScrollY();
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
//位置改變才去滑動
if (oldX != x || oldY != y) {
scrollTo(x, y);
//繪制背景 如果是不檢測閾值并且超過閾值則不繪制
if (mCheckThreshold || !mOverThreshold) {
invalidateBackground(x, y);
}
}
ViewCompat.postInvalidateOnAnimation(this);
return;
} else {
boolean originalState = Math.abs(oldX) == 0 && Math.abs(oldY) == 0;
boolean outParent = Math.abs(oldX) >= mMeasuredWidth || Math.abs(oldY) >= mMeasuredHeight;
//釋放狀態(tài)
if (originalState || outParent) {
mCurrentState = STATE_IDLE;
mIsBeingDragged = false;
}
if (outParent) {
if (mOnSlideListener != null) mOnSlideListener.onSlideFinish(this);
if (mActivity != null) mActivity.finish();
}
}
}
POINTER_DOWN事件
主要處理第二次按下,重新設(shè)置觸點位置,以及檢測觸點是否在直屬子視圖內(nèi)和禁止父視圖攔截事件
if (mMultiPointerEnable) {
pointerIndex = event.getActionIndex();
mDownX = (int) event.getX(pointerIndex);
mDownY = (int) event.getY(pointerIndex);
mActivePointerId = event.getPointerId(pointerIndex);
mCheckTouchInChild = checkTouchInChild(mChildRootView, mDownX, mDownY);
if (mIsBeingDragged) {
disallowInterceptTouchEvent();
}
}
POINTER_UP事件
再次檢測初次按下位置是否在直屬子視圖中,目的是為了第一次滑動可繼續(xù)
附屬功能
系統(tǒng)狀態(tài)欄
由于本次封裝的主要功能是用于側(cè)滑結(jié)束Activity,因此如果不做特殊處理,無法實現(xiàn)沉浸式Activity,側(cè)滑結(jié)束效果不理想。因此需要單獨對于系統(tǒng)欄做特殊處理。如實現(xiàn)沉浸式狀態(tài)欄,需要Android版本大于5.0,Api版本大于21,具體處理如下。
/**
* 下面三個方法主要用于處理狀態(tài)欄
*/
private void overlayStatusBar(Context context) {
//獲取系統(tǒng)默認(rèn)狀態(tài)欄顏色
if (ViewCompat.getFitsSystemWindows(this)) {
ViewCompat.setOnApplyWindowInsetsListener(this,
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
final SuperSlideLayout superSlideLayout = (SuperSlideLayout) view;
superSlideLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
return insets.consumeSystemWindowInsets();
}
});
//版本高于21才能采用透明狀態(tài)欄
if (Build.VERSION.SDK_INT >= 21) {
int[] THEME_STATUSBAR = {android.R.attr.statusBarColor};
final TypedArray a = context.obtainStyledAttributes(THEME_STATUSBAR);
try {
mStatusBarBackground = a.getDrawable(0);
} finally {
a.recycle();
}
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
}
@RestrictTo(LIBRARY_GROUP)
public void setChildInsets(WindowInsetsCompat insets, boolean draw) {
mLastInsets = insets;
mDrawStatusBarBackground = draw;
setWillNotDraw(!draw && getBackground() == null);
requestLayout();
}
/**
* 繪制狀態(tài)欄
*
* @param c
*/
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
if (mDrawStatusBarBackground && mStatusBarBackground != null) {
final int inset;
if (Build.VERSION.SDK_INT >= 21) {
inset = mLastInsets != null
? ((WindowInsetsCompat) mLastInsets).getSystemWindowInsetTop() : 0;
} else {
inset = 0;
}
if (inset > 0) {
mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
mStatusBarBackground.draw(c);
}
}
}
說明:要實現(xiàn)沉浸式狀態(tài)欄,需要三個條件,分別如下:
1、設(shè)置為Activity的setContentView()頂層視圖
2、設(shè)置android:fitsSystemWindows="true"
3、設(shè)置樣式
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
<style name="ImmersiveTheme" parent="@style/AppTheme.NoActionBar">
<!--透明導(dǎo)航欄-->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
Home鍵處理
按下home鍵的時候,需要設(shè)置恢復(fù)原始位置
/**
* 當(dāng)window焦點改變的時候回調(diào)
*
* @param hasWindowFocus
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus && mIsBeingDragged) {
//如果未歸位,則條用
int scrollX = getScrollX();
int scrollY = getScrollY();
revertOriginalState(scrollX, scrollY, false);
}
}
關(guān)于鏈?zhǔn)椒庋b
由于屬性過多,遂采用鏈?zhǔn)椒庋b,便于調(diào)用??捎糜贏ctivity和普通View,本文主要是通過Scroller來實現(xiàn)滑動子視圖,因此可用于Activity或普通View。
/**
* 綁定目標(biāo)
*
* @param builder
* @param target
*/
private SlideLayoutImpl attachTarget(Builder builder, Object target) {
if (builder.mContext != null) {
if (builder.mSlideEnable) {
//設(shè)置公共參數(shù)
SuperSlideLayout superSlideLayout = new SuperSlideLayout(builder.mContext);
superSlideLayout.setSlideEnable(builder.mSlideEnable);
superSlideLayout.setSlideEdge(builder.mSlideEdge);
superSlideLayout.setSlideThresholdRate(builder.mSlideThresholdRate);
superSlideLayout.setCheckThreshold(builder.mCheckThreshold);
superSlideLayout.setAlphaEnable(builder.mAlphaEnable);
superSlideLayout.setAlphaRate(builder.mAlphaRate);
superSlideLayout.setMinAlpha(builder.mMinAlpha);
superSlideLayout.setScaleEnable(builder.mScaleEnable);
superSlideLayout.setScaleRate(builder.mScaleRate);
superSlideLayout.setMinScale(builder.mMinScale);
superSlideLayout.setOverflowParent(builder.mOverflowParent);
superSlideLayout.setSingleDirection(builder.mSingleDirection);
superSlideLayout.setMultiPointerEnable(builder.mMultiPointerEnable);
superSlideLayout.setScrollTime(builder.mScrollTime);
superSlideLayout.setBackground(builder.mBackground);
superSlideLayout.setForeground(builder.mForeground);
superSlideLayout.setOnSlideListener(builder.mOnSlideListener);
if (target instanceof View) {
superSlideLayout.attachView((View) target);
} else if (target instanceof Activity) {
superSlideLayout.attachActivity((Activity) target);
}
return superSlideLayout;
}
}
return null;
}
/**
* 綁定子視圖
*/
public void attachView(View view) {
if (view != null) {
ViewParent parent = view.getParent();
if (parent != null) {
ViewGroup parentView = (ViewGroup) parent;
parentView.removeView(view);
mChildRootView = view;
addView(view);
parentView.addView(this);
}
} else {
throw new NullPointerException("ready to attach child view is null");
}
}
/**
* 綁定Activity
*/
public void attachActivity(Activity activity) {
if (activity != null) {
mActivity = activity;
ViewGroup decorView = (ViewGroup) mActivity.getWindow().getDecorView();
mChildRootView = decorView.getChildAt(0);//contentview+titlebar
View contentView = decorView.findViewById(android.R.id.content);
Drawable contentViewBackground = contentView.getBackground();
if (contentViewBackground == null) contentView.setBackground(mForeground);
decorView.removeView(mChildRootView);
addView(mChildRootView);
decorView.addView(this);
} else {
throw new NullPointerException("ready to attach activity is null");
}
}
說明:綁定普通View是否的時候一定只能有一個直屬子視圖
最后
事件分發(fā)在Android中異常重要,因為Android系統(tǒng)主要是靠人與屏幕的觸摸去交互。如果單純說事件分發(fā)簡單,也不是沒有道理,基本做過Android一段時間,都會對事件分發(fā)的原理有所了解,大家都喜歡舉的例子:公司上級分發(fā)事情給下級,這樣便于理解。不過事件分發(fā)遠(yuǎn)遠(yuǎn)沒有那么簡單。一方面是因為事件分發(fā)包含很多不同的事件(down、up、move、pointDown、pointUp等),二是系統(tǒng)對于不同的事件優(yōu)先級也不同(down優(yōu)先級高),三是幾乎所有的效果或者系統(tǒng)監(jiān)聽都需要不同的事件配合完成,四是事件分發(fā)還涉及到事件沖突問題,如何解決事件沖突成了事件分發(fā)中最難的部分。本次封裝的內(nèi)容還有諸多不足,如發(fā)現(xiàn)問題,請及時反饋,做進一步修改。