【问题标题】:Handle Touch Events for RecyclerView with frequently changing data使用频繁更改的数据处理 RecyclerView 的触摸事件
【发布时间】:2019-10-30 15:34:55
【问题描述】:

场景:

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

问题:

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

1getAdapterPosition():-1

2motionEvent.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 的刷新。 但是,因为我仍然需要有关单击了哪些项目的信息,所以我仍然需要仍然返回 -1 和 ACTION_CANCEL 的 onTouchListener 项目。

问题:

在频繁更新其数据的 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;
    }

}

【问题讨论】:

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


    【解决方案1】:

    我认为发生的情况是 notifyDataSetChanged 仍在进行中,而在此期间发生触摸,项目没有位置,我认为这无法修复。

    在我看来,监听器很好,你真正应该改变的是 notifyDataSetChanged() 部分。

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

    点击此处了解如何实现此功能的更多信息: https://medium.com/@iammert/using-diffutil-in-android-recyclerview-bdca8e4fbb00

    【讨论】:

    • 我能够通过将notifyDatasetChanged() 替换为notifyItemRangeChanged(0,dataStoreDataAdapter.getItemCount()) 来修复“-1”项目列表位置问题@ 987654322@ 给出了澄清它仍然不能修复“ACTION_CANCEL”的返回如果在“错误的时刻”触摸了 listItem。如果成功,我将查看 DiffUtil 并报告-
    • 我尝试了更多的东西,但我仍然偶尔收到 ACTION_CANCEL 事件:我将 notifyDataSetChanged() 更改为 notifyItemRangeChanged() 以避免结构更改。我还从 ViewHolders onTouch() 调用 requestDisallowInterceptTouchEvent(true) 以防止父 ViewGroups 拦截手势。 This question 对此进行了澄清。但是我仍然在某些触摸上收到 ACTION_CANCEL 事件。有人对此有想法吗?可以查询哪个viewGroup拦截了触摸吗?
    【解决方案2】:

    我自己想出来的。 以下是答案:

    1. getAdapterPosition(): -1: 正如我在问题中已经提到的那样,这种情况发生了,因为notifyDataSetChanged() 重建了整个布局。因此,如果在布局重建和布局传递之间触及列表项,该函数将返回 -1。 Finn Marquart 给出了一个解决方案:使用 DiffUtils 将列表中每个项目的刷新量减少到最低限度。这对我不起作用,因为我需要经常刷新该项目。所以我的解决方案是使用notifyItemRangeChanged(0,devicedataDataAdapter.getItemCount()) 而不是notifyDataSetChanged(),因为这只会刷新视图中的元素,不会重新创建整个列表。
    2. 避免拦截视图持有者中的触摸事件: 一些更高的视图截获了触摸手势*,因此为每个视图持有者实现的onTouch() 方法会收到一个ACTION_CANCEL 事件,并且不会再被告知该手势(ACTION_UP 事件只能在父视图上接收就像recyclerviewOnItemTouchListener())。 这是 itemAnimator 的责任,它是 recyclerview 的成员。将 itemAnimator 设置为 null 禁用后,触摸手势不再被拦截:

    recyclerView.setItemAnimator(null);

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


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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-02
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多