大屏幕手機(jī)在返回前頁操作時(shí)窥突,點(diǎn)擊左上角的 APP 內(nèi)返回鍵或者手機(jī)自帶的返回按鍵都不是很方便蒂秘,這時(shí)候能通過屏幕側(cè)滑退出當(dāng)前頁面體驗(yàn)就會(huì)好很多了本今。但是 Android 系統(tǒng)并沒有想 IOS 一樣自帶側(cè)滑返回提岔,好在 Android 輪子比較多仙蛉,本文記錄一下個(gè)人開源項(xiàng)目 PandaEye 中使用的側(cè)滑返回庫(kù) SwipBackLayout 笋敞。該庫(kù)參考 github 上的開源庫(kù) SwipeBackLayout 做了一些簡(jiǎn)化碱蒙;
使用方式
定義側(cè)滑基礎(chǔ) Activity
側(cè)滑返回的實(shí)現(xiàn)是基于 Activity 的,可以直接繼承 Activity 或者繼承自己應(yīng)用實(shí)現(xiàn)的 BaseActivity 然后實(shí)現(xiàn) SwipeBackLayout.SwipeListener 接口即可.
public class SwipeBackActivity extends BaseActivity implements SwipeBackLayout.SwipeListener {
protected SwipeBackLayout layout;
private ArgbEvaluator argbEvaluator;
@SuppressLint("InflateParams")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
layout = (SwipeBackLayout) LayoutInflater.from(this).inflate(
R.layout.swipeback_base, null);
layout.attachToActivity(this);
argbEvaluator = new ArgbEvaluator();
layout.addSwipeListener(this);
if (Build.VERSION.SDK_INT >= 23) {
currentStatusColor = getResources().getColor(R.color.colorPrimaryDark, null);
} else {
currentStatusColor = getResources().getColor(R.color.colorPrimaryDark);
}
}
// 提供給子類設(shè)置 ViewPager 的接口夯巷,用于 SwipeLayout 中處理滑動(dòng)沖突
public void addViewPager(ViewPager pager) {
layout.addViewPager(pager);
}
}
效果優(yōu)化
需要側(cè)滑返回的 Activity 繼承 SwipeBackActivity 即可實(shí)現(xiàn)側(cè)滑返回的功能了赛惩,但是側(cè)滑過程中返回界面會(huì)被默認(rèn)的窗口背景顏色覆蓋,因此我們需要把實(shí)現(xiàn)側(cè)滑返回的界面的 theme 做一些小小的優(yōu)化趁餐,將背景設(shè)置為透明狀態(tài)喷兼,并設(shè)置進(jìn)入和退出的動(dòng)畫。
style 中的屬性設(shè)置
<!--全屏加透明-->
<style name="TranslucentFullScreenTheme" parent="AppTheme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<!--<item name="android:windowAnimationStyle">@android:style/Animation</item>-->
<item name="android:windowAnimationStyle">@style/AnimationActivity</item>
</style>
<!--動(dòng)畫設(shè)置-->
<style name="AnimationActivity" mce_bogus="1" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/base_slide_right_in</item>
<item name="android:activityOpenExitAnimation">@anim/base_slide_right_out</item>
<item name="android:activityCloseEnterAnimation">@anim/base_slide_right_in</item>
<item name="android:activityCloseExitAnimation">@anim/base_slide_right_out</item>
</style>
界面進(jìn)入動(dòng)畫
<!--base_slide_right_in-->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="300"
android:fromXDelta="100.0%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0.0%" />
</set>
界面退出動(dòng)畫
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="300"
android:fromXDelta="100.0%"
android:interpolator="@android:anim/decelerate_interpolator"
android:toXDelta="0.0%" />
</set>
然后在 manifest 文件中將繼承 SwipeBackActivity 的 Activity 的 theme 設(shè)置為 TranslucentFullScreenTheme 即可解決滑動(dòng)過程中背景覆蓋問題后雷。
原理淺析
Activity 中 View 視圖層級(jí)
要明白側(cè)滑返回的原理我們得先明白 Android Activity 界面的視圖層級(jí)關(guān)系:
Activity 和 PhoneWindow 這里可以忽略季惯,重點(diǎn)在 DecorView上。這個(gè) DecorView 是 Activity 中 View 布局的祖宗級(jí)布局臀突,是一個(gè) FrameLayout勉抓,通過 getWindow().getDecorView() 可以獲取到對(duì)象;如圖中 DecorView 有且僅有一個(gè) LinearLayout 子布局候学,即圖中的黃色部分藕筋。這個(gè) LinearLayout 一般情況下又包含有 ViewStub 和 FrameLayout 兩部分(不同的主題 Theme 可能會(huì)多處一些對(duì)象),ViewStub 即是應(yīng)用的 ActionBar梳码,他會(huì)根據(jù) theme 來決定是否真正引入 ActionBar 到界面顯示纯出。而這個(gè) FrameLayout 中的內(nèi)容即是我們寫的 layout 布局文件中想要展示的內(nèi)容蹭越。需要注意如果 Activity 繼承自 AppComcatActivity 則這個(gè) FrameLayout 中還會(huì)有兩個(gè)子布局,第一個(gè)子布局中的內(nèi)容才是我們寫的布局文件中的內(nèi)容
實(shí)現(xiàn)原理
通過 SDK 自帶的視圖分析工具 Hierarchy View 我們可以看到視圖的如下分布:
界面所有顯示的內(nèi)容其實(shí)都在這個(gè) LinearLayout 中,如果我們給這個(gè) LinearLayout 增加一個(gè)父布局然后對(duì)這個(gè)父布局進(jìn)行滑動(dòng)處理就可以實(shí)現(xiàn)界面的整體滑動(dòng)务漩,即把整個(gè)可視界面放入一個(gè)滑動(dòng)抽屜。因此實(shí)現(xiàn)滑動(dòng)的界面視圖應(yīng)該變成如下的樣子:
如圖 SwipeBackLayout 即是添加的滑動(dòng)抽屜嗤练,接下來我們看一下 SwipeBackLayout 中是怎樣實(shí)現(xiàn)在 LinearLayout 上層插入一個(gè) SwipeBackLayout 布局的害碾。
在 SwipeBackActivity 中只調(diào)用了 attachToActivity() 方法,方法中代碼如下:
public void attachToActivity(Activity activity) {
mActivity = activity;
TypedArray a = activity.getTheme().obtainStyledAttributes(
new int[]{android.R.attr.windowBackground});
int background = a.getResourceId(0, 0);
a.recycle();
//獲取到 DecorView 對(duì)象
ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
Log.i("decorChildCount", decor.getChildCount() + "");
ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
Log.i("decorChild", decorChild.toString());
//重置背景色資源
decorChild.setBackgroundResource(background);
//decorView 中將子布局移除
decor.removeView(decorChild);
//SwipeBackLayout 添加從decorView中移除布局
addView(decorChild);
//將ContentView設(shè)置為decorChild的父布局即添加進(jìn)來的SwipeBackLayout
setContentView(decorChild);
//將SwipeBackLayout添加進(jìn)DecorView
decor.addView(this);
}
從中我添加的注釋不難看出,實(shí)現(xiàn)替換的流程:
- 1甸各、傳入的 activity 對(duì)象獲取到 DecorView
- 2垛贤、DecorView.getChildAt(0) 獲取到 LinearLayout 對(duì)象
- 3、將 LinearLayout 背景資源重置趣倾,并從 DecorView 中移除
- 4聘惦、將 LinearLayout 添加到自定義的 SwipeBackLayout 中
- 5、將自定義的 SwipeBackLayout 添加到 DecorView 中
滑動(dòng)處理及 ViewPager 處理
在 SwipeBackLayout 中通過重寫 onInterceptTouchEvent(MotionEvent ev) 方法和 onTouchEvent(MotionEvent ev) 方法來實(shí)現(xiàn)側(cè)滑返回事件的處理及對(duì) ViewPager 滑動(dòng)的兼容的儒恋。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//處理ViewPager沖突問題
ViewPager mViewPager = getTouchViewPager(mViewPagers, ev);
//當(dāng)無觸摸ViewPager或者該ViewPager未滑動(dòng)到最左則不對(duì)滑動(dòng)時(shí)間進(jìn)行攔截
if (mViewPager != null && mViewPager.getCurrentItem() != 0) {
return super.onInterceptTouchEvent(ev);
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = tempX = (int) ev.getRawX();
downY = (int) ev.getRawY();
canSwipe = downX <= viewWidth / 2;
if (!canSwipe) {
return super.onInterceptTouchEvent(ev);
}
break;
case MotionEvent.ACTION_MOVE:
if (!canSwipe) {
return super.onInterceptTouchEvent(ev);
}
int moveX = (int) ev.getRawX();
// 滿足此條件屏蔽SildingFinishLayout里面子類的touch事件
if (moveX - downX > mTouchSlop
&& Math.abs((int) ev.getRawY() - downY) < mTouchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
在手指按下的時(shí)候相較于 onTouchEvent() 方法 onInterceptTouchEvent() 方法會(huì)先執(zhí)行善绎,在此方法中先判斷當(dāng)前觸摸是否為 ViewPager,是 ViewPager 則判斷是否滑動(dòng)到了 ViewPager 的最左側(cè)诫尽。如果觸摸的 ViewPager 且未滑動(dòng)到最左側(cè)則不對(duì)事件進(jìn)行攔截交給 ViewPager 處理觸摸事件禀酱,否則觸摸位置進(jìn)行判斷,在有效區(qū)域內(nèi)則記錄觸摸開始點(diǎn)牧嫉,否則按系統(tǒng)默認(rèn)方式處理剂跟。在移動(dòng)事件中會(huì)根據(jù)按下事件的判斷結(jié)果決定是否按默認(rèn)方式處理,當(dāng)需要處理側(cè)滑時(shí)會(huì)再次判斷如果 X 方向的滑動(dòng)大于最小有效滑動(dòng)距離 Y方向滑動(dòng)距離小于最小有效滑動(dòng)距離則此次事件將會(huì)被 SwipeBackLayout 所消費(fèi)酣藻,將進(jìn)入 SwipeBackLayout 的 onTouchEvent() 方法中的處理邏輯曹洽。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
if (!canSwipe) {
return super.onInterceptTouchEvent(event);
}
int moveX = (int) event.getRawX();
int deltaX = tempX - moveX;
tempX = moveX;
if (moveX - downX > mTouchSlop
&& Math.abs((int) event.getRawY() - downY) < mTouchSlop) {
isSilding = true;
}
if (moveX - downX >= 0 && isSilding) {
//deltaX 為單次移動(dòng)的距離向右滑為負(fù)數(shù)
// TODO: 2017/6/22 實(shí)現(xiàn) y 方向的移動(dòng),即向右任意方向滑出界面
mContentView.scrollBy(deltaX, 0);
}
break;
case MotionEvent.ACTION_UP:
if (!canSwipe) {
return super.onInterceptTouchEvent(event);
}
isSilding = false;
if (mContentView.getScrollX() <= -viewWidth / 4) {
isFinish = true;
scrollRight();
} else {
scrollOrigin();
isFinish = false;
}
break;
}
return true;
}
同樣此方法中也會(huì)根據(jù) onInterceptTouchEvent() 中的 DOWN 事件的判定結(jié)果 canSwipe 來決定是否按默認(rèn)方式消費(fèi)事件辽剧,MOVE 事件中如果滿足側(cè)滑條件則會(huì)調(diào)用 scrollBy() 將 mContentView 按滑動(dòng)方向進(jìn)行移動(dòng)送淆,而此處的 mContentView 即是 SwipeBackLayout 自身,因此整個(gè)顯示的界面會(huì)被按照滑動(dòng)方向移動(dòng)怕轿。當(dāng)手指抬起時(shí)如果滑動(dòng)距離超過 1/4 界面寬度(可以按自己需求調(diào)整)偷崩,則視為側(cè)滑返回完成,讓 Scroller 自動(dòng)完成剩余距離的滑動(dòng)撞羽,否則讓 Scroller 恢復(fù)到滑動(dòng)起始位置
/**
* 滾動(dòng)出界面
*/
private void scrollRight() {
final int delta = (viewWidth + mContentView.getScrollX());
// 調(diào)用startScroll方法來設(shè)置一些滾動(dòng)的參數(shù)阐斜,我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來滾動(dòng)item
mScroller.startScroll(mContentView.getScrollX(), 0, -delta + 1, 0,
Math.abs(delta));
postInvalidate();
}
/**
* 滾動(dòng)到起始位置
*/
private void scrollOrigin() {
int delta = mContentView.getScrollX();
// 調(diào)用startScroll方法來設(shè)置一些滾動(dòng)的參數(shù),我們?cè)赾omputeScroll()方法中調(diào)用scrollTo來滾動(dòng)item
mScroller.startScroll(mContentView.getScrollX(), 0, -delta, 0,
Math.abs(delta));
postInvalidate();
}
/**
* 具體執(zhí)行 Scroller 中的滾動(dòng)及將滑動(dòng)距離傳遞給外部接口
*/
@Override
public void computeScroll() {
Log.i("computeScroll","computeScroll");
if (mSwipeListener != null) {
double scrollx = Math.abs(mContentView.getScrollX());
double offset = scrollx / viewWidth;
if (offset > 0.9) {
offset = 1d;
}
mSwipeListener.swipeValue(offset);
}
if (mScroller.computeScrollOffset()) {
Log.i("computeScroll","mScroller");
mContentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
if (mScroller.isFinished() && isFinish) {
mActivity.finish();
}
}
}
結(jié)語
以上就是簡(jiǎn)化后的側(cè)滑返回的基本使用和原理的簡(jiǎn)單分析放吩,完整代碼可以參考 PandaEye歡迎 Star智听。文章一遍過為反復(fù)檢查如有不妥之處歡迎大家踴躍交流。