自定义RecycleView实现TV应用上的item焦点获取以及设置当前选中的item居中
来源:互联网 发布:如何提升编程水平 编辑:程序博客网 时间:2024/06/08 10:40
RecycleView是个强大的控件,能代替ListView,GridView,还能实现瀑布流,还能实现横向ListView,只需要一句代码就能使纵向ListView变成横向的(主要实现就在布局管理器的选择上了)。
其功能用法这里不再赘述,有很多资料可供大家学习。
目前所在TV应用,经常使用到横向的列表。实现横向列表也有多种方式,Gallery,horizontalscrollview等,这些控件或多或少都存在这样那样的问题,感觉使用起来不是很方便。
既然RecycleView(后文使用rv代替)出现了,其又能支持横向ListView,我们还有不使用它的理由吗!
下面我们就来一步一步实现。
首先,我们得弄出一个横向的ListView来。这个简单,看代码
//线性布局 final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); mRecyclerView.setLayoutManager(linearLayoutManager);使用一个线性布局管理器,其方向设置横向,这样一个横向ListView样式的列表就出来了。
样式有了,我们就来绑定数据源了。这里我使用了一组图片作为rv的item数据,先来初始化数组
private void initDatas() { mDatas = new ArrayList<Integer>(Arrays.asList(R.mipmap.osd_blue_hl, R.mipmap.ic_launcher,R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher, R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.osd_hd_hl)); }数据有了,我们就需要使用适配器绑定数据了。还好,rv为我们提供了RecycleView.Adapter,并且封装了RecycleView.ViewHolder。我们不必为item的回收废脑子了。
具体使用和BaseAdapter一样,我们需要继承RecycleView.Adapter实现具体的逻辑。以下是adapter的实现
package com.example.txt.myrecyclerview;import android.content.Context;import android.nfc.Tag;import android.support.v7.widget.RecyclerView;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.ImageView;import android.widget.TextView;import org.w3c.dom.Text;import java.util.ArrayList;import java.util.List;import java.util.Random;/** * Created by txt on 2015/11/11. */public class GalleryAdapter extends RecyclerView.Adapter<GalleryAdapter.ViewHolder>{ private LayoutInflater mInflater; private List<Integer> mDatas; private List<Integer> heights; private int currentPosition; public interface OnItemClickListener { void onItemClick(View view, int position); void onItemLongClick(View view,int position); } public interface OnItemSelectListener{ void onItemSelect(View view,int position); } private OnItemClickListener mListener; private OnItemSelectListener mSelectListener; public void setOnItemSelectListener(OnItemSelectListener listener){ mSelectListener = listener; } public void setOnItemClickListener(OnItemClickListener listener){ mListener = listener; } public GalleryAdapter(Context context,List<Integer> datas){ mInflater = LayoutInflater.from(context); mDatas = datas; getRandomHeight(mDatas.size()); } public void setDatas(List datas){ mDatas = datas; } @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.item_rv, parent, false); ViewHolder holder = new ViewHolder(view); holder.mImg = (ImageView) view.findViewById(R.id.id_index_gallery_item_image); holder.mTxt = (TextView)view.findViewById(R.id.id_index_gallery_item_text); return holder; } @Override public void onBindViewHolder(final ViewHolder holder, final int position) { ViewGroup.LayoutParams params = holder.itemView.getLayoutParams(); params.height = heights.get(position%mDatas.size()); holder.itemView.setLayoutParams(params); holder.mImg.setImageResource(mDatas.get(position % mDatas.size())); holder.mTxt.setText(""+position); holder.itemView.setFocusable(true); holder.itemView.setTag(position); holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { Log.i("adapter", "hasfocus:" + position + "--" + hasFocus); if(hasFocus){ currentPosition = (int)holder.itemView.getTag(); mSelectListener.onItemSelect(holder.itemView,currentPosition); } } }); if(mListener!=null){ holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { mListener.onItemClick(v,holder.getLayoutPosition()); } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { mListener.onItemLongClick(v,holder.getLayoutPosition()); return true; } }); } } @Override public int getItemCount() {// return Integer.MAX_VALUE; return mDatas.size(); } private void getRandomHeight(int size){ heights = new ArrayList<>(); for(int i=0;i<size;i++){ heights.add((int)(200+Math.random()*400)); } } public static class ViewHolder extends RecyclerView.ViewHolder{ ImageView mImg; TextView mTxt; public ViewHolder(View itemView) { super(itemView); } }}实现过程很简单,构造行数里面得到数据源,onCreateViewHolder方法里面实例化ViewHolder对象,onBindViewHolder方法里面进行具体的数据绑定。细心的朋友会发现,RecycleView并没有提供onItemClickListener和onItemSelectedListener方法,所以我们需要自己来实现这两个监听。
item点击事件请移步鸿洋大神的Android RecyclerView 使用完全解析 体验艺术般的控件
这里我主要说下选择事件的处理。刚开始的时候,有点懵,没有头绪,打算仿照ListView的实现方式,看了下源码,没搞懂,遂放弃。
由于我的rv是要用到TV项目中的,TV项目中一个重要的事件就是焦点事件,常规ListView获得焦点后,其子item就能获取到焦点,不用刻意的去设置,遥控器切换上下键的时候,焦点就能移动到相应item上。起初,我把rv显示的获得焦点,但是子item并未获取到焦点(item获得焦点后,背景图片会改变),后来参考了一篇文章,需要手动设置让子item能获得焦点(具体可猛戳这里),这样一来焦点就在item上了,使用左右键,焦点就在item上移动了,并且焦点移动到当前可见列表的边缘item时,在选择下一个(或上一个)item时,整个item集合会往前(或往后)移动(窃喜,还好控件本身实现了这个功能,否则还得手动设置列表的滚动呢)。
焦点的问题就这样解决了,间接的实现了类似ListView的setSelection方法(rv没有setSelection方法,但是linearLayoutManager有scrollToPosition方法,感觉不太好用,没测试,貌似好多操作都移到了布局管理来实现)。
到这里我们使用rv实现横向listview的功能基本上算是实现了(TV上获取焦点)。
目前我们还有一个需求,就是当前选中的item希望它能一直保持在列表的中间位置,如下图所示
当焦点在第0、1个item时保持不变,当焦点移动到第2个item时,把第2个item挪到列表中间,如下
要实现这个效果,实际上需要控制当前选中的item向左或向右滚动的距离。这样就用到了Scroller这个对象,具体用法可参考
http://ipjmc.iteye.com/blog/1615828
http://blog.csdn.net/c_weibin/article/details/7438323
也可以自行百度。这里假设你已经了解了这个类。我们继续。
我们先来实现一个自定义的RecycleView。
public CustomRecycleView(Context context) { super(context); init(context); } public CustomRecycleView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CustomRecycleView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context); } private void init(Context context){ mScroller = new Scroller(context); }在构造函数里面初始化一个Scroller对象。复写computeScroll方法,这个方法在调用postInvalidate的时候会执行到
@Override public void computeScroll() { super.computeScroll(); //computeScrollOffset返回true表示滚动还在继续,持续时间应该就是startScroll设置的时间 if(mScroller!=null && mScroller.computeScrollOffset()){ Log.d(TAG, "getCurrX = " + mScroller.getCurrX()); scrollBy(mLastx - mScroller.getCurrX(), 0); mLastx = mScroller.getCurrX(); postInvalidate();//让系统继续重绘,则会继续重复执行computeScroll } }在这个方法里面计算滚动的偏移量,调用scrollBy方法执行滚动事件
下面这个方法就是具体的设置了
/** * 将指定item平滑移动到整个view的中间位置 * @param position */ public void smoothToCenter(int position){ int parentWidth = getWidth();//获取父视图的宽度 int childCount = getChildCount();//获取当前视图可见子view的总数 //获取可视范围内的选项的头尾位置 int firstvisiableposition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition(); int lastvisiableposition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition(); int count = ((LinearLayoutManager)getLayoutManager()).getItemCount();//获取item总数 Log.i(TAG,"count:"+count); mTargetPos = Math.max(0, Math.min(count - 1, position));//获取目标item的位置(参考listview中的smoothScrollToPosition方法) Log.i(TAG, "firstposition:" + firstvisiableposition + " lastposition:" + lastvisiableposition + " position:" + position+ " mTargetPos:"+mTargetPos); View targetChild = getChildAt(mTargetPos-firstvisiableposition);//获取目标item在当前可见视图item集合中的位置 View firstChild = getChildAt(0);//当前可见视图集合中的最左view View lastChild = getChildAt(childCount-1);//当前可见视图集合中的最右view Log.i(TAG,"first-->left:"+firstChild.getLeft()+" right:"+firstChild.getRight()); Log.i(TAG, "last-->left:" + lastChild.getLeft() + " right:" + lastChild.getRight()); int childLeftPx = targetChild.getLeft();//子view相对于父view的左边距 int childRightPx = targetChild.getRight();//子view相对于父view的右边距 Log.i(TAG, "target-->left:" + targetChild.getLeft() + " right:" + targetChild.getRight()); int childWidth = targetChild.getWidth(); int centerLeft = parentWidth/2-childWidth/2;//计算子view居中后相对于父view的左边距 int centerRight = parentWidth/2+childWidth/2;//计算子view居中后相对于父view的右边距 Log.i(TAG,"rv width:"+parentWidth+" item width:"+childWidth+" centerleft:"+centerLeft+" centerRight:"+centerRight); if(childLeftPx>centerLeft){//子view左边距比居中view大(说明子view靠父view的右边,此时需要把子view向左平移 //平移的起始位置就是子view的左边距,平移的距离就是两者之差 mLastx = childLeftPx; mScroller.startScroll(childLeftPx,0,centerLeft-childLeftPx,0,600);//600为移动时长,可自行设定 postInvalidate(); }else if(childRightPx<centerRight){ mLastx = childRightPx; mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600); postInvalidate(); } }注释非常清楚了,主要是通过传递过来的position计算出当前可见视图中该position 对应的item相对父布局的左右边距
View targetChild = getChildAt(mTargetPos-firstvisiableposition);//获取目标item在当前可见视图item集合中的位置
假定当前item在布局中间,获得该子item的左右目标边距
int centerLeft = parentWidth/2-childWidth/2;//计算子view居中后相对于父view的左边距 int centerRight = parentWidth/2+childWidth/2;//计算子view居中后相对于父view的右边距和实际的item的左右边距做比较
if(childLeftPx>centerLeft){//子view左边距比居中view大(说明子view靠父view的右边,此时需要把子view向左平移 //平移的起始位置就是子view的左边距,平移的距离就是两者之差 mLastx = childLeftPx; mScroller.startScroll(childLeftPx,0,centerLeft-childLeftPx,0,600);//600为移动时长,可自行设定 postInvalidate(); }else if(childRightPx<centerRight){ mLastx = childRightPx; mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600); postInvalidate(); }根据比较结果,进行滚动设置
mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600);
这样设置之后就可以实现当焦点变换时,当前选中的item能平滑的移动到屏幕中间位置。
在activity中如下调用,其中onItemSelect方法由子item的焦点变化触发,详见上面的adapter类
mAdapter.setOnItemSelectListener(new GalleryAdapter.OnItemSelectListener() { @Override public void onItemSelect(View view, int position) {// linearLayoutManager.scrollToPositionWithOffset(position,350); mRecyclerView.smoothToCenter(position); } });
参考利用Recycleview水平平移并自动挪动Item位置(仿Instagram效果)
由此整个需求就实现了。
我相信,实现此种效果也许还会有更好更优雅的方式,到时还望各位看官不吝赐教。
这里可下载demo,需要的可捡着关键方法看看,其他请忽略!
代码托管在github上
https://github.com/tianyasifan/MyRecyclerView
- 自定义RecycleView实现TV应用上的item焦点获取以及设置当前选中的item居中
- 自定义RecycleView实现TV应用上的item焦点获取以及设置当前选中的item居中
- Android TV开发:设置全局焦点框及listview中item的焦点获取
- recycleview的 item的自动居中以及自定义左对齐集代码分析
- Android TV 开发-listview(GridView)使用键盘获取焦点时,选中上次失去焦点时的item,而不是就近的item
- ListView获取item的焦点以及设置item点击的背景效果
- TV中RecyclerView添加item的点击事件和删除item之后获取焦点解决
- Android TV RecyclerView 焦点处理及获取焦点的Item保持在中间
- android tv 焦点居中自定义listview控件的实现
- 如何自定义RecycleView item的间距
- listview/gridview上checkbox和button以及item获取焦点的问题
- Android使listview(GridView) 获取焦点时,选中上次失去焦点时的item,而不是就近的item
- Android使listview(GridView) 获取焦点时,选中上次失去焦点时的item,而不是就近的item
- Android使listview(GridView) 获取焦点时,选中上次失去焦点时的item,而不是就近的item
- RecycleView如何实现item的点击事件
- RecycleView 当前可视item
- 让ListView指定的item获取焦点
- RecycleView 中的item移动到当前视图的顶部
- Android 常用效果(各种进度条,酷炫loading动画,火箭升空,撒花以及趋势图)
- MongoDB学习(一):MongoDB 环境的搭建
- 主机HostKey值改变导致SSH连接报出警告
- Android provider sample (2015.12.7)
- 传递消息--第三方开源--EventBus的简单使用
- 自定义RecycleView实现TV应用上的item焦点获取以及设置当前选中的item居中
- C++11 的 5 个实用特性
- Java类的实例化的初始化过程
- Spring实现AOP的4种方式
- Oracle10G/11G官方下载地址集合 直接迅雷下载
- Animation动画的解析
- 同步或者重构Activiti Identity用户数据的多种方案
- 让两台服务器的MySQL(5.7)数据同步_主主同步(互为主从关系)
- Java基础——Java笔记——File类