LeanBack是Google官方推出的TV端的功能库,里面包含了很多在TV Android端开发常用的控件,本文重点介绍其对RecyclerView适配TV端做的封装:HorizontalGridView、VerticalGridView。
HorizontalGridView和VerticalGridView都继承自RecyclerView,针对TV的特性,在item排版、焦点流转、上/失焦动画、记住焦点、焦点item对齐位置等方面做了比较好的封装,其继承结果如下
从上图可见,HorizontalGridView和VerticalGridView皆继承自BaseGridView,BaseGridView则继承自RecyclerView,BaseGridView实现了大部分上述的封装,包括列表的滑动、焦点事件的分发等等。其中,BaseGridView持有了GridLayoutManager的引用,实际上,BaseGridView大量的方法都流转到GridLayoutManager,所以上述的功能的实现其实是在GridLayoutManager实现的。
注意这里的GridLayoutManager跟平常使用GridLayoutManager不是同一个,上图中可以看到,它直接继承自RecyclerView.LayoutManager,我们知道,RecyclerView将列表的具体展示模式交由LayoutManager实现和管理,因此只要切换不同的LayoutManager就可以实现不同的列表模式。这里的GridLayoutManager则是针对TV特性,实现了前面所述的功能的一个实现。这里暂时不对具体实现做深入的探讨,着重介绍HorizontalGridView和VerticalGridView的使用方法。
这里只对HorizontalGridView做介绍,VerticalGridView类似。
HorizontalGridView的封装采用了MVP模式,涉及的几个类及其功能如下:
简单用法如下:
实现Presenter,用于创建item和绑定数据:
public class HPresenter extends Presenter {
/**
* 创建ViewHolder,作用同RecyclerView$Adapter的onCreateViewHolder
* @param viewGroup
* @return
*/
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
View inflate = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_h, viewGroup, false);
return new ViewHolder(inflate);
}
/**
* 同RecyclerView$Adapter的onBindViewHolder,但是解耦了position
* @param viewHolder
* @param o
*/
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object o) {
if (o instanceof Integer){
((TextView)viewHolder.view.findViewById(R.id.tv_index)).setText(o.toString());
}
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
//解绑时释放资源
}
}
模拟数据:
private void initData(){
if (mDataList==null){
mDataList=new ArrayList<>();
for (int i = 0; i <100 ; i++) {
mDataList.add(i);
}
}
}
配置HorizontalGridView:
private void initViews() {
mHgv= (HorizontalGridView) findViewById(R.id.hgv);
//3行
mHgv.setNumRows(3);
//item纵向和横向的距离
mHgv.setItemSpacing(20);
//item的对齐方式
mHgv.setGravity(Gravity.CENTER_VERTICAL);
//设置
mHgv.setOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
@Override
public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition) {
super.onChildViewHolderSelected(parent, child, position, subposition);
Log.d(TAG, "onChildViewHolderSelected() returned: " + position);
//大部分情况下可以通过该方法获取到position
}
@Override
public void onChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition) {
super.onChildViewHolderSelectedAndPositioned(parent, child, position, subposition);
Log.d(TAG, "onChildViewHolderSelectedAndPositioned() returned: " + position);
//当通过setSelectedPosition()方法大幅移动列表时,该方法会回调,返回的是最终的真实的position(当set的值超出范围时...)
}
});
HPresenter presenter=new HPresenter();
//创建ObjectAdapter,用于提供数据,当有多种类型时,传入PresenterSelector
ArrayObjectAdapter objectAdapter=new ArrayObjectAdapter(presenter);
//初始化模拟数据
initData();
//添加数据
objectAdapter.addAll(0,mDataList);
//通过前面创建的objectAdapter创建ItemBridgeAdapter,完成数据的传递
ItemBridgeAdapter bridgeAdapter=new ItemBridgeAdapter(objectAdapter);
//将ItemBridgeAdapter传入HorizontalGridView
mHgv.setAdapter(bridgeAdapter);
mHgv.requestFocus();
//设置上焦动画
FocusHighlightHelper.setupHeaderItemFocusHighlight(bridgeAdapter);
}
如果上焦的item除了要有动画,还需要有状态的改变,如背景变化等,则需要设置itemView的selected状态,FocusHighlightHelper内置的动画会在item被上焦时会调用itemView.setSelected(true):
private void viewFocused(View view, boolean hasFocus) {
lazyInit(view);
view.setSelected(hasFocus);
FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
if (animator == null) {
animator = new HeaderFocusAnimator(view, mSelectScale, mDuration);
view.setTag(R.id.lb_focus_animator, animator);
}
animator.animateFocus(hasFocus, false);
}
完整代码如下:
public class MainActivity extends Activity {
public static final String TAG="MainActivity";
private HorizontalGridView mHgv;
private List mDataList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
}
private void initViews() {
mHgv= (HorizontalGridView) findViewById(R.id.hgv);
//3行
mHgv.setNumRows(3);
//item纵向和横向的距离
mHgv.setItemSpacing(20);
//item的对齐方式
mHgv.setGravity(Gravity.CENTER_VERTICAL);
//设置
mHgv.setOnChildViewHolderSelectedListener(new OnChildViewHolderSelectedListener() {
@Override
public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition) {
super.onChildViewHolderSelected(parent, child, position, subposition);
Log.d(TAG, "onChildViewHolderSelected() returned: " + position);
//大部分情况下可以通过该方法获取到position
}
@Override
public void onChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder child, int position, int subposition) {
super.onChildViewHolderSelectedAndPositioned(parent, child, position, subposition);
Log.d(TAG, "onChildViewHolderSelectedAndPositioned() returned: " + position);
//当通过setSelectedPosition()方法大幅移动列表时,该方法会回调,返回的是最终的真实的position(当set的值超出范围时...)
}
});
HPresenter presenter=new HPresenter();
//创建ObjectAdapter,用于提供数据,当有多种类型时,传入PresenterSelector
ArrayObjectAdapter objectAdapter=new ArrayObjectAdapter(presenter);
//初始化模拟数据
initData();
//添加数据
objectAdapter.addAll(0,mDataList);
//通过前面创建的objectAdapter创建ItemBridgeAdapter,完成数据的传递
ItemBridgeAdapter bridgeAdapter=new ItemBridgeAdapter(objectAdapter);
//将ItemBridgeAdapter传入HorizontalGridView
mHgv.setAdapter(bridgeAdapter);
mHgv.requestFocus();
//设置上焦动画
FocusHighlightHelper.setupHeaderItemFocusHighlight(bridgeAdapter);
}
private void initData(){
if (mDataList==null){
mDataList=new ArrayList<>();
for (int i = 0; i <100 ; i++) {
mDataList.add(i);
}
}
}
}
效果如下:
HorizontalGridView效果图需要注意的是,item的排布方式是先纵向后横向,及第2个view位于第二行第一列,而非第一行第二列
HorizontalGridView默认上焦的item会居中显示,可以通过BaseGridView的方法改变对齐方式:
//设置滚动策略
public void setFocusScrollStrategy(int scrollStrategy) {
if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
&& scrollStrategy != FOCUS_SCROLL_PAGE) {
throw new IllegalArgumentException("Invalid scrollStrategy");
}
mLayoutManager.setFocusScrollStrategy(scrollStrategy);
requestLayout();
}
//支持三种策略
//移动时是上焦的item对齐某个位置
public final static int FOCUS_SCROLL_ALIGNED = 0;
//移动使上焦的item始终保持在显示区域内
public final static int FOCUS_SCROLL_ITEM = 1;
//当焦点要移出显示区域时,滚动一页
public final static int FOCUS_SCROLL_PAGE = 2;
//当上面设置了对齐到指定位置时,可以通过下面的方法设置对齐的方式
public void setWindowAlignment(int windowAlignment) {
mLayoutManager.setWindowAlignment(windowAlignment);
requestLayout();
}
//支持4中模式
//对齐左边或者上边
public final static int WINDOW_ALIGN_LOW_EDGE = 1;
//对齐右边或者下边
public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
//同时对齐以上两边
public final static int WINDOW_ALIGN_BOTH_EDGE =
WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
//对齐到指定的keyline
public final static int WINDOW_ALIGN_NO_EDGE = 0;
//以上4中方式都存在一条keyline,当焦点离开对齐的边时,保持在keyline的位置,keyline可以通过以下方法设置
//offset为正值,则表示keyline距离lowedge的像素值,如果为负值,则其绝对值表示距离highedge的距离,默认值为0
public void setWindowAlignmentOffset(int offset) {
mLayoutManager.setWindowAlignmentOffset(offset);
requestLayout();
}
//offsetPercent表示keyline距离lowedge的百分比(width的百分比),默认值为50
public void setWindowAlignmentOffsetPercent(float offsetPercent) {
mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
requestLayout();
}
以上便是HorizontalGridView的基本用法,VerticalGridView的用法类似,便不再赘述。