1. android 怎麼實現一個view懸浮到recyclerview上層
可以使用
layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
防止item 交換位置
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
layoutManager.invalidateSpanAssignments(); //防止第一行到頂部有空白區域
}
2. android 如何獲取一個界面最頂層的view並處理單擊事件的分發機制
android事件分發機制 就是一個觸摸事件發生了,從一個窗口傳遞到一個視圖,再傳遞到另外一個視圖,最後被消費的過程,在android中還是比較復雜的傳遞流程如下:
(1) 事件從Activity.dispatchTouchEvent()開始傳遞,只要沒有被停止或攔截,從最上層的View(ViewGroup)開始一直往下(子View)傳遞。子View可以通過onTouchEvent()對事件進行處理。
(2) 事件由父View(ViewGroup)傳遞給子View,ViewGroup可以通過onInterceptTouchEvent()對事件做攔截,停止其往下傳遞。
3. 安卓視圖層級大揭秘
最近接了一個語音控制的功能,UI上的具體實現就是在應用上遮蓋一個透明防觸層,在語音狀態下阻止用戶點擊,但不能影響物理返回鍵的Dialog呼出即控制,同時對於非物理返回鍵呼出的Dialog也要阻止操作。功能看起來很繞,我們用一張圖片來具體說明一下。
通過圖片不難看出,我們要實現的語音控制層其實是介於應用視圖與視圖內部提示框之上,同時又在Back返回鍵彈窗之下的一個層級。因為一直以來對安卓視圖層級的探究不是很深入,所以借著做這個功能對安卓視圖層級這一塊的知識進行了一下總結梳理。
首先讓我們通過一張層級圖來明確幾個重要的概念Window,DecorView和mContentParent。
在Android中不管是Activity、Toast、ActionBar還是Dialog,他們的視圖都是附加到Window上,其實基本上所有的view同時通過Window來呈現的,因此Window可以理解為是view的承載者和管理者。Window 有三種類型,分別是應用 Window、子 Window 和系統 Window。應用類 Window 對應一個 Acitivity,子 Window 不能單獨存在,需要依附在特定的父 Window 中,比如常見的一些 Dialog 就是一個子 Window。系統 Window是需要聲明許可權才能創建的 Window,比如 Toast 和系統狀態欄都是系統 Window。
DecorView是Windows中的View的最頂層View。其實DecorView是FrameLayout的子類,它裡麵包含了一個存有ActionBar以及mContentParent的LinearLayout。
mContentParent這個名字可能會有些陌生,其實他就是我們經常使用的應用根布局,即android.R.id.content。Activity中的setContentView其實就是通過LayoutInflater將XML布局轉換成View並添加到mContentParent中。
每個Activity都會持有一個Window,而在安卓中,Window只有唯一的一個實現類PhoneWindow ,所以每個Activity都會持有一個PhoneWindow,在PhoneWindow中會持有頂層視圖DecorView。那麼Activity是怎麼建立與PhoneWindow的聯系的呢,讓我們通過源碼來探究一下:
在Activity的啟動過程中會執行ActivityThread的performLaunchActivity方法,其中調用Activity的attach。在attach()方法中實例化Activity持有的mWindow。由於 Activity 實現了 Window 的 Callback 介面,因此當 Window 接受到外界的狀態改變時就會回調 Activity 的方法。
可以看到,在PhoneWindow裡面,出現了成員變數DecorView。而這里,DecorView則是PhoneWindow裡面的一個內部類,它是繼承於FrameLayout。
這是我們每次寫Activity都會調用的setContentView方法,它的內部調用了getWindow()的setContentView,這個mWindow就是PhoneWindow。
我們看到在PhoneWindow中有三個setContentView的重載方法。在setContentView(int layoutResID)中,首先判斷了mContentParent ,如果mContentParent 為空即為第一次調用的時候,就執行installDecor()方法,創建DecorView,並添加到mContentParent上。如果mContentParent不為空,那麼將mContentParent中的view移除。接著通過mLayoutInflater將XML轉換為View樹,並且添加至mContentParent視圖中。 添加完成後回調通知onContentChanged,表示完成界面載入。
首先判斷mDecor是否為空,如果為空則通過generateDecor創建一個DecorView,緊接著設置DecorView的獲取焦點能力為FOCUS_AFTER_DESCENDANTS,即先分發給Child View進行處理,如果所有的Child View都沒有處理,則自己再處理。第一次DecorView未載入到mContentParent,所以mContentParent為空,調用generateLayout將setContentView內容添加到mContentParent。
定製過Acitivity的Actionbar或是Fullscreen的同學一定都知道,requesetFeature方法需要在setContentView之前調用,這就是原因。setContentView的實質顯示是觸發了Activity的resume狀態,也就是觸發了makeVisible方法。
這里將getWindow().getAttributes()作為了LayoutParams,在WindowManager中:
可以看到Activity的窗口類型是TYPE_APPLICATION,這個TYPE類型決定了在Window層的顯示層級,TYPE類型總覽如下:
Dialog不屬於View,他是應用的子window,所以這也是為什麼我們通過給mContentParent添加view無法實現遮擋Dialog的原因。Dialog 中 Window 同樣是通過 PolicyManager 的 makeNewWindow 方法來完成的,普通的 Dialog 必須採用 Activity 的 Context,如果採用 Application 的 Context 就會報錯。這是因為沒有應用 token 導致的,而應用 token 一般只有 Activity 擁有。常規Dialog的TYPE為TYPE_APPLICATION_ATTACHED_DIALOG,通過不同的TYPE層級劃分我們可以找到置於常規Dialog之上的WindowManager LayoutParams 屬性,例如TYPE_SYSTEM_ALERT與TYPE_TOAST,設置了這兩個屬性的布局是可以將常規Dialog完全遮蓋的。他們的區別在於一個是系統級別的Dialog一個是Toast,系統Dialog需要申請許可權。所以我們的第一個方案就是可遮擋的Dialog使用常規Dialog,語音提示框採用TYPE_SYSTEM_ALERT。但是都知道安卓有一個無法逃避的問題,就是廠商定製,在MUI的framework層,出於對「安全」的考慮,默認為用戶關閉了懸浮窗許可權,也就是是說設置了TYPE_SYSTEM_ALERT屬性的視圖默認是無法顯示的,需要用戶手動開啟許可權以後方可顯示。
雖然可以在用戶啟動的時候根據用戶機型選擇跳轉開啟許可權頁,但作為一個有情懷的開發這種不完美的體驗還是不能接受的。根據之前對安卓視圖層級的學習,我們有了第二套方案。應用視圖是存放於mContentParent他與Activity同屬TYPE_APPLICATION Window層級屬於最下層,常規Dialog的層級是TYPE_APPLICATION_ATTACHED_DIALOG,所以我們將常規Dialog作為最上層不可遮擋的提示框,下面只需考慮可遮擋的彈窗與語音控制兩層即可。因為語音控制層需要能夠遮擋提示彈窗,所以需要語音控制層在彈窗的上層,經過之前的學習,我們把彈窗加入到mContentParent,把語音控制層添加到DecorView層即可完美的解決問題。mContentParent為一個FrameLayout,應用視圖通過sentContentView率先添加到mContentParent中,作為提示彈窗,添加順序一定相對應用視圖置後,所以當提示彈窗再次向mContentParent添加的時候,即會添加到應用視圖之上。而DecorView是mContentParent的父容器,也是一個FrameLayout,添加語音提示框的時候mContentParent一定已經存在,所以添加的時候一定會在mContentParent之上。
就這樣,一個看似復雜的需求通過對安卓源碼的探究完美的解決了,很多時候當我們遇到難以解決的問題,不妨試試回到問題的原點,思考一下問題的本質,很多時候都會有不一樣的發現。