一种TV(Android系统)通用焦点框的实现

来源:互联网 发布:淘宝双十一全民疯抢 编辑:程序博客网 时间:2024/05/29 04:45
在电视的交互设计中,通常需要一个焦点框来指示当前选中了哪个控件,如果每个控件都通过给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来代替。
源代码:https://github.com/pengyuntao/TvFocusDemo
1 0