Android - Toolbar事件穿透和背景漸變

摘要:

最近公司的App做了一次改版,對UI頁面做了一些用戶體驗上的優化。

(本文是對本次工作的踩坑總結)

頁面效果:

本次App改版,筆者這牽扯到兩個功能點的實現:

  1. 點擊“眼睛圖標”,實現顯示/隱藏各類資金 (老版有此功能,本次做些調整)

  2. 實現導航欄背景圖漸變

app.gif

實現策略:

功能一:點擊“眼睛圖標”,實現顯示/隱藏各類資金

第1個功能點很簡單,其實就是監聽“眼睛圖標”的點擊事件,將需要顯示的“數字”顯示成固定文本“ **** ”并將“開眼圖標”設置成“閉眼圖標”。點擊“閉眼圖標”時,對之前的操作進行還原。

理論上是這樣沒有錯,但實際操作起來就會發現有坑了/(ㄒoㄒ)/~

當筆者在點擊“眼睛圖標”時,發現Toolbar將點擊事件攔截了,事件無法傳遞到下層布局......

那么怎樣才能穿透Toolbar將點擊事件分發到下面的布局呢?

Toolbar是一個懸浮在最上層的、獨立的ViewGroup,里面沒有包含下層的內容布局。而且Toolbar源碼中onTouchEvent方法的返回值為true,意味著事件最多傳遞到這就會被消費掉。

Toolbar.png

難道要自定義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_bug.gif

可以看到,在華為手機上,筆者的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

lcs_actionbar_bg2.png 這張圖片獨立為這個導航欄漸變的頁面服務,不會影響到其他界面。

最后,華為手機導航欄透明度的Bug終于解決了。

題外話

有朋友問:搞個標題欄特效為什么不用 AppBarLayout + CollapsingToolbarLayou + Toolbar 做個折疊效果?這個應該很簡單吧?

答:產品說我們的App很多頁面都有這種效果,還是換種方式實現吧...

最后發表一下感慨:

mind.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容