RecyclerView的基礎(chǔ)使用及其與ListView的部分區(qū)別

先說說RecyclerView是怎么使用的,以其基礎(chǔ)代碼為本,大體以如下順序進(jìn)行分析。

  • 組件介紹
  • 點擊事件
    • ListView為什么會點擊item事件失效?
    • RecyclerView怎么設(shè)計點擊回調(diào)?
  • 與ListView的區(qū)別
  • 布局文件
public class testForRecyclerViewActivity{

    // 裝載item中數(shù)據(jù)
    private List<String> mDatas;
    // 初始化item中的數(shù)據(jù)
    protected void initData() {
        mDatas = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            mDatas.add("" + i);
        }
    }
    //定義點擊事件的接口
    public interface OnItemClickListener {
        void onItemClick(View view, int position);
    }

    // 繼承RecyclerView.Adapter , 按照其規(guī)定好的設(shè)計規(guī)范,定義具體內(nèi)容。
    class LogAdapter extends RecyclerView.Adapter<ViewHolder> {

        private OnItemClickListener mOnItemClickListener;

        public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {
            this.mOnItemClickListener = mOnItemClickListener;
        }

        @NonNull
        @NotNull
        @Override
        public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) {
            View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false);
            ViewHolder viewHolder = ViewHolder.create(view);
            viewHolder.getItemView(R.id.title);
            viewHolder.getItemView(R.id.date);
            
            if (mOnItemClickListener != null) {
                viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        mOnItemClickListener.onItemClick(view, i);
                    }
                });
            }
            return viewHolder;
        }

        @Override
        public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) {
            viewHolder.setTextView(R.id.title, "我是標(biāo)題");
            viewHolder.setTextView(R.id.date, "我是日期");
        }

        @Override
        public int getItemCount() {
            return mDatas.size();
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_rv);
        initData();
        RecyclerView mRecyclerView = findViewById(R.id.test_item_log);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        LogAdapter mLogAdapter = new LogAdapter();
        mLogAdapter.setOnItemClickListener(new OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                String webUrl = "https://www.baidu.com/";
                Intent intent = new Intent(testForRecyclerViewActivity.this, newDetailsActivity.class);
                intent.putExtra("ARGS_KEY_URL", webUrl );
                intent.putExtra("ARGS_KEY_TITLE", "標(biāo)題");
                startActivity(intent);
            }
        });
        mRecyclerView.setAdapter(mLogAdapter);

        //返回按鈕
        findViewById(R.id.return_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                finish();
            }
        });

    }
}

代碼介紹

  • 組件:(其包含的組件較多,挑選最常用的介紹)

    1. onCreateViewHolder(ViewGroup viewGroup, int i),該方法旨在創(chuàng)造一個持有者的類,將對應(yīng)id的view存到內(nèi)存中,避免每次都需要去布局文件中讀取對應(yīng)的view。需要注意的是,下面的ViewHolder為公司封裝后的類,使用更簡潔方便。

              public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) {
                  View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false);
                  ViewHolder viewHolder = ViewHolder.create(view);
                  viewHolder.getItemView(R.id.title);
                  viewHolder.getItemView(R.id.date);
                  
                  if (mOnItemClickListener != null) {
                      viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View view) {
                              mOnItemClickListener.onItemClick(view, i);
                          }
                      });
                  }
                  return viewHolder;
              }
      
    2. onBindViewHolder(ViewHolder viewHolder, int i),給持有者包含的組件賦予數(shù)據(jù),同時可以給組件添加事件。

            @Override
            public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) {
                viewHolder.setTextView(R.id.title, "我是標(biāo)題");
                viewHolder.setTextView(R.id.date, "我是日期");
            }
      
    3. getItemCount(),返回item的條目數(shù)

              @Override
              public int getItemCount() {
                  return mDatas.size();
              }
      

點擊事件(分析RecyclerView和ListView在點擊事件的差異)

  1. item點擊事件:與ListView不同的是,RecyclerView并沒有配備setOnItemClickListener()方法,只能通過配置回調(diào)接口來設(shè)置對應(yīng)的點擊事件。

    • 可能你會覺得,wc這么不方便,RecyclerView不用也罷。不急,慢慢聽我解釋,對于ListView的點擊事件有很重要的一個弊端,那就是某個時候你給ListView的item組件設(shè)置了setOnItemClickListener事件,正準(zhǔn)備高高興興去測試時,卻發(fā)現(xiàn)死活都點擊無效甚是懊惱,上網(wǎng)一查同仁還不在少數(shù),解釋為:當(dāng)listview中包含button,checkbox等控件的時候,android會默認(rèn)將focus給了這些控件,也就是說listview的item根本就獲取不到focus,所以導(dǎo)致onitemclick時間不能觸發(fā)。那我們就詳細(xì)聊聊為什么會產(chǎn)生這種情況,就隨著關(guān)鍵部分的代碼慢慢探索答案吧。
    • 對于ListView,點擊事件發(fā)生后,經(jīng)過事件分發(fā)機制判定(默認(rèn)不攔截),遂調(diào)用onTouchEvent()方法去處理該ItemClick時間,該方法處于其繼承的AbsListView類中:
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (!isEnabled()) {
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return isClickable() || isLongClickable();
            }
    
            if (mPositionScroller != null) {
                mPositionScroller.stop();
            }
    
            if (mIsDetaching || !isAttachedToWindow()) {
                // Something isn't right.
                // Since we rely on being attached to get data set change notifications,
                // don't risk doing anything where we might try to resync and find things
                // in a bogus state.
                return false;
            }
    
            startNestedScroll(SCROLL_AXIS_VERTICAL);
    
            if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
                return true;
            }
    
            initVelocityTrackerIfNotExists();
            final MotionEvent vtev = MotionEvent.obtain(ev);
    
            final int actionMasked = ev.getActionMasked();
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                mNestedYOffset = 0;
            }
            vtev.offsetLocation(0, mNestedYOffset);
            switch (actionMasked) {
                case MotionEvent.ACTION_DOWN: {
                    onTouchDown(ev);
                    break;
                }
    
                case MotionEvent.ACTION_MOVE: {
                    onTouchMove(ev, vtev);
                    break;
                }
    
                case MotionEvent.ACTION_UP: {
                    onTouchUp(ev);
                    break;
                }
    
                case MotionEvent.ACTION_CANCEL: {
                    onTouchCancel();
                    break;
                }
    
                case MotionEvent.ACTION_POINTER_UP: {
                    onSecondaryPointerUp(ev);
                    final int x = mMotionX;
                    final int y = mMotionY;
                    final int motionPosition = pointToPosition(x, y);
                    if (motionPosition >= 0) {
                        // Remember where the motion event started
                        final View child = getChildAt(motionPosition - mFirstPosition);
                        mMotionViewOriginalTop = child.getTop();
                        mMotionPosition = motionPosition;
                    }
                    mLastY = y;
                    break;
                }
    
                case MotionEvent.ACTION_POINTER_DOWN: {
                    // New pointers take over dragging duties
                    final int index = ev.getActionIndex();
                    final int id = ev.getPointerId(index);
                    final int x = (int) ev.getX(index);
                    final int y = (int) ev.getY(index);
                    mMotionCorrection = 0;
                    mActivePointerId = id;
                    mMotionX = x;
                    mMotionY = y;
                    final int motionPosition = pointToPosition(x, y);
                    if (motionPosition >= 0) {
                        // Remember where the motion event started
                        final View child = getChildAt(motionPosition - mFirstPosition);
                        mMotionViewOriginalTop = child.getTop();
                        mMotionPosition = motionPosition;
                    }
                    mLastY = y;
                    break;
                }
            }
    
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(vtev);
            }
            vtev.recycle();
            return true;
        }
    
    • 看著這么多的事件處理入口,是否一籌莫展?想當(dāng)初曹孟德東臨碣石以觀滄海,從大處著眼忽略細(xì)節(jié),望水何澹澹山島竦峙。同樣的道理,我們看著一大片的代碼段時,應(yīng)當(dāng)結(jié)合我們的目標(biāo)找尋與其最有關(guān)的方法,其他的大可以不看。比如我們需要的是看處理onItemClick的事件,而這個事件的觸發(fā)是來自我們的手指從屏幕抬起的那一刻,因此直接就定位到第47行的onTouchUp(ev)方法?,F(xiàn)在我們進(jìn)去該方法,看它是怎么處理事件的:
    private void onTouchUp(MotionEvent ev) {
            switch (mTouchMode) {
            case TOUCH_MODE_DOWN:
            case TOUCH_MODE_TAP:
            case TOUCH_MODE_DONE_WAITING:
                final int motionPosition = mMotionPosition;
                final View child = getChildAt(motionPosition - mFirstPosition);
                if (child != null) {
                    if (mTouchMode != TOUCH_MODE_DOWN) {
                        child.setPressed(false);
                    }
    
                    final float x = ev.getX();
                    final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
                    if (inList && !child.hasExplicitFocusable()) {
                        if (mPerformClick == null) {
                            mPerformClick = new PerformClick();
                        }
    
                        final AbsListView.PerformClick performClick = mPerformClick;
                        performClick.mClickMotionPosition = motionPosition;
                        performClick.rememberWindowAttachCount();
    
                        mResurrectToPosition = motionPosition;
    
                        if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
                            removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
                                    mPendingCheckForTap : mPendingCheckForLongPress);
                            mLayoutMode = LAYOUT_NORMAL;
                            if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                                mTouchMode = TOUCH_MODE_TAP;
                                setSelectedPositionInt(mMotionPosition);
                                layoutChildren();
                                child.setPressed(true);
                                positionSelector(mMotionPosition, child);
                                setPressed(true);
                                if (mSelector != null) {
                                    Drawable d = mSelector.getCurrent();
                                    if (d != null && d instanceof TransitionDrawable) {
                                        ((TransitionDrawable) d).resetTransition();
                                    }
                                    mSelector.setHotspot(x, ev.getY());
                                }
                                if (mTouchModeReset != null) {
                                    removeCallbacks(mTouchModeReset);
                                }
                                mTouchModeReset = new Runnable() {
                                    @Override
                                    public void run() {
                                        mTouchModeReset = null;
                                        mTouchMode = TOUCH_MODE_REST;
                                        child.setPressed(false);
                                        setPressed(false);
                                        if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
                                            performClick.run();
                                        }
                                    }
                                };
                                postDelayed(mTouchModeReset,
                                        ViewConfiguration.getPressedStateDuration());
                            } else {
                                mTouchMode = TOUCH_MODE_REST;
                                updateSelectorState();
                            }
                            return;
                        } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
                            performClick.run();
                        }
                    }
                }
                mTouchMode = TOUCH_MODE_REST;
                updateSelectorState();
                break;
            case TOUCH_MODE_SCROLL:
             ......
    
            case TOUCH_MODE_OVERSCROLL:
                ......
            }
    
            setPressed(false);
    
            if (shouldDisplayEdgeEffects()) {
                mEdgeGlowTop.onRelease();
                mEdgeGlowBottom.onRelease();
            }
    
            // Need to redraw since we probably aren't drawing the selector anymore
            invalidate();
            removeCallbacks(mPendingCheckForLongPress);
            recycleVelocityTracker();
    
            mActivePointerId = INVALID_POINTER;
    
            if (PROFILE_SCROLLING) {
                if (mScrollProfilingStarted) {
                    Debug.stopMethodTracing();
                    mScrollProfilingStarted = false;
                }
            }
    
            if (mScrollStrictSpan != null) {
                mScrollStrictSpan.finish();
                mScrollStrictSpan = null;
            }
        }
    
    • 我省略掉了部分代碼,那些不重要,定位到15行看到了這個大大的判斷語句,if (inList && !child.hasExplicitFocusable()) inList判斷觸發(fā)是否是item范圍內(nèi)的事件為true,重要的是 child.hasExplicitFocusable() 取反,該方法用來判斷該節(jié)點是否是獲取焦點的,如果是則不會觸發(fā)后續(xù)的點擊回調(diào)。由此變引申出了兩種解決方法:
      • 在button/checkbox等控件處設(shè)置android:clickable=”false” android:focusableInTouchMode=”false”,使其在點擊item時不會因該組件的獲取焦點屬性而影響了回調(diào)事件。
      • 在item最外層添加屬性android:descendantFocusability=”blocksDescendants”,該屬性使item覆蓋所有的子節(jié)點獲取焦點,故里面的子節(jié)點均不可獲取焦點。
    • 既然都到這一步了,不如看看他是怎么調(diào)用我們的方法的吧~~點擊第67行的performClick.run();
            @Override
            public void run() {
                // The data has changed since we posted this action in the event queue,
                // bail out before bad things happen
                if (mDataChanged) return;
    
                final ListAdapter adapter = mAdapter;
                final int motionPosition = mClickMotionPosition;
                if (adapter != null && mItemCount > 0 &&
                        motionPosition != INVALID_POSITION &&
                        motionPosition < adapter.getCount() && sameWindow() &&
                        adapter.isEnabled(motionPosition)) {
                    final View view = getChildAt(motionPosition - mFirstPosition);
                    // If there is no view, something bad happened (the view scrolled off the
                    // screen, etc.) and we should cancel the click
                    if (view != null) {
                        performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
                    }
                }
            }
    
    • 可以看到該方法最終調(diào)用的是AdapterView中的performItemClick()方法,貼出其代碼:
        public boolean performItemClick(View view, int position, long id) {
            final boolean result;
            if (mOnItemClickListener != null) {
                playSoundEffect(SoundEffectConstants.CLICK);
                mOnItemClickListener.onItemClick(this, view, position, id);
                result = true;
            } else {
                result = false;
            }
    
            if (view != null) {
                view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
            }
            return result;
        }
    
    • 終于,在第5行見到了它的廬山真面目,在這里調(diào)用的我們設(shè)置的mOnItemClickListener()方法。
  2. RecyclerView的點擊事件應(yīng)該如何設(shè)計其回調(diào)接口呢,我歸納了2種方式,當(dāng)然思想都是一樣的,那就是配置回調(diào)接口然后在對應(yīng)的時機實現(xiàn)該方法。

    1. 第一種方式 按照我在開頭貼的代碼,在Activity中就定義需要用的回調(diào)接口

          //定義點擊事件的接口
          public interface OnItemClickListener {
              void onItemClick(View view, int position);
          }
      

      然后在定義Adapter時,聲明變量中加入對應(yīng)變量與構(gòu)造方法,并且在onCreateViewHolder中綁定該方法

            private OnItemClickListener mOnItemClickListener;
      
            public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {
                  this.mOnItemClickListener = mOnItemClickListener;
              }
      
              public ViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup viewGroup, int i) {
                  View view = LayoutInflater.from(testForRecyclerViewActivity.this).inflate(R.layout.test_rv_item, viewGroup, false);
                  ViewHolder viewHolder = ViewHolder.create(view);
                  viewHolder.getItemView(R.id.title);
                  viewHolder.getItemView(R.id.date);
                  
                  if (mOnItemClickListener != null) {
                      viewHolder.getmConvertView().setOnClickListener(new View.OnClickListener() {
                          @Override
                          public void onClick(View view) {
                              mOnItemClickListener.onItemClick(view, i);
                          }
                      });
                  }
                  return viewHolder;
              }
      

      最后在onCreate中實現(xiàn)該接口并重寫該點擊方法,怎么樣,是不是很簡單,條理清晰井井有條。

            LogAdapter mLogAdapter = new LogAdapter();
              mLogAdapter.setOnItemClickListener(new OnItemClickListener() {
                  @Override
                  public void onItemClick(View view, int position) {
                      String webUrl = "https://www.baidu.com/";
                      Intent intent = new Intent(testForRecyclerViewActivity.this, newDetailsActivity.class);
                      intent.putExtra("ARGS_KEY_URL", webUrl );
                      intent.putExtra("ARGS_KEY_TITLE", "標(biāo)題");
                      startActivity(intent);
                  }
              });
      
    2. 第二種方式 ,在定義持有者時實現(xiàn)OnClickListener接口,重寫其onClick方法,根據(jù)點擊對象的不同而配置不同的方法。組件多的時候,可以通過getId來使用switch,case來分id處理事件。

      public static class ViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
      
          public TextView txtViewTitle;
          public ImageView imgViewIcon;
          public IMyViewHolderClicks mListener;
      
          public ViewHolder(View itemLayoutView, IMyViewHolderClicks listener) {
              super(itemLayoutView);
              mListener = listener;
              txtViewTitle = (TextView) itemLayoutView.findViewById(R.id.item_title);
              imgViewIcon = (ImageView) itemLayoutView.findViewById(R.id.item_icon);
              imgViewIcon.setOnClickListener(this);
              itemLayoutView.setOnClickListener(this);
          }
      
          @Override
          public void onClick(View v) {
              if (v instanceof ImageView){
                 mListener.onTomato((ImageView)v);
              } else {
                 mListener.onPotato(v);
              }
          }
      
          public static interface IMyViewHolderClicks {
              public void onPotato(View caller);
              public void onTomato(ImageView callerImage);
          }
      
      }
      

      在編寫好ViewHolder的代碼后,在適配器中我們只需要重寫其IMyViewHolderClicks方法即可。

      public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
      
         @Override
         public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
             View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.my_layout, parent, false);
      
             MyAdapter.ViewHolder vh = new ViewHolder(v, new MyAdapter.ViewHolder.IMyViewHolderClicks() {
                 public void onPotato(View caller) { Log.d("VEGETABLES","Poh-tah-tos"); };
                 public void onTomato(ImageView callerImage) {&nbsp;Log.d("VEGETABLES","To-m8-tohs"); }
              });
              return vh;
          }
      
        ...
      

與ListView的區(qū)別

  1. RecyclerView與ListView最大的區(qū)別就是它的布局方式十分豐富:線性布局(橫向或者縱向)、表格布局、瀑布流布局。而ListView只有一個縱向布局的效果,若需要不同的呈現(xiàn)方式還得自己去定義。面對現(xiàn)在更多樣的需求,無論是從美觀還是效率上說,RecyclerView都是首選;
  2. RecyclerView編寫的規(guī)范化,從上面可以得知RecyclerView的組件都是定義好的,在什么階段定義什么得到什么。而ListView需要重寫getView,布局的載入、持有、資源設(shè)置全部在里面完成。
  3. 緩存方法的優(yōu)勢,RecyclerView比ListView多兩級緩存,開發(fā)有緩存池,支持多個RecyclerView共同使用;RV緩存的是ViewHolder,支持屏幕外的列表項進(jìn)入屏幕時無須bindView就可以快速重用。

RecyclerView布局文件(test_rv.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/xxxx">
    <include layout="@layout/xxxx" />
    <ProgressBar
        android:id="@+id/我是進(jìn)度條"
        style="@android:style/xxx"
        android:layout_width="match_parent"
        android:layout_height="2dip"
        android:layout_below="@+id/title_bar"
        android:gravity="center_vertical"
        android:max="100"
        android:progressDrawable="@drawable/play_progress"
        android:indeterminateDrawable="@null" />

    <TextView
        android:id="@+id/activity_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/margin_5dp"
        android:layout_marginStart="25dp"
        android:layout_marginBottom="23dp"
        android:textColor="@color/x"
        android:textSize="@dimen/x"
        android:text="xxx"
        />

    <androidx.recyclerview.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        android:id="@+id/xxx"/>

</LinearLayout>

布局內(nèi)item組件(test_rv_item.xml)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="@dimen/xxx">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_marginEnd="@dimen/xxx"
            android:layout_marginRight="@dimen/xxx"
            android:importantForAccessibility="no"
            android:src="@drawable/我是右側(cè)的圖標(biāo)" />

        <LinearLayout
            android:id="@+id/xxxxxx"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentStart="true"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:layout_marginStart="24dp"
            android:layout_marginEnd="70dp"
            android:layout_marginRight="70dp"
            android:layout_toStartOf="@id/xxx"
            android:layout_toLeftOf="@id/xxx"
            android:orientation="vertical">

            <TextView
                android:id="@+id/title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/xxx"
                android:text=""
                android:textSize="@dimen/xxx" />
            <TextView
                android:id="@+id/date"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text=""
                android:textColor="@color/xxx"
                android:textSize="@dimen/xxx" />

        </LinearLayout>

</LinearLayout>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容