前言
Android 事件分發實例之右滑結束Activity(一)
Android 事件分發實例之右滑結束Activity(二)
前兩篇主要是介紹通過處理滑動事件實現右滑結束Activity,功能簡單,並且存在諸多不足之處。在思考之後,遂想實現在不同方向上的滑動,並且加入背景漸變、背景縮放、組合方向上滑動、支持多觸點、沉浸式系統狀態欄等。通過自定義屬性,設置不同的方向組合、背景屬性等實現不同方向組合的滑動,通過組合總有15種類型的組合滑動方式,可通過在XML中設置屬性或者動態設置屬性的方式,組合不同的屬性,實現不同的效果。遂起名爲SuperSlideLayout,其內部依然是通過Scroller實現內容滑動,通過判斷滑動條件以及滑動狀態,重寫攔截和消費事件以及解決事件衝突問題,處理滑動事件,實現滑動效果。所有特效都是使用APP過程發現的,SuperSlideLayout還可實現的更多的效果,下列的特效均爲市面APP常見效果,之後還會陸續實現更多的特效。
事件分發實例之SuperSlideLayout
實現效果
側滑結束Activity
可自由組合實現多種側滑結束Activity的效果,適配系統多種ViewGroup
上下滑動圖集
此效果模仿今日頭條的圖集瀏覽功能,剛工作那會就感覺今日頭條的圖集瀏覽功能真贊。右滑過程中,window背景漸變爲透明,當左右上滑過程,window背景設置爲透明,Activity中除了Viewpager部分內容(不是背景)漸變透明。效果如下:
底部彈出框
很多應用都會有底部可拖拽彈出框,基本都是採用BottomSheetDialog來實現的,效果極佳,但是也有不足之處,就是默認情況下如果子視圖過多可滾到頂部,有時候可能並不需要滾到頂部,但是系統並沒有直接設置的方法,不過已經總結解決方案,具體解決請參考上一篇。Android 修改BottomSheetDialog不滾動到頂部。因此本文也會介紹使用封裝的SuperSlideLayout實現如同BottomSheetDialog的效果。另外還可以通過實現設置滑動邊緣,實現今日頭條評論列表彈出框效果,具體請參考demo,如需要實現特效或者其他方向彈出等,需自己修改個別屬性。
全屏評論框
模仿今日頭條全屏評論框,看名稱肯定會覺得實現是使用上一步中的底部彈出框,其實不然,兩者沒有任何關係,除了都使用SuperSlideLayout之外。
主要是通過設置兩個SuperSlideLayout,設置不同的方向上滑動的屬性,並且外層的添加系統欄顏色,加以區分,裏層添加列表數據,效果上如同彈出框。裏層下拉過程中,需要外層透明並且系統欄漸變透明,具體參考demo。
可拖拽共享圖集
Android5.0之後出了過渡動畫,效果也是非常好,特別是共享元素,使Activity的跳轉更平滑。在圖集的基礎上添加可拖拽結束,並且加入共享元素功能。效果如下:
可拖拽視頻窗口
此效果模仿皮皮蝦APP的視頻詳情頁效果,下方評論列表可滑動,滑動頂部,向下拖拽可使背景漸變透明,視頻窗口大小不改變(可通過參數改變大小),視頻窗口隨着手勢改變,並且已經實現共享元素,超過閥值,視頻窗口會自動迴歸到主頁列表。
屬性介紹
自定義屬性
boolean mSlideEnable:是否支持滾動
int mSlideEdge:從哪個邊緣可滑動(是支持全屏,準確點是方向)
float mSlideThresholdRate:閾值比率
boolean mCheckThreshold:是否需要判斷閾值
boolean mAlphaEnable:是否支持背景透明度變化
float mAlphaRate:透明度變化比率
float mMinAlpha:最小背景透明度
boolean mScaleEnable:是否支持縮放
float mScaleRate:縮放比率
float mMinScale:最小縮放比例
boolean mOverflowParent:滑動是否可越過父類邊界
boolean mSingleDirection:滑動是否是單一方向
boolean mMultiPointerEnable:是否支持多點觸摸
int mScrollTime:總滑動時間
默認屬性
- 支持四個方向組合方式的滑動,組合方式總有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;
- 根據滑動過程變化分爲三種滑動狀態,閒置、拖拽、釋放
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:實現滑動
Activity mActivity:需要關閉的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:滑動監聽器
//下面是觸點
final int INVALID_POINTER = -1;
int mActivePointerId = INVALID_POINTER;
boolean mCheckTouchInChild;//觸點是否在子類中
//系統狀態欄
WindowInsetsCompat mLastInsets
boolean mDrawStatusBarBackground
Drawable mStatusBarBackground
處理滑動事件
本文主要採用Scroller實現其內部子視圖滑動,滑動的核心內容還是重寫ViewGroup的onInterceptTouchEvent和onTouchEvent方法。
攔截事件
分析攔截條件:
- 由於採用Scroller滑動,因此必須需要其內部包含至少一個子類
- 觸點必須落在其子視圖中才能攔截
- 觸點所在的子視圖無法實現自身的滑動
- 必須在指定的mSlideEdge滑動
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
int pointerIndex;
//第一步:監測是否含子類
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:
//如果在可拖拽情況下復位
if (mIsBeingDragged) {
revertOriginalState(getScrollY(), getScrollY(), false);
}
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(event);
break;
}
return mIsBeingDragged;
}
說明:使用event.getActionMasked()判斷事件的類型,主要是爲了多觸點,這個知識點可自行查知,本文不做特殊介紹。
解決分析第一點:
//第一步:監測是否含子類
boolean checkNullChild = checkNullChild();
if (!mSlideEnable || checkNullChild) {
return super.onInterceptTouchEvent(event);
}
/**
* 監測是否有子類
* 無子視圖禁止拖動
*
* @return
*/
private boolean checkNullChild() {
mChildRootView = getChildAt(0);
return getChildCount() == 0;
}
解決分析第二點:
/**
* 檢測觸點是否在當前view中
*/
private boolean checkTouchInChild(View childView, float x, float y) {
if (childView != null) {
int scrollX = getScrollX();
int scrollY = childView.getScrollY();
//需要加上已經滑動的距離
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,其實現內部內容滑動,因此應該獲取SuperSlideLayout的值,而不是其子視圖的getScrollX()值
2、計算觸點爲什麼要加上getScrollX()的值,目的是滑動過程中多觸點判斷,滑動過程中,子視圖的座標點位置不會改變,子視圖內容改變,爲了做到視覺上觸點的位置是否在子視圖中,因此需要加上getScrollX()的值。舉個例子:如果從左向右滑動過程中,滑動了100px,getScrollX()的值是負數(-100px),如果直屬子視圖寬度是等於父類的話,那麼getLeft()的值是不會隨着滑動改變的,因此一直是0,此時直屬子視圖的左邊距應當判定爲100px,其他的計算同理。
解決分析後兩點:
最後兩點包括,監測邊緣和方向上是否可滑動,還有一點就是外加的,關於方向上優先級處理
/**
* 檢測是否可以拖拽
*
* @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;
}
下面這個方法主要是根據設置的滑動邊緣判斷方向
/**
* 邊緣滾動
*
* @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;
}
/**
* 優先在某個方向上滾動
*/
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 {
//互斥方向的話無優先級
return true;
}
}
/**
* 檢測是否可以滾動
*
* @return
*/
private boolean checkCanScrolly(float dx, float dy) {
//如果優先處理子類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;
}
關於監測視圖水平或者垂直方向上是否可滑動,下面只以監測水平方向爲例子說明,垂直方向的可參考代碼。
/**
* 當前觸點所在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);
//只有當觸點在view之內才判斷
if (touchInChild && canScrollVertically(child, true, dy,
x + scrollX - child.getLeft(),
y + scrollY - child.getTop()))
return true;
}
}
return checkV && v.canScrollVertically(-dy);
}
說明:
1、如果視圖是ViewGroup類型,通過遞歸判斷其子類是否可水平滑動
2、判斷當前觸點所在View水平方向上是否可滑動
3、checkV:false代表不檢測自己,true代表檢測子視圖
DOWN事件:
主要是記錄觸點已經判斷觸點是否在直屬子視圖中,還有對於scroller滑動狀態的判斷,當處於滑動未結束的情況下,需要禁止父類攔截
/**
* 不讓父類攔截事件
*/
private void disallowInterceptTouchEvent() {
final ViewParent parent = getParent();
if (parent != null)
parent.requestDisallowInterceptTouchEvent(true);
}
UP和CANCLE事件
如果處於可拖拽狀態,需要恢復默認位置
/**
* 恢復初始狀態
*
* @param scrollX
* @param scrollY
*/
private void revertOriginalState(int scrollX, int scrollY, boolean overThreshold) {
//恢復真正的狀態
smoothllyScroll(scrollX, scrollY, -scrollX, -scrollY, mScrollTime);
//監聽
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事件
手勢第二次觸點擡起的動作,恢復觸點位置即可
/**
* 釋放第二次觸點
*
* @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);
}
}
消費事件
消費事件有三處來源,一是通過攔截攔截,二是不攔截,但是其子視圖不消費,自身消費事件,三是手指二次按下,由於第一次已經消費,因此此次按下當由自身消費。消費的條件與攔截條件幾乎一致,只是對事件做了不同的處理條件,具體如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
int pointerIndex;
//第一步:監測是否含子類
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:
// 根據手指釋放時的位置決定回彈還是關閉,只要有一方超越就結束
pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER) {
break;
}
performRelease();
mActivePointerId = INVALID_POINTER;
break;
case MotionEvent.ACTION_POINTER_DOWN:
//第二步:監測觸點範圍(必須有子類纔去監測觸點範圍)
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事件:
由於來源有三處,其中有通過攔截獲取,因此首先判斷當前觸點是否在直屬子視圖中。
/**
* 拖拽操作
*
* @param event
* @param dx
* @param dy
* @param pointerIndex
*/
private void performDrag(MotionEvent event, float dx, float dy, int pointerIndex) {
if (mIsBeingDragged) {
disallowInterceptTouchEvent();
//觸發監聽 UP的時候取消監聽
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;
//設置縮放
if (mScaleEnable && mChildRootView != null) {
//限制縮放最小值
mMaxScal = maxPercent / mScaleRate;
float limitScal = mMaxScal > 1 - mMinScale ? 1 - mMinScale : mMaxScal;
mMaxScal = 1 - limitScal;
mChildRootView.setScaleX(mMaxScal);
mChildRootView.setScaleY(mMaxScal);
}
//設置背景
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、當執行拖拽的時,禁止父類攔截事件
Up事件:
up事件主要是鬆手之後執行自動滑動,通過判斷拖拽位置是否超越閾值來設置最後的狀態爲原始狀態還是關閉狀態
/**
* 釋放手勢
*/
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可監測滑動是否結束,在釋放手勢的時候回調,並且需要繪製背景
/**
* 平滑的滾動到最終位置
*/
@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;
//釋放狀態
if (originalState || outParent) {
mCurrentState = STATE_IDLE;
mIsBeingDragged = false;
}
if (outParent) {
if (mOnSlideListener != null) mOnSlideListener.onSlideFinish(this);
if (mActivity != null) mActivity.finish();
}
}
}
POINTER_DOWN事件
主要處理第二次按下,重新設置觸點位置,以及檢測觸點是否在直屬子視圖內和禁止父視圖攔截事件
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事件
再次檢測初次按下位置是否在直屬子視圖中,目的是爲了第一次滑動可繼續
附屬功能
系統狀態欄
由於本次封裝的主要功能是用於側滑結束Activity,因此如果不做特殊處理,無法實現沉浸式Activity,側滑結束效果不理想。因此需要單獨對於系統欄做特殊處理。如實現沉浸式狀態欄,需要Android版本大於5.0,Api版本大於21,具體處理如下。
/**
* 下面三個方法主要用於處理狀態欄
*/
private void overlayStatusBar(Context context) {
//獲取系統默認狀態欄顏色
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才能採用透明狀態欄
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();
}
/**
* 繪製狀態欄
*
* @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);
}
}
}
說明:要實現沉浸式狀態欄,需要三個條件,分別如下:
1、設置爲Activity的setContentView()頂層視圖
2、設置android:fitsSystemWindows="true"
3、設置樣式
<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">
<!--透明導航欄-->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
</style>
Home鍵處理
按下home鍵的時候,需要設置恢復原始位置
/**
* 當window焦點改變的時候回調
*
* @param hasWindowFocus
*/
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (!hasWindowFocus && mIsBeingDragged) {
//如果未歸位,則條用
int scrollX = getScrollX();
int scrollY = getScrollY();
revertOriginalState(scrollX, scrollY, false);
}
}
關於鏈式封裝
由於屬性過多,遂採用鏈式封裝,便於調用。可用於Activity和普通View,本文主要是通過Scroller來實現滑動子視圖,因此可用於Activity或普通View。
/**
* 綁定目標
*
* @param builder
* @param target
*/
private SlideLayoutImpl attachTarget(Builder builder, Object target) {
if (builder.mContext != null) {
if (builder.mSlideEnable) {
//設置公共參數
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是否的時候一定只能有一個直屬子視圖
最後
事件分發在Android中異常重要,因爲Android系統主要是靠人與屏幕的觸摸去交互。如果單純說事件分發簡單,也不是沒有道理,基本做過Android一段時間,都會對事件分發的原理有所瞭解,大家都喜歡舉的例子:公司上級分發事情給下級,這樣便於理解。不過事件分發遠遠沒有那麼簡單。一方面是因爲事件分發包含很多不同的事件(down、up、move、pointDown、pointUp等),二是系統對於不同的事件優先級也不同(down優先級高),三是幾乎所有的效果或者系統監聽都需要不同的事件配合完成,四是事件分發還涉及到事件衝突問題,如何解決事件衝突成了事件分發中最難的部分。本次封裝的內容還有諸多不足,如發現問題,請及時反饋,做進一步修改。