前言:剧集类控件,在TV app中非常常见,今天将介绍构建一个TV app中的剧集列表控件,此控件上传到我的Github: https://github.com/hejunlin2013/EpisodeListView,点击【阅读原文】,可看对应的github, 喜欢可以star。Agenda如下:

  • 效果图

  • 效果图gif

  • 实现思路

  • 代码分析

效果图

Android TV开发总结(七)构建一个TV app中的剧集列表控件_Java

效果图gif:

Android TV开发总结(七)构建一个TV app中的剧集列表控件_Java_02

实现思路:

  • 1、用两个RecycleView作为控件横向布局

  • 2、PopupWindow作为该集剧情简介

  • 3、当焦点到达Parent时,对Child进行监听,并发生变化,同理,如果Child超过10个时,通知Parent

代码分析:

EpisodeListView.java 作用:

  • 负责组配两个RecycleView填充对应的数据

  • 焦点监听及获焦情况


public class EpisodeListView extends RelativeLayout implements View.OnFocusChangeListener {    public static final String TAG = EpisodeListView.class.getSimpleName();    private Context mContext;    private RelativeLayout mContentPanel;    private RecyclerView mChildrenView;    private RecyclerView mParentView;    private LinearLayoutManager mEpisodesLayoutManager;    private LinearLayoutManager mGroupLayoutManager;    private EpisodeListViewAdapter mEpisodeListAdapter;    private ChildrenAdapter mChildrenAdapter;    private ParentAdapter mParentAdapter;    private Handler mHandler = new Handler(Looper.getMainLooper());    public EpisodeListView(Context context) {        this(context, null);    }    public EpisodeListView(Context context, AttributeSet attrs) {        this(context, attrs, 0);    }    public EpisodeListView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        if (!isInEditMode()) {            mContext = context;            init();        }    }    private void init() {        LayoutInflater inflater = LayoutInflater.from(mContext);        inflater.inflate(R.layout.episodelist_layout, this, true);        mChildrenView = (RecyclerView) findViewById(R.id.episodes);        mParentView = (RecyclerView) findViewById(R.id.groups);        mEpisodesLayoutManager = new LinearLayoutManager(mContext, LinearLayout.HORIZONTAL, false);        mGroupLayoutManager = new LinearLayoutManager(mContext, LinearLayout.HORIZONTAL, false);        mChildrenView.setLayoutManager(mEpisodesLayoutManager);        mParentView.setLayoutManager(mGroupLayoutManager);        mChildrenView.setItemAnimator(new DefaultItemAnimator());        mParentView.setItemAnimator(new DefaultItemAnimator());        mChildrenView.setOnFocusChangeListener(this);        mParentView.setOnFocusChangeListener(this);        this.setOnFocusChangeListener(this);    }    public void setAdapter(final EpisodeListViewAdapter adapter) {        mEpisodeListAdapter = adapter;        mChildrenAdapter = adapter.getEpisodesAdapter();        mParentAdapter = adapter.getGroupAdapter();        mChildrenView.setAdapter(mChildrenAdapter);        mParentView.setAdapter(mParentAdapter);        mParentAdapter.setOnItemClickListener(new ParentAdapter.OnItemClickListener() {            @Override            public void onGroupItemClick(View view, int position) {                mEpisodesLayoutManager.scrollToPositionWithOffset(adapter.getChildrenPosition(position), 0);            }        });        mParentAdapter.setOnItemFocusListener(new ParentAdapter.OnItemFocusListener() {            @Override            public void onGroupItemFocus(View view, int position, boolean hasFocus) {                int episodePosition = adapter.getChildrenPosition(position);                mChildrenAdapter.setCurrentPosition(episodePosition);                mEpisodesLayoutManager.scrollToPositionWithOffset(adapter.getChildrenPosition(position), 0);            }        });        mChildrenAdapter.setOnItemFocusListener(new ChildrenAdapter.OnItemFocusListener() {            @Override            public void onEpisodesItemFocus(View view, int position, boolean hasFocus) {                if (hasFocus) {                    int groupPosition = adapter.getParentPosition(position);                    mGroupLayoutManager.scrollToPositionWithOffset(groupPosition, 0);                    mParentAdapter.setCurrentPosition(adapter.getParentPosition(groupPosition));                }            }        });        mChildrenAdapter.setOnItemClickListener(new ChildrenAdapter.OnItemClickListener() {            @Override            public void onEpisodesItemClick(View view, int position) {            }        });    }    public void setLongFocusListener(ChildrenAdapter.OnItemLongFocusListener listener) {        mChildrenAdapter.setOnItemLongFocusListener(listener);    }    @Override    public boolean dispatchKeyEvent(KeyEvent event) {        if (event.getAction() == KeyEvent.ACTION_DOWN) {            switch (event.getKeyCode()) {                case KeyEvent.KEYCODE_DPAD_UP:                    if (mParentView.hasFocus()) {                        mChildrenView.requestFocus();                        return true;                    }                    break;                case KeyEvent.KEYCODE_DPAD_DOWN:                    if (mChildrenView.hasFocus()) {                        mParentView.requestFocus();                        return true;                    }                    break;                case KeyEvent.KEYCODE_DPAD_RIGHT:                    if (mChildrenView.hasFocus()                            && mChildrenAdapter.getCurrentPosition() >= mChildrenAdapter.getData().size() - 1) {                        return true;                    }                    if (mParentView.hasFocus()                            && mParentAdapter.getCurrentPosition() >= mParentAdapter.getDatas().size() - 1) {                        return true;                    }            }        }        return super.dispatchKeyEvent(event);    }    @Override    public void onFocusChange(View v, boolean hasFocus) {        if (v == this && hasFocus) {            mChildrenView.requestFocus();        } else if (v == mChildrenView && hasFocus) {            View child = mChildrenView.getLayoutManager().findViewByPosition(mChildrenAdapter.getCurrentPosition());            if (child != null) {                child.requestFocus();            } else {                int lastPosition = mEpisodesLayoutManager.findLastVisibleItemPosition();                child = mEpisodesLayoutManager.findViewByPosition(lastPosition);                if (child != null)                    child.requestFocus();            }        } else if (v == mParentView && hasFocus) {            View child = mParentView.getLayoutManager().findViewByPosition(mParentAdapter.getCurrentPosition());            if (child != null) {                child.requestFocus();            }        }    }}


EpisodeListViewAdapter 作用:

  • 抽象类,在实例化时负责将外部数据转成list传入

  • 实例化ParentAdapter及ChildrenAdapter


public abstract class EpisodeListViewAdapter<T>{    private ChildrenAdapter mChildrenAdapter;    private ParentAdapter mParentAdapter;    public EpisodeListViewAdapter() {        mChildrenAdapter = new ChildrenAdapter(getChildrenList());        mParentAdapter = new ParentAdapter(getParentList());    }    public ChildrenAdapter getEpisodesAdapter() {        return mChildrenAdapter;    }    public ParentAdapter getGroupAdapter() {        return mParentAdapter;    }    public void setSelectedPositions(List<Integer> positions) {        mChildrenAdapter.setSelectedPositions(positions);    }    public abstract List<String> getChildrenList();    public abstract List<String> getParentList();    public abstract int getChildrenPosition(int childPosition);    public abstract int getParentPosition(int parentPosition);}

ParentAdapter 作用:

  • 每10集为一组,进行控制


public class ParentAdapter extends RecyclerView.Adapter<ParentAdapter.MyViewHolder> {    private static final int GROUPS_COLUMN_COUNT = 10;    private OnItemClickListener mItemClickListener;    private OnItemFocusListener mItemFocusListener;    private List<String> mDatas;    private int parentWidth;    private int itemWidth;    private int mCurrentPosition;    public ParentAdapter(List<String> datas) {        mDatas = datas;    }    @Override    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View view = LayoutInflater.from(parent.getContext())                .inflate(R.layout.parent_item, parent, false);        MyViewHolder holder = new MyViewHolder(view);        parentWidth = parent.getMeasuredWidth();        itemWidth = (parentWidth -                (holder.textView.getPaddingLeft() + holder.textView.getPaddingRight()) * (GROUPS_COLUMN_COUNT))                / GROUPS_COLUMN_COUNT + 1;        return holder;    }    @Override    public void onBindViewHolder(MyViewHolder holder, final int position) {        holder.textView.setText(mDatas.get(position));        holder.textView.setWidth(itemWidth);        holder.textView.setFocusable(true);        holder.textView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                mItemClickListener.onGroupItemClick(v, position);            }        });        holder.textView.setOnFocusChangeListener(new View.OnFocusChangeListener() {            @Override            public void onFocusChange(View v, boolean hasFocus) {                if (hasFocus) {                    mItemFocusListener.onGroupItemFocus(v, position, hasFocus);                    mCurrentPosition = position;                }            }        });    }    @Override    public int getItemCount() {        return mDatas.size();    }    public List<String> getDatas() {        return mDatas;    }    public int getCurrentPosition() {        return mCurrentPosition;    }    public void setCurrentPosition(int position) {        mCurrentPosition = position;    }    public void setOnItemFocusListener(OnItemFocusListener listener) {        mItemFocusListener = listener;    }    public void setOnItemClickListener(OnItemClickListener listener) {        mItemClickListener = listener;    }    class MyViewHolder extends RecyclerView.ViewHolder {        TextView textView;        public MyViewHolder(View view) {            super(view);            textView = (TextView) view.findViewById(R.id.item);        }    }    public interface OnItemClickListener {        void onGroupItemClick(View view, int position);    }    public interface OnItemFocusListener {        void onGroupItemFocus(View view, int position, boolean hasFocus);    }}


ChildrenAdapter 作用:

  • 每行最多显示10个,大于10可以左右变换

  • parent之间焦点变换时,children可立即响应。字数限制,不贴代码,可直接对应github看。