欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LeanBack:HorizontalGridView和VerticalGridView使用详解

程序员文章站 2022-07-14 23:18:05
...

LeanBack是Google官方推出的TV端的功能库,里面包含了很多在TV Android端开发常用的控件,本文重点介绍其对RecyclerView适配TV端做的封装:HorizontalGridView、VerticalGridView。

HorizontalGridView和VerticalGridView都继承自RecyclerView,针对TV的特性,在item排版、焦点流转、上/失焦动画、记住焦点、焦点item对齐位置等方面做了比较好的封装,其继承结果如下

LeanBack:HorizontalGridView和VerticalGridView使用详解
HorizontalGridView和VerticalGridView继承结构

从上图可见,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模式,涉及的几个类及其功能如下:

  1. HorizontalGridView:RecyclerView的子类;
  2. GridLayoutManager:适配的LayoutManager,支持横向的多行列表和纵向的多列列表,已包含在BaseGridView里,不需要额外设置,这点要注意;
  3. ObjectAdapter:承担MVP中model的职责,负责提供数据访问接口
  4. Presenter:职责类似RecyclerView的adapter,辅助item视图的创建和数据绑定等
  5. PresenterSelector:根据不同的数据类型选择不同的Presenter,用于多item type列表模型
  6. ItemBridgeAdapter:HorizontalGridView和ObjectAdapter的桥梁,用于解耦双方
  7. FocusHighlightHandler:item上焦的处理接口
  8. FocusHighlightHelper:上焦动画帮助类,内置了两种上焦动画

简单用法如下:

  1. 布局中添加HorizontalGridView
    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity"
        android:padding="20dp">
    
        <android.support.v17.leanback.widget.HorizontalGridView
            android:id="@+id/hgv"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:padding="10dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"
            android:layout_marginStart="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginEnd="10dp"/>
    
    </android.support.constraint.ConstraintLayout>

     

  2. 实现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) {
            //解绑时释放资源
        }
    }

     

  3. 模拟数据:

    private void initData(){
            if (mDataList==null){
                mDataList=new ArrayList<>();
                for (int i = 0; i <100 ; i++) {
                    mDataList.add(i);
                }
            }
        }

     

  4. 配置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);
        }
    

     

  5. 如果上焦的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);
            }

     

  6. 完整代码如下:

    public class MainActivity extends Activity {
    
        public static final String TAG="MainActivity";
        private HorizontalGridView mHgv;
        private List<Integer> 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);
                }
            }
        }
    }

     

  7. 效果如下:

    LeanBack:HorizontalGridView和VerticalGridView使用详解
    HorizontalGridView效果图

    需要注意的是,item的排布方式是先纵向后横向,及第2个view位于第二行第一列,而非第一行第二列

  8. 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的用法类似,便不再赘述。