1. 程式人生 > >自定義RecycleView實現TV應用上的item焦點獲取以及設定當前選中的item居中

自定義RecycleView實現TV應用上的item焦點獲取以及設定當前選中的item居中

github地址:https://github.com/tianyasifan/MyRecyclerView


RecycleView是個強大的控制元件,能代替ListView,GridView,還能實現瀑布流,還能實現橫向ListView,只需要一句程式碼就能使縱向ListView變成橫向的(主要實現就在佈局管理器的選擇上了)。

其功能用法這裡不再贅述,有很多資料可供大家學習。

目前所在TV應用,經常使用到橫向的列表。實現橫向列表也有多種方式,Gallery,horizontalscrollview等,這些控制元件或多或少都存在這樣那樣的問題,感覺使用起來不是很方便。

既然RecycleView(後文使用rv代替)出現了,其又能支援橫向ListView,我們還有不使用它的理由嗎!

下面我們就來一步一步實現。

首先,我們得弄出一個橫向的ListView來。這個簡單,看程式碼

  1. //線性佈局
  2.        final LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);  
  3.        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);  
  4.        mRecyclerView.setLayoutManager(linearLayoutManager);  
使用一個線性佈局管理器,其方向設定橫向,這樣一個橫向ListView樣式的列表就出來了。

樣式有了,我們就來繫結資料來源了。這裡我使用了一組圖片作為rv的item資料,先來初始化陣列

  1. privatevoid initDatas()  
  2.     {  
  3.         mDatas = new ArrayList<Integer>(Arrays.asList(R.mipmap.osd_blue_hl,  
  4.                 R.mipmap.ic_launcher,R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher,  
  5.                 R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,  
  6.                 R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.ic_launcher,R.mipmap.osd_hd_hl));  
  7.     }  
資料有了,我們就需要使用介面卡繫結資料了。還好,rv為我們提供了RecycleView.Adapter,並且封裝了RecycleView.ViewHolder。我們不必為item的回收廢腦子了。

具體使用和BaseAdapter一樣,我們需要繼承RecycleView.Adapter實現具體的邏輯。以下是adapter的實現

  1. package com.example.txt.myrecyclerview;  
  2. import android.content.Context;  
  3. import android.nfc.Tag;  
  4. import android.support.v7.widget.RecyclerView;  
  5. import android.util.Log;  
  6. import android.view.LayoutInflater;  
  7. import android.view.View;  
  8. import android.view.ViewGroup;  
  9. import android.widget.ImageView;  
  10. import android.widget.TextView;  
  11. import org.w3c.dom.Text;  
  12. import java.util.ArrayList;  
  13. import java.util.List;  
  14. import java.util.Random;  
  15. /** 
  16.  * Created by txt on 2015/11/11. 
  17.  */
  18. publicclass GalleryAdapter extends RecyclerView.Adapter<GalleryAdapter.ViewHolder>{  
  19.     private LayoutInflater mInflater;  
  20.     private List<Integer> mDatas;  
  21.     private List<Integer> heights;  
  22.     privateint currentPosition;  
  23.     publicinterface OnItemClickListener {  
  24.         void onItemClick(View view, int position);  
  25.         void onItemLongClick(View view,int position);  
  26.     }  
  27.     publicinterface OnItemSelectListener{  
  28.         void onItemSelect(View view,int position);  
  29.     }  
  30.     private OnItemClickListener mListener;  
  31.     private OnItemSelectListener mSelectListener;  
  32.     publicvoid setOnItemSelectListener(OnItemSelectListener listener){  
  33.         mSelectListener = listener;  
  34.     }  
  35.     publicvoid setOnItemClickListener(OnItemClickListener listener){  
  36.         mListener = listener;  
  37.     }  
  38.     public GalleryAdapter(Context context,List<Integer> datas){  
  39.         mInflater = LayoutInflater.from(context);  
  40.         mDatas = datas;  
  41.         getRandomHeight(mDatas.size());  
  42.     }  
  43.     publicvoid setDatas(List datas){  
  44.         mDatas = datas;  
  45.     }  
  46.     @Override
  47.     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {  
  48.         View view = mInflater.inflate(R.layout.item_rv, parent, false);  
  49.         ViewHolder holder = new ViewHolder(view);  
  50.         holder.mImg = (ImageView) view.findViewById(R.id.id_index_gallery_item_image);  
  51.         holder.mTxt = (TextView)view.findViewById(R.id.id_index_gallery_item_text);  
  52.         return holder;  
  53.     }  
  54.     @Override
  55.     publicvoid onBindViewHolder(final ViewHolder holder, finalint position) {  
  56.         ViewGroup.LayoutParams params = holder.itemView.getLayoutParams();  
  57.         params.height = heights.get(position%mDatas.size());  
  58.         holder.itemView.setLayoutParams(params);  
  59.         holder.mImg.setImageResource(mDatas.get(position % mDatas.size()));  
  60.         holder.mTxt.setText(""+position);  
  61.         holder.itemView.setFocusable(true);  
  62.         holder.itemView.setTag(position);  
  63.         holder.itemView.setOnFocusChangeListener(new View.OnFocusChangeListener() {  
  64.             @Override
  65.             publicvoid onFocusChange(View v, boolean hasFocus) {  
  66.                 Log.i("adapter""hasfocus:" + position + "--" + hasFocus);  
  67.                 if(hasFocus){  
  68.                     currentPosition = (int)holder.itemView.getTag();  
  69.                     mSelectListener.onItemSelect(holder.itemView,currentPosition);  
  70.                 }  
  71.             }  
  72.         });  
  73.         if(mListener!=null){  
  74.             holder.itemView.setOnClickListener(new View.OnClickListener() {  
  75.                 @Override
  76.                 publicvoid onClick(View v) {  
  77.                     mListener.onItemClick(v,holder.getLayoutPosition());  
  78.                 }  
  79.             });  
  80.             holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {  
  81.                 @Override
  82.                 publicboolean onLongClick(View v) {  
  83.                     mListener.onItemLongClick(v,holder.getLayoutPosition());  
  84.                     returntrue;  
  85.                 }  
  86.             });  
  87.         }  
  88.     }  
  89.     @Override
  90.     publicint getItemCount() {  
  91. //        return Integer.MAX_VALUE;
  92.         return mDatas.size();  
  93.     }  
  94.     privatevoid getRandomHeight(int size){  
  95.         heights = new ArrayList<>();  
  96.         for(int i=0;i<size;i++){  
  97.             heights.add((int)(200+Math.random()*400));  
  98.         }  
  99.     }  
  100.     publicstaticclass ViewHolder extends RecyclerView.ViewHolder{  
  101.         ImageView mImg;  
  102.         TextView mTxt;  
  103.         public ViewHolder(View itemView) {  
  104.             super(itemView);  
  105.         }  
  106.     }  
  107. }  
實現過程很簡單,構造行數裡面得到資料來源,onCreateViewHolder方法裡面例項化ViewHolder物件,onBindViewHolder方法裡面進行具體的資料繫結。細心的朋友會發現,RecycleView並沒有提供onItemClickListener和onItemSelectedListener方法,所以我們需要自己來實現這兩個監聽。

這裡我主要說下選擇事件的處理。剛開始的時候,有點懵,沒有頭緒,打算仿照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。

  1. public CustomRecycleView(Context context) {  
  2.        super(context);  
  3.        init(context);  
  4.    }  
  5.    public CustomRecycleView(Context context, AttributeSet attrs) {  
  6.        super(context, attrs);  
  7.        init(context);  
  8.    }  
  9.    public CustomRecycleView(Context context, AttributeSet attrs, int defStyle) {  
  10.        super(context, attrs, defStyle);  
  11.        init(context);  
  12.    }  
  13.    privatevoid init(Context context){  
  14.        mScroller = new Scroller(context);  
  15.    }  
在建構函式裡面初始化一個Scroller物件。複寫computeScroll方法,這個方法在呼叫postInvalidate的時候會執行到
  1. @Override
  2.     publicvoid computeScroll() {  
  3.         super.computeScroll();  
  4.         //computeScrollOffset返回true表示滾動還在繼續,持續時間應該就是startScroll設定的時間
  5.         if(mScroller!=null && mScroller.computeScrollOffset()){  
  6.             Log.d(TAG, "getCurrX = " + mScroller.getCurrX());  
  7.             scrollBy(mLastx - mScroller.getCurrX(), 0);  
  8.             mLastx = mScroller.getCurrX();  
  9.             postInvalidate();//讓系統繼續重繪,則會繼續重複執行computeScroll
  10.         }  
  11.     }  
在這個方法裡面計算滾動的偏移量,呼叫scrollBy方法執行滾動事件

下面這個方法就是具體的設定了
  1. /** 
  2.      * 將指定item平滑移動到整個view的中間位置 
  3.      * @param position 
  4.      */
  5.     publicvoid smoothToCenter(int position){  
  6.         int parentWidth = getWidth();//獲取父檢視的寬度
  7.         int childCount = getChildCount();//獲取當前檢視可見子view的總數
  8.         //獲取可視範圍內的選項的頭尾位置
  9.         int firstvisiableposition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();  
  10.         int lastvisiableposition = ((LinearLayoutManager) getLayoutManager()).findLastVisibleItemPosition();  
  11.         int count = ((LinearLayoutManager)getLayoutManager()).getItemCount();//獲取item總數
  12.         Log.i(TAG,"count:"+count);  
  13.         mTargetPos = Math.max(0, Math.min(count - 1, position));//獲取目標item的位置(參考listview中的smoothScrollToPosition方法)
  14.         Log.i(TAG, "firstposition:" + firstvisiableposition + "   lastposition:" + lastvisiableposition + "   position:" + position+  
  15.                 "   mTargetPos:"+mTargetPos);  
  16.         View targetChild = getChildAt(mTargetPos-firstvisiableposition);//獲取目標item在當前可見檢視item集合中的位置
  17.         View firstChild = getChildAt(0);//當前可見檢視集合中的最左view
  18.         View lastChild = getChildAt(childCount-1);//當前可見檢視集合中的最右view
  19.         Log.i(TAG,"first-->left:"+firstChild.getLeft()+"   right:"+firstChild.getRight());  
  20.         Log.i(TAG, "last-->left:" + lastChild.getLeft() + "   right:" + lastChild.getRight());  
  21.         int childLeftPx = targetChild.getLeft();//子view相對於父view的左邊距
  22.         int childRightPx = targetChild.getRight();//子view相對於父view的右邊距
  23.         Log.i(TAG, "target-->left:" + targetChild.getLeft() + "   right:" + targetChild.getRight());  
  24.         int childWidth = targetChild.getWidth();  
  25.         int centerLeft = parentWidth/2-childWidth/2;//計運算元view居中後相對於父view的左邊距
  26.         int centerRight = parentWidth/2+childWidth/2;//計運算元view居中後相對於父view的右邊距
  27.         Log.i(TAG,"rv width:"+parentWidth+"   item width:"+childWidth+"   centerleft:"+centerLeft+"   centerRight:"+centerRight);  
  28.         if(childLeftPx>centerLeft){//子view左邊距比居中view大(說明子view靠父view的右邊,此時需要把子view向左平移
  29.             //平移的起始位置就是子view的左邊距,平移的距離就是兩者之差
  30.             mLastx = childLeftPx;  
  31.             mScroller.startScroll(childLeftPx,0,centerLeft-childLeftPx,0,600);//600為移動時長,可自行設定
  32.             postInvalidate();  
  33.         }elseif(childRightPx<centerRight){  
  34.             mLastx = childRightPx;  
  35.             mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600);  
  36.             postInvalidate();  
  37.         }  
  38.     }  
註釋非常清楚了,主要是通過傳遞過來的position計算出當前可見檢視中該position 對應的item相對父佈局的左右邊距
  1. View targetChild = getChildAt(mTargetPos-firstvisiableposition);//獲取目標item在當前可見檢視item集合中的位置

假定當前item在佈局中間,獲得該子item的左右目標邊距

  1. int centerLeft = parentWidth/2-childWidth/2;//計運算元view居中後相對於父view的左邊距
  2.         int centerRight = parentWidth/2+childWidth/2;//計運算元view居中後相對於父view的右邊距
和實際的item的左右邊距做比較
  1. if(childLeftPx>centerLeft){//子view左邊距比居中view大(說明子view靠父view的右邊,此時需要把子view向左平移
  2.             //平移的起始位置就是子view的左邊距,平移的距離就是兩者之差
  3.             mLastx = childLeftPx;  
  4.             mScroller.startScroll(childLeftPx,0,centerLeft-childLeftPx,0,600);//600為移動時長,可自行設定
  5.             postInvalidate();  
  6.         }elseif(childRightPx<centerRight){  
  7.             mLastx = childRightPx;  
  8.             mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600);  
  9.             postInvalidate();  
  10.         }  
根據比較結果,進行滾動設定
  1. mScroller.startScroll(childRightPx,0,centerRight-childRightPx,0,600);  

這樣設定之後就可以實現當焦點變換時,當前選中的item能平滑的移動到螢幕中間位置。

在activity中如下呼叫,其中onItemSelect方法由子item的焦點變化觸發,詳見上面的adapter類

  1. mAdapter.setOnItemSelectListener(new GalleryAdapter.OnItemSelectListener() {  
  2.             @Override
  3.             publicvoid onItemSelect(View view, int position) {  
  4. //                linearLayoutManager.scrollToPositionWithOffset(position,350);
  5.                 mRecyclerView.smoothToCenter(position);  
  6.             }  
  7.         });