摘要:
最近公司的App做了一次改版,對UI頁面做了一些用戶體驗上的優化。
(本文是對本次工作的踩坑總結)
頁面效果:
本次App改版,筆者這牽扯到兩個功能點的實現:
點擊“眼睛圖標”,實現顯示/隱藏各類資金 (老版有此功能,本次做些調整)
實現導航欄背景圖漸變
實現策略:
功能一:點擊“眼睛圖標”,實現顯示/隱藏各類資金
第1個功能點很簡單,其實就是監聽“眼睛圖標”的點擊事件,將需要顯示的“數字”顯示成固定文本“ **** ”并將“開眼圖標”設置成“閉眼圖標”。點擊“閉眼圖標”時,對之前的操作進行還原。
理論上是這樣沒有錯,但實際操作起來就會發現有坑了/(ㄒoㄒ)/~
當筆者在點擊“眼睛圖標”時,發現Toolbar將點擊事件攔截了,事件無法傳遞到下層布局......
那么怎樣才能穿透Toolbar將點擊事件分發到下面的布局呢?
Toolbar是一個懸浮在最上層的、獨立的ViewGroup,里面沒有包含下層的內容布局。而且Toolbar源碼中onTouchEvent方法的返回值為true,意味著事件最多傳遞到這就會被消費掉。
難道要自定義Toolbar,重寫onTouchEvent方法?這是筆者最初的想法,但是后來想了想,這種修改源碼的方式有點作死(因為筆者沒有詳細看過Toolbar的源碼,而且就算看過Toolbar的源碼,源碼后期發生變更時還要維護)。
筆者的XML布局層次(簡化版):
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout >
<!-- 內容部分 -->
<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>
我們可以看到“標題欄”和“內容部分”在布局上是“平行關系”,但實際在視圖上“標題欄”是在“內容部分”的上方。
尋找解決辦法:
筆者思考了很長時間,想出了一個相對靠譜的解決辦法:Toolbar執行到onTouchEvent之后,事件就被消費了。我們可以在這之前搞點事情,比如改變事件流的傳遞方向,將事件流向下層布局分發。
實現事件穿透的代碼(僅包含觸摸監聽器的實現):
// 穿透Toolbar的點擊事件,向下層分發處理
mToolbarOnTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return findViewById(R.id.swipe_refresh_layout).dispatchTouchEvent(event);
}
};
略懂事件傳遞機制的Android程序員應該都知道,View觸摸監聽器的onTouch方法會在onTouchEvent方法執行完成之前執行,那么我們可以在onTouch方法里再進行1次事件分發,調用“內容部分”的最外層布局SwipeRefreshLayout的分發方法dispatchTouchEvent(MotionEvent event),并將正在Toolbar中進行傳遞的event事件作為參數傳遞進去,讓SwipeRefreshLayout繼續處理該事件,從而實現事件穿透。
解決辦法終于找到了,那么直接上來就給Toolbar設置完這個觸摸監聽器就萬事大吉了嗎?
我們還做了一個背景圖片漸變的功能,Toolbar背景不可見或半透明時響應下層布局的點擊事件很正常,很容易理解。但是背景圖片如果完全可見,點擊Toolbar還響應下層布局的點擊事件,是不是用戶體驗上有點別扭呢?(全都是細節)
最終的解決方案 (動態給Toolbar設置監聽器):
-
當背景圖片完全可見之前,給Toolbar設置觸摸監聽器mToolbarOnTouchListener。
mToolbar.setOnTouchListener(mToolbarOnTouchListener);
-
當背景圖片完全可見之后,將Toolbar的觸摸監聽器設置為null。
mToolbar.setOnTouchListener(null);
至此,第一個問題就描述完了。
功能二:實現導航欄背景圖漸變
之前筆者做過Toolbar背景顏色漸變的效果,但筆者的App基本上每個頁面的Toolbar背景用的都是圖片,不是顏色。仔細想了想,圖片的漸變也只能用透明度來搞了。
在網上百度&谷歌了一下,終于找到了設置圖片透明度的方法(大致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添加了一個滾動監聽器,根據RecyclerView豎直滾動的距離動態決定背景圖片的透明度,具體細節如下:
當RecyclerView豎直滾動距離<Toolbar的高度時,不設置背景圖(Toolbar背景透明),并設置上觸摸監聽器(穿透Toolbar點擊事件)。
當RecyclerView豎直滾動距離>=Toolbar的高度且<2.5倍的Toolbar高度時,漸變背景圖,并設置上觸摸監聽器(穿透Toolbar點擊事件)。
RecyclerView豎直滾動距離>=2.5倍的Toolbar高度時,全顯背景圖,觸摸監聽器設置為null。
接著運行了一下,發現已經實現了文章開始時的動態圖效果。
之后,放心的打包完App提交給測試MM測試了。但好景不長,測試MM那測試機類型很多,在很多華為手機上測出了一個Bug,如下圖所示:
可以看到,在華為手機上,筆者的APP中所有用到該背景圖的地方都會受到透明度影響。
后來從網上查閱了一些資料,才知道原來華為手機是做了一些優化處理的。同一張圖片(比如R.drawable.lcs_actionbar_bg)在被轉化成Drawable對象并在內存中使用時會被“共享”,意味著筆者App中所有對這個Drawable(R.drawable.lcs_actionbar_bg)的操作全都是在對同一個對象進行操作。
華為手機的這種處理方式減少多余對象的生成,減少了內存暫用,減少了GC,仔細想想還是挺贊的。(好像扯遠了,該回歸正題想想怎么處理了o(╯□╰)o....)
一開始想了想,覺得可以這樣處理:在離開這個頁面時,記錄一下透明度,把Drawable背景圖設置為完全可見的;回到這個頁面時,根據記錄值還原Drawable的透明度。
理想是美好的,現實是殘酷的。這個頁面的管理器是一個Fragment,筆者在onStop方法里做了上述這種處理,但還是會偶發出現這種透明度的Bug,看來筆者還有很多細節沒有考慮到(怎么感覺這細節越陷越深了/(ㄒoㄒ)/~~)。
最終的解決辦法:
筆者無奈之下放棄了之前的處理方式,想了一個投機取巧的辦法:
把資源文件 lcs_actionbar_bg.png 又復制了一份,命名為lcs_actionbar_bg2.png
讓 lcs_actionbar_bg2.png 這張圖片獨立為這個導航欄漸變的頁面服務,不會影響到其他界面。
最后,華為手機導航欄透明度的Bug終于解決了。
題外話
有朋友問:搞個標題欄特效為什么不用 AppBarLayout + CollapsingToolbarLayou + Toolbar 做個折疊效果?這個應該很簡單吧?
答:產品說我們的App很多頁面都有這種效果,還是換種方式實現吧...
最后發表一下感慨: