前言:剧集类控件,在TV app中非常常见,今天将介绍构建一个TV app中的剧集列表控件,此控件上传到我的Github: https://github.com/hejunlin2013/EpisodeListView,点击【阅读原文】,可看对应的github, 喜欢可以star。Agenda如下:
效果图
效果图gif
实现思路
代码分析
效果图
效果图gif:
实现思路:
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看。