RecyclerView的基础使用及其与ListView的部分区别

先说说RecyclerView是怎么使用的,以其基础代码为本,大体以如下顺序进行分析。

  • 组件介绍
  • 点击事件
    • ListView为什么会点击item事件失效?
    • RecyclerView怎么设计点击回调?
  • 与ListView的区别
  • 布局文件
public class testForRecyclerViewActivity{

    // 装载item中数据
    private List mDatas;
    // 初始化item中的数据
    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 , 按照其规定好的设计规范,定义具体内容。
    class LogAdapter extends RecyclerView.Adapter {

        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, "我是标题");
            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", "标题");
                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),该方法旨在创造一个持有者的类,将对应id的view存到内存中,避免每次都需要去布局文件中读取对应的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),给持有者包含的组件赋予数据,同时可以给组件添加事件。

            @Override
            public void onBindViewHolder(@NonNull @NotNull ViewHolder viewHolder, int i) {
                viewHolder.setTextView(R.id.title, "我是标题");
                viewHolder.setTextView(R.id.date, "我是日期");
            }
      
    3. getItemCount(),返回item的条目数

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

点击事件(分析RecyclerView和ListView在点击事件的差异)

  1. item点击事件:与ListView不同的是,RecyclerView并没有配备setOnItemClickListener()方法,只能通过配置回调接口来设置对应的点击事件。

    • 可能你会觉得,wc这么不方便,RecyclerView不用也罢。不急,慢慢听我解释,对于ListView的点击事件有很重要的一个弊端,那就是某个时候你给ListView的item组件设置了setOnItemClickListener事件,正准备高高兴兴去测试时,却发现死活都点击无效甚是懊恼,上网一查同仁还不在少数,解释为:当listview中包含button,checkbox等控件的时候,android会默认将focus给了这些控件,也就是说listview的item根本就获取不到focus,所以导致onitemclick时间不能触发。那我们就详细聊聊为什么会产生这种情况,就随着关键部分的代码慢慢探索答案吧。
    • 对于ListView,点击事件发生后,经过事件分发机制判定(默认不拦截),遂调用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;
        }
    
    • 看着这么多的事件处理入口,是否一筹莫展?想当初曹孟德东临碣石以观沧海,从大处着眼忽略细节,望水何澹澹山岛竦峙。同样的道理,我们看着一大片的代码段时,应当结合我们的目标找寻与其最有关的方法,其他的大可以不看。比如我们需要的是看处理onItemClick的事件,而这个事件的触发是来自我们的手指从屏幕抬起的那一刻,因此直接就定位到第47行的onTouchUp(ev)方法。现在我们进去该方法,看它是怎么处理事件的:
    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判断触发是否是item范围内的事件为true,重要的是 child.hasExplicitFocusable() 取反,该方法用来判断该节点是否是获取焦点的,如果是则不会触发后续的点击回调。由此变引申出了两种解决方法:
      • 在button/checkbox等控件处设置android:clickable=”false” android:focusableInTouchMode=”false”,使其在点击item时不会因该组件的获取焦点属性而影响了回调事件。
      • 在item最外层添加属性android:descendantFocusability=”blocksDescendants”,该属性使item覆盖所有的子节点获取焦点,故里面的子节点均不可获取焦点。
    • 既然都到这一步了,不如看看他是怎么调用我们的方法的吧~~点击第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));
                    }
                }
            }
    
    • 可以看到该方法最终调用的是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行见到了它的庐山真面目,在这里调用的我们设置的mOnItemClickListener()方法。
  2. RecyclerView的点击事件应该如何设计其回调接口呢,我归纳了2种方式,当然思想都是一样的,那就是配置回调接口然后在对应的时机实现该方法。

    1. 第一种方式 按照我在开头贴的代码,在Activity中就定义需要用的回调接口

          //定义点击事件的接口
          public interface OnItemClickListener {
              void onItemClick(View view, int position);
          }
      

      然后在定义Adapter时,声明变量中加入对应变量与构造方法,并且在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中实现该接口并重写该点击方法,怎么样,是不是很简单,条理清晰井井有条。

            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", "标题");
                      startActivity(intent);
                  }
              });
      
    2. 第二种方式 ,在定义持有者时实现OnClickListener接口,重写其onClick方法,根据点击对象的不同而配置不同的方法。组件多的时候,可以通过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 {
      
         @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) { Log.d("VEGETABLES","To-m8-tohs"); }
              });
              return vh;
          }
      
        ...
      

与ListView的区别

  1. RecyclerView与ListView最大的区别就是它的布局方式十分丰富:线性布局(横向或者纵向)、表格布局、瀑布流布局。而ListView只有一个纵向布局的效果,若需要不同的呈现方式还得自己去定义。面对现在更多样的需求,无论是从美观还是效率上说,RecyclerView都是首选;
  2. RecyclerView编写的规范化,从上面可以得知RecyclerView的组件都是定义好的,在什么阶段定义什么得到什么。而ListView需要重写getView,布局的载入、持有、资源设置全部在里面完成。
  3. 缓存方法的优势,RecyclerView比ListView多两级缓存,开发有缓存池,支持多个RecyclerView共同使用;RV缓存的是ViewHolder,支持屏幕外的列表项进入屏幕时无须bindView就可以快速重用。

RecyclerView布局文件(test_rv.xml)



    
    

    

    


布局内item组件(test_rv_item.xml)




        

        

            
            

        


你可能感兴趣的:(RecyclerView的基础使用及其与ListView的部分区别)