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

1,624 阅读3分钟

前言:剧集类控件,在TV app中非常常见,今天将介绍构建一个TV app中的剧集列表控件,此控件上传到我的Github:github.com/hejunlin201…,点击【阅读原文】,可看对应的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看。


第一时间获得博客更新提醒,以及更多 android,源码分析,最新开源项目推荐,更多有价值的思考,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。