处理RecyclerView的触摸事件以及频繁更改的数据

时间:2019-10-30 15:34:55

标签: android android-recyclerview onclicklistener ontouchlistener custom-adapter

场景:

  • RecyclerViewRecyclerView.Adapter。由于会显示对时间敏感的信息,因此notifyDataSetChanged()方法每10毫秒被调用一次,因此ViewHolders(类正在扩展RecyclerView.ViewHolder)会经常更新。
  • 基础数据来自本机C库,因此将重写适配器方法以查询本机库中的数据。
  • 源自this best practice video的接口在Adapter-class中实现,用于将onTouch-Events从适配器传递到Activity。因此,ViewHolder类实现了View.OnTouchListener并将所有触摸事件和listItem-position(由getAdapterPosition()查询)通过接口传递给Activity。
  • 每个ListItem的各种onTouch事件(单击,LongClick,滑动等)应在活动中识别。

问题:

在调用notifyDataSetChanged()之后不久触摸ListItem时,接收到的值为:

1 getAdapterPosition():-1

2 motionEvent.getAction()ACTION_DOWN后接ACTION_CANCEL

android-documentation说:

  

请注意,如果您调用了notifyDataSetChanged(),则直到下一个   布局传递,此方法的返回值为NO_POSITION。

因此,我猜测notifyDatasetChanges()刷新viewHolders时,每次触摸listItem都会发生错误:getAdapterPosition()根据文档返回-1。而且,可能是因为刷新了viewHolder中的元素,所以onTouchEvent引发了ACTION_CANCEL

我尝试过的方法:

  1. 我试图在ACTION_DOWN上停止RecyclerView的刷新,但是由于发生ACTION_CANCEL事件,我没有收到任何ACTION_UP事件,并且无法重新开始刷新数据。
  2. 我在活动的recyclerView中添加了一个RecyclerView.OnItemTouchListener(),以在onInterceptTouchEvent()中接收TouchEvent,然后将其传递给listItem及其onTouchListener并在那里停止刷新recyclerview。 但是,因为我仍然需要有关单击了哪个项目的信息,所以我仍然需要onTouchListener项,该项仍返回-1和ACTION_CANCEL

问题:

在经常更新其数据的RecyclerView中处理onTouch事件的最佳实践是什么?

活动级别

public class Activity extends AppCompatActivity implements DataStoreDataAdapter.OnListItemTouchListener {



    Handler dataUpdateHandler = new Handler();

    Runnable timerRunnable = new Runnable() {
        @Override
        public void run() {
            dataStoreDataAdapter.notifyDataSetChanged();

            dataUpdateHandler.postDelayed(this, 10);
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity);

        dataStoreDataAdapter = new DataStoreDataAdapter(getApplicationContext(),this);

        recyclerView = findViewById(R.id.list_view);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        recyclerView.setAdapter(dataStoreDataAdapter);

        recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
            @Override
            public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                Log.i(TAG, "onInterceptTouchEvent: " + " touched by type " + e);
                int action = e.getAction();
                if (action == MotionEvent.ACTION_DOWN) {
                    dataUpdateHandler.removeCallbacks(timerRunnable);
                } else if (action == MotionEvent.ACTION_UP) {
                    dataUpdateHandler.post(timerRunnable);
                }
                return false;
            }

            @Override
            public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
                Log.i(TAG, "onTouchEvent: " + " touched by type " + e);
            }


            @Override
            public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

            }
        });


        //set update Handler
        dataUpdateHandler.post(timerRunnable);
    }

    @Override
    public void onListItemTouch(int position, MotionEvent motionEvent) {
        Log.i(TAG, "onListItemTouch: ListItem position " + position + " motionEvent: " + motionEvent);
    }
}

适配器类

public class DataStoreDataAdapter extends RecyclerView.Adapter<DataStoreDataAdapter.ViewHolder> {

      private OnListItemTouchListener onListItemTouchListener;

    static class ViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener{
        View view;
        TextView Name;
       // ...  

        ViewHolder(@NonNull View itemView, OnListItemTouchListener onListItemTouchListener) {
            super(itemView);
            this.Name = itemView.findViewById(R.id.NameListItem);
            // ...

            this.onListItemTouchListener = onListItemTouchListener;
            itemView.setOnTouchListener(this);
        }

        @Override
        public boolean onTouch(View view, MotionEvent motionEvent) {
            onListItemTouchListener.onListItemTouch(getAdapterPosition(), motionEvent);
            return true;
        }
    }

    public interface OnListItemTouchListener{
        void onListItemTouch(int position, MotionEvent motionEvent);
    }

    public DataStoreDataAdapter(Context context, OnListItemTouchListener onListItemTouchListener) {
        super();
        this.onListItemTouchListener = onListItemTouchListener;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.list_item,parent,false);

        return new ViewHolder(view, onListItemTouchListener);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // get the stored data for this position
        // ...get stuff from native library

        // put data into view elements...

    }

    @Override
    public int getItemCount() {
        return // ... itemCount from native library;
    }

}

2 个答案:

答案 0 :(得分:0)

我认为发生的事情是notifyDataSetChanged仍在进行中,而在此期间发生触摸时,这些项目没有位置,并且我认为这无法解决。

在我看来,侦听器很好,您真正应该更改的是notifyDataSetChanged()部分。

您可以改用DiffUtil,以便仅更新实际更改的项目(位置或内容)。

转到此处以获取更多有关如何实现此目的的信息: https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00

答案 1 :(得分:0)

我自己弄清楚了。 答案如下:

  1. getAdapterPosition(): -1 : 正如我在问题中已经提到的那样,这是因为notifyDataSetChanged()重新构建了整个布局。因此,如果在布局重新生成和布局传递之间触摸了列表项,则该函数将返回-1。 Finn Marquart提供了一种解决方案:使用DiffUtils将列表中每个项目的刷新量减少到最少。这对我不起作用,因为我需要经常刷新该项目。因此,我的解决方案是使用notifyItemRangeChanged(0,devicedataDataAdapter.getItemCount())而不是notifyDataSetChanged() ,因为这只会刷新视图持有者中的元素,而不会重新创建整个列表。
  2. 避免在视图持有者中拦截触摸事件: 一些较高的View截获了触摸手势*,因此为每个视图持有者实现的onTouch()方法都会收到一个ACTION_CANCEL事件,并且不再会收到有关该手势的通知(ACTION_UP事件可以仅在父视图(如OnItemTouchListener()的{​​{1}}上收到)。 负责任的是itemAnimator ,它是recyclerview的成员。通过将itemAnimator设置为null来禁用itemAnimator之后,触摸手势将不再被拦截:

recyclerview

希望,这个答案可以帮助任何人。我花了很多时间解决这个问题:)


*我将“手势”定义为ACTION_DOWN和ACTION_UP事件之间所有触摸事件的总和。