在電視的交互設計中,通常需要一個焦點框來指示當前選中了哪個控件,如果每個控件都通過給background設置selector的方式,實現焦點框效果,需要寫很多xml文件。
所以這裏偷懶實現一個通用的焦點框,實現自動跟隨焦點變化,實現焦點框指示。原理如下圖。
原理就是在Activity的setContentView方法導入的View(上圖藍色)上層再添加一層充滿屏幕的View(半透明黃色)。這層View用來繪製焦點框。當真實view的焦點發生變化,獲取真實獲得焦點的view的位置,把焦點框繪製在它的上邊。這樣就實現了個通用焦點框。
完成上述操作需要解決以下幾個問題
1.如何監聽真實view的焦點變化
2.如何把蒙版層添加到原始view的上層
3.如何得到獲得到焦點真實view的位置
4.把焦點框繪製在真實view的正上方
焦點變化監聽
需要知道view樹焦點狀態的變化
Android中提供了ViewTreeObserver來監聽View樹中一系列狀態變化
看下ViewTreeObserver的解釋
這是一個註冊監聽視圖樹的觀察者(observer),在視圖樹種全局事件改變時得到通知。這個全局事件不僅還包括整個樹的佈局,從繪畫過程開始,觸摸模式的改變等。ViewTreeObserver不能夠被應用程序實例化,因爲它是由view提供,通過getViewTreeObserver()可以獲取到該實例。
該類中有以下接口
interface ViewTreeObserver.OnGlobalFocusChangeListener
當在一個視圖樹中的焦點狀態發生改變時,所要調用的回調函數的接口類
interface ViewTreeObserver.OnGlobalLayoutListener
當在一個視圖樹中全局佈局發生改變或者視圖樹中的某個視圖的可視狀態發生改變時,所要調用的回調函數的接口類
interface ViewTreeObserver.OnPreDrawListener
當一個視圖樹將要繪製時,所要調用的回調函數的接口類
interface ViewTreeObserver.OnScrollChangedListener
當一個視圖樹中的一些組件發生滾動時,所要調用的回調函數的接口類
interface ViewTreeObserver.OnTouchModeChangeListener
當一個視圖樹的觸摸模式發生改變時,所要調用的回調函數的接口類
這裏我們只用ViewTreeObserver.OnGlobalFocusChangeListener,通過調用ViewTreeObserver實例的addOnGlobalFocusChangeListener方法來給view樹設置view的focus發生變化的監聽。
private void bindListener() {
//獲取根元素
View mContainerView = this.getWindow().getDecorView();//.findViewById(android.R.id.content);
//得到整個view樹的viewTreeObserver
ViewTreeObserver viewTreeObserver = mContainerView.getViewTreeObserver();
//給觀察者設置焦點變化監聽
viewTreeObserver.addOnGlobalFocusChangeListener(mFocusLayout);
}
添加焦點層
焦點層是一個充滿屏幕的ViewGroup,作爲焦點框的父容器,用來放焦點框。可以通過addContentView向根佈局中添加這個ViewGroup,這裏添加的是自定義的FocusLayout。
這裏看下addContentView方法具體把view添加到了哪裏。用hierarchyViewer看下結構,紅色圈起來的view爲add進去的FocusLayout,其同級上邊的RelativeLayout爲setContentView方法添加的佈局。從下圖的view樹結構,可知addContentView添加的FocusLayout一直在我們平常使用的setContentView的佈局的上層。
繪製焦點框
這裏主要解決問題
3.如何得到獲得到焦點真實view的位置
4.把焦點框繪製在真實view的正上方
這裏的FocusLayout是繼承自RelativeLayout,其內部定義了一個普通的view,它就是我們要使用的焦點框mFocusView,我們把一個焦點框圖片設置爲它的背景(通過更改背景圖片可以實現不通樣式焦點框),最終通過調用mFocusView的layout方法把它繪製在指定位置。
現在需要解決的問題是知道把mFocusView繪製到哪裏。
我們的FocusLayout實現了OnGlobalFocusChangeListener接口,看下它的定義
public interface OnGlobalFocusChangeListener {
public void onGlobalFocusChanged(View oldFocus, View newFocus);
}
onGlobalFocusChanged方法分別傳入了失去了焦點的view,與獲得焦點的view。這樣我們就可以繪製view了,由於我們只是把焦點框layout到新view的上邊,這裏只使用了newFocus就夠了。想實現過度動畫可以嘗試使用屬性動畫,這裏不多贅述。
通過調用View的getGlobalVisibleRect方法,可以獲取當前view相對於整個屏幕的位置,getGlobalVisibleRect需要傳入一個Rect對象,因此通過調用newFocus的getGlobalVisibleRect可以得到新焦點的位置
Rect viewRect = new Rect();
newFocus.getGlobalVisibleRect(viewRect);
viewRect存儲的是獲得焦點的View的相對屏幕左上角的位置。當然爲了處理有ActionBar等其他標題欄的情況,因爲layout方法使用的是相對父控件的位置,我們要的到相對於FocusLayout左上角的位置,所以進行下位置修正。
/**
* 由於getGlobalVisibleRect獲取的位置是相對於全屏的,所以需要減去FocusLayout本身的左與上距離,變成相對於FocusLayout的
* @param rect
*/
private void correctLocation(Rect rect) {
Rect layoutRect = new Rect();
this.getGlobalVisibleRect(layoutRect);
rect.left -= layoutRect.left;
rect.right -= layoutRect.left;
rect.top -= layoutRect.top;
rect.bottom -= layoutRect.top;
}
最後在調用layout方法把焦點框繪製到指定位置,同時根據當前獲取焦點View的大小,計算當前焦點框的大小。
/**
* 設置焦點view的位置,計算焦點框的大小
*
* @param left
* @param top
* @param right
* @param bottom
*/
protected void setFocusLocation(int left, int top, int right, int bottom) {
int width = right - left;
int height = bottom - top;
this.mFocusLayoutParams.width = width;
this.mFocusLayoutParams.height = height;
this.mFocusLayoutParams.leftMargin = left;
this.mFocusLayoutParams.topMargin = top;
this.mFocusView.layout(left, top, right, bottom);
}
整個工程代碼量很少就自定義了一個不到100行代碼的FocusLayout控件,看下代碼一切都懂了。
最後實現的效果:
上述焦點框在普通的控件問題沒問題,在ListView,GridView上使用不能正常顯示,可以使用RecyclerView來代替。