摘要:
最近公司的App做了一次改版,對UI頁面做了一些用戶體驗上的優(yōu)化。
(本文是對本次工作的踩坑總結)
頁面效果:
本次App改版,筆者這牽扯到兩個功能點的實現(xiàn):
點擊“眼睛圖標”,實現(xiàn)顯示/隱藏各類資金 (老版有此功能,本次做些調(diào)整)
實現(xiàn)導航欄背景圖漸變

實現(xiàn)策略:
功能一:點擊“眼睛圖標”,實現(xiàn)顯示/隱藏各類資金
第1個功能點很簡單,其實就是監(jiān)聽“眼睛圖標”的點擊事件,將需要顯示的“數(shù)字”顯示成固定文本“ **** ”并將“開眼圖標”設置成“閉眼圖標”。點擊“閉眼圖標”時,對之前的操作進行還原。
理論上是這樣沒有錯,但實際操作起來就會發(fā)現(xiàn)有坑了/(ㄒoㄒ)/~
當筆者在點擊“眼睛圖標”時,發(fā)現(xiàn)Toolbar將點擊事件攔截了,事件無法傳遞到下層布局......
那么怎樣才能穿透Toolbar將點擊事件分發(fā)到下面的布局呢?
Toolbar是一個懸浮在最上層的、獨立的ViewGroup,里面沒有包含下層的內(nèi)容布局。而且Toolbar源碼中onTouchEvent方法的返回值為true,意味著事件最多傳遞到這就會被消費掉。

難道要自定義Toolbar,重寫onTouchEvent方法?這是筆者最初的想法,但是后來想了想,這種修改源碼的方式有點作死(因為筆者沒有詳細看過Toolbar的源碼,而且就算看過Toolbar的源碼,源碼后期發(fā)生變更時還要維護)。
筆者的XML布局層次(簡化版):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout >
<!-- 內(nèi)容部分 -->
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view" />
</android.support.v4.widget.SwipeRefreshLayout>
<!-- 標題欄 -->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
</android.support.v7.widget.Toolbar>
</RelativeLayout>
我們可以看到“標題欄”和“內(nèi)容部分”在布局上是“平行關系”,但實際在視圖上“標題欄”是在“內(nèi)容部分”的上方。
尋找解決辦法:
筆者思考了很長時間,想出了一個相對靠譜的解決辦法:Toolbar執(zhí)行到onTouchEvent之后,事件就被消費了。我們可以在這之前搞點事情,比如改變事件流的傳遞方向,將事件流向下層布局分發(fā)。
實現(xiàn)事件穿透的代碼(僅包含觸摸監(jiān)聽器的實現(xiàn)):
// 穿透Toolbar的點擊事件,向下層分發(fā)處理
mToolbarOnTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return findViewById(R.id.swipe_refresh_layout).dispatchTouchEvent(event);
}
};
略懂事件傳遞機制的Android程序員應該都知道,View觸摸監(jiān)聽器的onTouch方法會在onTouchEvent方法執(zhí)行完成之前執(zhí)行,那么我們可以在onTouch方法里再進行1次事件分發(fā),調(diào)用“內(nèi)容部分”的最外層布局SwipeRefreshLayout的分發(fā)方法dispatchTouchEvent(MotionEvent event),并將正在Toolbar中進行傳遞的event事件作為參數(shù)傳遞進去,讓SwipeRefreshLayout繼續(xù)處理該事件,從而實現(xiàn)事件穿透。
解決辦法終于找到了,那么直接上來就給Toolbar設置完這個觸摸監(jiān)聽器就萬事大吉了嗎?
我們還做了一個背景圖片漸變的功能,Toolbar背景不可見或半透明時響應下層布局的點擊事件很正常,很容易理解。但是背景圖片如果完全可見,點擊Toolbar還響應下層布局的點擊事件,是不是用戶體驗上有點別扭呢?(全都是細節(jié))
最終的解決方案 (動態(tài)給Toolbar設置監(jiān)聽器):
-
當背景圖片完全可見之前,給Toolbar設置觸摸監(jiān)聽器mToolbarOnTouchListener。
mToolbar.setOnTouchListener(mToolbarOnTouchListener); -
當背景圖片完全可見之后,將Toolbar的觸摸監(jiān)聽器設置為null。
mToolbar.setOnTouchListener(null);
至此,第一個問題就描述完了。
功能二:實現(xiàn)導航欄背景圖漸變
之前筆者做過Toolbar背景顏色漸變的效果,但筆者的App基本上每個頁面的Toolbar背景用的都是圖片,不是顏色。仔細想了想,圖片的漸變也只能用透明度來搞了。
在網(wǎng)上百度&谷歌了一下,終于找到了設置圖片透明度的方法(大致3步)
// 獲取Drawable對象
Drawable mDrawable = ContextCompat.getDrawable(mActivity, R.drawable.lcs_actionbar_bg2);
// 設置Drawable的透明度
mDrawable.setAlpha(255);
// 給Toolbar設置背景圖
mToolbar.setBackgroundDrawable(mDrawable);
有了解決方案終于可以開工了:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
// 獲取背景圖片
Drawable mDrawable = ContextCompat.getDrawable(mActivity, R.drawable.lcs_actionbar_bg2);
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 省略了與漸變背景圖無關的代碼
......
int toolBarHeight = mToolbar.getMeasuredHeight();
if ((recyclerView.computeVerticalScrollOffset()) >= (toolBarHeight * 2.5)) { // >=Toolbar高度的2.5倍時全顯背景圖
mDrawable.setAlpha(255);
mToolbar.setBackgroundDrawable(mDrawable);
mToolbar.setOnTouchListener(null);
} else if((recyclerView.computeVerticalScrollOffset()) >= toolBarHeight){ // >=Toolbar高度&&<Toolbar高度的2.5倍時開始漸變背景圖
mDrawable.setAlpha((int) (255 * ((recyclerView.computeVerticalScrollOffset() - toolBarHeight)/(toolBarHeight * 1.5F))));
mToolbar.setBackgroundDrawable(mDrawable);
mToolbar.setOnTouchListener(mToolbarOnTouchListener);
} else { // 小于Toolbar高度時不設置背景圖
mToolbar.setBackgroundDrawable(null);
mToolbar.setOnTouchListener(mToolbarOnTouchListener);
}
}
});
RecyclerView.computeVerticalScrollOffset() 可以返回給我們recyclerView豎直滾動的距離。
筆者給RecyclerView添加了一個滾動監(jiān)聽器,根據(jù)RecyclerView豎直滾動的距離動態(tài)決定背景圖片的透明度,具體細節(jié)如下:
當RecyclerView豎直滾動距離<Toolbar的高度時,不設置背景圖(Toolbar背景透明),并設置上觸摸監(jiān)聽器(穿透Toolbar點擊事件)。
當RecyclerView豎直滾動距離>=Toolbar的高度且<2.5倍的Toolbar高度時,漸變背景圖,并設置上觸摸監(jiān)聽器(穿透Toolbar點擊事件)。
RecyclerView豎直滾動距離>=2.5倍的Toolbar高度時,全顯背景圖,觸摸監(jiān)聽器設置為null。
接著運行了一下,發(fā)現(xiàn)已經(jīng)實現(xiàn)了文章開始時的動態(tài)圖效果。
之后,放心的打包完App提交給測試MM測試了。但好景不長,測試MM那測試機類型很多,在很多華為手機上測出了一個Bug,如下圖所示:

可以看到,在華為手機上,筆者的APP中所有用到該背景圖的地方都會受到透明度影響。
后來從網(wǎng)上查閱了一些資料,才知道原來華為手機是做了一些優(yōu)化處理的。同一張圖片(比如R.drawable.lcs_actionbar_bg)在被轉化成Drawable對象并在內(nèi)存中使用時會被“共享”,意味著筆者App中所有對這個Drawable(R.drawable.lcs_actionbar_bg)的操作全都是在對同一個對象進行操作。
華為手機的這種處理方式減少多余對象的生成,減少了內(nèi)存暫用,減少了GC,仔細想想還是挺贊的。(好像扯遠了,該回歸正題想想怎么處理了o(╯□╰)o....)
一開始想了想,覺得可以這樣處理:在離開這個頁面時,記錄一下透明度,把Drawable背景圖設置為完全可見的;回到這個頁面時,根據(jù)記錄值還原Drawable的透明度。
理想是美好的,現(xiàn)實是殘酷的。這個頁面的管理器是一個Fragment,筆者在onStop方法里做了上述這種處理,但還是會偶發(fā)出現(xiàn)這種透明度的Bug,看來筆者還有很多細節(jié)沒有考慮到(怎么感覺這細節(jié)越陷越深了/(ㄒoㄒ)/~~)。
最終的解決辦法:
筆者無奈之下放棄了之前的處理方式,想了一個投機取巧的辦法:
把資源文件 lcs_actionbar_bg.png 又復制了一份,命名為lcs_actionbar_bg2.png

讓 lcs_actionbar_bg2.png 這張圖片獨立為這個導航欄漸變的頁面服務,不會影響到其他界面。
最后,華為手機導航欄透明度的Bug終于解決了。
題外話
有朋友問:搞個標題欄特效為什么不用 AppBarLayout + CollapsingToolbarLayou + Toolbar 做個折疊效果?這個應該很簡單吧?
答:產(chǎn)品說我們的App很多頁面都有這種效果,還是換種方式實現(xiàn)吧...
最后發(fā)表一下感慨:
