自定義RecycleView實現TV應用上的item焦點獲取以及設定當前選中的item居中
github地址:https://github.com/tianyasifan/MyRecyclerView
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);
樣式有了,我們就來繫結資料來源了。這裡我使用了一組圖片作為rv的item資料,先來初始化陣列
- privatevoid 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));
- }
具體使用和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.
- */
- publicclass GalleryAdapter extends RecyclerView.Adapter<GalleryAdapter.ViewHolder>{
- private LayoutInflater mInflater;
- private List<Integer> mDatas;
- private List<Integer> heights;
- privateint currentPosition;
- publicinterface OnItemClickListener {
- void onItemClick(View view, int position);
- void onItemLongClick(View view,int position);
- }
- publicinterface OnItemSelectListener{
- void onItemSelect(View view,int position);
- }
- private OnItemClickListener mListener;
- private OnItemSelectListener mSelectListener;
- publicvoid setOnItemSelectListener(OnItemSelectListener listener){
- mSelectListener = listener;
- }
- publicvoid setOnItemClickListener(OnItemClickListener listener){
- mListener = listener;
- }
- public GalleryAdapter(Context context,List<Integer> datas){
- mInflater = LayoutInflater.from(context);
- mDatas = datas;
- getRandomHeight(mDatas.size());
- }
- publicvoid 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
- publicvoid onBindViewHolder(final ViewHolder holder, finalint 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
- publicvoid 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
- publicvoid onClick(View v) {
- mListener.onItemClick(v,holder.getLayoutPosition());
- }
- });
- holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
- @Override
- publicboolean onLongClick(View v) {
- mListener.onItemLongClick(v,holder.getLayoutPosition());
- returntrue;
- }
- });
- }
- }
- @Override
- publicint getItemCount() {
- // return Integer.MAX_VALUE;
- return mDatas.size();
- }
- privatevoid getRandomHeight(int size){
- heights = new ArrayList<>();
- for(int i=0;i<size;i++){
- heights.add((int)(200+Math.random()*400));
- }
- }
- publicstaticclass ViewHolder extends RecyclerView.ViewHolder{
- ImageView mImg;
- TextView mTxt;
- public ViewHolder(View itemView) {
- super(itemView);
- }
- }
- }
這裡我主要說下選擇事件的處理。剛開始的時候,有點懵,沒有頭緒,打算仿照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這個物件,具體用法可參考
也可以自行百度。這裡假設你已經瞭解了這個類。我們繼續。
我們先來實現一個自定義的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);
- }
- privatevoid init(Context context){
- mScroller = new Scroller(context);
- }
- @Override
- publicvoid 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
- }
- }
下面這個方法就是具體的設定了
- /**
- * 將指定item平滑移動到整個view的中間位置
- * @param position
- */
- publicvoid 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();
- }elseif(childRightPx<centerRight){
- mLastx = childRightPx;
- mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600);
- postInvalidate();
- }
- }
- 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的右邊距
- if(childLeftPx>centerLeft){//子view左邊距比居中view大(說明子view靠父view的右邊,此時需要把子view向左平移
- //平移的起始位置就是子view的左邊距,平移的距離就是兩者之差
- mLastx = childLeftPx;
- mScroller.startScroll(childLeftPx,0,centerLeft-childLeftPx,0,600);//600為移動時長,可自行設定
- postInvalidate();
- }elseif(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
- publicvoid onItemSelect(View view, int position) {
- // linearLayoutManager.scrollToPositionWithOffset(position,350);
- mRecyclerView.smoothToCenter(position);
- }
- });